@daniel156161/prism 0.2.92 → 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.
- package/README.md +0 -50
- 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/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 +5 -3
- 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/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
|
@@ -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
|
|
|
@@ -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[] {
|