@daniel156161/prism 0.2.92 → 0.2.97

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -181
  3. package/dist/pi/pi-context-files.d.ts +6 -0
  4. package/dist/pi/pi-context-files.js +17 -0
  5. package/dist/pi/pi-context-files.js.map +1 -0
  6. package/dist/pi/pi-extensions.d.ts +0 -1
  7. package/dist/pi/pi-extensions.js +7 -22
  8. package/dist/pi/pi-extensions.js.map +1 -1
  9. package/dist/pi/pi-npm-extension-entry.d.ts +2 -0
  10. package/dist/pi/pi-npm-extension-entry.js +74 -0
  11. package/dist/pi/pi-npm-extension-entry.js.map +1 -0
  12. package/dist/pi/pi-package.js +4 -0
  13. package/dist/pi/pi-package.js.map +1 -1
  14. package/dist/pi/pi-patch-context-files.d.ts +12 -0
  15. package/dist/pi/pi-patch-context-files.js +46 -0
  16. package/dist/pi/pi-patch-context-files.js.map +1 -0
  17. package/dist/pi/pi-patch-interactive.js +5 -1
  18. package/dist/pi/pi-patch-interactive.js.map +1 -1
  19. package/dist/pi/plan-mode-tools.d.ts +13 -0
  20. package/dist/pi/plan-mode-tools.js +54 -0
  21. package/dist/pi/plan-mode-tools.js.map +1 -0
  22. package/dist/prism-extensions/core/mex-anchor.d.ts +9 -0
  23. package/dist/prism-extensions/core/mex-anchor.js +29 -0
  24. package/dist/prism-extensions/core/mex-anchor.js.map +1 -0
  25. package/dist/prism-extensions/core/mex-cli.d.ts +68 -0
  26. package/dist/prism-extensions/core/mex-cli.js +183 -0
  27. package/dist/prism-extensions/core/mex-cli.js.map +1 -0
  28. package/dist/prism-extensions/core/mex-graph-db.d.ts +26 -0
  29. package/dist/prism-extensions/core/mex-graph-db.js +69 -0
  30. package/dist/prism-extensions/core/mex-graph-db.js.map +1 -0
  31. package/dist/prism-extensions/integrations/ai-memory-system.js +27 -6
  32. package/dist/prism-extensions/integrations/ai-memory-system.js.map +1 -1
  33. package/dist/prism-extensions/integrations/mex-memory.d.ts +21 -0
  34. package/dist/prism-extensions/integrations/mex-memory.js +231 -0
  35. package/dist/prism-extensions/integrations/mex-memory.js.map +1 -0
  36. package/dist/prism-extensions/tools/builtin-tools.js +2 -0
  37. package/dist/prism-extensions/tools/builtin-tools.js.map +1 -1
  38. package/dist/prism-extensions/tools/mex-tools.d.ts +2 -0
  39. package/dist/prism-extensions/tools/mex-tools.js +78 -0
  40. package/dist/prism-extensions/tools/mex-tools.js.map +1 -0
  41. package/dist/prism-extensions/tools/prism-diagnostic-tools.js +10 -10
  42. package/dist/prism-extensions/tools/prism-diagnostic-tools.js.map +1 -1
  43. package/dist/prism-extensions/tools/toolbox.js +1 -1
  44. package/dist/prism-extensions/tools/toolbox.js.map +1 -1
  45. package/dist/prism-extensions/ui/toolbar.d.ts +2 -0
  46. package/dist/prism-extensions/ui/toolbar.js +10 -1
  47. package/dist/prism-extensions/ui/toolbar.js.map +1 -1
  48. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +15 -0
  49. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -6
  50. package/package.json +17 -4
  51. package/src/prism-extensions/README.md +10 -1
  52. package/src/prism-extensions/core/mex-anchor.ts +29 -0
  53. package/src/prism-extensions/core/mex-cli.ts +217 -0
  54. package/src/prism-extensions/core/mex-graph-db.ts +76 -0
  55. package/src/prism-extensions/integrations/ai-memory-system.ts +30 -6
  56. package/src/prism-extensions/integrations/mex-memory.ts +264 -0
  57. package/src/prism-extensions/tools/builtin-tools.ts +2 -0
  58. package/src/prism-extensions/tools/mex-tools.ts +93 -0
  59. package/src/prism-extensions/tools/prism-diagnostic-tools.ts +10 -10
  60. package/src/prism-extensions/tools/toolbox.ts +1 -1
  61. package/src/prism-extensions/ui/toolbar.ts +10 -1
  62. package/src/prism-extensions/commands/voice-command.ts +0 -49
  63. package/src/prism-extensions/core/voice-runtime.ts +0 -322
  64. package/src/prism-extensions/core/voicebox-client.ts +0 -205
  65. package/src/prism-extensions/core/voicebox-service.ts +0 -137
  66. package/src/prism-extensions/integrations/honcho-memory.ts +0 -299
