mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -61,6 +61,90 @@ function _maskJsRegexLiteral(src, out, start) {
61
61
  return j;
62
62
  }
63
63
 
64
+ // Interpolation dialect of a string literal, decided when the literal opens:
65
+ // 'dollar-brace' — `${expr}` (JS/TS backticks, kotlin "…")
66
+ // 'brace' — `{expr}` (python f-strings, C# $"…", `{{` = literal)
67
+ // 'bash' — `$(cmd)` / `${var}` inside double quotes
68
+ // null = the whole literal is opaque text. Without this only JS backticks had
69
+ // their interpolations analysed, so calls inside python/C#/kotlin/bash
70
+ // interpolation were masked away entirely.
71
+ function _stringLiteralPrefix(src, quoteIndex, allowed) {
72
+ let k = quoteIndex - 1;
73
+ let prefix = '';
74
+ while (k >= 0 && allowed.test(src[k]) && prefix.length < 2) {
75
+ prefix = src[k] + prefix;
76
+ k -= 1;
77
+ }
78
+ // A prefix glued to a longer identifier is not a string prefix.
79
+ if (prefix && k >= 0 && _isWordChar(src[k])) return '';
80
+ return prefix;
81
+ }
82
+
83
+ function _stringInterpKind(lang, delim, src, quoteIndex) {
84
+ const triple = delim === '"""' || delim === "'''";
85
+ if (delim === '`') return _isJsLike(lang) ? 'dollar-brace' : null;
86
+ // Kotlin templates work the same in "…" and in """…""" raw strings.
87
+ if (lang === 'kotlin' && (delim === '"' || triple)) return 'dollar-brace';
88
+ if (lang === 'bash' && delim === '"') return 'bash';
89
+ if (lang === 'python' && (triple || delim === '"' || delim === '\'')) {
90
+ const prefix = _stringLiteralPrefix(src, quoteIndex, /[fFrRbBuU]/);
91
+ return /[fF]/.test(prefix) ? 'brace' : null;
92
+ }
93
+ if (lang === 'csharp' && (delim === '"' || triple)) {
94
+ const prefix = _stringLiteralPrefix(src, quoteIndex, /[$@]/);
95
+ return prefix.includes('$') ? 'brace' : null;
96
+ }
97
+ return null;
98
+ }
99
+
100
+ // Interpolation handling shared by single- and triple-quoted frames. Returns
101
+ // the next index when it consumed bytes, or -1 when the character is ordinary
102
+ // string text.
103
+ function _handleInterpolationInString(src, out, i, frame, stack) {
104
+ const kind = frame?.interp;
105
+ if (!kind) return -1;
106
+ if (kind === 'brace' && src[i] === '{' && src[i + 1] === '{') {
107
+ // `{{` is an escaped literal brace inside an f-string / $"…".
108
+ if (src[i] !== '\n') out[i] = ' ';
109
+ if (src[i + 1] !== '\n') out[i + 1] = ' ';
110
+ return i + 2;
111
+ }
112
+ const opened = _interpolationOpenerAt(src, i, kind);
113
+ if (opened) {
114
+ // Enter interpolation: the expression bytes stay intact.
115
+ stack.push({ kind: 'interp', depth: 1, open: opened.open, close: opened.close });
116
+ return i + opened.length;
117
+ }
118
+ // Bare `$name` (kotlin template, bash expansion) is a real reference, so its
119
+ // identifier bytes stay visible; only the `$` is masked. JS template literals
120
+ // have no bare form — `$foo` there is literal text.
121
+ if ((kind === 'bash' || (kind === 'dollar-brace' && frame.lang === 'kotlin'))
122
+ && src[i] === '$' && _isWordStartChar(src[i + 1])) {
123
+ if (src[i] !== '\n') out[i] = ' ';
124
+ let j = i + 1;
125
+ while (j < src.length && _isWordChar(src[j])) j += 1;
126
+ return j;
127
+ }
128
+ return -1;
129
+ }
130
+
131
+ // Interpolation opener at `i` for a frame of the given dialect.
132
+ function _interpolationOpenerAt(src, i, kind) {
133
+ if (kind === 'dollar-brace') {
134
+ return src.startsWith('${', i) ? { length: 2, open: '{', close: '}' } : null;
135
+ }
136
+ if (kind === 'brace') {
137
+ // `{{` is an escaped literal brace — handled by the caller.
138
+ if (src[i] !== '{' || src[i + 1] === '{') return null;
139
+ return { length: 1, open: '{', close: '}' };
140
+ }
141
+ if (kind === 'bash') {
142
+ if (src.startsWith('$(', i)) return { length: 2, open: '(', close: ')' };
143
+ if (src.startsWith('${', i)) return { length: 2, open: '{', close: '}' };
144
+ }
145
+ return null;
146
+ }
147
+
64
148
  export function _maskNonCodeText(text, lang) {
65
149
  const src = String(text || '');
66
150
  const out = src.split('');
@@ -69,9 +153,11 @@ export function _maskNonCodeText(text, lang) {
69
153
  // Stack of scanner frames. Top describes current state:
70
154
  // { kind: 'string', delim } — inside single-line string literal (mask body)
71
155
  // { kind: 'triple', delim } — inside triple-quote string (mask body)
72
- // { kind: 'interp', braceDepth } — inside backtick `${...}` interpolation
73
- // (code mode; bytes preserved so callers
74
- // analysis can see fn-calls inside)
156
+ // { kind: 'interp', depth, open, close }
157
+ // — inside a string interpolation
158
+ // (`${…}`, f-string `{…}`, bash `$( … )`;
159
+ // code mode, bytes preserved so caller
160
+ // analysis sees fn-calls inside)
75
161
  // Empty stack = top-level code.
76
162
  const stack = [];
77
163
  const top = () => (stack.length ? stack[stack.length - 1] : null);
@@ -104,6 +190,13 @@ export function _maskNonCodeText(text, lang) {
104
190
  prevToken = 'value';
105
191
  continue;
106
192
  }
193
+ // Triple-quoted literals interpolate too (python f"""…""", kotlin """…""").
194
+ const nextIndex = _handleInterpolationInString(src, out, i, t, stack);
195
+ if (nextIndex >= 0) {
196
+ i = nextIndex;
197
+ prevToken = 'expr';
198
+ continue;
199
+ }
107
200
  if (src[i] !== '\n') out[i] = ' ';
108
201
  i++;
109
202
  continue;
@@ -127,12 +220,13 @@ export function _maskNonCodeText(text, lang) {
127
220
  }
128
221
  if (t && t.kind === 'string') {
129
222
  const d = t.delim;
130
- if (d === '`' && src.startsWith('${', i)) {
131
- // Enter interpolation. `${` itself is code-relevant — leave bytes intact.
132
- stack.push({ kind: 'interp', braceDepth: 1 });
133
- i += 2;
134
- prevToken = 'expr';
135
- continue;
223
+ {
224
+ const nextIndex = _handleInterpolationInString(src, out, i, t, stack);
225
+ if (nextIndex >= 0) {
226
+ i = nextIndex;
227
+ prevToken = 'expr';
228
+ continue;
229
+ }
136
230
  }
137
231
  // In bash single-quotes `'...'`, backslash is literal (no escape) — the
138
232
  // string closes at the first `'`. Skip the escape consumption there so
@@ -165,23 +259,19 @@ export function _maskNonCodeText(text, lang) {
165
259
  continue;
166
260
  }
167
261
  if (t && t.kind === 'interp') {
168
- // Code mode inside `${...}`. Bytes preserved; track brace depth and
169
- // nested constructs so masking resumes once interpolation closes.
170
- if (src[i] === '{') {
171
- t.braceDepth++;
262
+ // Code mode inside the interpolation. Bytes preserved; track the frame's
263
+ // own delimiter depth so masking resumes once the expression closes.
264
+ if (src[i] === t.open) {
265
+ t.depth++;
172
266
  prevToken = 'expr';
173
267
  i++;
174
268
  continue;
175
269
  }
176
- if (src[i] === '}') {
177
- t.braceDepth--;
270
+ if (src[i] === t.close) {
271
+ t.depth--;
178
272
  i++;
179
- if (t.braceDepth === 0) {
180
- stack.pop();
181
- prevToken = 'value';
182
- } else {
183
- prevToken = 'value';
184
- }
273
+ if (t.depth === 0) stack.pop();
274
+ prevToken = 'value';
185
275
  continue;
186
276
  }
187
277
  if (_supportsSlashComments(lang) && src.startsWith('/*', i)) {
@@ -205,7 +295,7 @@ export function _maskNonCodeText(text, lang) {
205
295
  }
206
296
  if (src[i] === '"' || (_supportsSingleQuoteStrings(lang) && src[i] === '\'') || (_supportsBacktickStrings(lang) && src[i] === '`')) {
207
297
  if (src[i] !== '\n') out[i] = ' ';
208
- stack.push({ kind: 'string', delim: src[i], lang });
298
+ stack.push({ kind: 'string', delim: src[i], lang, interp: _stringInterpKind(lang, src[i], src, i) });
209
299
  i++;
210
300
  continue;
211
301
  }
@@ -298,7 +388,7 @@ export function _maskNonCodeText(text, lang) {
298
388
  if (i + 1 < out.length) out[i + 1] = ' ';
299
389
  if (i + 2 < out.length) out[i + 2] = ' ';
300
390
  i += 3;
301
- stack.push({ kind: 'triple', delim: "'''" });
391
+ stack.push({ kind: 'triple', delim: "'''", lang, interp: _stringInterpKind(lang, "'''", src, i - 3) });
302
392
  continue;
303
393
  }
304
394
  if (_supportsTripleDoubleQuoteStrings(lang) && src.startsWith('"""', i)) {
@@ -306,7 +396,7 @@ export function _maskNonCodeText(text, lang) {
306
396
  if (i + 1 < out.length) out[i + 1] = ' ';
307
397
  if (i + 2 < out.length) out[i + 2] = ' ';
308
398
  i += 3;
309
- stack.push({ kind: 'triple', delim: '"""' });
399
+ stack.push({ kind: 'triple', delim: '"""', lang, interp: _stringInterpKind(lang, '"""', src, i - 3) });
310
400
  continue;
311
401
  }
312
402
  if (src[i] === '/' && _isJsLike(lang) && prevToken === 'expr') {
@@ -316,7 +406,7 @@ export function _maskNonCodeText(text, lang) {
316
406
  }
317
407
  if (src[i] === '"' || (_supportsSingleQuoteStrings(lang) && src[i] === '\'') || (_supportsBacktickStrings(lang) && src[i] === '`')) {
318
408
  if (src[i] !== '\n') out[i] = ' ';
319
- stack.push({ kind: 'string', delim: src[i], lang });
409
+ stack.push({ kind: 'string', delim: src[i], lang, interp: _stringInterpKind(lang, src[i], src, i) });
320
410
  i++;
321
411
  continue;
322
412
  }
@@ -0,0 +1,268 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { _inferSpanEndByIndent } from './span.mjs';
5
+ import { _maskNonCodeText } from './text-mask.mjs';
6
+ import { _atJsRegexPosition, _extractCallees, _jsBraceKindAt } from './search.mjs';
7
+
8
+ test('indentation span inference keeps a multiline arrow signature whole', () => {
9
+ const lines = [
10
+ 'const handler = (',
11
+ ' a,',
12
+ ' b,',
13
+ ') => {',
14
+ ' return a + b;',
15
+ '};',
16
+ ];
17
+ assert.equal(_inferSpanEndByIndent(lines, 1), 6);
18
+ });
19
+
20
+ test('indentation span inference still ends at a plain matching closer', () => {
21
+ const lines = [
22
+ 'const rows = [',
23
+ ' 1,',
24
+ ' 2,',
25
+ '];',
26
+ 'const other = 3;',
27
+ ];
28
+ assert.equal(_inferSpanEndByIndent(lines, 1), 4);
29
+ });
30
+
31
+ test('python f-string interpolation keeps its call visible', () => {
32
+ const masked = _maskNonCodeText('x = f"total {compute_total(rows)} done"\n', 'python');
33
+ assert.match(masked, /compute_total\(rows\)/);
34
+ assert.doesNotMatch(masked, /done/);
35
+ });
36
+
37
+ test('C# interpolated string exposes the call and keeps {{ }} literal', () => {
38
+ const masked = _maskNonCodeText('var s = $"{Compute(x)} {{literal}}";\n', 'csharp');
39
+ assert.match(masked, /Compute\(x\)/);
40
+ assert.doesNotMatch(masked, /literal/);
41
+ });
42
+
43
+ test('kotlin string template exposes the call', () => {
44
+ const masked = _maskNonCodeText('val s = "n=${computeTotal(rows)}"\n', 'kotlin');
45
+ assert.match(masked, /computeTotal\(rows\)/);
46
+ });
47
+
48
+ test('bash command substitution inside double quotes stays code', () => {
49
+ const masked = _maskNonCodeText('echo "value $(compute_total rows) done"\n', 'bash');
50
+ assert.match(masked, /compute_total rows/);
51
+ assert.doesNotMatch(masked, /value/);
52
+ });
53
+
54
+ test('a plain python string is still fully masked', () => {
55
+ const masked = _maskNonCodeText('x = "total {compute_total(rows)} done"\n', 'python');
56
+ assert.doesNotMatch(masked, /compute_total/);
57
+ });
58
+
59
+ test('a python triple-quoted f-string interpolates', () => {
60
+ const masked = _maskNonCodeText('x = f"""head {compute(rows)} tail"""\n', 'python');
61
+ assert.match(masked, /compute\(rows\)/);
62
+ assert.doesNotMatch(masked, /head/);
63
+ });
64
+
65
+ test('a kotlin raw string template interpolates', () => {
66
+ const masked = _maskNonCodeText('val s = """head ${compute(rows)} tail"""\n', 'kotlin');
67
+ assert.match(masked, /compute\(rows\)/);
68
+ assert.doesNotMatch(masked, /head/);
69
+ });
70
+
71
+ test('a kotlin bare $name keeps the identifier but a JS template does not', () => {
72
+ const kotlin = _maskNonCodeText('val s = "id=$userId done"\n', 'kotlin');
73
+ assert.match(kotlin, /userId/);
74
+ assert.doesNotMatch(kotlin, /done/);
75
+
76
+ // JS template literals have no bare form: `$userId` is literal text.
77
+ const js = _maskNonCodeText('const s = `id=$userId`;\n', 'javascript');
78
+ assert.doesNotMatch(js, /userId/);
79
+ });
80
+
81
+ test('a non-matching same-indent closer does not end the span', () => {
82
+ const lines = [
83
+ 'const handler = {',
84
+ ' run: (',
85
+ ' ) => 1,',
86
+ ')',
87
+ '};',
88
+ ];
89
+ assert.equal(_inferSpanEndByIndent(lines, 1), 5);
90
+ });
91
+
92
+ test('a division after an object literal is not read as a regex', () => {
93
+ const text = 'function f(){ const x = {} / 2; } outside();';
94
+ assert.equal(_atJsRegexPosition(text, text.indexOf('/ 2')), false);
95
+ });
96
+
97
+ test('a statement-position regex after a closing brace is recognized', () => {
98
+ const text = '}\n/re/.test(x)';
99
+ assert.equal(_atJsRegexPosition(text, text.indexOf('/re/')), true);
100
+ });
101
+
102
+ test('a postfix increment before a slash stays division', () => {
103
+ const text = 'const y = a++ / b;';
104
+ assert.equal(_atJsRegexPosition(text, text.indexOf('/ b')), false);
105
+ });
106
+
107
+ test('brace kinds separate object literals from blocks', () => {
108
+ assert.equal(_jsBraceKindAt('const x = {}', 10), 'object');
109
+ assert.equal(_jsBraceKindAt('call({})', 5), 'object');
110
+ assert.equal(_jsBraceKindAt('return {}', 7), 'object');
111
+ // Property value: the scanner passes the ENCLOSING brace kind, which is
112
+ // what separates `{ key: {…} }` from a labeled block.
113
+ assert.equal(_jsBraceKindAt('const y = { a: {} }', 15, null, 'object'), 'object');
114
+ // Grammatical position, not a character list: unary `!` and binary `-`.
115
+ assert.equal(_jsBraceKindAt('!{}', 1), 'object');
116
+ assert.equal(_jsBraceKindAt('1 - {}', 4), 'object');
117
+ assert.equal(_jsBraceKindAt('function f() {}', 13), 'block');
118
+ assert.equal(_jsBraceKindAt('const f = () => {}', 16), 'block');
119
+ assert.equal(_jsBraceKindAt('if (x) {}', 7), 'block');
120
+ assert.equal(_jsBraceKindAt('do {} while (x)', 3), 'block');
121
+ assert.equal(_jsBraceKindAt('if (x) {} else {}', 15), 'block');
122
+ });
123
+
124
+ test('object-literal divisions after ! and - do not swallow the rest of the file', () => {
125
+ for (const body of ['!{} / 2;', '1 - {} / 2;']) {
126
+ const rel = 'probe-op.mjs';
127
+ const text = `function f(){ ${body} } outside();\n`;
128
+ const node = {
129
+ rel,
130
+ abs: rel,
131
+ lang: 'javascript',
132
+ fingerprint: '',
133
+ symbols: [],
134
+ topLevelTypes: [],
135
+ resolvedImports: [],
136
+ rawImports: [],
137
+ };
138
+ const graph = {
139
+ nodes: new Map([[rel, node]]),
140
+ reverse: new Map(),
141
+ _sourceTextCache: new Map([[rel, { fingerprint: '', text }]]),
142
+ _sourceLinesCache: new Map(),
143
+ _maskedLinesCache: new Map(),
144
+ };
145
+ const rows = _extractCallees(
146
+ graph,
147
+ { rel, lang: 'javascript', line: 1, col: 1, declarationLike: true },
148
+ '.',
149
+ { callerSymbol: 'f' },
150
+ );
151
+ assert.deepEqual(rows.map((row) => row.name), [], `body: ${body}`);
152
+ }
153
+ });
154
+
155
+ test('a labeled block is a block, an object property and a ternary are values', () => {
156
+ assert.equal(_jsBraceKindAt('label: {}', 7, null, 'block'), 'block');
157
+ assert.equal(_jsBraceKindAt('({ key: {} })', 8, null, 'object'), 'object');
158
+ assert.equal(_jsBraceKindAt('const v = c ? a : {};', 18, null, 'block'), 'object');
159
+ });
160
+
161
+ test('a case label with a ternary expression still opens a block', () => {
162
+ const src = 'switch (v) { case c ? a : b: {} }';
163
+ assert.equal(_jsBraceKindAt(src, src.indexOf('{}'), null, 'block'), 'block');
164
+ const plain = 'switch (v) { case 1: {} }';
165
+ assert.equal(_jsBraceKindAt(plain, plain.indexOf('{}'), null, 'block'), 'block');
166
+ const ternary = 'const v = c ? a : {};';
167
+ assert.equal(_jsBraceKindAt(ternary, ternary.indexOf('{}'), null, 'block'), 'object');
168
+ });
169
+
170
+ test('a case-label block keeps the callee behind it', () => {
171
+ const rel = 'probe-case.mjs';
172
+ const text = 'function f(){ switch (v) { case c ? a : b: {} /[}]/.test(x); tail(); } }\n';
173
+ const node = {
174
+ rel,
175
+ abs: rel,
176
+ lang: 'javascript',
177
+ fingerprint: '',
178
+ symbols: [],
179
+ topLevelTypes: [],
180
+ resolvedImports: [],
181
+ rawImports: [],
182
+ };
183
+ const graph = {
184
+ nodes: new Map([[rel, node]]),
185
+ reverse: new Map(),
186
+ _sourceTextCache: new Map([[rel, { fingerprint: '', text }]]),
187
+ _sourceLinesCache: new Map(),
188
+ _maskedLinesCache: new Map(),
189
+ };
190
+
191
+ const names = _extractCallees(
192
+ graph,
193
+ { rel, lang: 'javascript', line: 1, col: 1, declarationLike: true },
194
+ '.',
195
+ { callerSymbol: 'f' },
196
+ ).map((row) => row.name);
197
+ assert.ok(names.includes('tail'), `expected 'tail' in ${JSON.stringify(names)}`);
198
+ });
199
+
200
+ test('a labeled block keeps the regex after it and the calls behind it', () => {
201
+ const rel = 'probe-label.mjs';
202
+ const text = 'function f(){ label: {} /[}]/.test(x); inside(); }\n';
203
+ const node = {
204
+ rel,
205
+ abs: rel,
206
+ lang: 'javascript',
207
+ fingerprint: '',
208
+ symbols: [],
209
+ topLevelTypes: [],
210
+ resolvedImports: [],
211
+ rawImports: [],
212
+ };
213
+ const graph = {
214
+ nodes: new Map([[rel, node]]),
215
+ reverse: new Map(),
216
+ _sourceTextCache: new Map([[rel, { fingerprint: '', text }]]),
217
+ _sourceLinesCache: new Map(),
218
+ _maskedLinesCache: new Map(),
219
+ };
220
+
221
+ const rows = _extractCallees(
222
+ graph,
223
+ { rel, lang: 'javascript', line: 1, col: 1, declarationLike: true },
224
+ '.',
225
+ { callerSymbol: 'f' },
226
+ );
227
+ const names = rows.map((row) => row.name);
228
+ assert.ok(names.includes('test'), `expected 'test' in ${JSON.stringify(names)}`);
229
+ assert.ok(names.includes('inside'), `expected 'inside' in ${JSON.stringify(names)}`);
230
+ });
231
+
232
+ test('a multiline object-literal division does not swallow the rest of the file', () => {
233
+ // Probe: the `/` sits on its own line after `{}`. A line-break heuristic
234
+ // reads it as a regex, runs past the closing brace and attributes the
235
+ // following top-level call as a callee of f.
236
+ const rel = 'probe.mjs';
237
+ const text = 'function f(){ const x = {}\n/ 2; } outside();\n';
238
+ const node = {
239
+ rel,
240
+ abs: rel,
241
+ lang: 'javascript',
242
+ fingerprint: '',
243
+ symbols: [],
244
+ topLevelTypes: [],
245
+ resolvedImports: [],
246
+ rawImports: [],
247
+ };
248
+ const graph = {
249
+ nodes: new Map([[rel, node]]),
250
+ reverse: new Map(),
251
+ _sourceTextCache: new Map([[rel, { fingerprint: '', text }]]),
252
+ _sourceLinesCache: new Map(),
253
+ _maskedLinesCache: new Map(),
254
+ };
255
+
256
+ // The discriminator: a bare line break says "regex", the tracked brace
257
+ // kind says "division". The scanner uses the tracked kind.
258
+ assert.equal(_atJsRegexPosition(text, text.indexOf('/ 2')), true);
259
+ assert.equal(_atJsRegexPosition(text, text.indexOf('/ 2'), 'object'), false);
260
+
261
+ const rows = _extractCallees(
262
+ graph,
263
+ { rel, lang: 'javascript', line: 1, col: 1, declarationLike: true },
264
+ '.',
265
+ { callerSymbol: 'f' },
266
+ );
267
+ assert.deepEqual(rows.map((row) => row.name), []);
268
+ });
@@ -3,12 +3,12 @@ export const CODE_GRAPH_TOOL_DEFS = [
3
3
  name: 'code_graph',
4
4
  title: 'Code Graph',
5
5
  annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
6
- description: 'Read-only; safe to batch in parallel. Source-file structure, symbol relations, and flow. Exact identifiers route directly to find_symbol/references/callers/callees; symbol-name keywords use symbol_search/search. Text, literals, and regex belong to grep. File modes use files[]; symbol modes use symbols[]. overview and mode:symbols return file summaries. find_symbol returns declaration/body; references returns declaration/usages plus optional body; callers/callees return locations.',
6
+ description: 'Read-only; safe to batch in parallel. Source-file structure, symbol relations, and flow. mode:symbols with files[] is the cheap direct outline of known source files: declarations with line numbers, no file body. Exact identifiers route directly to find_symbol/references/callers/callees; symbol-name keywords use symbol_search/search. Text, literals, and regex belong to grep. File modes use files[]; symbol modes use symbols[]. find_symbol returns declaration/body; references returns declaration/usages plus optional body; callers/callees return locations.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
10
- mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'File modes: overview, imports, dependents, related, and impact. symbols with files[] returns an optionally filtered file outline; remaining modes use symbols[].' },
11
- files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Project-relative source path(s); required by file modes, optional to scope symbol modes.' },
10
+ mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'File modes: overview, imports, dependents, related, and impact. symbols with files[] returns an optionally filtered file outline of those exact files. It needs no prior search; remaining modes use symbols[].' },
11
+ files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Source path(s), project-relative or absolute; required by file modes, optional to scope symbol modes.' },
12
12
  symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Exact identifiers or symbol-name keywords; batch in one symbols[] call; required by symbol modes, optional filter for mode:symbols.' },
13
13
  body: { type: 'boolean', description: 'Include declaration body: find_symbol defaults true; references is opt-in.' },
14
14
  limit: { type: 'integer', minimum: 1, maximum: 500, description: 'Maximum results; modes may enforce lower caps.' },