context-mode 1.0.108 → 1.0.110
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/cli.js +38 -14
- package/build/opencode-plugin.js +4 -1
- package/build/server.js +38 -3
- package/build/session/analytics.d.ts +7 -0
- package/build/session/analytics.js +39 -2
- package/build/session/db.d.ts +3 -1
- package/build/session/persist-tool-calls.d.ts +54 -0
- package/build/session/persist-tool-calls.js +105 -0
- package/build/session/project-attribution.d.ts +1 -1
- package/cli.bundle.mjs +109 -109
- package/hooks/session-db.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +88 -88
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.110"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.110",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.110",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.110",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.110",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/build/cli.js
CHANGED
|
@@ -672,21 +672,45 @@ async function upgrade() {
|
|
|
672
672
|
if (detection.platform !== 'opencode' && detection.platform !== 'kilo') {
|
|
673
673
|
// Rebuild native addons for current Node.js ABI (fixes #131)
|
|
674
674
|
s.start("Rebuilding native addons");
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
675
|
+
const bsqBindingPath = resolve(pluginRoot, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node");
|
|
676
|
+
// Skip rebuild when the binding from `npm install --production` is
|
|
677
|
+
// already present. Earlier code ran `npm rebuild better-sqlite3`
|
|
678
|
+
// unconditionally — its internal prebuild-install spawn raced with
|
|
679
|
+
// the prior install's tree-prune, intermittently failing to resolve
|
|
680
|
+
// `rc/index.js` and printing a scary "rebuild warning" even though
|
|
681
|
+
// the binding was healthy. Pre-check eliminates the race for the
|
|
682
|
+
// 99% case (binding survived install).
|
|
683
|
+
if (existsSync(bsqBindingPath)) {
|
|
684
|
+
s.stop(color.green("Native addons OK") + color.dim(" — binding present"));
|
|
685
|
+
changes.push("better-sqlite3 binding already present (no rebuild needed)");
|
|
683
686
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
687
|
+
else {
|
|
688
|
+
// Binding actually missing — delegate to the shared 3-layer heal
|
|
689
|
+
// (scripts/heal-better-sqlite3.mjs, PR #410) instead of raw
|
|
690
|
+
// `npm rebuild`. Single source of truth across postinstall +
|
|
691
|
+
// ensure-deps + cli upgrade. Layer A spawns prebuild-install
|
|
692
|
+
// directly via process.execPath, bypassing PATH/MSVC and the
|
|
693
|
+
// npm-internal rc-resolution race that bit `npm rebuild`.
|
|
694
|
+
try {
|
|
695
|
+
const healUrl = pathToFileURL(resolve(pluginRoot, "scripts", "heal-better-sqlite3.mjs")).href;
|
|
696
|
+
const { healBetterSqlite3Binding } = await import(healUrl);
|
|
697
|
+
const result = healBetterSqlite3Binding(pluginRoot);
|
|
698
|
+
if (result?.healed) {
|
|
699
|
+
s.stop(color.green("Native addons healed") + color.dim(` (${result.reason})`));
|
|
700
|
+
changes.push(`Healed better-sqlite3 binding via ${result.reason}`);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
s.stop(color.yellow("Native addon heal needs manual step"));
|
|
704
|
+
p.log.warn(color.dim(` Run: cd "${pluginRoot}" && npm install better-sqlite3`));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
709
|
+
s.stop(color.yellow("Native addon heal unavailable"));
|
|
710
|
+
p.log.warn(color.yellow("better-sqlite3 heal helper missing") +
|
|
711
|
+
` — ${message}` +
|
|
712
|
+
color.dim(`\n Try manually: cd "${pluginRoot}" && npm rebuild better-sqlite3`));
|
|
713
|
+
}
|
|
690
714
|
}
|
|
691
715
|
}
|
|
692
716
|
// Update global npm
|
package/build/opencode-plugin.js
CHANGED
|
@@ -194,7 +194,10 @@ async function createContextModePlugin(ctx) {
|
|
|
194
194
|
// Mutate output.args — OpenCode reads the mutated output object
|
|
195
195
|
Object.assign(output.args, decision.updatedInput);
|
|
196
196
|
}
|
|
197
|
-
|
|
197
|
+
if (decision.action === "context" && decision.additionalContext) {
|
|
198
|
+
// Mutate output.args — OpenCode reads the mutated output object
|
|
199
|
+
output.args.additionalContext = decision.additionalContext;
|
|
200
|
+
}
|
|
198
201
|
},
|
|
199
202
|
// ── PostToolUse: Session event capture ──────────────
|
|
200
203
|
"tool.execute.after": async (input, output) => {
|
package/build/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime
|
|
|
19
19
|
import { classifyNonZeroExit } from "./exit-classify.js";
|
|
20
20
|
import { startLifecycleGuard } from "./lifecycle.js";
|
|
21
21
|
import { getWorktreeSuffix, SessionDB } from "./session/db.js";
|
|
22
|
+
import { persistToolCallCounter, restoreSessionStats } from "./session/persist-tool-calls.js";
|
|
22
23
|
import { searchAllSources } from "./search/unified.js";
|
|
23
24
|
import { buildNodeCommand } from "./adapters/types.js";
|
|
24
25
|
import { detectPlatform, getSessionDirSegments } from "./adapters/detect.js";
|
|
@@ -169,6 +170,15 @@ function hashProjectDir() {
|
|
|
169
170
|
const normalized = projectDir.replace(/\\/g, "/");
|
|
170
171
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
171
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve the per-project SessionDB path the way 4742160 originally did
|
|
175
|
+
* for `persistToolCallCounter`. Centralized so the write-back, the
|
|
176
|
+
* restore-on-startup, and any future SessionDB consumer all hash to the
|
|
177
|
+
* same file under worktree isolation.
|
|
178
|
+
*/
|
|
179
|
+
function getSessionDbPath() {
|
|
180
|
+
return join(getSessionDir(), `${hashProjectDir()}${getWorktreeSuffix()}.db`);
|
|
181
|
+
}
|
|
172
182
|
/**
|
|
173
183
|
* Compute a per-project, per-platform persistent path for the ContentStore.
|
|
174
184
|
* Derives content dir from the adapter's session dir so each platform
|
|
@@ -351,10 +361,13 @@ function trackResponse(toolName, response) {
|
|
|
351
361
|
// Persist a sidecar JSON snapshot for the statusline — read at ~3-5 Hz by
|
|
352
362
|
// bin/statusline.mjs (and any external dashboard) so they don't have to
|
|
353
363
|
// open the SQLite database. Throttled inside persistStats() (500ms) so
|
|
354
|
-
// it's safe to call on every response.
|
|
355
|
-
// dropped the SessionDB tool-call counter (`persistToolCallCounter`); we
|
|
356
|
-
// keep persistStats here because the statusline depends on it.
|
|
364
|
+
// it's safe to call on every response.
|
|
357
365
|
persistStats();
|
|
366
|
+
// Persist to SessionDB so counters survive process restart, --continue,
|
|
367
|
+
// upgrade. Re-introduces the write path 4742160 added and b392c2f dropped.
|
|
368
|
+
// setImmediate keeps this off the response hot path; the helper itself
|
|
369
|
+
// is best-effort (never throws).
|
|
370
|
+
setImmediate(() => persistToolCallCounter(getSessionDbPath(), toolName, bytes));
|
|
358
371
|
return response;
|
|
359
372
|
}
|
|
360
373
|
function trackIndexed(bytes) {
|
|
@@ -2823,6 +2836,28 @@ async function main() {
|
|
|
2823
2836
|
}
|
|
2824
2837
|
}
|
|
2825
2838
|
catch { /* best effort — _detectedAdapter stays null, falls back to .claude */ }
|
|
2839
|
+
// Restore tool-call counters from SessionDB BEFORE the heartbeat fires
|
|
2840
|
+
// so the very first persistStats() carries the prior PID's totals into
|
|
2841
|
+
// the sidecar JSON the statusline reads. Otherwise `/ctx-upgrade` flashes
|
|
2842
|
+
// `0 calls / $0.00` until the user makes another MCP tool call. Wrapped
|
|
2843
|
+
// in try/catch — a stats-restore failure must never block server startup.
|
|
2844
|
+
try {
|
|
2845
|
+
const restored = restoreSessionStats(getSessionDbPath());
|
|
2846
|
+
if (restored) {
|
|
2847
|
+
for (const [tool, count] of Object.entries(restored.calls)) {
|
|
2848
|
+
sessionStats.calls[tool] = count;
|
|
2849
|
+
}
|
|
2850
|
+
for (const [tool, bytes] of Object.entries(restored.bytesReturned)) {
|
|
2851
|
+
sessionStats.bytesReturned[tool] = bytes;
|
|
2852
|
+
}
|
|
2853
|
+
// Anchor uptime_ms to the original session start so `/ctx-upgrade`
|
|
2854
|
+
// doesn't reset the "session age" the statusline shows.
|
|
2855
|
+
if (restored.sessionStart > 0) {
|
|
2856
|
+
sessionStats.sessionStart = restored.sessionStart;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
catch { /* best effort — never block startup on a stats restore failure */ }
|
|
2826
2861
|
// Non-blocking version check — result stored for trackResponse warnings.
|
|
2827
2862
|
// First fetch at startup, then refresh every hour so long-running sessions
|
|
2828
2863
|
// (some users keep the MCP server alive 24h+) catch new releases without a
|
|
@@ -185,6 +185,13 @@ export interface LifetimeStats {
|
|
|
185
185
|
autoMemoryProjects: number;
|
|
186
186
|
/** Per-prefix breakdown of auto-memory files (user/feedback/project/...). */
|
|
187
187
|
autoMemoryByPrefix: Record<string, number>;
|
|
188
|
+
/**
|
|
189
|
+
* Per-category event counts aggregated across every SessionDB on disk.
|
|
190
|
+
* Keys are the raw category strings (file/cwd/rule/...) — the renderer
|
|
191
|
+
* looks them up against `categoryLabels` for display. Empty `{}` when no
|
|
192
|
+
* sidecar has any events. Optional for back-compat with older fixtures.
|
|
193
|
+
*/
|
|
194
|
+
categoryCounts: Record<string, number>;
|
|
188
195
|
}
|
|
189
196
|
/**
|
|
190
197
|
* Aggregate lifetime stats from all SessionDB files in `sessionsDir` and
|
|
@@ -332,6 +332,7 @@ export function getLifetimeStats(opts) {
|
|
|
332
332
|
?? join(homedir(), ".claude", "projects");
|
|
333
333
|
let totalEvents = 0;
|
|
334
334
|
let totalSessions = 0;
|
|
335
|
+
const categoryCounts = {};
|
|
335
336
|
// ── SessionDB aggregation ──
|
|
336
337
|
if (existsSync(sessionsDir)) {
|
|
337
338
|
let dbFiles = [];
|
|
@@ -358,6 +359,20 @@ export function getLifetimeStats(opts) {
|
|
|
358
359
|
const ss = sdb.prepare("SELECT COUNT(*) AS cnt FROM session_meta").get();
|
|
359
360
|
totalEvents += ev?.cnt ?? 0;
|
|
360
361
|
totalSessions += ss?.cnt ?? 0;
|
|
362
|
+
// Per-category aggregation across every sidecar so the
|
|
363
|
+
// Persistent memory bars stay populated even when the
|
|
364
|
+
// current project's local DB is fresh / empty.
|
|
365
|
+
try {
|
|
366
|
+
const catRows = sdb.prepare("SELECT category, COUNT(*) AS cnt FROM session_events GROUP BY category").all();
|
|
367
|
+
for (const row of catRows) {
|
|
368
|
+
if (!row.category)
|
|
369
|
+
continue;
|
|
370
|
+
categoryCounts[row.category] = (categoryCounts[row.category] ?? 0) + (row.cnt ?? 0);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// older schema / no category column — ignore
|
|
375
|
+
}
|
|
361
376
|
}
|
|
362
377
|
finally {
|
|
363
378
|
sdb.close();
|
|
@@ -414,6 +429,7 @@ export function getLifetimeStats(opts) {
|
|
|
414
429
|
autoMemoryCount,
|
|
415
430
|
autoMemoryProjects,
|
|
416
431
|
autoMemoryByPrefix,
|
|
432
|
+
categoryCounts,
|
|
417
433
|
};
|
|
418
434
|
}
|
|
419
435
|
// ─────────────────────────────────────────────────────────
|
|
@@ -503,7 +519,25 @@ function renderProjectMemory(pm, opts) {
|
|
|
503
519
|
const lifetimeTokens = lifeEvents * 256 + sessionTokensSaved;
|
|
504
520
|
out.push(` ${fmtNum(lifeEvents)} events · ${sessionLabel} · ~${tokensToUsd(lifetimeTokens)} saved lifetime`);
|
|
505
521
|
out.push("");
|
|
506
|
-
|
|
522
|
+
// Prefer lifetime categoryCounts (aggregated across every SessionDB) so
|
|
523
|
+
// the bar block matches the lifetime header above. Falls back to the
|
|
524
|
+
// project-local pm.by_category when lifetime data is absent (tests, older
|
|
525
|
+
// callers) or when no sidecar has any events yet.
|
|
526
|
+
const lifetimeCats = opts?.lifetime?.categoryCounts;
|
|
527
|
+
let cats;
|
|
528
|
+
if (lifetimeCats && Object.keys(lifetimeCats).length > 0) {
|
|
529
|
+
cats = Object.entries(lifetimeCats)
|
|
530
|
+
.filter(([, c]) => c > 0)
|
|
531
|
+
.map(([category, count]) => ({
|
|
532
|
+
category,
|
|
533
|
+
count,
|
|
534
|
+
label: categoryLabels[category] || category,
|
|
535
|
+
}))
|
|
536
|
+
.sort((a, b) => b.count - a.count);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
cats = pm.by_category;
|
|
540
|
+
}
|
|
507
541
|
const visible = cats.slice(0, topN);
|
|
508
542
|
const maxCount = visible.length > 0 ? visible[0].count : 1;
|
|
509
543
|
for (const cat of visible) {
|
|
@@ -529,8 +563,11 @@ function renderAutoMemory(lifetime) {
|
|
|
529
563
|
const entries = Object.entries(lifetime.autoMemoryByPrefix)
|
|
530
564
|
.sort((a, b) => b[1] - a[1])
|
|
531
565
|
.slice(0, 6);
|
|
566
|
+
// Top entry sets the bar scale so the visual stays proportional even when
|
|
567
|
+
// the absolute counts are tiny. Entries are pre-sorted desc.
|
|
568
|
+
const maxCount = entries.length > 0 ? entries[0][1] : 1;
|
|
532
569
|
for (const [prefix, count] of entries) {
|
|
533
|
-
out.push(` ${prefix.padEnd(12)} ${String(count).padStart(2)}`);
|
|
570
|
+
out.push(` ${prefix.padEnd(12)} ${String(count).padStart(2)} ${dataBar(count, maxCount, 20)}`);
|
|
534
571
|
}
|
|
535
572
|
return out;
|
|
536
573
|
}
|
package/build/session/db.d.ts
CHANGED
|
@@ -77,7 +77,9 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
77
77
|
* Eviction: if session exceeds MAX_EVENTS_PER_SESSION, evicts the
|
|
78
78
|
* lowest-priority (then oldest) event.
|
|
79
79
|
*/
|
|
80
|
-
insertEvent(sessionId: string, event: SessionEvent,
|
|
80
|
+
insertEvent(sessionId: string, event: Omit<SessionEvent, "data_hash"> & {
|
|
81
|
+
data_hash?: string;
|
|
82
|
+
}, sourceHook?: string, attribution?: Partial<ProjectAttribution>): void;
|
|
81
83
|
/**
|
|
82
84
|
* Bulk-insert N events in a SINGLE transaction.
|
|
83
85
|
*
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persist-tool-calls — runtime glue between MCP server's in-memory
|
|
3
|
+
* `sessionStats` and the on-disk `tool_calls` SessionDB table.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists
|
|
6
|
+
* ──────────────────────
|
|
7
|
+
* Commit 4742160 (May 2 16:58) added the SessionDB write path so the
|
|
8
|
+
* statusline counters survived `npm update -g context-mode` and
|
|
9
|
+
* `claude --continue`. Commit b392c2f (May 2 21:43) — the concurrency
|
|
10
|
+
* refactor — silently dropped that wiring as collateral. Same-session
|
|
11
|
+
* `/ctx-upgrade` flips the statusline back to `0 calls / $0.00`
|
|
12
|
+
* because the new PID starts with an empty `sessionStats` and never
|
|
13
|
+
* looks at the table the old PID was writing to.
|
|
14
|
+
*
|
|
15
|
+
* This module re-introduces the write path AND adds the read-side
|
|
16
|
+
* restore that 4742160 never shipped — both pure helpers so the
|
|
17
|
+
* server.ts wiring is a one-liner and the unit tests don't need to
|
|
18
|
+
* boot the MCP server.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Shape returned by {@link restoreSessionStats}. Subset of the in-memory
|
|
22
|
+
* `sessionStats` object the MCP server keeps — only the fields that can
|
|
23
|
+
* be recovered from SessionDB.
|
|
24
|
+
*/
|
|
25
|
+
export interface RestoredSessionStats {
|
|
26
|
+
/** Per-tool call counts. */
|
|
27
|
+
calls: Record<string, number>;
|
|
28
|
+
/** Per-tool returned bytes. */
|
|
29
|
+
bytesReturned: Record<string, number>;
|
|
30
|
+
/**
|
|
31
|
+
* Epoch-ms for `session_meta.started_at` of the latest session, so the
|
|
32
|
+
* statusline `uptime_ms` reflects the original session start instead of
|
|
33
|
+
* resetting to `Date.now()` on every PID change.
|
|
34
|
+
*/
|
|
35
|
+
sessionStart: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Increment the persistent tool-call counter for `toolName` under whatever
|
|
39
|
+
* session_id `session_meta` currently treats as the most recent. This is
|
|
40
|
+
* called from {@link trackResponse} on every tool response and must be
|
|
41
|
+
* cheap, non-throwing, and best-effort — a stats failure must never break
|
|
42
|
+
* the MCP tool call.
|
|
43
|
+
*/
|
|
44
|
+
export declare function persistToolCallCounter(sessionDbPath: string, toolName: string, bytes: number): void;
|
|
45
|
+
/**
|
|
46
|
+
* Read the latest session's tool-call totals back out of SessionDB so the
|
|
47
|
+
* MCP server can hydrate its in-memory `sessionStats` on startup. Returns
|
|
48
|
+
* `null` when the DB is missing or empty so the caller can keep the
|
|
49
|
+
* default zero-state without branching twice.
|
|
50
|
+
*
|
|
51
|
+
* Used during MCP server boot (BEFORE the heartbeat fires) so the
|
|
52
|
+
* statusline doesn't briefly flash `0 calls / $0.00` after upgrade.
|
|
53
|
+
*/
|
|
54
|
+
export declare function restoreSessionStats(sessionDbPath: string): RestoredSessionStats | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persist-tool-calls — runtime glue between MCP server's in-memory
|
|
3
|
+
* `sessionStats` and the on-disk `tool_calls` SessionDB table.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists
|
|
6
|
+
* ──────────────────────
|
|
7
|
+
* Commit 4742160 (May 2 16:58) added the SessionDB write path so the
|
|
8
|
+
* statusline counters survived `npm update -g context-mode` and
|
|
9
|
+
* `claude --continue`. Commit b392c2f (May 2 21:43) — the concurrency
|
|
10
|
+
* refactor — silently dropped that wiring as collateral. Same-session
|
|
11
|
+
* `/ctx-upgrade` flips the statusline back to `0 calls / $0.00`
|
|
12
|
+
* because the new PID starts with an empty `sessionStats` and never
|
|
13
|
+
* looks at the table the old PID was writing to.
|
|
14
|
+
*
|
|
15
|
+
* This module re-introduces the write path AND adds the read-side
|
|
16
|
+
* restore that 4742160 never shipped — both pure helpers so the
|
|
17
|
+
* server.ts wiring is a one-liner and the unit tests don't need to
|
|
18
|
+
* boot the MCP server.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { SessionDB } from "./db.js";
|
|
22
|
+
/**
|
|
23
|
+
* Increment the persistent tool-call counter for `toolName` under whatever
|
|
24
|
+
* session_id `session_meta` currently treats as the most recent. This is
|
|
25
|
+
* called from {@link trackResponse} on every tool response and must be
|
|
26
|
+
* cheap, non-throwing, and best-effort — a stats failure must never break
|
|
27
|
+
* the MCP tool call.
|
|
28
|
+
*/
|
|
29
|
+
export function persistToolCallCounter(sessionDbPath, toolName, bytes) {
|
|
30
|
+
try {
|
|
31
|
+
if (!existsSync(sessionDbPath))
|
|
32
|
+
return;
|
|
33
|
+
const sdb = new SessionDB({ dbPath: sessionDbPath });
|
|
34
|
+
try {
|
|
35
|
+
const sid = sdb.getLatestSessionId();
|
|
36
|
+
if (!sid)
|
|
37
|
+
return;
|
|
38
|
+
sdb.incrementToolCall(sid, toolName, bytes);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
sdb.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Best-effort: counter must never throw and break the parent tool call.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read the latest session's tool-call totals back out of SessionDB so the
|
|
50
|
+
* MCP server can hydrate its in-memory `sessionStats` on startup. Returns
|
|
51
|
+
* `null` when the DB is missing or empty so the caller can keep the
|
|
52
|
+
* default zero-state without branching twice.
|
|
53
|
+
*
|
|
54
|
+
* Used during MCP server boot (BEFORE the heartbeat fires) so the
|
|
55
|
+
* statusline doesn't briefly flash `0 calls / $0.00` after upgrade.
|
|
56
|
+
*/
|
|
57
|
+
export function restoreSessionStats(sessionDbPath) {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(sessionDbPath))
|
|
60
|
+
return null;
|
|
61
|
+
const sdb = new SessionDB({ dbPath: sessionDbPath });
|
|
62
|
+
try {
|
|
63
|
+
const sid = sdb.getLatestSessionId();
|
|
64
|
+
if (!sid)
|
|
65
|
+
return null;
|
|
66
|
+
const stats = sdb.getToolCallStats(sid);
|
|
67
|
+
const calls = {};
|
|
68
|
+
const bytesReturned = {};
|
|
69
|
+
for (const [tool, row] of Object.entries(stats.byTool)) {
|
|
70
|
+
calls[tool] = row.calls;
|
|
71
|
+
bytesReturned[tool] = row.bytesReturned;
|
|
72
|
+
}
|
|
73
|
+
// started_at is "YYYY-MM-DD HH:MM:SS" in UTC (SQLite datetime() default);
|
|
74
|
+
// append "Z" so Date.parse interprets it as UTC, matching how the
|
|
75
|
+
// session was actually persisted.
|
|
76
|
+
let sessionStart = Date.now();
|
|
77
|
+
try {
|
|
78
|
+
const meta = sdb.getSessionStats(sid);
|
|
79
|
+
if (meta?.started_at) {
|
|
80
|
+
const parsed = Date.parse(`${meta.started_at}Z`);
|
|
81
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
82
|
+
sessionStart = parsed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// best-effort — keep `Date.now()` fallback
|
|
87
|
+
}
|
|
88
|
+
// Skip empty restores so callers can `if (restored)` and not stomp
|
|
89
|
+
// their already-zero default with another zero.
|
|
90
|
+
if (Object.keys(calls).length === 0 &&
|
|
91
|
+
Object.keys(bytesReturned).length === 0) {
|
|
92
|
+
// Still useful to return sessionStart so uptime_ms doesn't reset
|
|
93
|
+
// even when no tool calls were made — but only if we found a session.
|
|
94
|
+
return { calls, bytesReturned, sessionStart };
|
|
95
|
+
}
|
|
96
|
+
return { calls, bytesReturned, sessionStart };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
sdb.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -36,7 +36,7 @@ export declare const ATTRIBUTION_CONFIDENCE: {
|
|
|
36
36
|
/** Fallback: session_origin without path signal */
|
|
37
37
|
readonly FALLBACK_SESSION_ORIGIN: 0.35;
|
|
38
38
|
};
|
|
39
|
-
export type AttributionSource = "event_path" | "cwd_event" | "input_cwd" | "workspace_root" | "last_seen" | "session_origin" | "unknown";
|
|
39
|
+
export type AttributionSource = "event_path" | "cwd_event" | "input_cwd" | "workspace_root" | "last_seen" | "session_origin" | "env" | "test" | "unknown";
|
|
40
40
|
export interface ProjectAttribution {
|
|
41
41
|
projectDir: string;
|
|
42
42
|
source: AttributionSource;
|