@@ -104,6 +104,21 @@ export function loadProjectContextFiles(options) {
104
104
  currentDir = parentDir;
105
105
  }
106
106
  contextFiles.push(...ancestorContextFiles);
107
+ // prism: extra context files (e.g. .mex/AGENTS.md) load like AGENTS.md so they appear under "Context".
108
+ for (const extraContextEntry of (process.env.PRISM_EXTRA_CONTEXT_FILES ?? "").split("\n")) {
109
+ const extraContextPath = extraContextEntry.trim();
110
+ if (!extraContextPath || seenPaths.has(extraContextPath))
111
+ continue;
112
+ try {
113
+ if (!statSync(extraContextPath).isFile())
114
+ continue;
115
+ contextFiles.push({ path: extraContextPath, content: readFileSync(extraContextPath, "utf-8") });
116
+ seenPaths.add(extraContextPath);
117
+ }
118
+ catch {
119
+ // Unreadable extra context file: ignore, it is optional by design.
120
+ }
121
+ }
107
122
  return contextFiles;
108
123
  }
109
124
  export class DefaultResourceLoader {
@@ -484,8 +484,8 @@ export class InteractiveMode {
484
484
  }));
485
485
  // Convert extension commands to SlashCommand format
486
486
  const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
487
- const prismInternalCommandExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "voice-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "honcho-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter"]);
488
- const prismInternalCommandNpmPackages = new Set(["pi-mcp-adapter"]);
487
+ const prismInternalCommandExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "mex-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter", "@juicesharp/rpiv-voice", "@juicesharp/rpiv-todo"]);
488
+ const prismInternalCommandNpmPackages = new Set(["pi-mcp-adapter", "@juicesharp/rpiv-voice", "@juicesharp/rpiv-todo"]);
489
489
  const isPrismInternalCommand = (cmd) => (() => { const prismExtensionPath = String(cmd.sourceInfo?.path || "").replaceAll("\\", "/"); const prismExtensionSource = String(cmd.sourceInfo?.source || ""); return prismInternalCommandExtensions.has(path.basename(prismExtensionPath)) || Array.from(prismInternalCommandNpmPackages).some((pkg) => prismExtensionSource === "npm:" + pkg || prismExtensionPath.includes("/node_modules/" + pkg + "/")); })();
490
490
  const extensionCommands = this.session.extensionRunner
491
491
  .getRegisteredCommands()
