context-mode 1.0.103 → 1.0.104
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/README.md +66 -5
- package/bin/statusline.mjs +321 -0
- package/build/adapters/antigravity/index.d.ts +6 -0
- package/build/adapters/antigravity/index.js +10 -0
- package/build/adapters/base.d.ts +23 -0
- package/build/adapters/base.js +29 -0
- package/build/adapters/codex/index.d.ts +10 -0
- package/build/adapters/codex/index.js +22 -4
- package/build/adapters/cursor/index.d.ts +7 -0
- package/build/adapters/cursor/index.js +11 -0
- package/build/adapters/detect.d.ts +12 -1
- package/build/adapters/detect.js +69 -7
- package/build/adapters/gemini-cli/index.d.ts +8 -1
- package/build/adapters/gemini-cli/index.js +19 -7
- package/build/adapters/jetbrains-copilot/index.d.ts +7 -0
- package/build/adapters/jetbrains-copilot/index.js +12 -0
- package/build/adapters/kiro/index.d.ts +8 -0
- package/build/adapters/kiro/index.js +12 -0
- package/build/adapters/openclaw/index.d.ts +17 -0
- package/build/adapters/openclaw/index.js +29 -4
- package/build/adapters/opencode/index.d.ts +8 -0
- package/build/adapters/opencode/index.js +18 -6
- package/build/adapters/qwen-code/index.d.ts +1 -0
- package/build/adapters/qwen-code/index.js +3 -0
- package/build/adapters/types.d.ts +33 -0
- package/build/adapters/vscode-copilot/index.d.ts +6 -0
- package/build/adapters/vscode-copilot/index.js +10 -0
- package/build/adapters/zed/index.d.ts +1 -0
- package/build/adapters/zed/index.js +3 -0
- package/build/cli.d.ts +15 -0
- package/build/cli.js +62 -16
- package/build/concurrency/runPool.d.ts +36 -0
- package/build/concurrency/runPool.js +51 -0
- package/build/executor.d.ts +11 -1
- package/build/executor.js +59 -16
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +6 -2
- package/build/lifecycle.js +29 -2
- package/build/opencode-plugin.d.ts +6 -0
- package/build/opencode-plugin.js +60 -1
- package/build/routing-block.d.ts +8 -0
- package/build/routing-block.js +86 -0
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +54 -3
- package/build/search/auto-memory.d.ts +23 -10
- package/build/search/auto-memory.js +64 -26
- package/build/search/unified.d.ts +3 -0
- package/build/search/unified.js +2 -2
- package/build/server.d.ts +42 -0
- package/build/server.js +693 -164
- package/build/session/analytics.d.ts +49 -1
- package/build/session/analytics.js +278 -16
- package/build/session/db.d.ts +39 -8
- package/build/session/db.js +170 -19
- package/build/session/extract.js +124 -2
- package/build/tool-naming.d.ts +4 -0
- package/build/tool-naming.js +24 -0
- package/cli.bundle.mjs +201 -159
- package/configs/antigravity/GEMINI.md +11 -0
- package/configs/claude-code/CLAUDE.md +11 -0
- package/configs/codex/AGENTS.md +11 -0
- package/configs/cursor/context-mode.mdc +11 -0
- package/configs/gemini-cli/GEMINI.md +11 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +3 -0
- package/configs/kilo/AGENTS.md +11 -0
- package/configs/kiro/KIRO.md +11 -0
- package/configs/openclaw/AGENTS.md +11 -0
- package/configs/opencode/AGENTS.md +11 -0
- package/configs/pi/AGENTS.md +11 -0
- package/configs/qwen-code/QWEN.md +11 -0
- package/configs/vscode-copilot/copilot-instructions.md +3 -0
- package/configs/zed/AGENTS.md +11 -0
- package/hooks/auto-injection.mjs +36 -10
- package/hooks/cache-heal-utils.mjs +231 -0
- package/hooks/codex/sessionstart.mjs +7 -4
- package/hooks/core/routing.mjs +5 -0
- package/hooks/cursor/sessionstart.mjs +7 -4
- package/hooks/formatters/claude-code.mjs +20 -0
- package/hooks/gemini-cli/sessionstart.mjs +7 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +7 -2
- package/hooks/normalize-hooks.mjs +184 -0
- package/hooks/session-db.bundle.mjs +33 -14
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +68 -20
- package/hooks/session-loaders.mjs +8 -2
- package/hooks/sessionstart.mjs +8 -2
- package/hooks/vscode-copilot/sessionstart.mjs +7 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +164 -125
- package/skills/ctx-insight/SKILL.md +1 -1
- package/start.mjs +63 -3
package/build/session/db.js
CHANGED
|
@@ -19,31 +19,46 @@ import { execFileSync } from "node:child_process";
|
|
|
19
19
|
* (useful in CI environments or when git is unavailable).
|
|
20
20
|
* Set to empty string to disable isolation entirely.
|
|
21
21
|
*/
|
|
22
|
+
// Memoized per (cwd, env override) — recomputing on every tool call cost
|
|
23
|
+
// ~12ms (git worktree list subprocess fork) on macOS, 50ms+ on Windows.
|
|
24
|
+
// Key by cwd so a defensive `process.chdir()` invalidates rather than
|
|
25
|
+
// returning stale data.
|
|
26
|
+
let _wtCache;
|
|
22
27
|
export function getWorktreeSuffix() {
|
|
23
28
|
const envSuffix = process.env.CONTEXT_MODE_SESSION_SUFFIX;
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
if (_wtCache && _wtCache.cwd === cwd && _wtCache.envSuffix === envSuffix) {
|
|
31
|
+
return _wtCache.suffix;
|
|
32
|
+
}
|
|
33
|
+
let suffix = "";
|
|
24
34
|
if (envSuffix !== undefined) {
|
|
25
|
-
|
|
35
|
+
suffix = envSuffix ? `__${envSuffix}` : "";
|
|
26
36
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
else {
|
|
38
|
+
try {
|
|
39
|
+
const mainWorktree = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
timeout: 2000,
|
|
42
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
43
|
+
})
|
|
44
|
+
.split(/\r?\n/)
|
|
45
|
+
.find((l) => l.startsWith("worktree "))
|
|
46
|
+
?.replace("worktree ", "")
|
|
47
|
+
?.trim();
|
|
48
|
+
if (mainWorktree && cwd !== mainWorktree) {
|
|
49
|
+
suffix = `__${createHash("sha256").update(cwd).digest("hex").slice(0, 8)}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// git not available or not a git repo — no suffix
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
_wtCache = { cwd, envSuffix, suffix };
|
|
57
|
+
return suffix;
|
|
58
|
+
}
|
|
59
|
+
// Test-only helper: clear the memoization between cases.
|
|
60
|
+
export function _resetWorktreeSuffixCacheForTests() {
|
|
61
|
+
_wtCache = undefined;
|
|
47
62
|
}
|
|
48
63
|
// ─────────────────────────────────────────────────────────
|
|
49
64
|
// Constants
|
|
@@ -77,6 +92,9 @@ const S = {
|
|
|
77
92
|
deleteResume: "deleteResume",
|
|
78
93
|
getOldSessions: "getOldSessions",
|
|
79
94
|
searchEvents: "searchEvents",
|
|
95
|
+
incrementToolCall: "incrementToolCall",
|
|
96
|
+
getToolCallTotals: "getToolCallTotals",
|
|
97
|
+
getToolCallByTool: "getToolCallByTool",
|
|
80
98
|
};
|
|
81
99
|
// ─────────────────────────────────────────────────────────
|
|
82
100
|
// SessionDB
|
|
@@ -140,6 +158,17 @@ export class SessionDB extends SQLiteBase {
|
|
|
140
158
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
141
159
|
consumed INTEGER NOT NULL DEFAULT 0
|
|
142
160
|
);
|
|
161
|
+
|
|
162
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
163
|
+
session_id TEXT NOT NULL,
|
|
164
|
+
tool TEXT NOT NULL,
|
|
165
|
+
calls INTEGER NOT NULL DEFAULT 0,
|
|
166
|
+
bytes_returned INTEGER NOT NULL DEFAULT 0,
|
|
167
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
168
|
+
PRIMARY KEY (session_id, tool)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
|
|
143
172
|
`);
|
|
144
173
|
// Migration: add per-event attribution columns for existing DBs.
|
|
145
174
|
try {
|
|
@@ -236,6 +265,18 @@ export class SessionDB extends SQLiteBase {
|
|
|
236
265
|
LIMIT ?`);
|
|
237
266
|
// ── Cleanup ──
|
|
238
267
|
p(S.getOldSessions, `SELECT session_id FROM session_meta WHERE started_at < datetime('now', ? || ' days')`);
|
|
268
|
+
// ── Tool calls (persistent counter) ──
|
|
269
|
+
p(S.incrementToolCall, `INSERT INTO tool_calls (session_id, tool, calls, bytes_returned)
|
|
270
|
+
VALUES (?, ?, 1, ?)
|
|
271
|
+
ON CONFLICT(session_id, tool) DO UPDATE SET
|
|
272
|
+
calls = calls + 1,
|
|
273
|
+
bytes_returned = bytes_returned + excluded.bytes_returned,
|
|
274
|
+
updated_at = datetime('now')`);
|
|
275
|
+
p(S.getToolCallTotals, `SELECT COALESCE(SUM(calls), 0) AS calls,
|
|
276
|
+
COALESCE(SUM(bytes_returned), 0) AS bytes_returned
|
|
277
|
+
FROM tool_calls WHERE session_id = ?`);
|
|
278
|
+
p(S.getToolCallByTool, `SELECT tool, calls, bytes_returned
|
|
279
|
+
FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`);
|
|
239
280
|
}
|
|
240
281
|
// ═══════════════════════════════════════════
|
|
241
282
|
// Events
|
|
@@ -287,6 +328,60 @@ export class SessionDB extends SQLiteBase {
|
|
|
287
328
|
});
|
|
288
329
|
this.withRetry(() => transaction());
|
|
289
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Bulk-insert N events in a SINGLE transaction.
|
|
333
|
+
*
|
|
334
|
+
* PostToolUse hooks emit 5–15 events per tool call. Calling insertEvent()
|
|
335
|
+
* in a loop runs N transactions = N WAL commits = N fsync candidates,
|
|
336
|
+
* which is painful on Windows NTFS where commit latency dominates.
|
|
337
|
+
* One transaction = one commit, dedup/evict checks reuse cached statements.
|
|
338
|
+
*
|
|
339
|
+
* Cross-platform: uses the same WAL-mode transaction primitive as
|
|
340
|
+
* insertEvent — behavior identical on macOS / Linux / Windows.
|
|
341
|
+
*/
|
|
342
|
+
bulkInsertEvents(sessionId, events, sourceHook = "PostToolUse", attributions) {
|
|
343
|
+
if (!events || events.length === 0)
|
|
344
|
+
return;
|
|
345
|
+
if (events.length === 1) {
|
|
346
|
+
// Cheaper to fall through to insertEvent (its own dedicated transaction).
|
|
347
|
+
this.insertEvent(sessionId, events[0], sourceHook, attributions?.[0]);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// Pre-compute hashes + normalized attribution outside the transaction
|
|
351
|
+
// so the SQL transaction holds only DB work (shorter lock window).
|
|
352
|
+
const prepared = events.map((event, i) => {
|
|
353
|
+
const dataHash = createHash("sha256")
|
|
354
|
+
.update(event.data)
|
|
355
|
+
.digest("hex")
|
|
356
|
+
.slice(0, 16)
|
|
357
|
+
.toUpperCase();
|
|
358
|
+
const attribution = attributions?.[i];
|
|
359
|
+
const projectDir = String(attribution?.projectDir ?? event.project_dir ?? "").trim();
|
|
360
|
+
const attributionSource = String(attribution?.source ?? event.attribution_source ?? "unknown");
|
|
361
|
+
const rawConfidence = Number(attribution?.confidence ?? event.attribution_confidence ?? 0);
|
|
362
|
+
const attributionConfidence = Number.isFinite(rawConfidence)
|
|
363
|
+
? Math.max(0, Math.min(1, rawConfidence))
|
|
364
|
+
: 0;
|
|
365
|
+
return { event, dataHash, projectDir, attributionSource, attributionConfidence };
|
|
366
|
+
});
|
|
367
|
+
const transaction = this.db.transaction(() => {
|
|
368
|
+
let cnt = this.stmt(S.getEventCount).get(sessionId).cnt;
|
|
369
|
+
for (const row of prepared) {
|
|
370
|
+
const dup = this.stmt(S.checkDuplicate).get(sessionId, DEDUP_WINDOW, row.event.type, row.dataHash);
|
|
371
|
+
if (dup)
|
|
372
|
+
continue;
|
|
373
|
+
if (cnt >= MAX_EVENTS_PER_SESSION) {
|
|
374
|
+
this.stmt(S.evictLowestPriority).run(sessionId);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
cnt++;
|
|
378
|
+
}
|
|
379
|
+
this.stmt(S.insertEvent).run(sessionId, row.event.type, row.event.category, row.event.priority, row.event.data, row.projectDir, row.attributionSource, row.attributionConfidence, sourceHook, row.dataHash);
|
|
380
|
+
}
|
|
381
|
+
this.stmt(S.updateMetaLastEvent).run(sessionId);
|
|
382
|
+
});
|
|
383
|
+
this.withRetry(() => transaction());
|
|
384
|
+
}
|
|
290
385
|
/**
|
|
291
386
|
* Retrieve events for a session with optional filtering.
|
|
292
387
|
*/
|
|
@@ -383,6 +478,62 @@ export class SessionDB extends SQLiteBase {
|
|
|
383
478
|
markResumeConsumed(sessionId) {
|
|
384
479
|
this.stmt(S.markResumeConsumed).run(sessionId);
|
|
385
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Return the most recent session_id from session_meta, or null if none.
|
|
483
|
+
* Used by the runtime to attach persistent counters to the right session
|
|
484
|
+
* after a process restart.
|
|
485
|
+
*/
|
|
486
|
+
getLatestSessionId() {
|
|
487
|
+
try {
|
|
488
|
+
const row = this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get();
|
|
489
|
+
return row?.session_id ?? null;
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// ═══════════════════════════════════════════
|
|
496
|
+
// Tool call counters (Bug #1 + #2 — survive restart, --continue, upgrade)
|
|
497
|
+
// ═══════════════════════════════════════════
|
|
498
|
+
/**
|
|
499
|
+
* Increment the persistent tool-call counter for `tool` in `sessionId`.
|
|
500
|
+
* Adds `bytesReturned` to the cumulative total. Idempotent across
|
|
501
|
+
* SessionDB instances — counters survive process restart.
|
|
502
|
+
*/
|
|
503
|
+
incrementToolCall(sessionId, tool, bytesReturned = 0) {
|
|
504
|
+
const safeBytes = Number.isFinite(bytesReturned) && bytesReturned > 0 ? Math.round(bytesReturned) : 0;
|
|
505
|
+
try {
|
|
506
|
+
this.stmt(S.incrementToolCall).run(sessionId, tool, safeBytes);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// best-effort: counter must never throw and break the parent call
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get aggregated tool-call stats for `sessionId`. Returns zero-stats
|
|
514
|
+
* when the session has no recorded calls.
|
|
515
|
+
*/
|
|
516
|
+
getToolCallStats(sessionId) {
|
|
517
|
+
try {
|
|
518
|
+
const totals = this.stmt(S.getToolCallTotals).get(sessionId);
|
|
519
|
+
const rows = this.stmt(S.getToolCallByTool).all(sessionId);
|
|
520
|
+
const byTool = {};
|
|
521
|
+
for (const row of rows) {
|
|
522
|
+
byTool[row.tool] = {
|
|
523
|
+
calls: row.calls,
|
|
524
|
+
bytesReturned: row.bytes_returned,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
totalCalls: totals?.calls ?? 0,
|
|
529
|
+
totalBytesReturned: totals?.bytes_returned ?? 0,
|
|
530
|
+
byTool,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return { totalCalls: 0, totalBytesReturned: 0, byTool: {} };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
386
537
|
// ═══════════════════════════════════════════
|
|
387
538
|
// Lifecycle
|
|
388
539
|
// ═══════════════════════════════════════════
|
package/build/session/extract.js
CHANGED
|
@@ -32,8 +32,23 @@ function extractFileAndRule(input) {
|
|
|
32
32
|
const events = [];
|
|
33
33
|
if (tool_name === "Read") {
|
|
34
34
|
const filePath = String(tool_input["file_path"] ?? "");
|
|
35
|
-
// Rule detection
|
|
36
|
-
|
|
35
|
+
// Rule detection — covers every supported platform's instruction
|
|
36
|
+
// file convention plus per-user memory directories. Hardcoding here
|
|
37
|
+
// (instead of dispatching through the adapter) keeps extract.ts
|
|
38
|
+
// pure / sync / hot-path-safe — the tradeoff is that adding a new
|
|
39
|
+
// platform requires updating this regex.
|
|
40
|
+
//
|
|
41
|
+
// Filenames: CLAUDE.md, AGENTS.md, AGENTS.override.md, GEMINI.md,
|
|
42
|
+
// QWEN.md, KIRO.md, copilot-instructions.md,
|
|
43
|
+
// context-mode.mdc
|
|
44
|
+
// Directories: .claude/, .codex/memories/, .qwen/memory/,
|
|
45
|
+
// .gemini/memory/, .config/<plat>/memory/, .cursor/memory/,
|
|
46
|
+
// .github/memory/, .kiro/memory/, etc.
|
|
47
|
+
const isRuleFile = /(?:CLAUDE|AGENTS(?:\.override)?|GEMINI|QWEN|KIRO)\.md$/i.test(filePath)
|
|
48
|
+
|| /\/copilot-instructions\.md$/i.test(filePath)
|
|
49
|
+
|| /\/context-mode\.mdc$/i.test(filePath)
|
|
50
|
+
|| /\.claude[\\/]/i.test(filePath)
|
|
51
|
+
|| /[\\/]memor(?:y|ies)[\\/][^\\/]+\.md$/i.test(filePath);
|
|
37
52
|
if (isRuleFile) {
|
|
38
53
|
events.push({
|
|
39
54
|
type: "rule",
|
|
@@ -415,6 +430,112 @@ function extractMcp(input) {
|
|
|
415
430
|
priority: 3,
|
|
416
431
|
}];
|
|
417
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Category 27: mcp_tool_call
|
|
435
|
+
* Records the raw MCP call shape (tool_name + tool_input) so analytics
|
|
436
|
+
* can compute usage patterns like batch concurrency.
|
|
437
|
+
*
|
|
438
|
+
* Distinct from `extractMcp` (category "mcp"), which captures the textual
|
|
439
|
+
* call+response for FTS5 search. This emits a structured JSON payload
|
|
440
|
+
* keyed by tool_name + params, capped to ~2KB to keep SQLite rows small.
|
|
441
|
+
*
|
|
442
|
+
* Priority 4 (informational) — should not crowd out high-signal events
|
|
443
|
+
* during FIFO eviction.
|
|
444
|
+
*/
|
|
445
|
+
const MCP_PARAMS_BUDGET_BYTES = 2048;
|
|
446
|
+
/**
|
|
447
|
+
* UTF-8-aware string truncation. Returns the longest prefix of `s` whose
|
|
448
|
+
* UTF-8 byte length is <= `maxBytes`, never landing mid-multibyte-codepoint.
|
|
449
|
+
*
|
|
450
|
+
* Naive `s.slice(0, N)` operates on UTF-16 code units, so a 2KB cap could
|
|
451
|
+
* either over-shoot (multi-byte codepoints occupy fewer code units than
|
|
452
|
+
* bytes — e.g. a chunk of CJK / emoji-heavy JSON would silently exceed
|
|
453
|
+
* the byte budget) or land mid surrogate pair (corrupt JSON downstream).
|
|
454
|
+
*/
|
|
455
|
+
function truncateToBytes(s, maxBytes) {
|
|
456
|
+
if (Buffer.byteLength(s, "utf8") <= maxBytes)
|
|
457
|
+
return { value: s, truncated: false };
|
|
458
|
+
const buf = Buffer.from(s, "utf8");
|
|
459
|
+
// Walk back from maxBytes until the byte starts a fresh codepoint:
|
|
460
|
+
// 0xxxxxxx → ASCII (start)
|
|
461
|
+
// 11xxxxxx → start of multi-byte
|
|
462
|
+
// 10xxxxxx → continuation; keep walking
|
|
463
|
+
let cut = maxBytes;
|
|
464
|
+
while (cut > 0 && (buf[cut] & 0xc0) === 0x80)
|
|
465
|
+
cut--;
|
|
466
|
+
return { value: buf.subarray(0, cut).toString("utf8"), truncated: true };
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Keys whose VALUES must be redacted before persisting tool_input — secrets,
|
|
470
|
+
* tokens, credentials, signatures. Match is on the LAST path segment of the
|
|
471
|
+
* key (case-insensitive substring), so `headers.Authorization`, `auth.token`,
|
|
472
|
+
* `apiKey`, `API_KEY`, `password`, `secret`, `cookie`, `set-cookie`, `signature`,
|
|
473
|
+
* `private_key`, etc. all redact. False-positive risk acceptable — we'd rather
|
|
474
|
+
* over-redact than ship a Bearer token to SQLite.
|
|
475
|
+
*/
|
|
476
|
+
const SECRET_KEY_PATTERN = /(authorization|auth_token|access_token|refresh_token|bearer|token|secret|password|passwd|pwd|api[-_]?key|apikey|cookie|set-cookie|signature|private[-_]?key|client[-_]?secret|x[-_]?api[-_]?key)/i;
|
|
477
|
+
const REDACTED = "[REDACTED]";
|
|
478
|
+
/**
|
|
479
|
+
* Walk an arbitrary JSON-serializable value and return a clone with values
|
|
480
|
+
* redacted under any key matching SECRET_KEY_PATTERN. Cycle-safe.
|
|
481
|
+
*/
|
|
482
|
+
function redactSecrets(value, ancestors = new WeakSet()) {
|
|
483
|
+
if (value == null || typeof value !== "object")
|
|
484
|
+
return value;
|
|
485
|
+
// Path-based ancestor check: only flag TRUE cycles, not DAG / shared refs
|
|
486
|
+
// (e.g., a single `headers` object passed to multiple sub-requests must
|
|
487
|
+
// be processed at every reference site, not flagged as circular).
|
|
488
|
+
if (ancestors.has(value))
|
|
489
|
+
return "[CIRCULAR]";
|
|
490
|
+
ancestors.add(value);
|
|
491
|
+
let out;
|
|
492
|
+
if (Array.isArray(value)) {
|
|
493
|
+
out = value.map((v) => redactSecrets(v, ancestors));
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const obj = {};
|
|
497
|
+
for (const [k, v] of Object.entries(value)) {
|
|
498
|
+
if (SECRET_KEY_PATTERN.test(k)) {
|
|
499
|
+
obj[k] = REDACTED;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
obj[k] = redactSecrets(v, ancestors);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
out = obj;
|
|
506
|
+
}
|
|
507
|
+
ancestors.delete(value); // pop ancestor — siblings can re-visit
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
function extractMcpToolCall(input) {
|
|
511
|
+
const { tool_name, tool_input } = input;
|
|
512
|
+
if (!tool_name.startsWith("mcp__"))
|
|
513
|
+
return [];
|
|
514
|
+
// Redact secrets BEFORE serialization. Any `tool_input` carrying
|
|
515
|
+
// `Authorization: Bearer …`, `api_key: "sk-…"`, cookies, signatures, etc.
|
|
516
|
+
// is masked before it touches SQLite. Over-redaction acceptable — under-
|
|
517
|
+
// redaction is a credential leak to SessionDB.
|
|
518
|
+
const redactedInput = redactSecrets(tool_input ?? {});
|
|
519
|
+
// Serialize the redacted shape, then truncate the *string* (not the object)
|
|
520
|
+
// so the diagnosable shape survives huge payloads.
|
|
521
|
+
let paramsStr;
|
|
522
|
+
try {
|
|
523
|
+
paramsStr = JSON.stringify(redactedInput);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
paramsStr = "{}";
|
|
527
|
+
}
|
|
528
|
+
const { value: cappedStr, truncated } = truncateToBytes(paramsStr, MCP_PARAMS_BUDGET_BYTES);
|
|
529
|
+
const payload = truncated
|
|
530
|
+
? `{"tool_name":${JSON.stringify(tool_name)},"params_raw":${JSON.stringify(cappedStr)},"truncated":true}`
|
|
531
|
+
: `{"tool_name":${JSON.stringify(tool_name)},"params":${cappedStr}}`;
|
|
532
|
+
return [{
|
|
533
|
+
type: "mcp_tool_call",
|
|
534
|
+
category: "mcp_tool_call",
|
|
535
|
+
data: safeString(payload),
|
|
536
|
+
priority: 4,
|
|
537
|
+
}];
|
|
538
|
+
}
|
|
418
539
|
/**
|
|
419
540
|
* Category 6 (tool-based): decision
|
|
420
541
|
* AskUserQuestion tool — tracks questions posed to user and their answers.
|
|
@@ -751,6 +872,7 @@ export function extractEvents(input) {
|
|
|
751
872
|
events.push(...extractSkill(input));
|
|
752
873
|
events.push(...extractSubagent(input));
|
|
753
874
|
events.push(...extractMcp(input));
|
|
875
|
+
events.push(...extractMcpToolCall(input));
|
|
754
876
|
events.push(...extractDecision(input));
|
|
755
877
|
events.push(...extractConstraint(input));
|
|
756
878
|
events.push(...extractWorktree(input));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const TOOL_PREFIXES = {
|
|
2
|
+
"claude-code": (tool) => `mcp__plugin_context-mode_context-mode__${tool}`,
|
|
3
|
+
"gemini-cli": (tool) => `mcp__context-mode__${tool}`,
|
|
4
|
+
"antigravity": (tool) => `mcp__context-mode__${tool}`,
|
|
5
|
+
"opencode": (tool) => `context-mode_${tool}`,
|
|
6
|
+
"kilo": (tool) => `context-mode_${tool}`,
|
|
7
|
+
"vscode-copilot": (tool) => `context-mode_${tool}`,
|
|
8
|
+
"jetbrains-copilot": (tool) => `context-mode_${tool}`,
|
|
9
|
+
"kiro": (tool) => `@context-mode/${tool}`,
|
|
10
|
+
"zed": (tool) => `mcp:context-mode:${tool}`,
|
|
11
|
+
"cursor": (tool) => tool,
|
|
12
|
+
"codex": (tool) => tool,
|
|
13
|
+
"openclaw": (tool) => tool,
|
|
14
|
+
"pi": (tool) => tool,
|
|
15
|
+
"qwen-code": (tool) => `mcp__context-mode__${tool}`,
|
|
16
|
+
};
|
|
17
|
+
export function getToolName(platform, bareTool) {
|
|
18
|
+
const fn = TOOL_PREFIXES[platform] || TOOL_PREFIXES["claude-code"];
|
|
19
|
+
return fn(bareTool);
|
|
20
|
+
}
|
|
21
|
+
export function createToolNamer(platform) {
|
|
22
|
+
return (bareTool) => getToolName(platform, bareTool);
|
|
23
|
+
}
|
|
24
|
+
export const KNOWN_PLATFORMS = Object.keys(TOOL_PREFIXES);
|