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
21 changes: 19 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@ This log starts at v1.6.5 (the reliability-pillar repositioning). Earlier histor

## [Unreleased]

_Nothing landed since [1.6.16]._
_Nothing landed since [1.6.17]._

## [1.6.17] — 2026-06-25

Quality-of-life release: a real update path, cleaner numbers, and the current model name.

### Added
- **`sipcode update`** — a one-stop command that shows your installed version and the exact command to upgrade. Run it plain to print `npm i -g sipcode@latest` (plus a link to this changelog), or `sipcode update --run` to upgrade in place. Sipcode still never checks for updates on its own — zero network calls — so updating stays a deliberate, manual step. README gained a matching "Updating Sipcode" section and FAQ entry.

### Fixed
- **Stats no longer counts sessions that aren't yours.** Observer/telemetry plugins (e.g. claude-mem) create synthetic, zero-work session folders under `~/.claude/projects`. Those were inflating `stats`, `today`, and `why` — one machine showed 1,446 sessions when only ~21 were real, and `today` reported "0 tokens" across dozens of empty sessions. Sipcode now skips observer project folders and empty (zero-token) sessions everywhere, so counts, daily spend, and the "latest session" pick reflect real work. The same fix flows through the MCP tools, which read the identical discovery path.
- **`estimate` now names the current model.** It showed "opus 4.7"; it now shows "opus 4.8" (alongside sonnet 4.6 / haiku 4.5).
- **Friendlier first run.** On a machine with no Claude Code sessions yet, `sipcode stats` used to claim "transcripts exist, but none in the window" — false for a brand-new user. It now says "no Claude Code sessions found yet" and points you at getting started.

### Internal
- Test count: 1,363 → 1,373. Full suite green. The new `sipcode update` command stays inside the zero-network privacy guard (no `node:http` / `node:https` / `node:net` / `node:dns`) and the no-shell-args guard (npm is resolved as `npm.cmd` on Windows, no `shell: true`).

---