@@ -679,7 +679,7 @@ export class InteractiveMode {
679
679
  await this.themeController.applyFromSettings();
680
680
  // Add header with keybindings from config (unless silenced)
681
681
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
682
- const logo = "▲ \u001b[38;5;196mp\u001b[38;5;202mr\u001b[38;5;226mi\u001b[38;5;46ms\u001b[38;5;51mm\u001b[0m v0.2.92";
682
+ const logo = "▲ \u001b[38;5;196mp\u001b[38;5;202mr\u001b[38;5;226mi\u001b[38;5;46ms\u001b[38;5;51mm\u001b[0m v0.2.97";
683
683
  // Build startup instructions using keybinding hint helpers
684
684
  const hint = (keybinding, description) => keyHint(keybinding, description);
685
685
  const expandedInstructions = [
@@ -1348,8 +1348,8 @@ export class InteractiveMode {
1348
1348
  const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
1349
1349
  addLoadedSection("Prompts", promptCompactList, templateList);
1350
1350
  }
1351
- const prismInternalExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "voice-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "honcho-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter"]);
1352
- const prismInternalNpmPackages = new Set(["pi-mcp-adapter"]);
1351
+ const prismInternalExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "mex-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter", "@juicesharp/rpiv-voice", "@juicesharp/rpiv-todo"]);
1352
+ const prismInternalNpmPackages = new Set(["pi-mcp-adapter", "@juicesharp/rpiv-voice", "@juicesharp/rpiv-todo"]);
1353
1353
  extensions = extensions.filter((extension) => !(() => { const prismExtensionPath = String(extension.path || "").replaceAll("\\", "/"); const prismExtensionSource = String(extension.sourceInfo?.source || ""); return prismInternalExtensions.has(path.basename(prismExtensionPath)) || Array.from(prismInternalNpmPackages).some((pkg) => prismExtensionSource === "npm:" + pkg || prismExtensionPath.includes("/node_modules/" + pkg + "/")); })());
1354
1354
  if (extensions.length > 0) {
1355
1355
  const groups = this.buildScopeGroups(extensions);
@@ -2932,7 +2932,7 @@ export class InteractiveMode {
2932
2932
  });
2933
2933
  }
2934
2934
  getPrismReadOnlyToolNames() {
2935
- const readOnly = new Set(["read", "grep", "find", "ls", "obsidian_memory_read", "obsidian_memory_search", "obsidian_memory_list", "web_search", "web_fetch"]);
2935
+ const readOnly = new Set(["read", "grep", "find", "ls", "web_search", "web_fetch", "obsidian_memory_read", "obsidian_memory_search", "obsidian_memory_list", "logseq_read", "logseq_search", "logseq_list", "ai_memory_search", "ai_memory_read", "ai_memory_status", "ai_memory_vault_list", "mex_scope", "mex_get", "mex_query", "mex_impact", "rp_state", "rp_search", "toolbox_search", "self_improvement_list", "todo"]);
2936
2936
  return this.session.getActiveToolNames().filter((name) => readOnly.has(name));
2937
2937
  }
