sandoichi 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,25 +1,21 @@
1
1
  # sandoichi
2
2
 
3
- Cuts what Claude Code and Codex charge you to re-read their own output. Redacts secrets, caps oversized tool results, and, if you turn it on, trims request history before it's sent — all without calling an LLM itself.
3
+ Sando's local JavaScript library for reducing repeated tool-output context.
4
4
 
5
- This is the core library (`@sando/core` renamed `sandoichi` for npm). For the Claude Code plugin, Codex plugin, and provider proxy, see the [main repo](https://github.com/yuzushi-dev/Sando).
6
-
7
- ## Install
8
-
9
- ```sh
5
+ ```bash
10
6
  npm install sandoichi
11
7
  ```
12
8
 
13
- Requires Node.js `22.22.x`.
14
-
15
- ## Usage
16
-
17
9
  ```js
18
10
  import { optimizeToolOutput, createProviderProxy } from 'sandoichi';
19
11
  ```
20
12
 
21
- See [index.mjs](./index.mjs) for the full list of exports (tool-output optimization, provider request transforms, semantic compaction, metrics, status line rendering).
13
+ Telemetry is off by default. The npm install asks once for consent.
14
+
15
+ The Claude Code and Codex plugins are installed from the Yuzushi marketplace:
22
16
 
23
- ## License
17
+ ```text
18
+ /plugin install sando@yuzushi
19
+ ```
24
20
 
25
- MIT
21
+ MIT.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sandoichi",
3
- "version": "0.1.1",
4
- "description": "Cuts what Claude Code and Codex charge you to re-read their own output.",
3
+ "version": "0.1.3",
4
+ "description": "Reduce repeated tool-output context in Claude Code and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "yuzushi",
7
7
  "repository": {
package/src/hook-cli.mjs CHANGED
@@ -5,21 +5,25 @@ import path from 'node:path';
5
5
  import { createReceipt, normalizeEvent, normalizePolicy, optimizeToolOutput } from './core.mjs';
6
6
  import { defaultMetricsPath, recordMetrics } from './metrics.mjs';
7
7
  import {
8
- defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
8
+ closeFinishedDays, defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
9
9
  } from './telemetry.mjs';
10
10
 
11
+ const PLUGIN_VERSION = '0.1';
12
+
11
13
  function todayUtc() { return new Date().toISOString().slice(0, 10); }
12
14
 
13
15
  /** Only counts (never content, paths, or IDs) — see docs/plans/2026-08-25-sando-telemetry-design.md.
14
16
  * `enforce` covers both `apply` and `dry-run`: both walk the real rewrite path, only
15
17
  * `observe` collects without deciding anything. */
16
18
  function recordHookTelemetry({ host, env, policy, optimization }) {
19
+ const configPath = defaultTelemetryConfigPath(env);
17
20
  let config;
18
- try { config = readTelemetryConfig(defaultTelemetryConfigPath(env)); } catch { return; }
21
+ try { config = readTelemetryConfig(configPath); } catch { return; }
19
22
  if (!config.enabled) return;
20
23
  try {
24
+ const statePaths = defaultTelemetryStatePaths(env);
21
25
  incrementCounter({
22
- statePaths: defaultTelemetryStatePaths(env),
26
+ statePaths,
23
27
  day: todayUtc(),
24
28
  event: 'hook_summary',
25
29
  host,
@@ -31,6 +35,7 @@ function recordHookTelemetry({ host, env, policy, optimization }) {
31
35
  bytesSaved: Math.max(0, optimization.stats.inputBytes - optimization.stats.inlineBytes),
32
36
  },
33
37
  });
38
+ closeFinishedDays({ statePaths, configPath, day: todayUtc(), pluginVersion: PLUGIN_VERSION });
34
39
  } catch { /* telemetry is best-effort and must never affect hook output */ }
35
40
  }
36
41
 
package/src/proxy.mjs CHANGED
@@ -4,9 +4,11 @@ import { estimateTokens } from './core.mjs';
4
4
  import { detectProviderBody, listSemanticCandidates, transformProviderRequest } from './context-transform.mjs';
5
5
  import { recordProxyRequest } from './proxy-metrics.mjs';
6
6
  import {
7
- defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
7
+ closeFinishedDays, defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
8
8
  } from './telemetry.mjs';
9
9
 
10
+ const PLUGIN_VERSION = '0.1';
11
+
10
12
  function todayUtc() { return new Date().toISOString().slice(0, 10); }
11
13
 
12
14
  /** Only counts (never request/response content) — see docs/plans/2026-08-25-sando-telemetry-design.md.
@@ -15,13 +17,15 @@ function todayUtc() { return new Date().toISOString().slice(0, 10); }
15
17
  * under a fixed `claude` label — this is the one place the design doc's host/provider split is
16
18
  * approximate, flagged for the design doc rather than silently assumed. */
17
19
  function recordProxyTelemetry({ env, transformed, beforeText, afterText }) {
20
+ const configPath = defaultTelemetryConfigPath(env);
18
21
  let config;
19
- try { config = readTelemetryConfig(defaultTelemetryConfigPath(env)); } catch { return; }
22
+ try { config = readTelemetryConfig(configPath); } catch { return; }
20
23
  if (!config.enabled) return;
21
24
  const cacheWarm = transformed.stats.cacheRewriteRatio !== null;
22
25
  try {
26
+ const statePaths = defaultTelemetryStatePaths(env);
23
27
  incrementCounter({
24
- statePaths: defaultTelemetryStatePaths(env),
28
+ statePaths,
25
29
  day: todayUtc(),
26
30
  event: 'proxy_summary',
27
31
  host: 'claude',
@@ -33,6 +37,7 @@ function recordProxyTelemetry({ env, transformed, beforeText, afterText }) {
33
37
  cacheHitNo: cacheWarm ? 0 : 1,
34
38
  },
35
39
  });
40
+ closeFinishedDays({ statePaths, configPath, day: todayUtc(), pluginVersion: PLUGIN_VERSION });
36
41
  } catch { /* telemetry is best-effort and must never affect the proxied response */ }
37
42
  }
38
43
 
@@ -0,0 +1,28 @@
1
+ import path from 'node:path';
2
+
3
+ import { defaultTelemetryConfigPath, readTelemetryConfig } from './telemetry.mjs';
4
+
5
+ export function runSessionStart({
6
+ env = process.env,
7
+ stdout = process.stdout,
8
+ rootEnv = 'PLUGIN_ROOT',
9
+ } = {}) {
10
+ try {
11
+ const config = readTelemetryConfig(defaultTelemetryConfigPath(env));
12
+ if (config.prompted_consent_version > 0) {
13
+ stdout.write('{}\n');
14
+ return;
15
+ }
16
+ const pluginRoot = env[rootEnv] || path.resolve(import.meta.dirname, '..');
17
+ const cli = path.join(pluginRoot, 'lib', 'telemetry-cli.mjs');
18
+ stdout.write(`${JSON.stringify({
19
+ hookSpecificOutput: {
20
+ hookEventName: 'SessionStart',
21
+ systemMessage: 'Sando can send anonymous aggregate telemetry (opt-in, off by default). '
22
+ + `Run \`node "${cli}" enable\` to turn it on. Details: TELEMETRY.md`,
23
+ },
24
+ })}\n`);
25
+ } catch {
26
+ stdout.write('{}\n');
27
+ }
28
+ }
package/src/telemetry.mjs CHANGED
@@ -1,6 +1,8 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
4
6
 
5
7
  import { atomicWrite, ensureDirectory, withLock } from './provider-usage.mjs';
6
8
 
@@ -257,6 +259,37 @@ export function closeDay({ statePaths, day, pluginVersion }) {
257
259
  return closedRows;
258
260
  }
259
261
 
262
+ function launchDetachedFlush({ statePaths, configPath, spawnImpl }) {
263
+ try {
264
+ const entryPath = fileURLToPath(new URL('./telemetry-flush-entry.mjs', import.meta.url));
265
+ const child = spawnImpl(process.execPath, [entryPath, '--queue', statePaths.queue, '--config', configPath], {
266
+ detached: true, env: {}, stdio: 'ignore', windowsHide: true,
267
+ });
268
+ child.unref();
269
+ } catch { /* telemetry must never affect the caller */ }
270
+ }
271
+
272
+ /** Closes every raw counter day before `day` and starts one detached uploader.
273
+ * The child receives only local state paths and an empty environment. */
274
+ export function closeFinishedDays({
275
+ statePaths, configPath = defaultTelemetryConfigPath(), day, pluginVersion,
276
+ spawnImpl = spawn,
277
+ } = {}) {
278
+ const days = new Set();
279
+ if (fs.existsSync(statePaths.counters)) {
280
+ const state = readCounters(statePaths.counters);
281
+ for (const entry of Object.values(state.counters)) {
282
+ if (typeof entry.day === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.day) && entry.day < day) days.add(entry.day);
283
+ }
284
+ }
285
+ const closedRows = [];
286
+ for (const closedDay of [...days].sort()) {
287
+ closedRows.push(...closeDay({ statePaths, day: closedDay, pluginVersion }));
288
+ }
289
+ if (closedRows.length) launchDetachedFlush({ statePaths, configPath, spawnImpl });
290
+ return closedRows;
291
+ }
292
+
260
293
  export function loadBatch({ statePaths, max = DEFAULT_BATCH_MAX } = {}) {
261
294
  return readQueueRows(statePaths.queue).slice(0, max);
262
295
  }