@daniel156161/prism 0.2.91 → 0.2.96

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 (59) hide show
  1. package/README.md +0 -50
  2. package/dist/pi/pi-context-files.d.ts +6 -0
  3. package/dist/pi/pi-context-files.js +17 -0
  4. package/dist/pi/pi-context-files.js.map +1 -0
  5. package/dist/pi/pi-extensions.d.ts +0 -1
  6. package/dist/pi/pi-extensions.js +7 -22
  7. package/dist/pi/pi-extensions.js.map +1 -1
  8. package/dist/pi/pi-npm-extension-entry.d.ts +2 -0
  9. package/dist/pi/pi-npm-extension-entry.js +74 -0
  10. package/dist/pi/pi-npm-extension-entry.js.map +1 -0
  11. package/dist/pi/pi-package.js +4 -0
  12. package/dist/pi/pi-package.js.map +1 -1
  13. package/dist/pi/pi-patch-context-files.d.ts +12 -0
  14. package/dist/pi/pi-patch-context-files.js +46 -0
  15. package/dist/pi/pi-patch-context-files.js.map +1 -0
  16. package/dist/pi/pi-patch-interactive.js +5 -1
  17. package/dist/pi/pi-patch-interactive.js.map +1 -1
  18. package/dist/pi/plan-mode-tools.d.ts +13 -0
  19. package/dist/pi/plan-mode-tools.js +54 -0
  20. package/dist/pi/plan-mode-tools.js.map +1 -0
  21. package/dist/prism-extensions/core/mex-anchor.d.ts +9 -0
  22. package/dist/prism-extensions/core/mex-anchor.js +29 -0
  23. package/dist/prism-extensions/core/mex-anchor.js.map +1 -0
  24. package/dist/prism-extensions/core/mex-cli.d.ts +68 -0
  25. package/dist/prism-extensions/core/mex-cli.js +183 -0
  26. package/dist/prism-extensions/core/mex-cli.js.map +1 -0
  27. package/dist/prism-extensions/core/mex-graph-db.d.ts +26 -0
  28. package/dist/prism-extensions/core/mex-graph-db.js +69 -0
  29. package/dist/prism-extensions/core/mex-graph-db.js.map +1 -0
  30. package/dist/prism-extensions/integrations/ai-memory-system.js +27 -6
  31. package/dist/prism-extensions/integrations/ai-memory-system.js.map +1 -1
  32. package/dist/prism-extensions/integrations/mex-memory.d.ts +21 -0
  33. package/dist/prism-extensions/integrations/mex-memory.js +231 -0
  34. package/dist/prism-extensions/integrations/mex-memory.js.map +1 -0
  35. package/dist/prism-extensions/tools/builtin-tools.js +2 -0
  36. package/dist/prism-extensions/tools/builtin-tools.js.map +1 -1
  37. package/dist/prism-extensions/tools/mex-tools.d.ts +2 -0
  38. package/dist/prism-extensions/tools/mex-tools.js +78 -0
  39. package/dist/prism-extensions/tools/mex-tools.js.map +1 -0
  40. package/dist/prism-extensions/ui/toolbar.d.ts +2 -0
  41. package/dist/prism-extensions/ui/toolbar.js +10 -1
  42. package/dist/prism-extensions/ui/toolbar.js.map +1 -1
  43. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +15 -0
  44. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -6
  45. package/package.json +5 -6
  46. package/src/prism-extensions/README.md +10 -1
  47. package/src/prism-extensions/core/mex-anchor.ts +29 -0
  48. package/src/prism-extensions/core/mex-cli.ts +217 -0
  49. package/src/prism-extensions/core/mex-graph-db.ts +76 -0
  50. package/src/prism-extensions/integrations/ai-memory-system.ts +30 -6
  51. package/src/prism-extensions/integrations/mex-memory.ts +264 -0
  52. package/src/prism-extensions/tools/builtin-tools.ts +2 -0
  53. package/src/prism-extensions/tools/mex-tools.ts +93 -0
  54. package/src/prism-extensions/ui/toolbar.ts +10 -1
  55. package/src/prism-extensions/commands/voice-command.ts +0 -49
  56. package/src/prism-extensions/core/voice-runtime.ts +0 -322
  57. package/src/prism-extensions/core/voicebox-client.ts +0 -205
  58. package/src/prism-extensions/core/voicebox-service.ts +0 -137
  59. package/src/prism-extensions/integrations/honcho-memory.ts +0 -299