2938
2938
  setPrismPlanStatus(enabled) {
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@daniel156161/prism",
3
- "version": "0.2.92",
3
+ "version": "0.2.97",
4
4
  "description": "Prism-branded wrapper around pi that stores config in ~/.prism",
5
+ "license": "MIT",
6
+ "author": "Daniel Dolezal",
7
+ "keywords": [
8
+ "prism",
9
+ "pi",
10
+ "coding-agent",
11
+ "cli",
12
+ "ai",
13
+ "llm"
14
+ ],
5
15
  "type": "module",
6
16
  "engines": {
7
17
  "node": ">=25"
@@ -27,8 +37,10 @@
27
37
  "dependencies": {
28
38
  "@earendil-works/pi-ai": "^0.84.2",
29
39
  "@earendil-works/pi-coding-agent": "^0.84.2",
30
- "pi-mcp-adapter": "^2.26.0",
31
- "typebox": "^1.3.14"
40
+ "@juicesharp/rpiv-todo": "^2.6.2",
41
+ "@juicesharp/rpiv-voice": "^2.6.2",
42
+ "pi-mcp-adapter": "^2.26.1",
43
+ "typebox": "^1.3.15"
32
44
  },
33
45
  "bundledDependencies": [
34
46
  "@earendil-works/pi-coding-agent"
@@ -42,7 +54,8 @@
42
54
  "dist",
43
55
  "src/prism-extensions",
44
56
  "themes",
45
- "README.md"
57
+ "README.md",
58
+ "LICENSE"
46
59
  ],
47
60
  "pi": {
48
61
  "extensions": [
@@ -2,7 +2,7 @@
2
2
  Prism extension code is grouped by responsibility:
3
3
  - `commands/` — slash-command extensions (`/caveman`, `/plan`, `/git-commit`, `/self-improve`, `/eval`, `/vote`, `/prompt-review`, terminal clearing).
4
4
  - `core/` — shared config and utility primitives used by multiple extensions.
5
- - `integrations/` — external integrations and agent tools (Obsidian memory, AI Memory search/read, web research).
5
+ - `integrations/` — external integrations and agent tools (Obsidian memory, AI Memory search/read, web research, mex code graph).
6
6
  - `providers/` — optional model/provider adapters (Ollama, Manifest).
7
7
  - `tools/` — toolbox registry, hidden built-in toolbox tools, and self-improvement persistence tools.
8
8
  - `ui/` — toolbar, status, context usage and tool-call rendering helpers.
@@ -17,3 +17,12 @@ Prism's self-improvement helpers are intentionally human-gated:
17
17
  - `/prompt-review <focus>` reviews prompts/behavior patterns and proposes safer candidates without auto-applying them.
18
18
 
19
19
  See `docs/safe-harness-improvement.md` for the full workflow and safety boundaries.
20
+
21
+ ## mex code graph
22
+ `integrations/mex-memory.ts` only loads when the `mex` CLI (`npm install -g mex-agent`) is installed **and** the current repo has a `.mex/` scaffold (`npx mex-agent setup`). It then exposes `mex_scope`, `mex_get`, `mex_query`, `mex_impact`, `mex_log` plus the `/mex` command. Maintenance commands (`mex_graph_build`, `mex_check`, `mex_timeline`) live in the hidden toolbox.
23
+
24
+ A missing `.mex/graph.db` is rebuilt automatically via `core/mex-graph-db.ts`: in the background on session start and lazily before the first graph-reading tool call. It requires an existing `.mex/` scaffold; without one nothing is built. Builds are deduplicated per scaffold and failures degrade to mex's own `GRAPH_UNAVAILABLE` handling.
25
+
26
+ The `🕸 mex` toolbar segment only appears when mex actually works, i.e. scaffold **and** a non-empty `graph.db`. While a build runs it shows `🕸 mex ⟳`; disabled, scaffold-less or failed states show nothing.
27
+
28
+ Env switches: `PRISM_DISABLE_MEX=1` (off), `PRISM_MEX_FORCE=1` (load without scaffold), `PRISM_MEX_BIN` (custom binary), `PRISM_MEX_TELEMETRY=1` (allow mex telemetry; off by default), `PRISM_DISABLE_MEX_AUTO_GRAPH=1` (never auto-build `graph.db`).
@@ -0,0 +1,29 @@
1
+ import * as fs from "node:fs"
2
+ import * as path from "node:path"
3
+ import { isMexAvailable, mexScaffoldDir } from "./mex-cli.js"
4
+
5
+ /** Anchor file every mex scaffold exposes as its always-loaded entry point. */
6
+ export const MEX_ANCHOR_FILE = "AGENTS.md"
7
+
8
+ export function mexAnchorPath(cwd: string = process.cwd()): string {
9
+ return path.join(mexScaffoldDir(cwd), MEX_ANCHOR_FILE)
10
+ }
11
+
12
+ export function isMexAnchorEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
13
+ return env.PRISM_DISABLE_MEX_ANCHOR !== "1"
14
+ }
15
+
16
+ /**
17
+ * Absolute path of `.mex/AGENTS.md` when it should be loaded as a pi context file,
18
+ * i.e. the mex CLI plus scaffold are available and the anchor is not disabled.
19
+ */
20
+ export function mexAnchorContextFile(cwd: string = process.cwd(), env: NodeJS.ProcessEnv = process.env): string | undefined {
21
+ if (!isMexAnchorEnabled(env)) return undefined
22
+ if (!isMexAvailable(env, cwd)) return undefined
23
+ const anchor = mexAnchorPath(cwd)
24
+ try {
25
+ return fs.statSync(anchor).isFile() ? anchor : undefined
26
+ } catch {
27
+ return undefined
28
+ }
29
+ }
@@ -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