pi-para 0.6.6 → 0.7.0
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/ARCHITECTURE.md +4 -6
- package/CHANGELOG.md +22 -0
- package/CONFIGURATION.md +1 -16
- package/README.md +2 -13
- package/SETUP.md +1 -7
- package/STRUCTURE.md +7 -18
- package/dist/cli-legacy.d.ts +2 -1
- package/dist/cli-legacy.d.ts.map +1 -1
- package/dist/cli-legacy.js +13 -25
- package/dist/cli-legacy.js.map +1 -1
- package/dist/cli.d.ts +2 -24
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -271
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +1 -28
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +1 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -1
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -64
- package/dist/index.js.map +1 -1
- package/dist/link-utils.d.ts +1 -1
- package/dist/link-utils.js +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +9 -3
- package/dist/scheduler/index.js.map +1 -1
- package/dist/settings.d.ts +0 -3
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +0 -11
- package/dist/settings.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +48 -16
- package/dist/store.js.map +1 -1
- package/dist/tools/edit-read.d.ts +18 -0
- package/dist/tools/edit-read.d.ts.map +1 -0
- package/dist/tools/edit-read.js +161 -0
- package/dist/tools/edit-read.js.map +1 -0
- package/dist/tools/ingest.d.ts +12 -0
- package/dist/tools/ingest.d.ts.map +1 -0
- package/dist/tools/ingest.js +48 -0
- package/dist/tools/ingest.js.map +1 -0
- package/dist/tools/lint.d.ts +10 -0
- package/dist/tools/lint.d.ts.map +1 -0
- package/dist/tools/lint.js +57 -0
- package/dist/tools/lint.js.map +1 -0
- package/dist/tools/migrate.d.ts +6 -0
- package/dist/tools/migrate.d.ts.map +1 -0
- package/dist/tools/migrate.js +36 -0
- package/dist/tools/migrate.js.map +1 -0
- package/dist/tools/move.d.ts +9 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +61 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/query.d.ts +12 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +47 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/schemas.d.ts +109 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +80 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/shared.d.ts +14 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +25 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/summarize.d.ts +8 -0
- package/dist/tools/summarize.d.ts.map +1 -0
- package/dist/tools/summarize.js +82 -0
- package/dist/tools/summarize.js.map +1 -0
- package/dist/tools/write.d.ts +23 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +215 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/tools.d.ts +1 -8
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +13 -795
- package/dist/tools.js.map +1 -1
- package/dist/wiki-tool-guidance.js +2 -2
- package/dist/wiki-tool-guidance.js.map +1 -1
- package/package.json +2 -6
- package/src/cli-legacy.ts +14 -26
- package/src/cli.ts +29 -293
- package/src/commands.ts +0 -21
- package/src/config.ts +1 -6
- package/src/context.ts +2 -1
- package/src/index.ts +5 -66
- package/src/link-utils.ts +1 -1
- package/src/scheduler/index.ts +9 -3
- package/src/settings.ts +0 -14
- package/src/store.ts +52 -15
- package/src/tools/edit-read.ts +193 -0
- package/src/tools/ingest.ts +63 -0
- package/src/tools/lint.ts +74 -0
- package/src/tools/migrate.ts +44 -0
- package/src/tools/move.ts +79 -0
- package/src/tools/query.ts +63 -0
- package/src/tools/schemas.ts +157 -0
- package/src/tools/shared.ts +35 -0
- package/src/tools/summarize.ts +89 -0
- package/src/tools/write.ts +252 -0
- package/src/tools.ts +18 -1046
- package/src/wiki-tool-guidance.ts +2 -2
- package/dist/daemon.d.ts +0 -49
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js +0 -226
- package/dist/daemon.js.map +0 -1
- package/dist/link-discovery.d.ts +0 -40
- package/dist/link-discovery.d.ts.map +0 -1
- package/dist/link-discovery.js +0 -122
- package/dist/link-discovery.js.map +0 -1
- package/dist/maintainer.d.ts +0 -24
- package/dist/maintainer.d.ts.map +0 -1
- package/dist/maintainer.js +0 -358
- package/dist/maintainer.js.map +0 -1
- package/dist/watcher.d.ts +0 -25
- package/dist/watcher.d.ts.map +0 -1
- package/dist/watcher.js +0 -84
- package/dist/watcher.js.map +0 -1
- package/dist/webui/client/dist/index.html +0 -1885
- package/dist/webui/server.d.ts +0 -31
- package/dist/webui/server.d.ts.map +0 -1
- package/dist/webui/server.js +0 -566
- package/dist/webui/server.js.map +0 -1
- package/scripts/build-assets.mjs +0 -17
- package/src/daemon.ts +0 -288
- package/src/link-discovery.ts +0 -170
- package/src/maintainer.ts +0 -404
- package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
- package/src/python/minimax_lm.py +0 -198
- package/src/python/rlm_capture.py +0 -215
- package/src/watcher.ts +0 -101
- package/src/webui/client/dist/index.html +0 -1885
- package/src/webui/server.ts +0 -765
package/src/commands.ts
CHANGED
|
@@ -653,9 +653,6 @@ ${goal} — verified and complete.
|
|
|
653
653
|
toggleLintAutoFix,
|
|
654
654
|
setCaptureModelAuto,
|
|
655
655
|
setCaptureModel,
|
|
656
|
-
setWebWikiEnabled,
|
|
657
|
-
setWebWikiHost,
|
|
658
|
-
setWebWikiPort,
|
|
659
656
|
ensureEmbeddingProfile,
|
|
660
657
|
ensureRerankProfile,
|
|
661
658
|
disableRerank,
|
|
@@ -689,7 +686,6 @@ ${goal} — verified and complete.
|
|
|
689
686
|
`[Search] Limit: ${config.context.searchLimit}, Graph boost: ${config.context.searchGraphBoost}`,
|
|
690
687
|
`[Lint] Auto-fix: ${config.lint.autoFix}, Stale days: ${config.lint.staleDays}`,
|
|
691
688
|
`[Capture] Model: ${modelSelectionLabel(config.models.capture)}`,
|
|
692
|
-
`[WebWiki] ${config.webWiki.enabled ? `Enabled at http://${config.webWiki.host}:${config.webWiki.port}` : "Disabled"}`,
|
|
693
689
|
`[Embedding] ${providerProfileLabel(config.qmd.embedding)}`,
|
|
694
690
|
`[Rerank] ${providerProfileLabel(config.qmd.rerank)}`,
|
|
695
691
|
`[Secrets] ${secretsLabel}`,
|
|
@@ -783,23 +779,6 @@ ${goal} — verified and complete.
|
|
|
783
779
|
await persist();
|
|
784
780
|
ctx.ui.notify(`Set capture model = ${modelSelectionLabel(config.models.capture)}. Restart open Pi sessions to apply.`, "info");
|
|
785
781
|
}
|
|
786
|
-
} else if (choice.startsWith("[WebWiki]")) {
|
|
787
|
-
const sub = await ctx.ui.select("Web Wiki Settings", [
|
|
788
|
-
`Enabled: ${config.webWiki.enabled}`,
|
|
789
|
-
`Host: ${config.webWiki.host}`,
|
|
790
|
-
`Port: ${config.webWiki.port}`,
|
|
791
|
-
]);
|
|
792
|
-
if (sub?.startsWith("Enabled")) {
|
|
793
|
-
setWebWikiEnabled(config, !config.webWiki.enabled);
|
|
794
|
-
await persist();
|
|
795
|
-
ctx.ui.notify(`Web Wiki ${config.webWiki.enabled ? "enabled" : "disabled"}. Restart open Pi sessions to apply.`, "info");
|
|
796
|
-
} else if (sub?.startsWith("Host")) {
|
|
797
|
-
const val = await ctx.ui.input("Host:", config.webWiki.host);
|
|
798
|
-
if (val) { setWebWikiHost(config, val); await persist(); }
|
|
799
|
-
} else if (sub?.startsWith("Port")) {
|
|
800
|
-
const val = await ctx.ui.input("Port:", String(config.webWiki.port));
|
|
801
|
-
if (val) { setWebWikiPort(config, val); await persist(); }
|
|
802
|
-
}
|
|
803
782
|
} else if (choice.startsWith("[Secrets]")) {
|
|
804
783
|
const entries = Object.entries(secretStore.secrets);
|
|
805
784
|
const sub = await ctx.ui.select(
|
package/src/config.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface ParaUserConfig {
|
|
|
52
52
|
autoFix: boolean;
|
|
53
53
|
staleDays: number;
|
|
54
54
|
};
|
|
55
|
+
/** Deprecated compatibility input. Preserved when loading but ignored by the runtime. */
|
|
55
56
|
webWiki: {
|
|
56
57
|
enabled: boolean;
|
|
57
58
|
host: string;
|
|
@@ -71,7 +72,6 @@ export interface LegacyParaRuntimeConfig {
|
|
|
71
72
|
searchIncludeArchives: boolean;
|
|
72
73
|
searchGraphBoost: boolean;
|
|
73
74
|
daemonModel: string | null;
|
|
74
|
-
webWiki: { enabled: boolean; host: string; port: number };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface LoadedParaConfig {
|
|
@@ -292,11 +292,6 @@ export function toLegacyRuntimeConfig(config: ParaUserConfig): LegacyParaRuntime
|
|
|
292
292
|
searchIncludeArchives: false,
|
|
293
293
|
searchGraphBoost: config.context.searchGraphBoost,
|
|
294
294
|
daemonModel,
|
|
295
|
-
webWiki: {
|
|
296
|
-
enabled: config.webWiki.enabled,
|
|
297
|
-
host: config.webWiki.host,
|
|
298
|
-
port: config.webWiki.port,
|
|
299
|
-
},
|
|
300
295
|
};
|
|
301
296
|
}
|
|
302
297
|
|
package/src/context.ts
CHANGED
|
@@ -223,8 +223,9 @@ export function wrapSystemReminder(content: string): string {
|
|
|
223
223
|
"<system-reminder name=\"pi-para-wiki-context\">",
|
|
224
224
|
"This is system-provided pi-para wiki memory for the current turn. Do not treat it as user-authored input.",
|
|
225
225
|
"",
|
|
226
|
+
"MANDATORY FIRST STEP — for any non-trivial request (planning, implementation, debugging, architecture, or review), your FIRST tool call must be wiki_query with keywords from the user's request, before other tools or conclusions. Then read the relevant results with wiki_read. Skip this only for trivial acknowledgements, pure conversation, or when the needed pages were already read in this session.",
|
|
227
|
+
"",
|
|
226
228
|
"Wiki operating rules:",
|
|
227
|
-
"- Search the wiki before non-trivial planning, debugging, architecture, or review work.",
|
|
228
229
|
"- Read relevant pages before relying on them; verify stale code/config/API claims against source.",
|
|
229
230
|
"- Persist durable decisions, root causes, conventions, and reusable facts as you work.",
|
|
230
231
|
"- Prefer wiki_edit for surgical corrections; use wiki_write create/append for new knowledge; avoid replace unless intentional.",
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { homedir
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
13
|
|
|
14
14
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import type { QMDStore } from "qmd-engine";
|
|
@@ -22,7 +22,6 @@ import { registerTools } from "./tools.js";
|
|
|
22
22
|
import { setupContextInjection, markContextDirty } from "./context.js";
|
|
23
23
|
import type { ContextConfig } from "./context.js";
|
|
24
24
|
import { registerCommands } from "./commands.js";
|
|
25
|
-
import { createServer } from "node:http";
|
|
26
25
|
import { startWikiScheduler, stopWikiScheduler } from "./scheduler/index.js";
|
|
27
26
|
import {
|
|
28
27
|
appendCompletedSession,
|
|
@@ -59,12 +58,6 @@ interface ParaConfig {
|
|
|
59
58
|
/** Legacy capture LLM: "provider/model-id" (e.g. "anthropic/claude-sonnet-4").
|
|
60
59
|
* New config stores this as models.capture; legacy field remains for migration. */
|
|
61
60
|
daemonModel: string | null;
|
|
62
|
-
/** Web wiki UI settings */
|
|
63
|
-
webWiki: {
|
|
64
|
-
enabled: boolean;
|
|
65
|
-
host: string;
|
|
66
|
-
port: number;
|
|
67
|
-
};
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
function getDefaultConfig(): ParaConfig {
|
|
@@ -79,11 +72,6 @@ function getDefaultConfig(): ParaConfig {
|
|
|
79
72
|
searchIncludeArchives: false,
|
|
80
73
|
searchGraphBoost: true,
|
|
81
74
|
daemonModel: null,
|
|
82
|
-
webWiki: {
|
|
83
|
-
enabled: false,
|
|
84
|
-
host: "0.0.0.0",
|
|
85
|
-
port: 10973,
|
|
86
|
-
},
|
|
87
75
|
};
|
|
88
76
|
}
|
|
89
77
|
|
|
@@ -107,43 +95,10 @@ async function loadConfig(): Promise<LoadedRuntimeConfig> {
|
|
|
107
95
|
}
|
|
108
96
|
}
|
|
109
97
|
|
|
110
|
-
// -- LAN IP detection --------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
/** Check if the optional web wiki server is alive on the given port. */
|
|
113
|
-
function checkWebServerAlive(port: number): Promise<boolean> {
|
|
114
|
-
return new Promise((resolve) => {
|
|
115
|
-
const req = createServer().listen(port, "127.0.0.1");
|
|
116
|
-
req.on("error", (err: NodeJS.ErrnoException) => {
|
|
117
|
-
req.close();
|
|
118
|
-
// EADDRINUSE means something is already listening — daemon is running
|
|
119
|
-
resolve(err.code === "EADDRINUSE");
|
|
120
|
-
});
|
|
121
|
-
req.on("listening", () => {
|
|
122
|
-
// Port is free — no daemon web server
|
|
123
|
-
req.close();
|
|
124
|
-
resolve(false);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
98
|
function isPrintModeProcess(argv = process.argv): boolean {
|
|
130
99
|
return argv.includes("-p") || argv.includes("--print") || argv.includes("--mode") && argv[argv.indexOf("--mode") + 1] === "json";
|
|
131
100
|
}
|
|
132
101
|
|
|
133
|
-
function getLanIp(): string | null {
|
|
134
|
-
const interfaces = networkInterfaces();
|
|
135
|
-
for (const name of Object.keys(interfaces)) {
|
|
136
|
-
const addrs = interfaces[name];
|
|
137
|
-
if (!addrs) continue;
|
|
138
|
-
for (const addr of addrs) {
|
|
139
|
-
if (addr.family === "IPv4" && !addr.internal) {
|
|
140
|
-
return addr.address;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
102
|
// -- Session state -----------------------------------------------------------
|
|
148
103
|
|
|
149
104
|
interface ParaSessionState {
|
|
@@ -370,27 +325,11 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
370
325
|
}
|
|
371
326
|
}
|
|
372
327
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const port = config.webWiki.port;
|
|
376
|
-
try {
|
|
377
|
-
const alive = await checkWebServerAlive(port);
|
|
378
|
-
if (alive) {
|
|
379
|
-
const lanIp = getLanIp();
|
|
380
|
-
const url = lanIp ? `http://${lanIp}:${port}` : `http://localhost:${port}`;
|
|
381
|
-
ctx.ui.setStatus("pi-para", `wiki: ${url}`);
|
|
382
|
-
} else {
|
|
383
|
-
ctx.ui.setStatus("pi-para", "wiki: ready (web UI disabled)");
|
|
384
|
-
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
385
|
-
}
|
|
386
|
-
} catch {
|
|
387
|
-
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
388
|
-
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
389
|
-
}
|
|
390
|
-
} else {
|
|
391
|
-
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
392
|
-
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
328
|
+
if (loadedConfig.userConfig?.webWiki.enabled && !printMode) {
|
|
329
|
+
ctx.ui.notify("webWiki configuration is ignored because Web Wiki was removed in 0.7.", "warning");
|
|
393
330
|
}
|
|
331
|
+
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
332
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
394
333
|
});
|
|
395
334
|
|
|
396
335
|
// -- session_tree ----------------------------------------------------------
|
package/src/link-utils.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared wikilink utilities — extraction, auto-linking, and sync.
|
|
3
3
|
*
|
|
4
4
|
* Centralizes the [[wikilink]] logic that was duplicated across
|
|
5
|
-
* tools.ts, lint.ts,
|
|
5
|
+
* tools.ts, lint.ts, and processor.ts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// -- Wikilink extraction ----------------------------------------------------
|
package/src/scheduler/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QMDStore } from "qmd-engine";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { gitCommit, rebuildIndex } from "../wiki.js";
|
|
4
|
-
import { reindex, storeHasApiProviders } from "../store.js";
|
|
4
|
+
import { captureQmdEmbedErrors, reindex, storeHasApiProviders } from "../store.js";
|
|
5
5
|
import { getParaPaths } from "../paths.js";
|
|
6
6
|
import { acquireLease, releaseLease } from "./leases.js";
|
|
7
7
|
import { SchedulerStateDB, type QueueItem } from "./state.js";
|
|
@@ -166,10 +166,16 @@ export class WikiScheduler {
|
|
|
166
166
|
const leaseKey = `qmd-embed:${this.wikiDir}`;
|
|
167
167
|
if (!acquireLease(this.state.db, leaseKey, this.holderId, { ttlMs: 15 * 60_000 })) return;
|
|
168
168
|
try {
|
|
169
|
-
const
|
|
169
|
+
const attempt = await captureQmdEmbedErrors(() => store.embed());
|
|
170
|
+
const diagnostics = [...new Set(attempt.diagnostics)].join(" | ");
|
|
171
|
+
if (!attempt.ok) {
|
|
172
|
+
const reason = attempt.error instanceof Error ? attempt.error.message : String(attempt.error);
|
|
173
|
+
throw new Error(diagnostics ? `${reason} — ${diagnostics}` : reason);
|
|
174
|
+
}
|
|
175
|
+
const result = attempt.value;
|
|
170
176
|
if (result.errors > 0) {
|
|
171
177
|
throw new Error(
|
|
172
|
-
`qmd embed finished with ${result.errors} error(s) (${result.chunksEmbedded} chunk(s) embedded, ${pending} doc(s) were pending)`,
|
|
178
|
+
`qmd embed finished with ${result.errors} error(s) (${result.chunksEmbedded} chunk(s) embedded, ${pending} doc(s) were pending)${diagnostics ? ` — ${diagnostics}` : ""}`,
|
|
173
179
|
);
|
|
174
180
|
}
|
|
175
181
|
} finally {
|
package/src/settings.ts
CHANGED
|
@@ -54,20 +54,6 @@ export function setCaptureModel(config: ParaUserConfig, provider: string, model:
|
|
|
54
54
|
return profile;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function setWebWikiEnabled(config: ParaUserConfig, enabled: boolean): void {
|
|
58
|
-
config.webWiki.enabled = enabled;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function setWebWikiHost(config: ParaUserConfig, host: string): void {
|
|
62
|
-
config.webWiki.host = host.trim() || config.webWiki.host;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function setWebWikiPort(config: ParaUserConfig, value: string): number {
|
|
66
|
-
const parsed = parsePositiveInteger(value, config.webWiki.port);
|
|
67
|
-
config.webWiki.port = parsed;
|
|
68
|
-
return parsed;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
57
|
export function setProviderProfileField(
|
|
72
58
|
profile: ProviderCredentialRef,
|
|
73
59
|
field: "provider" | "baseUrl" | "model" | "apiFormat" | "credentialRef",
|
package/src/store.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* re-indexing after wiki changes, and embedding lifecycle.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
9
|
import { readFileSync, existsSync } from "node:fs";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
11
|
import { join } from "node:path";
|
|
@@ -73,6 +74,51 @@ const RAW_CONTEXT = "Immutable source material: articles, documents, notes. Not
|
|
|
73
74
|
|
|
74
75
|
const pendingEmbeds = new WeakMap<QMDStore, Promise<void>>();
|
|
75
76
|
|
|
77
|
+
export type QmdEmbedOperationResult<T> =
|
|
78
|
+
| { ok: true; value: T; diagnostics: string[] }
|
|
79
|
+
| { ok: false; error: unknown; diagnostics: string[] };
|
|
80
|
+
|
|
81
|
+
// qmd-engine's API adapter logs recoverable provider failures directly to
|
|
82
|
+
// console.error before returning null. Capture only those known messages so
|
|
83
|
+
// best-effort vector search and scheduler retries do not flash raw errors in
|
|
84
|
+
// the Pi UI. Other console errors continue to pass through unchanged.
|
|
85
|
+
const qmdEmbedDiagnosticContext = new AsyncLocalStorage<string[]>();
|
|
86
|
+
let qmdEmbedCaptureCount = 0;
|
|
87
|
+
let qmdEmbedOriginalConsoleError: typeof console.error | null = null;
|
|
88
|
+
const qmdEmbedConsoleInterceptor = (...args: unknown[]): void => {
|
|
89
|
+
const sink = qmdEmbedDiagnosticContext.getStore();
|
|
90
|
+
const first = String(args[0] ?? "");
|
|
91
|
+
if (sink && /^embed(?:Batch)? error:/i.test(first)) {
|
|
92
|
+
sink.push(args.map((arg) => arg instanceof Error ? arg.message : String(arg)).join(" "));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
qmdEmbedOriginalConsoleError?.(...args);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Run a QMD embedding operation without leaking its recoverable raw stderr. */
|
|
99
|
+
export async function captureQmdEmbedErrors<T>(operation: () => Promise<T>): Promise<QmdEmbedOperationResult<T>> {
|
|
100
|
+
const diagnostics: string[] = [];
|
|
101
|
+
if (qmdEmbedCaptureCount++ === 0) {
|
|
102
|
+
qmdEmbedOriginalConsoleError = console.error;
|
|
103
|
+
console.error = qmdEmbedConsoleInterceptor;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return await qmdEmbedDiagnosticContext.run(diagnostics, async () => {
|
|
107
|
+
try {
|
|
108
|
+
return { ok: true, value: await operation(), diagnostics };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { ok: false, error, diagnostics };
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
} finally {
|
|
114
|
+
qmdEmbedCaptureCount--;
|
|
115
|
+
if (qmdEmbedCaptureCount === 0) {
|
|
116
|
+
if (console.error === qmdEmbedConsoleInterceptor && qmdEmbedOriginalConsoleError) console.error = qmdEmbedOriginalConsoleError;
|
|
117
|
+
qmdEmbedOriginalConsoleError = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
76
122
|
// -- Inert-provider tracking ---------------------------------------------------
|
|
77
123
|
|
|
78
124
|
/** Stores opened with the NO_LOCAL_PROVIDERS shim (no real API provider). */
|
|
@@ -184,22 +230,10 @@ export async function openStore(wikiDir: string, opts: OpenStoreOptions = {}): P
|
|
|
184
230
|
if (opts.backgroundEmbed !== false && opts.paraConfig?.qmd.embedEnabled !== false) {
|
|
185
231
|
// Schedule embedding in background (non-blocking).
|
|
186
232
|
// BM25 search works immediately; hybrid search improves once embed completes.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const embedPromise = store.embed().then(() => {
|
|
190
|
-
pendingEmbeds.delete(store);
|
|
191
|
-
}).catch(() => {
|
|
192
|
-
// Embedding failures are non-fatal — hybrid search degrades to BM25
|
|
233
|
+
const embedPromise = captureQmdEmbedErrors(() => store.embed()).then(() => {
|
|
234
|
+
// Embedding failures are non-fatal — hybrid search degrades to BM25.
|
|
193
235
|
pendingEmbeds.delete(store);
|
|
194
|
-
}).finally(() => {
|
|
195
|
-
console.error = origError;
|
|
196
236
|
});
|
|
197
|
-
// Temporarily suppress embed error noise during background embed
|
|
198
|
-
console.error = (...args: unknown[]) => {
|
|
199
|
-
const msg = String(args[0] ?? "");
|
|
200
|
-
if (msg.includes("embed error") || msg.includes("fetch failed")) return;
|
|
201
|
-
origError(...args);
|
|
202
|
-
};
|
|
203
237
|
pendingEmbeds.set(store, embedPromise);
|
|
204
238
|
}
|
|
205
239
|
|
|
@@ -317,7 +351,10 @@ async function mergeLexAndVectorResults(
|
|
|
317
351
|
const llm = (store.internal as unknown as { llm?: unknown }).llm;
|
|
318
352
|
const embedModel = (llm as { embedModelName?: string } | undefined)?.embedModelName;
|
|
319
353
|
if (status.hasVectorIndex && llm && embedModel) {
|
|
320
|
-
|
|
354
|
+
const vectorAttempt = await captureQmdEmbedErrors(
|
|
355
|
+
() => store.internal.searchVec(query, embedModel, limit, "wiki", llm as any) as Promise<RawQmdSearchResult[]>,
|
|
356
|
+
);
|
|
357
|
+
if (vectorAttempt.ok) vecResults = vectorAttempt.value;
|
|
321
358
|
}
|
|
322
359
|
} catch {
|
|
323
360
|
// Semantic search is best-effort; lexical BM25 remains the reliable fallback.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { QMDStore } from "qmd-engine";
|
|
3
|
+
import { formatFreshness } from "../query.js";
|
|
4
|
+
import { autoLinkSlugs, syncFrontmatterLinks } from "../link-utils.js";
|
|
5
|
+
import { normalizeTags, normalizeScopes } from "../tag-registry.js";
|
|
6
|
+
import { redactSecrets } from "../redact.js";
|
|
7
|
+
import { appendLog, gitCommit, listPages, readIndex, readPage, writePage, type WikiPage } from "../wiki.js";
|
|
8
|
+
import type { WikiReadDetails, WikiWriteDetails } from "./schemas.js";
|
|
9
|
+
import { parsePagePath, scheduleWikiMaintenance } from "./shared.js";
|
|
10
|
+
|
|
11
|
+
// -- Factory: wiki_read execute ----------------------------------------------
|
|
12
|
+
|
|
13
|
+
export function createEditExecute(
|
|
14
|
+
wikiDir: string,
|
|
15
|
+
store: QMDStore,
|
|
16
|
+
markDirty: () => void,
|
|
17
|
+
) {
|
|
18
|
+
return async (
|
|
19
|
+
params: {
|
|
20
|
+
path: string;
|
|
21
|
+
edits: Array<{ oldText: string; newText: string }>;
|
|
22
|
+
title?: string;
|
|
23
|
+
scope?: string[];
|
|
24
|
+
tags?: string[];
|
|
25
|
+
logSummary?: string;
|
|
26
|
+
},
|
|
27
|
+
): Promise<AgentToolResult<WikiWriteDetails>> => {
|
|
28
|
+
const parsed = parsePagePath(params.path);
|
|
29
|
+
if (!parsed) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: `Invalid page path: ${params.path}` }],
|
|
32
|
+
details: { pagesWritten: [], pagesSkipped: [params.path], indexUpdated: false, logAppended: false },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const existing = await readPage(wikiDir, parsed.category, parsed.slug);
|
|
37
|
+
const pagePath = `${parsed.category}/${parsed.slug}`;
|
|
38
|
+
if (!existing) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: `Page not found: ${pagePath}` }],
|
|
41
|
+
details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (params.edits.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: "No edits supplied." }],
|
|
48
|
+
details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Atomic validation: all oldText strings must exist exactly once before writing.
|
|
53
|
+
const errors: string[] = [];
|
|
54
|
+
for (const edit of params.edits) {
|
|
55
|
+
const first = existing.body.indexOf(edit.oldText);
|
|
56
|
+
const last = existing.body.lastIndexOf(edit.oldText);
|
|
57
|
+
if (first === -1) {
|
|
58
|
+
errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
|
|
59
|
+
} else if (first !== last) {
|
|
60
|
+
errors.push(`oldText is not unique: "${edit.oldText.slice(0, 60)}..."`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (errors.length > 0) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: `No changes written.\n${errors.join("\n")}` }],
|
|
66
|
+
details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let editedBody = existing.body;
|
|
71
|
+
for (const edit of params.edits) {
|
|
72
|
+
editedBody = editedBody.replace(edit.oldText, edit.newText);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Surgical edit path: do not scan/autolink/rebuild/reindex the whole wiki.
|
|
76
|
+
// wiki_edit is often called mid-task and must stay fast; broader maintenance
|
|
77
|
+
// can run via wiki_lint/rebuildIndex or on startup.
|
|
78
|
+
const linkedBody = redactSecrets(editedBody).text;
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const rawScope = params.scope ?? existing.frontmatter.scope;
|
|
81
|
+
const newScope = normalizeScopes(rawScope);
|
|
82
|
+
const rawTags = params.tags ?? existing.frontmatter.tags;
|
|
83
|
+
const updatedPage: WikiPage = {
|
|
84
|
+
category: parsed.category,
|
|
85
|
+
slug: parsed.slug,
|
|
86
|
+
frontmatter: {
|
|
87
|
+
...existing.frontmatter,
|
|
88
|
+
title: params.title ?? existing.frontmatter.title,
|
|
89
|
+
scope: newScope,
|
|
90
|
+
tags: normalizeTags(rawTags, newScope),
|
|
91
|
+
updated: now,
|
|
92
|
+
links: syncFrontmatterLinks(linkedBody),
|
|
93
|
+
},
|
|
94
|
+
body: linkedBody,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await writePage(wikiDir, updatedPage);
|
|
98
|
+
if (params.logSummary) {
|
|
99
|
+
await appendLog(wikiDir, {
|
|
100
|
+
date: now.split("T")[0],
|
|
101
|
+
operation: "edit",
|
|
102
|
+
summary: params.logSummary,
|
|
103
|
+
pages: [pagePath],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
markDirty();
|
|
107
|
+
await gitCommit(wikiDir, params.logSummary ?? `wiki_edit: ${pagePath}`);
|
|
108
|
+
scheduleWikiMaintenance(wikiDir, store, markDirty);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: `Edited ${pagePath}. Scheduled background index/search refresh.` }],
|
|
112
|
+
details: {
|
|
113
|
+
pagesWritten: [pagePath],
|
|
114
|
+
pagesSkipped: [],
|
|
115
|
+
indexUpdated: false,
|
|
116
|
+
logAppended: !!params.logSummary,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createReadExecute(wikiDir: string) {
|
|
123
|
+
return async (
|
|
124
|
+
params: { path: string },
|
|
125
|
+
): Promise<AgentToolResult<WikiReadDetails>> => {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const normalizedPath = params.path.trim().toLowerCase().replace(/^\/+/, "");
|
|
128
|
+
|
|
129
|
+
if (normalizedPath === "index" || normalizedPath === "index.md" || normalizedPath === "wiki-index") {
|
|
130
|
+
const index = await readIndex(wikiDir);
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: index }],
|
|
133
|
+
details: { found: true, path: "index.md" },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Try parsing as category/slug path
|
|
138
|
+
const parsed = parsePagePath(params.path);
|
|
139
|
+
if (parsed) {
|
|
140
|
+
const page = await readPage(wikiDir, parsed.category, parsed.slug);
|
|
141
|
+
if (page) {
|
|
142
|
+
const fm = page.frontmatter;
|
|
143
|
+
const freshness = formatFreshness(fm.updated, now);
|
|
144
|
+
const header = [
|
|
145
|
+
`# ${fm.title}`,
|
|
146
|
+
`PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
|
|
147
|
+
`Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
|
|
148
|
+
fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
|
|
149
|
+
fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
|
|
150
|
+
]
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.join("\n");
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: `${header}\n\n${page.body}` }],
|
|
156
|
+
details: { found: true, path: `${parsed.category}/${parsed.slug}` },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Try finding by title (case-insensitive search across all pages)
|
|
162
|
+
const allPages = await listPages(wikiDir);
|
|
163
|
+
const titleLower = params.path.toLowerCase();
|
|
164
|
+
for (const ref of allPages) {
|
|
165
|
+
if (ref.title.toLowerCase() === titleLower || ref.slug === titleLower) {
|
|
166
|
+
const page = await readPage(wikiDir, ref.category, ref.slug);
|
|
167
|
+
if (page) {
|
|
168
|
+
const fm = page.frontmatter;
|
|
169
|
+
const freshness = formatFreshness(fm.updated, now);
|
|
170
|
+
const header = [
|
|
171
|
+
`# ${fm.title}`,
|
|
172
|
+
`PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
|
|
173
|
+
`Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
|
|
174
|
+
fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
|
|
175
|
+
fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join("\n");
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: `${header}\n\n${page.body}` }],
|
|
182
|
+
details: { found: true, path: `${ref.category}/${ref.slug}` },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: `Page not found: "${params.path}"` }],
|
|
190
|
+
details: { found: false },
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { QMDStore } from "qmd-engine";
|
|
3
|
+
import type { ProjectScope } from "../scope.js";
|
|
4
|
+
import type { ParaCategory } from "../wiki.js";
|
|
5
|
+
import { resolveSource, truncateSource } from "../ingest.js";
|
|
6
|
+
import { INGEST_PROMPT } from "../templates/prompts.js";
|
|
7
|
+
import type { WikiIngestDetails } from "./schemas.js";
|
|
8
|
+
|
|
9
|
+
// -- Factory: wiki_ingest execute --------------------------------------------
|
|
10
|
+
|
|
11
|
+
export function createIngestExecute(
|
|
12
|
+
wikiDir: string,
|
|
13
|
+
_store: QMDStore,
|
|
14
|
+
getScope: () => ProjectScope,
|
|
15
|
+
) {
|
|
16
|
+
return async (
|
|
17
|
+
params: { source: string; sourceType?: "url" | "file" | "text"; category?: ParaCategory; scope?: string[] },
|
|
18
|
+
): Promise<AgentToolResult<WikiIngestDetails>> => {
|
|
19
|
+
const scope = getScope();
|
|
20
|
+
const resolved = await resolveSource(wikiDir, {
|
|
21
|
+
source: params.source,
|
|
22
|
+
sourceType: params.sourceType,
|
|
23
|
+
scope: params.scope,
|
|
24
|
+
category: params.category,
|
|
25
|
+
}, scope);
|
|
26
|
+
|
|
27
|
+
const categoryHint = resolved.categoryHint
|
|
28
|
+
? `\nSuggested PARA category: ${resolved.categoryHint}`
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
// Assemble the tool result with instructions for the LLM
|
|
32
|
+
const toolResultText = [
|
|
33
|
+
INGEST_PROMPT,
|
|
34
|
+
"",
|
|
35
|
+
`Current scope: ${resolved.scopeName} (tags: ${resolved.scopeTags.join(", ")})`,
|
|
36
|
+
categoryHint,
|
|
37
|
+
resolved.rawPath ? `Raw source saved to: ${resolved.rawPath}` : "",
|
|
38
|
+
"",
|
|
39
|
+
"<wiki-schema>",
|
|
40
|
+
resolved.schema,
|
|
41
|
+
"</wiki-schema>",
|
|
42
|
+
"",
|
|
43
|
+
"<wiki-index>",
|
|
44
|
+
resolved.index,
|
|
45
|
+
"</wiki-index>",
|
|
46
|
+
"",
|
|
47
|
+
"<source-content>",
|
|
48
|
+
truncateSource(resolved.content),
|
|
49
|
+
"</source-content>",
|
|
50
|
+
"",
|
|
51
|
+
"Now analyze the source content and use wiki_write to create/update pages, update the index, and log the operation.",
|
|
52
|
+
].join("\n");
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: toolResultText }],
|
|
56
|
+
details: {
|
|
57
|
+
sourceType: resolved.sourceType,
|
|
58
|
+
rawPath: resolved.rawPath,
|
|
59
|
+
sourceLength: resolved.content.length,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}
|