@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.
- package/LICENSE +21 -0
- package/README.md +386 -181
- package/dist/pi/pi-context-files.d.ts +6 -0
- package/dist/pi/pi-context-files.js +17 -0
- package/dist/pi/pi-context-files.js.map +1 -0
- package/dist/pi/pi-extensions.d.ts +0 -1
- package/dist/pi/pi-extensions.js +7 -22
- package/dist/pi/pi-extensions.js.map +1 -1
- package/dist/pi/pi-npm-extension-entry.d.ts +2 -0
- package/dist/pi/pi-npm-extension-entry.js +74 -0
- package/dist/pi/pi-npm-extension-entry.js.map +1 -0
- package/dist/pi/pi-package.js +4 -0
- package/dist/pi/pi-package.js.map +1 -1
- package/dist/pi/pi-patch-context-files.d.ts +12 -0
- package/dist/pi/pi-patch-context-files.js +46 -0
- package/dist/pi/pi-patch-context-files.js.map +1 -0
- package/dist/pi/pi-patch-interactive.js +5 -1
- package/dist/pi/pi-patch-interactive.js.map +1 -1
- package/dist/pi/plan-mode-tools.d.ts +13 -0
- package/dist/pi/plan-mode-tools.js +54 -0
- package/dist/pi/plan-mode-tools.js.map +1 -0
- package/dist/prism-extensions/core/mex-anchor.d.ts +9 -0
- package/dist/prism-extensions/core/mex-anchor.js +29 -0
- package/dist/prism-extensions/core/mex-anchor.js.map +1 -0
- package/dist/prism-extensions/core/mex-cli.d.ts +68 -0
- package/dist/prism-extensions/core/mex-cli.js +183 -0
- package/dist/prism-extensions/core/mex-cli.js.map +1 -0
- package/dist/prism-extensions/core/mex-graph-db.d.ts +26 -0
- package/dist/prism-extensions/core/mex-graph-db.js +69 -0
- package/dist/prism-extensions/core/mex-graph-db.js.map +1 -0
- package/dist/prism-extensions/integrations/ai-memory-system.js +27 -6
- package/dist/prism-extensions/integrations/ai-memory-system.js.map +1 -1
- package/dist/prism-extensions/integrations/mex-memory.d.ts +21 -0
- package/dist/prism-extensions/integrations/mex-memory.js +231 -0
- package/dist/prism-extensions/integrations/mex-memory.js.map +1 -0
- package/dist/prism-extensions/tools/builtin-tools.js +2 -0
- package/dist/prism-extensions/tools/builtin-tools.js.map +1 -1
- package/dist/prism-extensions/tools/mex-tools.d.ts +2 -0
- package/dist/prism-extensions/tools/mex-tools.js +78 -0
- package/dist/prism-extensions/tools/mex-tools.js.map +1 -0
- package/dist/prism-extensions/tools/prism-diagnostic-tools.js +10 -10
- package/dist/prism-extensions/tools/prism-diagnostic-tools.js.map +1 -1
- package/dist/prism-extensions/tools/toolbox.js +1 -1
- package/dist/prism-extensions/tools/toolbox.js.map +1 -1
- package/dist/prism-extensions/ui/toolbar.d.ts +2 -0
- package/dist/prism-extensions/ui/toolbar.js +10 -1
- package/dist/prism-extensions/ui/toolbar.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +15 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -6
- package/package.json +17 -4
- package/src/prism-extensions/README.md +10 -1
- package/src/prism-extensions/core/mex-anchor.ts +29 -0
- package/src/prism-extensions/core/mex-cli.ts +217 -0
- package/src/prism-extensions/core/mex-graph-db.ts +76 -0
- package/src/prism-extensions/integrations/ai-memory-system.ts +30 -6
- package/src/prism-extensions/integrations/mex-memory.ts +264 -0
- package/src/prism-extensions/tools/builtin-tools.ts +2 -0
- package/src/prism-extensions/tools/mex-tools.ts +93 -0
- package/src/prism-extensions/tools/prism-diagnostic-tools.ts +10 -10
- package/src/prism-extensions/tools/toolbox.ts +1 -1
- package/src/prism-extensions/ui/toolbar.ts +10 -1
- package/src/prism-extensions/commands/voice-command.ts +0 -49
- package/src/prism-extensions/core/voice-runtime.ts +0 -322
- package/src/prism-extensions/core/voicebox-client.ts +0 -205
- package/src/prism-extensions/core/voicebox-service.ts +0 -137
- 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 {
|
package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
CHANGED
|
@@ -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", "
|
|
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.
|
|
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", "
|
|
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", "
|
|
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.
|
|
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
|
-
"
|
|
31
|
-
"
|
|
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
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
681
|
+
reportOnceThisTurn("candidate injection", error)
|
|
658
682
|
}
|
|
659
683
|
}
|
|
660
684
|
|