Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,27 @@ This log starts at v1.6.5 (the reliability-pillar repositioning). Earlier histor

## [Unreleased]

_Nothing landed since [1.6.18]._
_Nothing landed since [1.6.19]._

## [1.6.19] — 2026-06-28

A growth feature plus a thorough hardening pass. A full component audit found and fixed a batch of edge-case bugs (mostly in the analytics/reporting layer and the proxy's output caps) before more users hit them.

### Added
- **One-time GitHub-star nudge.** After `sipcode proxy --stats` shows real savings, a single line points to the repo, shown once per machine ever (gated on a `~/.sipcode/.star-nudge` marker). Never in `--json`, never when there are no rewrites, never blocks, zero network calls.
- README gained live GitHub-stars and npm-downloads social-proof badges.

### Fixed
- **Proxy output caps no longer hide a command's failure.** Every cap (`tsc`, `git`, `find`, `ls`, `grep`, `npm`) used `cmd | head`, which returns exit 0 even when the command failed — so a failing `tsc --noEmit` could look like a clean build to Claude. All caps now use `set -o pipefail; cmd | awk 'NR<=N'`, which caps the output while preserving the command's real exit code (awk reads the whole stream, so there is no early-close SIGPIPE false-failure either).
- **`--here` now works for any project path.** It previously matched only paths without special characters; a folder like `C:\Projects\just research` (or any path with a dot, space, or parenthesis) silently found nothing. It now matches Claude Code's exact directory encoding.
- **`today`, `forecast`, and `trend` no longer crash** on `--agent auto` (which the help advertised) or a mistyped agent name.
- **Opt-out flags are honored.** `init --no-proxy` / `--no-marker` / `--no-verify-mcp` / `--no-claude-md` and `receipt --no-share` were silently ignored.
- **`cat` / `grep` rewriters corrected.** The proxy no longer rewrites bash's `type` builtin (which is not a file reader), and `grep -rn` keeps the matching lines and numbers instead of collapsing to per-file counts.
- **Empty / in-flight sessions are skipped consistently.** `impact`, `why`, and the `audit_latest_session` MCP tool no longer build an all-zero report from an empty latest session.
- Docs: `sipcode receipt` produces a PNG, not a "PDF"; a sample output file updated to opus 4.8.

### Internal
- Test count: 1,373 → 1,388. Full suite green; privacy and no-shell-args guards green. Findings verified by a 3-part component audit.

## [1.6.18] — 2026-06-25

Expand Down Expand Up @@ -237,7 +257,8 @@ This release rolls v1.6.9's B3 work (bumped but never published to npm) together

---

[Unreleased]: https://github.com/Anuj7411/sipcode/compare/v1.6.18...HEAD
[Unreleased]: https://github.com/Anuj7411/sipcode/compare/v1.6.19...HEAD
[1.6.19]: https://github.com/Anuj7411/sipcode/compare/v1.6.18...v1.6.19
[1.6.18]: https://github.com/Anuj7411/sipcode/compare/v1.6.17...v1.6.18
[1.6.17]: https://github.com/Anuj7411/sipcode/compare/v1.6.16...v1.6.17
[1.6.16]: https://github.com/Anuj7411/sipcode/compare/v1.6.15...v1.6.16
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

<p align="center">
<a href="https://www.npmjs.com/package/sipcode"><img src="https://img.shields.io/npm/v/sipcode?color=5B4FCF&label=npm" alt="npm" /></a>
<a href="https://github.com/Anuj7411/sipcode/stargazers"><img src="https://img.shields.io/github/stars/Anuj7411/sipcode?color=5B4FCF&label=stars" alt="GitHub stars" /></a>
<a href="https://www.npmjs.com/package/sipcode"><img src="https://img.shields.io/npm/dm/sipcode?color=5B4FCF&label=downloads" alt="npm downloads per month" /></a>
<a href="https://github.com/Anuj7411/sipcode/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-5B4FCF" alt="MIT licensed" /></a>
<img src="https://img.shields.io/badge/tests-1%2C373%20passing-28C840" alt="1373 tests passing" />
<img src="https://img.shields.io/badge/tests-1%2C388%20passing-28C840" alt="1388 tests passing" />
<img src="https://img.shields.io/badge/network%20calls-0-2D3142" alt="zero network calls" />
</p>

Expand Down Expand Up @@ -177,7 +179,7 @@ Verify it installed:
sipcode --version
```

You should see `1.6.18` or higher.
You should see `1.6.19` or higher.

### Step 3. Run `sipcode init` to wire it into Claude Code

Expand Down Expand Up @@ -323,11 +325,11 @@ npm uninstall -g sipcode
| `sipcode why` | Per-session forensics: where your tokens died |
| `sipcode impact` | A/B compare your spend before vs after Sipcode |
| `sipcode score` | Audit your repo for AI-friendliness (0-100, tiered badge) |
| `sipcode receipt` | Generate a shareable PDF receipt of a session |
| `sipcode receipt` | Generate a shareable PNG receipt of a session |

Run any of them with `--help` for full options.

**Tip: which session do these report on?** `sipcode why`, `today`, and `stats` look at your most recent session across **all** projects by default, not the folder you happen to be standing in. So if you run `sipcode why` inside project A but project B had the most recent activity, you will see project B. To scope a command to the project you are currently in, add `--here` (for example, `sipcode why --here`). Use `sipcode why --list` to see every session and pick one with `--session <id>`.
**Tip: which session do these report on?** `sipcode why` and `sipcode stats` look at your most recent session across **all** projects by default, not the folder you happen to be standing in. So if you run `sipcode why` inside project A but project B had the most recent activity, you will see project B. To scope either to the project you are currently in, add `--here` (for example, `sipcode why --here`). Use `sipcode why --list` to see every session and pick one with `--session <id>`. (`sipcode today` always summarizes all of today's sessions and has no `--here`.)

---

Expand Down
2 changes: 1 addition & 1 deletion docs/screenshots/sipcode-why-sample.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(showing latest session 84bbf968 from C--Projects-Sipcode; pass --here to scope to this directory)

sipcode why · session 84bbf968 · 60h 26m · claude-opus-4-7
sipcode why · session 84bbf968 · 60h 26m · claude-opus-4-8
project: C--Projects-Sipcode

you burned 225,576,407 tokens. 936,947 were code output. the other 224,639,460 were exploration, re-reads, and idle context.
Expand Down
6 changes: 3 additions & 3 deletions docs/site/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Sipcode is a context-hygiene proxy for [Claude Code](https://www.anthropic.com/c

On a locked, public 20-task benchmark corpus, Sipcode delivers 62.6% median tool-output savings (range 37.4% to 80.6%), totalling 3,567,170 tokens saved and $67.43 at current Claude Sonnet pricing. Anyone can reproduce these numbers with `sipcode benchmark`. Anthropic's published research finds cleaner context gives a 29% quality lift and 40% fewer agent errors, which is the mechanism Sipcode targets.

Sipcode is solo-maintained, MIT licensed, has 1,373 passing tests, and makes zero network calls during normal use. A privacy test in the repo fails the build if any `node:http`, `node:https`, `node:net`, or `node:dns` import is added to `src/`. Your code and transcripts never leave your laptop.
Sipcode is solo-maintained, MIT licensed, has 1,388 passing tests, and makes zero network calls during normal use. A privacy test in the repo fails the build if any `node:http`, `node:https`, `node:net`, or `node:dns` import is added to `src/`. Your code and transcripts never leave your laptop.

## How Sipcode differs from neighboring tools

Expand Down Expand Up @@ -140,7 +140,7 @@ Verify: `node --version` should show v18.0.0 or higher.
npm i -g sipcode
```

Verify: `sipcode --version` should show 1.6.18 or higher.
Verify: `sipcode --version` should show 1.6.19 or higher.

### Step 3. Run `sipcode init`

Expand Down Expand Up @@ -221,7 +221,7 @@ npm uninstall -g sipcode
| `sipcode impact` | Before vs after Sipcode comparison |
| `sipcode why` | Per-session forensics |
| `sipcode score` | Audit a repo for AI-friendliness |
| `sipcode receipt` | Generate a shareable PDF receipt of a session |
| `sipcode receipt` | Generate a shareable PNG receipt of a session |

## Requirements

Expand Down
6 changes: 3 additions & 3 deletions docs/site/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Sipcode is a context-hygiene proxy for [Claude Code](https://www.anthropic.com/c

On a locked, public 20-task benchmark corpus, Sipcode delivers 62.6% median tool-output savings (range 37.4% to 80.6%), totalling 3,567,170 tokens saved and $67.43 at current Claude Sonnet pricing. Anyone can reproduce these numbers with `sipcode benchmark`. Anthropic's published research finds cleaner context gives a 29% quality lift and 40% fewer agent errors, which is the mechanism Sipcode targets.

Sipcode is solo-maintained, MIT licensed, has 1,373 passing tests, and makes zero network calls during normal use. A privacy test in the repo fails the build if any `node:http`, `node:https`, `node:net`, or `node:dns` import is added to `src/`. Your code and transcripts never leave your laptop.
Sipcode is solo-maintained, MIT licensed, has 1,388 passing tests, and makes zero network calls during normal use. A privacy test in the repo fails the build if any `node:http`, `node:https`, `node:net`, or `node:dns` import is added to `src/`. Your code and transcripts never leave your laptop.

## How Sipcode differs from neighboring tools

Expand Down Expand Up @@ -53,8 +53,8 @@ Sipcode ships an MCP server exposing 15 tools so Claude Code can introspect its

## Source code

- [GitHub repository](https://github.com/Anuj7411/sipcode): MIT licensed source, 1,373 tests
- [npm package](https://www.npmjs.com/package/sipcode): `sipcode` on npm, current version 1.6.18
- [GitHub repository](https://github.com/Anuj7411/sipcode): MIT licensed source, 1,388 tests
- [npm package](https://www.npmjs.com/package/sipcode): `sipcode` on npm, current version 1.6.19
- [Privacy test](https://github.com/Anuj7411/sipcode/blob/main/src): build-blocking check that no network modules are imported in `src/`

## License
Expand Down
4 changes: 2 additions & 2 deletions docs/site/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
<div class="badges">
<span class="badge">
<span class="badge-dot"></span>
v1.6.18
v1.6.19
</span>
<span class="badge">
<span class="badge-dot badge-dot--green"></span>
1,373 tests passing
1,388 tests passing
</span>
</div>
<a
Expand Down
6 changes: 3 additions & 3 deletions docs/site/src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import PlotBackground from "./PlotBackground.astro";

// Tier-1 fix #2 — test count from the project root. Single source of truth.
// Update on each release alongside Hero/Footer version labels.
// Last bumped: 2026-06-25 (v1.6.18MCP registry mcpName casing fix; v1.6.17 feature set).
const TEST_COUNT = 1373;
// Last bumped: 2026-06-28 (v1.6.19hardening: exit-code preservation, --here, agent-crash, flag fixes).
const TEST_COUNT = 1388;
---

<section class="hero">
Expand All @@ -19,7 +19,7 @@ const TEST_COUNT = 1373;

<div class="container">
<div class="hero-head-row">
<span class="hero-eyebrow"><span class="hero-tick"></span>v1.6.18 · MIT licensed</span>
<span class="hero-eyebrow"><span class="hero-tick"></span>v1.6.19 · MIT licensed</span>
<span class="hero-head-meta">{TEST_COUNT.toLocaleString()} tests passing</span>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sipcode",
"version": "1.6.18",
"version": "1.6.19",
"mcpName": "io.github.Anuj7411/sipcode",
"description": "Sip your tokens, don't gulp them. Keep Claude Code's context clean: drift detection, re-read dedup, integrity scoring, AST-aware reads, and 15 MCP tools for Claude Desktop.",
"keywords": [
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
"url": "https://github.com/Anuj7411/sipcode",
"source": "github"
},
"version": "1.6.18",
"version": "1.6.19",
"packages": [
{
"registryType": "npm",
"identifier": "sipcode",
"version": "1.6.18",
"version": "1.6.19",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
Expand Down
19 changes: 17 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ program
.action(async (opts) => {
const { runInit } = await import("./commands/init.js");
const { homedir } = await import("node:os");
const r = await runInit(opts, { homeDir: homedir() });
// Commander stores --no-X as opts.x=false; the runner reads opts.noX.
const r = await runInit(
{
...opts,
noClaudeMd: opts.claudeMd === false,
noProxy: opts.proxy === false,
noMarker: opts.marker === false,
noVerifyMcp: opts.verifyMcp === false,
},
{ homeDir: homedir() },
);
if (r?.exitCode) process.exit(r.exitCode);
});

Expand Down Expand Up @@ -83,7 +93,12 @@ program
.option("--agent <id>", "which agent to source transcripts from: claude-code | cursor | auto")
.action(async (sessionId, opts) => {
const { runReceipt } = await import("./commands/receipt.js");
const result = await runReceipt({ ...opts, session: sessionId });
// Commander stores --no-share as opts.share=false; runner reads opts.noShare.
const result = await runReceipt({
...opts,
session: sessionId,
noShare: opts.share === false,
});
if (result?.exitCode) process.exit(result.exitCode);
});

Expand Down
20 changes: 17 additions & 3 deletions src/commands/forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ void ASSERT_NO_NETWORK;
import { RealFileSystem, type FileSystem } from "../lib/fs.js";
import { RealClock, type Clock } from "../lib/clock.js";
import { RealProcessEnv, type ProcessEnv } from "../lib/process.js";
import { getAgentById } from "../modules/agents/registry.js";
import type { AgentId } from "../modules/agents/types.js";
import { resolveAgentFromOpts } from "../modules/agents/cli.js";
import { MESSAGES } from "../lib/messages.js";
import { loadPricingForDate } from "../lib/pricing/load.js";
import { analyzeTokens, isEmptySession } from "../modules/transcript/analyzers/tokens.js";
import { runForecast, type ForecastSession } from "../modules/forecast/runForecast.js";
Expand Down Expand Up @@ -43,7 +43,21 @@ export async function runForecastCmd(
const stdout = deps.stdout ?? ((s: string) => process.stdout.write(s + "\n"));
const stderr = deps.stderr ?? ((s: string) => process.stderr.write(s + "\n"));

const agent = getAgentById((opts.agent ?? "claude-code") as AgentId);
const agentResolve = await resolveAgentFromOpts({
agent: opts.agent,
fs,
env,
cwd: opts.cwd ?? process.cwd(),
stdout,
stderr,
quiet: true,
});
if (!agentResolve.ok) return { exitCode: 1 };
const agent = agentResolve.agent;
if (!agent.transcriptParsingSupported) {
stderr(MESSAGES.cursorTranscriptNotSupported());
return { exitCode: 1 };
}
const now = clock.now();
const pricing = loadPricingForDate(now);

Expand Down
6 changes: 5 additions & 1 deletion src/commands/impact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { RealClock, type Clock } from "../lib/clock.js";
import { RealProcessEnv, type ProcessEnv } from "../lib/process.js";
import { resolveAgentFromOpts } from "../modules/agents/cli.js";
import { resolveProjectsDir } from "../modules/transcript/discover.js";
import { analyzeTokens } from "../modules/transcript/analyzers/tokens.js";
import {
analyzeTokens,
isEmptySession,
} from "../modules/transcript/analyzers/tokens.js";
import { analyzeDuplicateReads } from "../modules/transcript/analyzers/duplicateReads.js";
import { analyzeIdleContext } from "../modules/transcript/analyzers/idleContext.js";
import { loadPricingForDate } from "../lib/pricing/load.js";
Expand Down Expand Up @@ -118,6 +121,7 @@ export async function runImpactCommand(
if (!parseResult.ok) continue;
const parsed = parseResult.value;
const totals = analyzeTokens(parsed, pricing);
if (isEmptySession(totals)) continue;
const dups = analyzeDuplicateReads(parsed);
const idle = analyzeIdleContext(parsed);
aggregated.push(
Expand Down
23 changes: 22 additions & 1 deletion src/commands/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
import { generateProxyHookScript } from "../modules/proxy/proxyHookScript.js";
import { readReport } from "../modules/proxy/stats-store.js";
import { renderProxyReport } from "../modules/proxy/format-terminal.js";
import { shouldShowStarNudge } from "../lib/starNudge.js";
import { MESSAGES } from "../lib/messages.js";

export interface ProxyOptions {
install?: boolean;
Expand Down Expand Up @@ -88,7 +90,26 @@ export async function runProxy(
// --stats —
if (opts.stats) {
const report = await readReport(statsDir);
stdout(opts.json ? JSON.stringify(report, null, 2) : renderProxyReport(report));
if (opts.json) {
stdout(JSON.stringify(report, null, 2));
return { exitCode: 0 };
}
stdout(renderProxyReport(report));
// One-time star nudge, only at a real value moment (rewrites happened).
// Marker lives under ~/.sipcode so it shows once per machine, ever.
if (report.totalInvocations > 0) {
const shown = await shouldShowStarNudge(
{
hasMarker: async (p) => (await readFile(p)) !== undefined,
writeMarker: writeFile,
},
homeDir,
);
if (shown) {
stdout("");
stdout(MESSAGES.starNudge());
}
}
return { exitCode: 0 };
}

Expand Down
7 changes: 5 additions & 2 deletions src/commands/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import { RealClock, type Clock } from "../lib/clock.js";
import { RealProcessEnv, type ProcessEnv } from "../lib/process.js";
import { MESSAGES } from "../lib/messages.js";
import { resolveAgentFromOpts } from "../modules/agents/cli.js";
import { resolveProjectsDir } from "../modules/transcript/discover.js";
import {
resolveProjectsDir,
cwdToProjectHash,
} from "../modules/transcript/discover.js";
import { analyzeTokens, isEmptySession } from "../modules/transcript/analyzers/tokens.js";
import { analyzeDuplicateReads } from "../modules/transcript/analyzers/duplicateReads.js";
import { analyzeIdleContext } from "../modules/transcript/analyzers/idleContext.js";
Expand Down Expand Up @@ -151,7 +154,7 @@ export async function runStats(

// --here filter: scope to the cwd's projectHash.
if (opts.here) {
const cwdHash = cwd.replace(/:/g, "-").replace(/[\\/]/g, "-");
const cwdHash = cwdToProjectHash(cwd);
metas = metas.filter(
(m) => m.projectHash === cwdHash || cwdHash.endsWith(m.projectHash),
);
Expand Down
20 changes: 17 additions & 3 deletions src/commands/today.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ void ASSERT_NO_NETWORK;
import { RealFileSystem, type FileSystem } from "../lib/fs.js";
import { RealClock, type Clock } from "../lib/clock.js";
import { RealProcessEnv, type ProcessEnv } from "../lib/process.js";
import { getAgentById } from "../modules/agents/registry.js";
import type { AgentId } from "../modules/agents/types.js";
import { resolveAgentFromOpts } from "../modules/agents/cli.js";
import { MESSAGES } from "../lib/messages.js";
import { loadPricingForDate } from "../lib/pricing/load.js";
import { analyzeTokens, isEmptySession } from "../modules/transcript/analyzers/tokens.js";
import { analyzeDuplicateReads } from "../modules/transcript/analyzers/duplicateReads.js";
Expand Down Expand Up @@ -46,7 +46,21 @@ export async function runTodayCmd(
const stdout = deps.stdout ?? ((s: string) => process.stdout.write(s + "\n"));
const stderr = deps.stderr ?? ((s: string) => process.stderr.write(s + "\n"));

const agent = getAgentById((opts.agent ?? "claude-code") as AgentId);
const agentResolve = await resolveAgentFromOpts({
agent: opts.agent,
fs,
env,
cwd: opts.cwd ?? process.cwd(),
stdout,
stderr,
quiet: true,
});
if (!agentResolve.ok) return { exitCode: 1 };
const agent = agentResolve.agent;
if (!agent.transcriptParsingSupported) {
stderr(MESSAGES.cursorTranscriptNotSupported());
return { exitCode: 1 };
}
const now = clock.now();
const pricing = loadPricingForDate(now);

Expand Down
Loading
Loading