@@ -0,0 +1,217 @@
1
+ import { execFile, spawnSync } from "node:child_process"
2
+ import * as fs from "node:fs"
3
+ import * as path from "node:path"
4
+ import { promisify } from "node:util"
5
+
6
+ const execFileAsync = promisify(execFile)
7
+
8
+ export type MexExecRunner = typeof execFileAsync
9
+ export type MexDetail = "minimal" | "standard" | "source"
10
+ export type MexRelation = "who-calls" | "what-calls" | "where-defined"
11
+ export type MexLogType = "decision" | "note" | "risk" | "todo"
12
+
13
+ export const MEX_DETAIL_LEVELS: MexDetail[] = ["minimal", "standard", "source"]
14
+ export const MEX_RELATIONS: MexRelation[] = ["who-calls", "what-calls", "where-defined"]
15
+ export const MEX_LOG_TYPES: MexLogType[] = ["decision", "note", "risk", "todo"]
16
+
17
+ export const MEX_SCAFFOLD_DIR = ".mex"
18
+ export const DEFAULT_MEX_TIMEOUT_MS = 60_000
19
+
20
+ function nonEmptyString(value: unknown): string | undefined {
21
+ return typeof value === "string" && value.trim() ? value.trim() : undefined
22
+ }
23
+
24
+ export function mexBinary(env: NodeJS.ProcessEnv = process.env): string {
25
+ return nonEmptyString(env.PRISM_MEX_BIN) ?? "mex"
26
+ }
27
+
28
+ /** mex telemetry is opt-out; Prism keeps it off unless explicitly enabled. */
29
+ export function mexEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
30
+ if (env.PRISM_MEX_TELEMETRY === "1") return { ...env }
31
+ return { ...env, DO_NOT_TRACK: "1", MEX_TELEMETRY: "0" }
32
+ }
33
+
34
+ export function isMexCliInstalled(env: NodeJS.ProcessEnv = process.env, runSync: typeof spawnSync = spawnSync): boolean {
35
+ if (env.PRISM_DISABLE_MEX === "1") return false
36
+ const result = runSync(mexBinary(env), ["--version"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], env: mexEnv(env), timeout: 5_000 })
37
+ return result.status === 0 && !!result.stdout?.trim()
38
+ }
39
+
40
+ export function mexScaffoldDir(cwd: string = process.cwd()): string {
41
+ return path.join(cwd, MEX_SCAFFOLD_DIR)
42
+ }
43
+
44
+ export function hasMexScaffold(cwd: string = process.cwd()): boolean {
45
+ // We treat a `.mex/` directory as a usable scaffold only when it contains
46
+ // the always-loaded anchor and router instruction files.
47
+ // This prevents accidentally counting a merely existing home/global `.mex/`
48
+ // directory that may not actually be a functional scaffold.
49
+ const dir = mexScaffoldDir(cwd)
50
+ const agentsFile = path.join(dir, "AGENTS.md")
51
+ const routerFile = path.join(dir, "ROUTER.md")
52
+ try {
53
+ return fs.statSync(dir).isDirectory() && fs.statSync(agentsFile).isFile() && fs.statSync(routerFile).isFile()
54
+ } catch {
55
+ return false
56
+ }
57
+ }
58
+
59
+ /** Tools only make sense when the CLI exists and the repo is scaffolded (or forced). */
60
+ export function isMexAvailable(env: NodeJS.ProcessEnv = process.env, cwd: string = process.cwd(), runSync: typeof spawnSync = spawnSync): boolean {
61
+ if (env.PRISM_DISABLE_MEX === "1") return false
62
+ if (!hasMexScaffold(cwd) && env.PRISM_MEX_FORCE !== "1") return false
63
+ return isMexCliInstalled(env, runSync)
64
+ }
65
+
66
+ // --- argument builders (pure, easy to test) --------------------------------
67
+
68
+ export type MexRetrievalOptions = {
69
+ detail?: unknown
70
+ maxNodes?: unknown
71
+ maxOutputTokens?: unknown
72
+ maxSourceLines?: unknown
73
+ }
74
+
75
+ function requiredText(value: unknown, name: string): string {
76
+ const text = String(value ?? "").trim()
77
+ if (!text) throw new Error(`${name} is required`)
78
+ return text
79
+ }
80
+
81
+ function positiveIntFlag(flag: string, value: unknown): string[] {
82
+ if (value === undefined || value === null || value === "") return []
83
+ const parsed = Math.trunc(Number(value))
84
+ if (!Number.isFinite(parsed) || parsed < 1) throw new Error(`${flag} must be a positive number`)
85
+ return [flag, String(parsed)]
86
+ }
87
+
88
+ export function mexDetail(value: unknown, fallback: MexDetail = "minimal"): MexDetail {
89
+ const detail = String(value ?? fallback).toLowerCase()
90
+ if (!MEX_DETAIL_LEVELS.includes(detail as MexDetail)) throw new Error(`detail must be one of ${MEX_DETAIL_LEVELS.join(", ")}`)
91
+ return detail as MexDetail
92
+ }
93
+
94
+ function retrievalFlags(options: MexRetrievalOptions, fallbackDetail: MexDetail): string[] {
95
+ return [
96
+ "--detail",
97
+ mexDetail(options.detail, fallbackDetail),
98
+ ...positiveIntFlag("--max-nodes", options.maxNodes),
99
+ ...positiveIntFlag("--max-output-tokens", options.maxOutputTokens),
100
+ ...positiveIntFlag("--max-source-lines", options.maxSourceLines),
101
+ ]
102
+ }
103
+
104
+ export function buildScopeArgs(task: unknown, options: MexRetrievalOptions & { fingerprint?: unknown } = {}): string[] {
105
+ return [
106
+ "graph",
107
+ "scope",
108
+ ...retrievalFlags(options, "minimal"),
109
+ ...(options.fingerprint ? ["--fingerprint"] : []),
110
+ "--",
111
+ requiredText(task, "task"),
112
+ ]
113
+ }
114
+
115
+ export function buildGetArgs(ids: unknown, options: Pick<MexRetrievalOptions, "maxOutputTokens" | "maxSourceLines"> = {}): string[] {
116
+ const list = (Array.isArray(ids) ? ids : [ids]).map((id) => String(id ?? "").trim()).filter(Boolean)
117
+ if (list.length === 0) throw new Error("ids is required")
118
+ return [
119
+ "graph",
120
+ "get",
121
+ ...positiveIntFlag("--max-source-lines", options.maxSourceLines),
122
+ ...positiveIntFlag("--max-output-tokens", options.maxOutputTokens),
123
+ "--",
124
+ ...list,
125
+ ]
126
+ }
127
+
128
+ export function buildQueryArgs(relation: unknown, target: unknown, options: MexRetrievalOptions = {}): string[] {
129
+ const value = String(relation ?? "").toLowerCase().trim()
130
+ if (!MEX_RELATIONS.includes(value as MexRelation)) throw new Error(`relation must be one of ${MEX_RELATIONS.join(", ")}`)
131
+ return ["graph", "query", ...retrievalFlags(options, "minimal"), "--", value, requiredText(target, "target")]
132
+ }
133
+
134
+ export function buildImpactArgs(target: unknown, options: MexRetrievalOptions & { depth?: unknown } = {}): string[] {
135
+ return [
136
+ "impact",
137
+ "--detail",
138
+ mexDetail(options.detail, "minimal"),
139
+ ...positiveIntFlag("--depth", options.depth),
140
+ ...positiveIntFlag("--max-nodes", options.maxNodes),
141
+ ...positiveIntFlag("--max-output-tokens", options.maxOutputTokens),
142
+ ...positiveIntFlag("--max-source-lines", options.maxSourceLines),
143
+ "--",
144
+ requiredText(target, "target"),
145
+ ]
146
+ }
147
+
148
+ export function buildGraphArgs(options: { json?: unknown; root?: unknown } = {}): string[] {
149
+ const root = nonEmptyString(options.root)
150
+ return ["graph", ...(options.json ? ["--json"] : []), ...(root ? ["--root", root] : [])]
151
+ }
152
+
153
+ export function buildCheckArgs(options: { json?: unknown; quiet?: unknown } = {}): string[] {
154
+ return ["check", ...(options.json ? ["--json"] : []), ...(options.quiet ? ["--quiet"] : [])]
155
+ }
156
+
157
+ export function buildTimelineArgs(options: { json?: unknown; since?: unknown; type?: unknown; limit?: unknown } = {}): string[] {
158
+ const since = nonEmptyString(options.since)
159
+ const type = nonEmptyString(options.type)?.toLowerCase()
160
+ if (type && !MEX_LOG_TYPES.includes(type as MexLogType)) throw new Error(`type must be one of ${MEX_LOG_TYPES.join(", ")}`)
161
+ return [
162
+ "timeline",
163
+ ...(options.json ? ["--json"] : []),
164
+ ...(since ? ["--since", since] : []),
165
+ ...(type ? ["--type", type] : []),
166
+ ...positiveIntFlag("--limit", options.limit),
167
+ ]
168
+ }
169
+
170
+ export function buildLogArgs(message: unknown, options: { type?: unknown; files?: unknown; source?: unknown; status?: unknown } = {}): string[] {
171
+ const type = String(options.type ?? "note").toLowerCase()
172
+ if (!MEX_LOG_TYPES.includes(type as MexLogType)) throw new Error(`type must be one of ${MEX_LOG_TYPES.join(", ")}`)
173
+ const files = (Array.isArray(options.files) ? options.files : options.files ? [options.files] : []).map((file) => String(file ?? "").trim()).filter(Boolean)
174
+ const source = nonEmptyString(options.source) ?? "prism-agent"
175
+ const status = nonEmptyString(options.status)
176
+ return [
177
+ "log",
178
+ "--type",
179
+ type,
180
+ ...files.flatMap((file) => ["--file", file]),
181
+ "--source",
182
+ source,
183
+ ...(status ? ["--status", status] : []),
184
+ "--",
185
+ requiredText(message, "message"),
186
+ ]
187
+ }
188
+
189
+ // --- execution -------------------------------------------------------------
190
+
191
+ export type MexRunResult = { ok: boolean; exitCode: number; output: string }
192
+
193
+ export type MexRunOptions = {
194
+ cwd?: string
195
+ env?: NodeJS.ProcessEnv
196
+ timeoutMs?: number
197
+ maxBuffer?: number
198
+ runner?: MexExecRunner
199
+ }
200
+
201
+ export async function runMex(args: string[], options: MexRunOptions = {}): Promise<MexRunResult> {
202
+ const env = options.env ?? process.env
203
+ const runner = options.runner ?? execFileAsync
204
+ try {
205
+ const { stdout, stderr } = await runner(mexBinary(env), args, {
206
+ cwd: options.cwd ?? process.cwd(),
207
+ env: mexEnv(env),
208
+ timeout: options.timeoutMs ?? DEFAULT_MEX_TIMEOUT_MS,
209
+ maxBuffer: options.maxBuffer ?? 32 * 1024 * 1024,
210
+ })
211
+ const output = [String(stdout ?? ""), String(stderr ?? "")].join("\n").trim()
212
+ return { ok: true, exitCode: 0, output }
213
+ } catch (error: any) {
214
+ const output = [String(error?.stdout ?? ""), String(error?.stderr ?? ""), error?.message ? `ERROR: ${error.message}` : ""].filter((part) => part.trim()).join("\n").trim()
215
+ return { ok: false, exitCode: Number.isFinite(error?.code) ? Number(error.code) : 1, output: output || `ERROR: mex ${args[0] ?? ""} failed` }
216
+ }
217
+ }
@@ -0,0 +1,76 @@
1
+ import * as fs from "node:fs"
2
+ import * as path from "node:path"
3
+ import { buildGraphArgs, hasMexScaffold, mexScaffoldDir, runMex, type MexRunOptions } from "./mex-cli.js"
4
+
5
+ /** SQLite code graph mex builds into the scaffold. */
6
+ export const MEX_GRAPH_DB_FILE = "graph.db"
7
+ export const DEFAULT_MEX_GRAPH_BUILD_TIMEOUT_MS = 300_000
8
+
9
+ /** Actions that read the code graph and therefore need `.mex/graph.db`. */
10
+ export const MEX_GRAPH_ACTIONS = ["scope", "get", "query", "impact"] as const
11
+
12
+ export type MexGraphEnsureStatus = "present" | "built" | "failed" | "disabled" | "no-scaffold"
13
+ export type MexGraphEnsureResult = { status: MexGraphEnsureStatus; path: string; output?: string }
14
+ export type MexGraphEnsureOptions = MexRunOptions & { force?: boolean }
15
+
16
+ export function mexGraphDbPath(cwd: string = process.cwd()): string {
17
+ return path.join(mexScaffoldDir(cwd), MEX_GRAPH_DB_FILE)
18
+ }
19
+
20
+ export function hasMexGraphDb(cwd: string = process.cwd()): boolean {
21
+ try {
22
+ const stats = fs.statSync(mexGraphDbPath(cwd))
23
+ return stats.isFile() && stats.size > 0
24
+ } catch {
25
+ return false
26
+ }
27
+ }
28
+
29
+ export function isMexAutoGraphEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
30
+ return env.PRISM_DISABLE_MEX_AUTO_GRAPH !== "1"
31
+ }
32
+
33
+ export function needsMexGraphAction(action: string): boolean {
34
+ return (MEX_GRAPH_ACTIONS as readonly string[]).includes(action)
35
+ }
36
+
37
+ /** One build per scaffold at a time, so a session start and a tool call never race. */
38
+ const inflightBuilds = new Map<string, Promise<MexGraphEnsureResult>>()
39
+
40
+ /**
41
+ * Builds `.mex/graph.db` when it is missing (or when `force` is set).
42
+ * Never throws: mex degrades gracefully with GRAPH_UNAVAILABLE if the build fails.
43
+ */
44
+ export async function ensureMexGraphDb(options: MexGraphEnsureOptions = {}): Promise<MexGraphEnsureResult> {
45
+ const cwd = options.cwd ?? process.cwd()
46
+ const env = options.env ?? process.env
47
+ const dbPath = mexGraphDbPath(cwd)
48
+ if (!isMexAutoGraphEnabled(env)) return { status: "disabled", path: dbPath }
49
+ if (!hasMexScaffold(cwd)) return { status: "no-scaffold", path: dbPath }
50
+ if (!options.force && hasMexGraphDb(cwd)) return { status: "present", path: dbPath }
51
+
52
+ const pending = inflightBuilds.get(dbPath)
53
+ if (pending) return pending
54
+ const build = buildMexGraphDb(cwd, dbPath, options).finally(() => inflightBuilds.delete(dbPath))
55
+ inflightBuilds.set(dbPath, build)
56
+ return build
57
+ }
58
+
59
+ async function buildMexGraphDb(cwd: string, dbPath: string, options: MexGraphEnsureOptions): Promise<MexGraphEnsureResult> {
60
+ const result = await runMex(buildGraphArgs({}), {
61
+ cwd,
62
+ env: options.env,
63
+ runner: options.runner,
64
+ maxBuffer: options.maxBuffer,
65
+ timeoutMs: options.timeoutMs ?? DEFAULT_MEX_GRAPH_BUILD_TIMEOUT_MS,
66
+ })
67
+ const built = result.ok && hasMexGraphDb(cwd)
68
+ return { status: built ? "built" : "failed", path: dbPath, output: result.output || undefined }
69
+ }
70
+
71
+ /** Short user-facing line for a build attempt, or undefined when nothing happened. */
72
+ export function describeMexGraphEnsure(result: MexGraphEnsureResult): string | undefined {
73
+ if (result.status === "built") return `mex code graph built: ${result.path}`
74
+ if (result.status === "failed") return [`mex code graph build failed: ${result.path}`, result.output].filter(Boolean).join("\n")
75
+ return undefined
76
+ }
@@ -276,8 +276,14 @@ export function formatAiMemoryToolCall(toolName: string, args: any, theme: Theme
276
276
  * ignores the per-turn query so a broken API does not warn on every prompt.
277
277
  */
278
278
  function reportBackgroundFailure(ctx: any, label: string, error: unknown): void {
279
- const message = `AI Memory ${label} failed: ${error instanceof Error ? error.message : String(error)}`
280
- const key = `${label}|${message.split(" | query=")[0]}`
279
+ const rawErrorMessage = error instanceof Error ? error.message : String(error)
280
+ // Background injection warnings should not be cluttered with the full
281
+ // query payload (often long JSON/string). Keep the code message still
282
+ // useful while making it readable.
283
+ const errorMessageForUser = rawErrorMessage.replace(/\s\|\squery=.*?(?=\s\|\sfilters=|$)/, "")
284
+
285
+ const message = `AI Memory ${label} failed: ${errorMessageForUser}`
286
+ const key = `${label}|${rawErrorMessage.split(" | query=")[0]}`
281
287
  if (reportedBackgroundFailures.has(key)) return
282
288
  reportedBackgroundFailures.add(key)
283
289
  ctx?.ui?.notify?.(message, "warning")
@@ -632,18 +638,33 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
632
638
  pi.on?.("before_agent_start", async (event: any, ctx: any) => {
633
639
  const patch: { systemPrompt?: string; message?: any } = {}
634
640
 
641
+ // Background injection may fail when the local AI Memory HTTP daemon is
642
+ // down. We try every turn, but we only emit a single warning per turn to
643
+ // avoid spamming (and we also hide the full query payload).
644
+ let warnedThisTurn = false
645
+ const reportOnceThisTurn = (label: string, error: unknown) => {
646
+ if (warnedThisTurn) return
647
+ warnedThisTurn = true
648
+ reportBackgroundFailure(ctx, label, error)
649
+ }
650
+
635
651
  // Always-loaded durable context (mapped from 00 Kontext) is injected as a
636
652
  // system-prompt chunk so it remains in the cached prefix. It is opt-in so a
637
653
  // missing local AI Memory daemon does not produce startup/per-turn warnings.
638
654
  if (shouldInjectAiMemoryAlwaysContext()) {
639
655
  try {
640
- const alwaysBlock = buildAlwaysContextMessage(await loadAlwaysContext())?.content?.trim()
656
+ // If the daemon is reachable again, allow warnings to re-appear on the
657
+ // next failure (clear the dedupe set on successful injection).
658
+ const alwaysContent = await loadAlwaysContext()
659
+ reportedBackgroundFailures.clear()
660
+
661
+ const alwaysBlock = buildAlwaysContextMessage(alwaysContent)?.content?.trim()
641
662
  if (alwaysBlock) {
642
663
  const prev = String(event?.systemPrompt ?? "")
643
664
  patch.systemPrompt = `${prev}${prev ? "\n\n" : ""}${alwaysBlock}`
644
665
  }
645
666
  } catch (error) {
646
- reportBackgroundFailure(ctx, "always-context injection", error)
667
+ reportOnceThisTurn("always-context injection", error)
647
668
  }
648
669
  }
649
670
 
@@ -651,10 +672,13 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
651
672
  const query = String(event?.prompt ?? "").trim()
652
673
  if (shouldInjectAiMemoryCandidates() && query) {
653
674
  try {
654
- const message = buildAiMemoryContextMessage(await loadAiMemoryInjectResults(query))
675
+ const injectResults = await loadAiMemoryInjectResults(query)
676
+ reportedBackgroundFailures.clear()
677
+
678
+ const message = buildAiMemoryContextMessage(injectResults)
655
679
  if (message) patch.message = message
656
680
  } catch (error) {
657
- reportBackgroundFailure(ctx, "candidate injection", error)
681
+ reportOnceThisTurn("candidate injection", error)
658
682
  }
659
683
  }
660
684
 
@@ -0,0 +1,264 @@
1
+ import { Type } from "typebox"
2
+ import {
3
+ buildCheckArgs,
4
+ buildGetArgs,
5
+ buildGraphArgs,
6
+ buildImpactArgs,
7
+ buildLogArgs,
8
+ buildQueryArgs,
9
+ buildScopeArgs,
10
+ buildTimelineArgs,
11
+ hasMexScaffold,
12
+ isMexAvailable,
13
+ MEX_LOG_TYPES,
14
+ MEX_RELATIONS,
15
+ runMex,
16
+ type MexRunOptions,
17
+ } from "../core/mex-cli.js"
18
+ import {
19
+ describeMexGraphEnsure,
20
+ ensureMexGraphDb,
21
+ hasMexGraphDb,
22
+ isMexAutoGraphEnabled,
23
+ needsMexGraphAction,
24
+ type MexGraphEnsureResult,
25
+ } from "../core/mex-graph-db.js"
26
+ import { textToolResult } from "../tools/toolbox-utils.js"
27
+ import { formatBracketedToolCall, renderSingleLineToolCall, type ThemeLike } from "../ui/tool-call-rendering.js"
28
+
29
+ type ExtensionAPI = any
30
+
31
+ const MEX_STATUS_KEY = "mex"
32
+ const MEX_STATUS_LABEL = "🕸 mex"
33
+ const MEX_STATUS_BUILDING_LABEL = "🕸 mex ⟳"
34
+
35
+ const RETRIEVAL_PARAMS = {
36
+ detail: Type.Optional(Type.String({ description: "minimal | standard | source. Default minimal." })),
37
+ maxNodes: Type.Optional(Type.Number({ description: "Maximum nodes to return." })),
38
+ maxOutputTokens: Type.Optional(Type.Number({ description: "Hard output token ceiling." })),
39
+ maxSourceLines: Type.Optional(Type.Number({ description: "Per-node source line cap (detail=source)." })),
40
+ }
41
+
42
+ export function formatMexOutput(action: string, result: { ok: boolean; exitCode: number; output: string }) {
43
+ const text = result.output || (result.ok ? `mex ${action} produced no output.` : `mex ${action} failed.`)
44
+ return textToolResult(text, { action, ok: result.ok, exitCode: result.exitCode })
45
+ }
46
+
47
+ /** Registers the mex code-graph tools. Exported for tests. */
48
+ export function registerMexTools(pi: ExtensionAPI, options: MexRunOptions = {}): string[] {
49
+ const registered: string[] = []
50
+ const exec = async (action: string, args: string[]) => {
51
+ // For code-graph actions we must have a `.mex/` scaffold.
52
+ // Otherwise `mex` itself may auto-create the scaffold/DB, which we explicitly want to avoid.
53
+ if (needsMexGraphAction(action)) {
54
+ const ensured = await ensureMexGraphDb(graphEnsureOptions(options))
55
+ if (ensured.status === "no-scaffold" || ensured.status === "disabled") {
56
+ return formatMexOutput(action, {
57
+ ok: false,
58
+ exitCode: 1,
59
+ output: `mex ${action} disabled: missing .mex scaffold (needed for graph.db). Run npx mex-agent setup in the repo root, or disable mex via PRISM_DISABLE_MEX=1.`,
60
+ })
61
+ }
62
+ }
63
+ return formatMexOutput(action, await runMex(args, options))
64
+ }
65
+
66
+ function register(tool: any): void {
67
+ registered.push(tool.name)
68
+ pi.registerTool(tool)
69
+ }
70
+
71
+ register({
72
+ name: "mex_scope",
73
+ label: "Mex Scope",
74
+ description: "Retrieve a compact, task-relevant code neighborhood from the local mex code graph (JSONL: symbols, signatures, relations, node ids).",
75
+ promptSnippet: "Use mex_scope first for codebase questions to get a compact set of relevant symbols instead of scanning files.",
76
+ promptGuidelines: [
77
+ "Describe the task in natural language, e.g. 'trace the authentication flow'.",
78
+ "Scope matching is word-based, not semantic. If results look weak, rephrase once, then fall back to grep/read.",
79
+ "Expand promising node ids with mex_get instead of reading whole files.",
80
+ ],
81
+ parameters: Type.Object({
82
+ task: Type.String({ description: "Task description, e.g. 'trace the authentication flow'." }),
83
+ ...RETRIEVAL_PARAMS,
84
+ fingerprint: Type.Optional(Type.Boolean({ description: "Attach node fingerprints (grounding workflow)." })),
85
+ }),
86
+ async execute(_id: string, params: any) {
87
+ return exec("scope", buildScopeArgs(params.task, params))
88
+ },
89
+ renderCall(args: any, theme: ThemeLike, context: any) {
90
+ return renderSingleLineToolCall(formatBracketedToolCall("mex scope", args?.task, theme), context)
91
+ },
92
+ })
93
+
94
+ register({
95
+ name: "mex_get",
96
+ label: "Mex Get",
97
+ description: "Expand source code for specific mex code-graph node ids returned by mex_scope, mex_query, or mex_impact.",
98
+ promptSnippet: "Expand mex node ids to source instead of reading whole files.",
99
+ parameters: Type.Object({
100
+ ids: Type.Array(Type.String({ description: "Node id, e.g. function:a3f8...c21" })),
101
+ maxSourceLines: Type.Optional(Type.Number({ description: "Per-node source line cap." })),
102
+ maxOutputTokens: Type.Optional(Type.Number({ description: "Hard output token ceiling." })),
103
+ }),
104
+ async execute(_id: string, params: any) {
105
+ return exec("get", buildGetArgs(params.ids, params))
106
+ },
107
+ renderCall(args: any, theme: ThemeLike, context: any) {
108
+ const ids = Array.isArray(args?.ids) ? args.ids : []
109
+ return renderSingleLineToolCall(formatBracketedToolCall("mex get", ids.length > 1 ? `${ids[0]} +${ids.length - 1}` : ids[0], theme), context)
110
+ },
111
+ })
112
+
113
+ register({
114
+ name: "mex_query",
115
+ label: "Mex Query",
116
+ description: `Query structural code relations in the mex graph: ${MEX_RELATIONS.join(", ")}.`,
117
+ promptSnippet: "Use mex_query for precise structural questions like who calls a function or where a symbol is defined.",
118
+ parameters: Type.Object({
119
+ relation: Type.String({ description: MEX_RELATIONS.join(" | ") }),
120
+ target: Type.String({ description: "Symbol or file name." }),
121
+ ...RETRIEVAL_PARAMS,
122
+ }),
123
+ async execute(_id: string, params: any) {
124
+ return exec("query", buildQueryArgs(params.relation, params.target, params))
125
+ },
126
+ renderCall(args: any, theme: ThemeLike, context: any) {
127
+ return renderSingleLineToolCall(formatBracketedToolCall("mex query", [args?.relation, args?.target].filter(Boolean).join(" "), theme), context)
128
+ },
129
+ })
130
+
131
+ register({
132
+ name: "mex_impact",
133
+ label: "Mex Impact",
134
+ description: "Show the transitive code and wiki blast radius for a symbol or file before refactoring.",
135
+ promptSnippet: "Use mex_impact before refactors to see what a symbol or file change touches.",
136
+ parameters: Type.Object({
137
+ target: Type.String({ description: "Symbol name or file path." }),
138
+ depth: Type.Optional(Type.Number({ description: "Transitive caller depth. Default 2." })),
139
+ ...RETRIEVAL_PARAMS,
140
+ }),
141
+ async execute(_id: string, params: any) {
142
+ return exec("impact", buildImpactArgs(params.target, params))
143
+ },
144
+ renderCall(args: any, theme: ThemeLike, context: any) {
145
+ return renderSingleLineToolCall(formatBracketedToolCall("mex impact", args?.target, theme), context)
146
+ },
147
+ })
148
+
149
+ register({
150
+ name: "mex_log",
151
+ label: "Mex Log",
152
+ description: `Append a ${MEX_LOG_TYPES.join("/")} entry to the repo-local mex event log (.mex/events).`,
153
+ promptSnippet: "Log durable repo decisions, risks, or todos into the mex event log.",
154
+ promptGuidelines: [
155
+ "Log decisions only when they are actually settled in this conversation.",
156
+ "Keep entries short and repo-specific; no secrets, credentials, or private hostnames.",
157
+ ],
158
+ parameters: Type.Object({
159
+ message: Type.String({ description: "Short entry text." }),
160
+ type: Type.Optional(Type.String({ description: `${MEX_LOG_TYPES.join(" | ")}. Default note.` })),
161
+ files: Type.Optional(Type.Array(Type.String({ description: "Related file path." }))),
162
+ status: Type.Optional(Type.String({ description: "Lifecycle status, e.g. decided or implemented." })),
163
+ }),
164
+ async execute(_id: string, params: any) {
165
+ return exec("log", buildLogArgs(params.message, params))
166
+ },
167
+ renderCall(args: any, theme: ThemeLike, context: any) {
168
+ return renderSingleLineToolCall(formatBracketedToolCall(`mex log ${String(args?.type ?? "note")}`, args?.message, theme), context)
169
+ },
170
+ })
171
+
172
+ return registered
173
+ }
174
+
175
+ export default function mexMemoryExtension(pi: ExtensionAPI): void {
176
+ const cwd = process.cwd()
177
+ if (!isMexAvailable(process.env, cwd)) return
178
+
179
+ registerMexTools(pi, { cwd })
180
+
181
+ pi.on?.("session_start", (_event: any, ctx: any) => {
182
+ ctx?.ui?.setStatus?.(MEX_STATUS_KEY, mexStatusLabel(cwd))
183
+ void autoBuildMissingGraph(cwd, ctx)
184
+ })
185
+
186
+ pi.registerCommand("mex", {
187
+ description: "Mex code graph: /mex [status] | graph | check | timeline | scope <task>",
188
+ handler: async (args: string, ctx: any) => {
189
+ const [sub, ...rest] = (args || "").trim().split(/\s+/)
190
+ const argument = rest.join(" ").trim()
191
+ const action = (sub || "status").toLowerCase()
192
+ try {
193
+ if (action === "status") {
194
+ const result = await runMex(buildCheckArgs({ quiet: true }), { cwd })
195
+ const lines = [
196
+ `mex scaffold: ${hasMexScaffold(cwd) ? mexPathLabel(cwd) : "missing"}`,
197
+ `mex graph.db: ${hasMexGraphDb(cwd) ? "present" : isMexAutoGraphEnabled() ? "missing (auto-build on demand)" : "missing (auto-build disabled)"}`,
198
+ result.output,
199
+ ]
200
+ ctx.ui.notify(lines.filter(Boolean).join("\n"), result.ok ? "info" : "warning")
201
+ return
202
+ }
203
+ if (action === "graph") {
204
+ // Never let `mex graph` create a scaffold/DB in `~/.mex` when we're running outside a repo scaffold.
205
+ // Don't rebuild when graph.db already exists; just skip to avoid touching ~/.mex.
206
+ const ensured = await ensureMexGraphDb({ cwd, env: process.env, runner: undefined, timeoutMs: 300_000 })
207
+ const message = describeMexGraphEnsure(ensured)
208
+ if (message) ctx.ui.notify(message, ensured.status === "built" ? "info" : "warning")
209
+ else ctx.ui.notify("mex graph skipped.", ensured.status === "built" ? "info" : "warning")
210
+ return
211
+ }
212
+ if (action === "check") {
213
+ const result = await runMex(buildCheckArgs({}), { cwd, timeoutMs: 300_000 })
214
+ ctx.ui.notify(result.output || "No drift output.", result.ok ? "info" : "warning")
215
+ return
216
+ }
217
+ if (action === "timeline") {
218
+ const result = await runMex(buildTimelineArgs({ limit: 20 }), { cwd })
219
+ ctx.ui.notify(result.output || "No mex events.", "info")
220
+ return
221
+ }
222
+ if (action === "scope") {
223
+ if (!argument) return ctx.ui.notify("Usage: /mex scope <task>", "warning")
224
+ const result = await runMex(buildScopeArgs(argument, {}), { cwd })
225
+ ctx.ui.notify(result.output || "No scope results.", result.ok ? "info" : "warning")
226
+ return
227
+ }
228
+ ctx.ui.notify("Usage: /mex [status] | graph | check | timeline | scope <task>", "warning")
229
+ } catch (error) {
230
+ ctx.ui.notify(`mex command failed:\n${error instanceof Error ? error.message : String(error)}`, "error")
231
+ }
232
+ },
233
+ })
234
+ }
235
+
236
+ function mexPathLabel(cwd: string): string {
237
+ return `${cwd.replace(/\/+$/, "")}/.mex`
238
+ }
239
+
240
+ function graphEnsureOptions(options: MexRunOptions): MexRunOptions {
241
+ return { cwd: options.cwd, env: options.env, runner: options.runner }
242
+ }
243
+
244
+ /** Only a scaffold with a usable code graph counts as "mex is working"; otherwise the segment stays hidden. */
245
+ export function mexStatusLabel(cwd: string = process.cwd()): string | undefined {
246
+ return hasMexScaffold(cwd) && hasMexGraphDb(cwd) ? MEX_STATUS_LABEL : undefined
247
+ }
248
+
249
+ /** Builds a missing code graph in the background so the first graph tool call is not blocked. */
250
+ async function autoBuildMissingGraph(cwd: string, ctx: any): Promise<MexGraphEnsureResult | undefined> {
251
+ if (!hasMexScaffold(cwd) || hasMexGraphDb(cwd) || !isMexAutoGraphEnabled()) return undefined
252
+ ctx?.ui?.setStatus?.(MEX_STATUS_KEY, MEX_STATUS_BUILDING_LABEL)
253
+ try {
254
+ const result = await ensureMexGraphDb({ cwd })
255
+ const message = describeMexGraphEnsure(result)
256
+ if (message) ctx?.ui?.notify?.(message, result.status === "built" ? "info" : "warning")
257
+ return result
258
+ } catch (error) {
259
+ ctx?.ui?.notify?.(`mex graph build failed:\n${error instanceof Error ? error.message : String(error)}`, "warning")
260
+ return undefined
261
+ } finally {
262
+ ctx?.ui?.setStatus?.(MEX_STATUS_KEY, mexStatusLabel(cwd))
263
+ }
264
+ }
@@ -2,6 +2,7 @@ import type { ToolboxToolDefinition } from "./toolbox.js"
2
2
  import { createDiagnosticToolboxTools } from "./diagnostic-tools.js"
3
3
  import { createPrismDiagnosticToolboxTools } from "./prism-diagnostic-tools.js"
4
4
  import { createSshConfigTool } from "./ssh-config-tool.js"
5
+ import { createMexToolboxTools } from "./mex-tools.js"
5
6
 
6
7
  export function defineToolboxTool(definition: ToolboxToolDefinition): ToolboxToolDefinition {
7
8
  return { source: "toolbox/builtin", ...definition }
@@ -11,6 +12,7 @@ export const builtinToolboxTools: ToolboxToolDefinition[] = [
11
12
  createSshConfigTool(),
12
13
  ...createDiagnosticToolboxTools(),
13
14
  ...createPrismDiagnosticToolboxTools(),
15
+ ...createMexToolboxTools(),
14
16
  ]
15
17
 
16
18
  export function createBuiltinToolboxTools(): ToolboxToolDefinition[] {