Expand Down Expand Up @@ -215,7 +230,9 @@ 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.15...HEAD
[Unreleased]: https://github.com/Anuj7411/sipcode/compare/v1.6.17...HEAD
[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
[1.6.15]: https://github.com/Anuj7411/sipcode/compare/v1.6.14...v1.6.15
[1.6.14]: https://github.com/Anuj7411/sipcode/compare/v1.6.13...v1.6.14
[1.6.13]: https://github.com/Anuj7411/sipcode/compare/v1.6.12...v1.6.13
Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<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/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%2C363%20passing-28C840" alt="1363 tests passing" />
<img src="https://img.shields.io/badge/tests-1%2C373%20passing-28C840" alt="1373 tests passing" />
<img src="https://img.shields.io/badge/network%20calls-0-2D3142" alt="zero network calls" />
</p>

Expand Down Expand Up @@ -209,6 +209,29 @@ If drift says "no drift" and proxy --stats shows a few dozen rewrites with savin

---

## Updating Sipcode

Sipcode never checks for updates on its own. That would require a network call, and zero network calls in normal use is a guaranteed property of the codebase (a test fails the build otherwise). Updating is always a manual, explicit step.

To update to the latest version:

```bash
npm i -g sipcode@latest
```

Or use the built-in helper:

```bash
sipcode update # prints your current version + the update command
sipcode update --run # runs the update in place for you
```

After updating, confirm with `sipcode --version` and see what changed in the [CHANGELOG](CHANGELOG.md). The MCP config never needs re-pasting; Claude asks the server for its tools on every reconnect, so new tools appear automatically.

**Release cadence:** patch releases ship whenever a fix or improvement is ready, often within a day or two of a reported issue. Watch or star the [repo](https://github.com/Anuj7411/sipcode) to be notified of new releases.

---

## What you get

| Feature | What it does for you |
Expand Down Expand Up @@ -244,6 +267,14 @@ When Claude's context window fills up with stale, repeated, or off-topic informa

Today, only with Claude Code (since Claude Code is the only tool that exposes the hooks Sipcode needs). Support for other CLIs is something we are exploring.

### How do I update?

```bash
npm i -g sipcode@latest
```

Or run `sipcode update` to see your version and the command, or `sipcode update --run` to update in place. Sipcode never auto-checks for updates (zero network calls), so updating is always explicit. See the [CHANGELOG](CHANGELOG.md) for what changed.

### How do I uninstall?

```bash
Expand All @@ -258,6 +289,7 @@ npm uninstall -g sipcode
| Command | What it does |
|---|---|
| `sipcode init` | Set up Sipcode in a project (manifest + CLAUDE.md + proxy hook) |
| `sipcode update` | Show how to update to the latest version (or `--run` to update now) |
| `sipcode drift` | Check if the current session is drifting from your norm |
| `sipcode proxy --stats` | See what the proxy caught this session |
| `sipcode benchmark` | Run the locked 20-task corpus |
Expand Down
4 changes: 2 additions & 2 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,363 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,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.

## 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.16 or higher.
Verify: `sipcode --version` should show 1.6.17 or higher.

### Step 3. Run `sipcode init`

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,363 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,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.

## 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,363 tests
- [npm package](https://www.npmjs.com/package/sipcode): `sipcode` on npm, current version 1.6.16
- [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.17
- [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.15
v1.6.17
</span>
<span class="badge">
<span class="badge-dot badge-dot--green"></span>
1,317 tests passing
1,373 tests passing
</span>
</div>
</div>
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-15 (v1.6.15 added 51 warmfill + init system-setup tests).
const TEST_COUNT = 1317;
// Last bumped: 2026-06-25 (v1.6.17 — sipcode update command, session-pollution filter, opus 4.8).
const TEST_COUNT = 1373;
---

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

<div class="container">
<div class="hero-head-row">
<span class="hero-eyebrow"><span class="hero-tick"></span>v1.6.15 · MIT licensed</span>
<span class="hero-eyebrow"><span class="hero-tick"></span>v1.6.17 · 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.16",
"version": "1.6.17",
"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": [
"claude-code",
Expand Down
10 changes: 10 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ program
if (r?.exitCode) process.exit(r.exitCode);
});

program
.command("update")
.description("Show how to update Sipcode to the latest version (or --run to update now).")
.option("--run", "run `npm i -g sipcode@latest` now instead of just printing it")
.action(async (opts) => {
const { runUpdate } = await import("./commands/update.js");
const r = await runUpdate(opts, { version: pkg.version });
if (r?.exitCode) process.exit(r.exitCode);
});

program
.command("hygiene")
.description("Install Sipcode Session Hygiene: read-once rules + context-pressure hooks.")
Expand Down
3 changes: 2 additions & 1 deletion src/commands/forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { RealProcessEnv, type ProcessEnv } from "../lib/process.js";
import { getAgentById } from "../modules/agents/registry.js";
import type { AgentId } from "../modules/agents/types.js";
import { loadPricingForDate } from "../lib/pricing/load.js";
import { analyzeTokens } from "../modules/transcript/analyzers/tokens.js";
import { analyzeTokens, isEmptySession } from "../modules/transcript/analyzers/tokens.js";
import { runForecast, type ForecastSession } from "../modules/forecast/runForecast.js";
import { formatForecastTerminal } from "../modules/forecast/format-terminal.js";
import { formatForecastJson } from "../modules/forecast/format-json.js";
Expand Down Expand Up @@ -66,6 +66,7 @@ export async function runForecastCmd(
const parsed = parseResult.value;
const startedAt = parsed.startedAt ?? new Date(meta.mtimeMs).toISOString();
const tokens = analyzeTokens(parsed, pricing);
if (isEmptySession(tokens)) continue;
sessions.push({ startedAt, estCostUSD: tokens.estCostUSD });
}

Expand Down
11 changes: 10 additions & 1 deletion src/commands/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 { 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 {
Expand Down Expand Up @@ -145,6 +145,9 @@ export async function runStats(
return { exitCode: 1 };
}
let metas = discovery.value;
// Raw count before any --here scoping — lets us tell a brand-new user
// (zero transcripts anywhere) from "none in this window/cwd".
const totalDiscovered = discovery.value.length;

// --here filter: scope to the cwd's projectHash.
if (opts.here) {
Expand Down Expand Up @@ -189,6 +192,7 @@ export async function runStats(
if (!isInWindow(window, startedAt)) continue;

const totals = analyzeTokens(parsed, pricing);
if (isEmptySession(totals)) continue;
const dups = analyzeDuplicateReads(parsed);
const idle = analyzeIdleContext(parsed);

Expand Down Expand Up @@ -224,6 +228,11 @@ export async function runStats(
stdout(formatJson(empty));
return { exitCode: 0 };
}
if (totalDiscovered === 0) {
// Brand-new user: no transcripts exist anywhere. Don't claim they do.
stdout(MESSAGES.statsNoSessionsYet());
return { exitCode: 0 };
}
stderr(MESSAGES.statsNoSessionsInWindow(window.raw));
return { exitCode: 1 };
}
Expand Down
3 changes: 2 additions & 1 deletion src/commands/today.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { RealProcessEnv, type ProcessEnv } from "../lib/process.js";
import { getAgentById } from "../modules/agents/registry.js";
import type { AgentId } from "../modules/agents/types.js";
import { loadPricingForDate } from "../lib/pricing/load.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 { runToday, type TodaySession } from "../modules/today/runToday.js";
import { formatTodayTerminal } from "../modules/today/format-terminal.js";
Expand Down Expand Up @@ -70,6 +70,7 @@ export async function runTodayCmd(
const startedAt = parsed.startedAt ?? new Date(meta.mtimeMs).toISOString();

const tokens = analyzeTokens(parsed, pricing);
if (isEmptySession(tokens)) continue;
const dups = analyzeDuplicateReads(parsed);
const totalTokens =
tokens.inputTokens +
Expand Down
3 changes: 2 additions & 1 deletion src/commands/trend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { RealProcessEnv, type ProcessEnv } from "../lib/process.js";
import { getAgentById } from "../modules/agents/registry.js";
import type { AgentId } from "../modules/agents/types.js";
import { loadPricingForDate } from "../lib/pricing/load.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 {
computeTrend,
Expand Down Expand Up @@ -116,6 +116,7 @@ export async function runTrend(
if (startedDay < sinceIso || startedDay > untilIso) continue;

const totals = analyzeTokens(parsed, pricing);
if (isEmptySession(totals)) continue;
const dups = analyzeDuplicateReads(parsed);
const totalTokens =
totals.inputTokens +
Expand Down
79 changes: 79 additions & 0 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* `sipcode update` — shows how to update Sipcode to the latest version.
*
* Sipcode never checks for updates on its own: that would require a network
* call, and zero-network-in-normal-use is a guaranteed property of this
* codebase (enforced by tests/privacy/no-network.test.ts). So this command
* just PRINTS the npm command by default. With --run, it executes
* `npm i -g sipcode@latest` locally on the user's behalf — the user's own npm
* talks to the registry, Sipcode itself still phones nothing home.
*/
import { ASSERT_NO_NETWORK } from "../lib/privacy.js";
void ASSERT_NO_NETWORK;
import { spawnSync } from "node:child_process";

export interface UpdateOptions {
/** Execute the update instead of just printing it. */
run?: boolean;
}

export interface UpdateDeps {
stdout?: (s: string) => void;
stderr?: (s: string) => void;
/** Current installed version, injected from cli.ts (reads package.json). */
version?: string;
/**
* Injectable npm runner. Tests pass a fake so no real process spawns.
* Returns the child's exit status (null if it was killed by a signal).
*/
runNpm?: (args: readonly string[]) => { status: number | null };
}

export interface UpdateResult {
exitCode: 0 | 1;
}

const UPDATE_CMD = "npm i -g sipcode@latest";
const CHANGELOG_URL =
"https://github.com/Anuj7411/sipcode/blob/main/CHANGELOG.md";

/** Default npm runner. Resolves npm.cmd on Windows (no shell — DEP0190 safe). */
function defaultRunNpm(args: readonly string[]): { status: number | null } {
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
const res = spawnSync(npmBin, [...args], { stdio: "inherit" });
return { status: res.status };
}

export async function runUpdate(
opts: UpdateOptions,
deps: UpdateDeps = {},
): Promise<UpdateResult> {
const stdout = deps.stdout ?? ((s: string) => process.stdout.write(s + "\n"));
const stderr = deps.stderr ?? ((s: string) => process.stderr.write(s + "\n"));
const version = deps.version ?? "unknown";

if (!opts.run) {
stdout(`sipcode is currently v${version}.`);
stdout("");
stdout("to update to the latest version, run:");
stdout(` ${UPDATE_CMD}`);
stdout("");
stdout("sipcode never checks for updates on its own (zero network calls).");
stdout(`see what changed: ${CHANGELOG_URL}`);
stdout("or re-run with --run to update now.");
return { exitCode: 0 };
}

const runNpm = deps.runNpm ?? defaultRunNpm;
stdout(`updating sipcode from v${version} to latest...`);
const result = runNpm(["i", "-g", "sipcode@latest"]);
if (result.status === 0) {
stdout("done. run `sipcode --version` to confirm.");
stdout(`see what changed: ${CHANGELOG_URL}`);
return { exitCode: 0 };
}
stderr("update failed. try running it manually:");
stderr(` ${UPDATE_CMD}`);
stderr(`on macOS/Linux you may need: sudo ${UPDATE_CMD}`);
return { exitCode: 1 };
}
9 changes: 9 additions & 0 deletions src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@ export const MESSAGES = {
`next: npx sipcode stats --group-by project`,
].join("\n"),

statsNoSessionsYet: () =>
[
`no Claude Code sessions found yet.`,
``,
`why: sipcode reads the transcripts Claude Code writes per session, and none exist yet.`,
``,
`fix: open Claude Code, run any prompt, then come back and run this again.`,
].join("\n"),

statsNoSessionsInWindow: (raw: string) =>
[
`no sessions found in the last ${raw}.`,
Expand Down
Loading
Loading