context-mode 1.0.101 → 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.
Files changed (98) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +66 -5
  6. package/bin/statusline.mjs +321 -0
  7. package/build/adapters/antigravity/index.d.ts +6 -0
  8. package/build/adapters/antigravity/index.js +10 -0
  9. package/build/adapters/base.d.ts +23 -0
  10. package/build/adapters/base.js +29 -0
  11. package/build/adapters/codex/index.d.ts +10 -0
  12. package/build/adapters/codex/index.js +22 -4
  13. package/build/adapters/cursor/index.d.ts +7 -0
  14. package/build/adapters/cursor/index.js +11 -0
  15. package/build/adapters/detect.d.ts +12 -1
  16. package/build/adapters/detect.js +69 -7
  17. package/build/adapters/gemini-cli/index.d.ts +8 -1
  18. package/build/adapters/gemini-cli/index.js +19 -7
  19. package/build/adapters/jetbrains-copilot/index.d.ts +7 -0
  20. package/build/adapters/jetbrains-copilot/index.js +12 -0
  21. package/build/adapters/kiro/index.d.ts +8 -0
  22. package/build/adapters/kiro/index.js +12 -0
  23. package/build/adapters/openclaw/index.d.ts +17 -0
  24. package/build/adapters/openclaw/index.js +29 -4
  25. package/build/adapters/opencode/index.d.ts +8 -0
  26. package/build/adapters/opencode/index.js +18 -6
  27. package/build/adapters/qwen-code/index.d.ts +1 -0
  28. package/build/adapters/qwen-code/index.js +3 -0
  29. package/build/adapters/types.d.ts +33 -0
  30. package/build/adapters/vscode-copilot/index.d.ts +6 -0
  31. package/build/adapters/vscode-copilot/index.js +10 -0
  32. package/build/adapters/zed/index.d.ts +1 -0
  33. package/build/adapters/zed/index.js +3 -0
  34. package/build/cli.d.ts +15 -0
  35. package/build/cli.js +62 -16
  36. package/build/concurrency/runPool.d.ts +36 -0
  37. package/build/concurrency/runPool.js +51 -0
  38. package/build/executor.d.ts +11 -1
  39. package/build/executor.js +59 -16
  40. package/build/fetch-cache.d.ts +13 -0
  41. package/build/fetch-cache.js +15 -0
  42. package/build/lifecycle.d.ts +6 -2
  43. package/build/lifecycle.js +29 -2
  44. package/build/opencode-plugin.d.ts +6 -0
  45. package/build/opencode-plugin.js +60 -1
  46. package/build/routing-block.d.ts +8 -0
  47. package/build/routing-block.js +86 -0
  48. package/build/runtime.d.ts +1 -0
  49. package/build/runtime.js +54 -3
  50. package/build/search/auto-memory.d.ts +23 -10
  51. package/build/search/auto-memory.js +64 -26
  52. package/build/search/unified.d.ts +3 -0
  53. package/build/search/unified.js +2 -2
  54. package/build/server.d.ts +42 -0
  55. package/build/server.js +693 -164
  56. package/build/session/analytics.d.ts +49 -1
  57. package/build/session/analytics.js +278 -16
  58. package/build/session/db.d.ts +39 -8
  59. package/build/session/db.js +170 -19
  60. package/build/session/extract.js +124 -2
  61. package/build/tool-naming.d.ts +4 -0
  62. package/build/tool-naming.js +24 -0
  63. package/cli.bundle.mjs +201 -159
  64. package/configs/antigravity/GEMINI.md +11 -0
  65. package/configs/claude-code/CLAUDE.md +11 -0
  66. package/configs/codex/AGENTS.md +11 -0
  67. package/configs/cursor/context-mode.mdc +11 -0
  68. package/configs/gemini-cli/GEMINI.md +11 -0
  69. package/configs/jetbrains-copilot/copilot-instructions.md +3 -0
  70. package/configs/kilo/AGENTS.md +11 -0
  71. package/configs/kiro/KIRO.md +11 -0
  72. package/configs/openclaw/AGENTS.md +11 -0
  73. package/configs/opencode/AGENTS.md +11 -0
  74. package/configs/pi/AGENTS.md +11 -0
  75. package/configs/qwen-code/QWEN.md +11 -0
  76. package/configs/vscode-copilot/copilot-instructions.md +3 -0
  77. package/configs/zed/AGENTS.md +11 -0
  78. package/hooks/auto-injection.mjs +36 -10
  79. package/hooks/cache-heal-utils.mjs +231 -0
  80. package/hooks/codex/sessionstart.mjs +7 -4
  81. package/hooks/core/routing.mjs +5 -0
  82. package/hooks/cursor/sessionstart.mjs +7 -4
  83. package/hooks/formatters/claude-code.mjs +20 -0
  84. package/hooks/gemini-cli/sessionstart.mjs +7 -2
  85. package/hooks/jetbrains-copilot/sessionstart.mjs +7 -2
  86. package/hooks/normalize-hooks.mjs +184 -0
  87. package/hooks/session-db.bundle.mjs +33 -14
  88. package/hooks/session-extract.bundle.mjs +2 -2
  89. package/hooks/session-helpers.mjs +68 -20
  90. package/hooks/session-loaders.mjs +8 -2
  91. package/hooks/sessionstart.mjs +8 -2
  92. package/hooks/vscode-copilot/sessionstart.mjs +7 -2
  93. package/insight/src/routes/index.tsx +1 -1
  94. package/openclaw.plugin.json +1 -1
  95. package/package.json +2 -1
  96. package/server.bundle.mjs +164 -125
  97. package/skills/ctx-insight/SKILL.md +1 -1
  98. package/start.mjs +63 -3
@@ -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
- return envSuffix ? `__${envSuffix}` : "";
35
+ suffix = envSuffix ? `__${envSuffix}` : "";
26
36
  }
27
- try {
28
- const cwd = process.cwd();
29
- const mainWorktree = execFileSync("git", ["worktree", "list", "--porcelain"], {
30
- encoding: "utf-8",
31
- timeout: 2000,
32
- stdio: ["ignore", "pipe", "ignore"],
33
- })
34
- .split(/\r?\n/)
35
- .find((l) => l.startsWith("worktree "))
36
- ?.replace("worktree ", "")
37
- ?.trim();
38
- if (mainWorktree && cwd !== mainWorktree) {
39
- const suffix = createHash("sha256").update(cwd).digest("hex").slice(0, 8);
40
- return `__${suffix}`;
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
- catch {
44
- // git not available or not a git repo — no suffix
45
- }
46
- return "";
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
  // ═══════════════════════════════════════════
@@ -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: CLAUDE.md or anything inside a .claude/ directory
36
- const isRuleFile = /CLAUDE\.md$|\.claude[\\/]/i.test(filePath);
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,4 @@
1
+ export declare function getToolName(platform: string, bareTool: string): string;
2
+ export type ToolNamer = (bareTool: string) => string;
3
+ export declare function createToolNamer(platform: string): ToolNamer;
4
+ export declare const KNOWN_PLATFORMS: string[];
@@ -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);