pi-para 0.5.2 → 0.6.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 +160 -0
- package/CHANGELOG.md +51 -0
- package/CONFIGURATION.md +243 -0
- package/README.md +140 -237
- package/SETUP.md +196 -112
- package/STRUCTURE.md +140 -0
- package/package.json +86 -18
- package/scripts/build-assets.mjs +29 -0
- package/scripts/install.ps1 +55 -0
- package/scripts/install.sh +63 -0
- package/scripts/smoke-install.mjs +63 -0
- package/skills/para/SKILL.md +1 -1
- package/skills/setup/SKILL.md +1 -1
- package/src/atomic-write.ts +17 -0
- package/src/capture.ts +4 -4
- package/src/cli-legacy.ts +43 -0
- package/src/cli.ts +133 -28
- package/src/commands.ts +203 -243
- package/src/config.ts +355 -0
- package/src/context.ts +29 -14
- package/src/credentials.ts +100 -0
- package/src/daemon.ts +1 -1
- package/src/doctor.ts +209 -0
- package/src/gepa/index.ts +1 -1
- package/src/index.ts +119 -40
- package/src/maintainer.ts +4 -4
- package/src/model-resolver.ts +96 -0
- package/src/paths.ts +48 -0
- package/src/processor.ts +4 -4
- package/src/qmd-providers.ts +63 -0
- package/src/query.ts +15 -1
- package/src/repair.ts +49 -0
- package/src/scheduler/controls.ts +125 -0
- package/src/scheduler/index.ts +210 -0
- package/src/scheduler/leases.ts +98 -0
- package/src/scheduler/session-capture.ts +146 -0
- package/src/scheduler/state.ts +304 -0
- package/src/session-tools.ts +1 -1
- package/src/settings.ts +126 -0
- package/src/setup.ts +116 -0
- package/src/status.ts +115 -0
- package/src/store.ts +116 -31
- package/src/summarize.ts +72 -16
- package/src/templates/prompts.ts +1 -1
- package/src/tools.ts +280 -90
- package/src/wiki-tool-guidance.ts +84 -0
- package/src/wiki.ts +98 -26
- package/dist/capture.d.ts +0 -34
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js +0 -461
- package/dist/capture.js.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -405
- package/dist/cli.js.map +0 -1
- package/dist/commands.d.ts +0 -15
- package/dist/commands.d.ts.map +0 -1
- package/dist/commands.js +0 -786
- package/dist/commands.js.map +0 -1
- package/dist/context.d.ts +0 -61
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -289
- package/dist/context.js.map +0 -1
- 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/frontmatter.d.ts +0 -59
- package/dist/frontmatter.d.ts.map +0 -1
- package/dist/frontmatter.js +0 -170
- package/dist/frontmatter.js.map +0 -1
- package/dist/gepa/dataset.d.ts +0 -36
- package/dist/gepa/dataset.d.ts.map +0 -1
- package/dist/gepa/dataset.js +0 -148
- package/dist/gepa/dataset.js.map +0 -1
- package/dist/gepa/evaluator.d.ts +0 -51
- package/dist/gepa/evaluator.d.ts.map +0 -1
- package/dist/gepa/evaluator.js +0 -317
- package/dist/gepa/evaluator.js.map +0 -1
- package/dist/gepa/index.d.ts +0 -66
- package/dist/gepa/index.d.ts.map +0 -1
- package/dist/gepa/index.js +0 -240
- package/dist/gepa/index.js.map +0 -1
- package/dist/gepa/llm.d.ts +0 -35
- package/dist/gepa/llm.d.ts.map +0 -1
- package/dist/gepa/llm.js +0 -268
- package/dist/gepa/llm.js.map +0 -1
- package/dist/gepa/optimized/capture-prompt.txt +0 -65
- package/dist/gepa/optimizer.d.ts +0 -29
- package/dist/gepa/optimizer.d.ts.map +0 -1
- package/dist/gepa/optimizer.js +0 -249
- package/dist/gepa/optimizer.js.map +0 -1
- package/dist/gepa/targets.d.ts +0 -39
- package/dist/gepa/targets.d.ts.map +0 -1
- package/dist/gepa/targets.js +0 -326
- package/dist/gepa/targets.js.map +0 -1
- package/dist/gepa/types.d.ts +0 -168
- package/dist/gepa/types.d.ts.map +0 -1
- package/dist/gepa/types.js +0 -124
- package/dist/gepa/types.js.map +0 -1
- package/dist/index.d.ts +0 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -294
- package/dist/index.js.map +0 -1
- package/dist/ingest.d.ts +0 -88
- package/dist/ingest.d.ts.map +0 -1
- package/dist/ingest.js +0 -105
- package/dist/ingest.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/link-utils.d.ts +0 -30
- package/dist/link-utils.d.ts.map +0 -1
- package/dist/link-utils.js +0 -145
- package/dist/link-utils.js.map +0 -1
- package/dist/lint.d.ts +0 -39
- package/dist/lint.d.ts.map +0 -1
- package/dist/lint.js +0 -827
- package/dist/lint.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/processor.d.ts +0 -14
- package/dist/processor.d.ts.map +0 -1
- package/dist/processor.js +0 -361
- package/dist/processor.js.map +0 -1
- package/dist/query.d.ts +0 -69
- package/dist/query.d.ts.map +0 -1
- package/dist/query.js +0 -107
- package/dist/query.js.map +0 -1
- package/dist/raw.d.ts +0 -57
- package/dist/raw.d.ts.map +0 -1
- package/dist/raw.js +0 -219
- package/dist/raw.js.map +0 -1
- package/dist/redact.d.ts +0 -21
- package/dist/redact.d.ts.map +0 -1
- package/dist/redact.js +0 -143
- package/dist/redact.js.map +0 -1
- package/dist/scope.d.ts +0 -54
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -161
- package/dist/scope.js.map +0 -1
- package/dist/scripts/bulk-link-fix.d.ts +0 -9
- package/dist/scripts/bulk-link-fix.d.ts.map +0 -1
- package/dist/scripts/bulk-link-fix.js +0 -106
- package/dist/scripts/bulk-link-fix.js.map +0 -1
- package/dist/session-tools.d.ts +0 -33
- package/dist/session-tools.d.ts.map +0 -1
- package/dist/session-tools.js +0 -211
- package/dist/session-tools.js.map +0 -1
- package/dist/state.d.ts +0 -54
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js +0 -176
- package/dist/state.js.map +0 -1
- package/dist/store.d.ts +0 -56
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -227
- package/dist/store.js.map +0 -1
- package/dist/summarize.d.ts +0 -56
- package/dist/summarize.d.ts.map +0 -1
- package/dist/summarize.js +0 -231
- package/dist/summarize.js.map +0 -1
- package/dist/tag-registry.d.ts +0 -56
- package/dist/tag-registry.d.ts.map +0 -1
- package/dist/tag-registry.js +0 -146
- package/dist/tag-registry.js.map +0 -1
- package/dist/templates/prompts.d.ts +0 -63
- package/dist/templates/prompts.d.ts.map +0 -1
- package/dist/templates/prompts.js +0 -260
- package/dist/templates/prompts.js.map +0 -1
- package/dist/tools.d.ts +0 -26
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -1033
- package/dist/tools.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/dist/wiki.d.ts +0 -103
- package/dist/wiki.d.ts.map +0 -1
- package/dist/wiki.js +0 -428
- package/dist/wiki.js.map +0 -1
- package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
- package/scripts/gepa/uv.lock +0 -2241
package/src/status.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { loadParaConfig } from "./config.js";
|
|
3
|
+
import { getParaPaths } from "./paths.js";
|
|
4
|
+
import { listPages, type ParaCategory } from "./wiki.js";
|
|
5
|
+
import { SchedulerStateDB, type QueueStatus } from "./scheduler/state.js";
|
|
6
|
+
|
|
7
|
+
export interface PiParaStatusOptions {
|
|
8
|
+
homeDir?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PiParaStatusResult {
|
|
12
|
+
configPath: string;
|
|
13
|
+
wikiDir: string;
|
|
14
|
+
configSource: "canonical" | "migrated";
|
|
15
|
+
schedulerDbPath: string;
|
|
16
|
+
qmdDbPath: string;
|
|
17
|
+
pages: {
|
|
18
|
+
total: number;
|
|
19
|
+
byCategory: Record<ParaCategory, number>;
|
|
20
|
+
};
|
|
21
|
+
scheduler: Record<QueueStatus, number>;
|
|
22
|
+
qmdDbExists: boolean;
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const QUEUE_STATUSES: QueueStatus[] = ["queued", "running", "done", "failed"];
|
|
27
|
+
|
|
28
|
+
export async function getPiParaStatus(options: PiParaStatusOptions = {}): Promise<PiParaStatusResult> {
|
|
29
|
+
const loaded = await loadParaConfig({ homeDir: options.homeDir, migrate: true });
|
|
30
|
+
const paths = getParaPaths({ homeDir: options.homeDir, wikiDir: loaded.config.wiki.dir });
|
|
31
|
+
const warnings: string[] = [];
|
|
32
|
+
|
|
33
|
+
const pages = await getPageCounts(paths.wikiDir, warnings);
|
|
34
|
+
const scheduler = getSchedulerCounts(paths.schedulerDbPath, warnings);
|
|
35
|
+
|
|
36
|
+
if (!existsSync(paths.wikiDir)) warnings.push(`wiki directory not found: ${paths.wikiDir}`);
|
|
37
|
+
if (!existsSync(paths.schedulerDbPath)) warnings.push("scheduler DB not created yet; it will be initialized when Pi starts");
|
|
38
|
+
if (!existsSync(paths.qmdDbPath)) warnings.push("QMD DB not created yet; search index is initialized on first store open");
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
configPath: paths.userConfigPath,
|
|
42
|
+
wikiDir: paths.wikiDir,
|
|
43
|
+
configSource: loaded.migratedFromLegacy ? "migrated" : "canonical",
|
|
44
|
+
schedulerDbPath: paths.schedulerDbPath,
|
|
45
|
+
qmdDbPath: paths.qmdDbPath,
|
|
46
|
+
pages,
|
|
47
|
+
scheduler,
|
|
48
|
+
qmdDbExists: existsSync(paths.qmdDbPath),
|
|
49
|
+
warnings,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatPiParaStatus(status: PiParaStatusResult): string {
|
|
54
|
+
const lines = [
|
|
55
|
+
"pi-para status",
|
|
56
|
+
` Config: ${status.configPath} (${status.configSource})`,
|
|
57
|
+
` Wiki: ${status.wikiDir}`,
|
|
58
|
+
` Pages: ${status.pages.total} total (${formatCategoryCounts(status.pages.byCategory)})`,
|
|
59
|
+
` Scheduler: ${status.scheduler.queued} queued, ${status.scheduler.running} running, ${status.scheduler.failed} failed`,
|
|
60
|
+
` QMD SDK DB: ${status.qmdDbExists ? status.qmdDbPath : "not initialized"}`,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
if (status.warnings.length > 0) {
|
|
64
|
+
lines.push("", "Warnings:");
|
|
65
|
+
for (const warning of status.warnings) lines.push(` ⚠ ${warning}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push("", "Next: use 'pi-para doctor' for diagnostics or '/wiki-scheduler status' inside Pi.");
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getPageCounts(wikiDir: string, warnings: string[]): Promise<PiParaStatusResult["pages"]> {
|
|
73
|
+
const byCategory: Record<ParaCategory, number> = {
|
|
74
|
+
projects: 0,
|
|
75
|
+
areas: 0,
|
|
76
|
+
resources: 0,
|
|
77
|
+
archives: 0,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(wikiDir)) return { total: 0, byCategory };
|
|
82
|
+
const pages = await listPages(wikiDir);
|
|
83
|
+
for (const page of pages) byCategory[page.category]++;
|
|
84
|
+
return { total: pages.length, byCategory };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
warnings.push(`could not count wiki pages: ${err instanceof Error ? err.message : String(err)}`);
|
|
87
|
+
return { total: 0, byCategory };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getSchedulerCounts(dbPath: string, warnings: string[]): Record<QueueStatus, number> {
|
|
92
|
+
const counts: Record<QueueStatus, number> = {
|
|
93
|
+
queued: 0,
|
|
94
|
+
running: 0,
|
|
95
|
+
done: 0,
|
|
96
|
+
failed: 0,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const db = new SchedulerStateDB(dbPath);
|
|
101
|
+
try {
|
|
102
|
+
for (const status of QUEUE_STATUSES) counts[status] = db.list(status).length;
|
|
103
|
+
} finally {
|
|
104
|
+
db.close();
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
warnings.push(`could not read scheduler DB: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return counts;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatCategoryCounts(counts: Record<ParaCategory, number>): string {
|
|
114
|
+
return `projects ${counts.projects}, areas ${counts.areas}, resources ${counts.resources}, archives ${counts.archives}`;
|
|
115
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { parse as parseYaml } from "yaml";
|
|
|
12
12
|
import { createStore, extractSnippet } from "qmd-engine";
|
|
13
13
|
import type { QMDStore, CollectionConfig } from "qmd-engine";
|
|
14
14
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
15
|
+
import type { ParaUserConfig } from "./config.js";
|
|
16
|
+
import { buildQmdProvidersFromParaConfig, type QmdProvidersConfig } from "./qmd-providers.js";
|
|
15
17
|
import { matchesScope } from "./scope.js";
|
|
16
18
|
import type { PageFrontmatter, PageRef, ParaCategory } from "./wiki.js";
|
|
17
19
|
import type { ProjectScope } from "./scope.js";
|
|
@@ -21,6 +23,18 @@ export type { QMDStore } from "qmd-engine";
|
|
|
21
23
|
|
|
22
24
|
// -- Types ------------------------------------------------------------------
|
|
23
25
|
|
|
26
|
+
export interface OpenStoreOptions {
|
|
27
|
+
paraConfig?: ParaUserConfig;
|
|
28
|
+
secretsPath?: string;
|
|
29
|
+
authStorage?: { getApiKey(provider: string): Promise<string | undefined> };
|
|
30
|
+
/**
|
|
31
|
+
* Start slow vector embedding in the background after BM25 indexing.
|
|
32
|
+
* Pi session startup disables this so single-shot `pi -p` processes can exit
|
|
33
|
+
* promptly; callers can still invoke embedIfNeeded() explicitly.
|
|
34
|
+
*/
|
|
35
|
+
backgroundEmbed?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
export interface WikiSearchOptions {
|
|
25
39
|
scope?: ProjectScope;
|
|
26
40
|
category?: ParaCategory;
|
|
@@ -36,6 +50,14 @@ export interface WikiSearchResult {
|
|
|
36
50
|
frontmatter: PageFrontmatter;
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
interface RawQmdSearchResult {
|
|
54
|
+
displayPath: string;
|
|
55
|
+
score: number;
|
|
56
|
+
body?: string;
|
|
57
|
+
title?: string;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
// -- PARA context descriptions -----------------------------------------------
|
|
40
62
|
|
|
41
63
|
const PARA_CONTEXTS: Record<string, string> = {
|
|
@@ -51,6 +73,22 @@ const RAW_CONTEXT = "Immutable source material: articles, documents, notes. Not
|
|
|
51
73
|
|
|
52
74
|
const pendingEmbeds = new WeakMap<QMDStore, Promise<void>>();
|
|
53
75
|
|
|
76
|
+
// -- qmd provider config -----------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* qmd-engine currently falls back to node-llama-cpp when no API provider is
|
|
80
|
+
* configured. pi-para never uses local LLMs, so provide an inert API endpoint
|
|
81
|
+
* whenever neither pi-para profiles nor legacy provider config exists. BM25
|
|
82
|
+
* search works without calling this endpoint; accidental vector/chat calls fail
|
|
83
|
+
* fast instead of downloading/building local models.
|
|
84
|
+
*/
|
|
85
|
+
const NO_LOCAL_PROVIDERS: QmdProvidersConfig = {
|
|
86
|
+
embed: {
|
|
87
|
+
url: "http://127.0.0.1:9",
|
|
88
|
+
model: "disabled-local-llm",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
54
92
|
// -- qmd config loading ------------------------------------------------------
|
|
55
93
|
|
|
56
94
|
/**
|
|
@@ -79,13 +117,21 @@ function loadQmdProviders(): Record<string, unknown> | undefined {
|
|
|
79
117
|
* - wiki: all .md files except raw/
|
|
80
118
|
* - raw: .md files under raw/, not included in default queries
|
|
81
119
|
*
|
|
82
|
-
* Adds PARA category contexts
|
|
83
|
-
*
|
|
120
|
+
* Adds PARA category contexts and runs update() for BM25. When requested,
|
|
121
|
+
* also schedules embed() in the background.
|
|
84
122
|
*/
|
|
85
|
-
export async function openStore(wikiDir: string): Promise<QMDStore> {
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
const
|
|
123
|
+
export async function openStore(wikiDir: string, opts: OpenStoreOptions = {}): Promise<QMDStore> {
|
|
124
|
+
// Prefer pi-para's in-memory provider profiles. Legacy QMD YAML remains a
|
|
125
|
+
// fallback for migrated installs and tests that do not pass paraConfig.
|
|
126
|
+
const configuredProviders = opts.paraConfig
|
|
127
|
+
? opts.paraConfig.qmd.providerConfig === "legacy-qmd-compatible"
|
|
128
|
+
? loadQmdProviders()
|
|
129
|
+
: await buildQmdProvidersFromParaConfig(opts.paraConfig, {
|
|
130
|
+
secretsPath: opts.secretsPath,
|
|
131
|
+
authStorage: opts.authStorage,
|
|
132
|
+
})
|
|
133
|
+
: loadQmdProviders();
|
|
134
|
+
const providers = configuredProviders ?? NO_LOCAL_PROVIDERS;
|
|
89
135
|
|
|
90
136
|
const store = await createStore({
|
|
91
137
|
dbPath: join(wikiDir, ".qmd.sqlite"),
|
|
@@ -102,7 +148,7 @@ export async function openStore(wikiDir: string): Promise<QMDStore> {
|
|
|
102
148
|
includeByDefault: false,
|
|
103
149
|
},
|
|
104
150
|
},
|
|
105
|
-
|
|
151
|
+
providers: providers as CollectionConfig["providers"],
|
|
106
152
|
},
|
|
107
153
|
});
|
|
108
154
|
|
|
@@ -117,25 +163,27 @@ export async function openStore(wikiDir: string): Promise<QMDStore> {
|
|
|
117
163
|
// Sync filesystem -> BM25 index (fast, search ready immediately)
|
|
118
164
|
await store.update();
|
|
119
165
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
166
|
+
if (opts.backgroundEmbed !== false && opts.paraConfig?.qmd.embedEnabled !== false) {
|
|
167
|
+
// Schedule embedding in background (non-blocking).
|
|
168
|
+
// BM25 search works immediately; hybrid search improves once embed completes.
|
|
169
|
+
// Suppress console.error from qmd's internal embed failures.
|
|
170
|
+
const origError = console.error;
|
|
171
|
+
const embedPromise = store.embed().then(() => {
|
|
172
|
+
pendingEmbeds.delete(store);
|
|
173
|
+
}).catch(() => {
|
|
174
|
+
// Embedding failures are non-fatal — hybrid search degrades to BM25
|
|
175
|
+
pendingEmbeds.delete(store);
|
|
176
|
+
}).finally(() => {
|
|
177
|
+
console.error = origError;
|
|
178
|
+
});
|
|
179
|
+
// Temporarily suppress embed error noise during background embed
|
|
180
|
+
console.error = (...args: unknown[]) => {
|
|
181
|
+
const msg = String(args[0] ?? "");
|
|
182
|
+
if (msg.includes("embed error") || msg.includes("fetch failed")) return;
|
|
183
|
+
origError(...args);
|
|
184
|
+
};
|
|
185
|
+
pendingEmbeds.set(store, embedPromise);
|
|
186
|
+
}
|
|
139
187
|
|
|
140
188
|
return store;
|
|
141
189
|
}
|
|
@@ -147,7 +195,10 @@ export async function closeStore(store: QMDStore): Promise<void> {
|
|
|
147
195
|
const pending = pendingEmbeds.get(store);
|
|
148
196
|
if (pending) {
|
|
149
197
|
// Wait for pending embed with a short timeout — don't hang forever
|
|
150
|
-
const timeout = new Promise<void>((resolve) =>
|
|
198
|
+
const timeout = new Promise<void>((resolve) => {
|
|
199
|
+
const timer = setTimeout(resolve, 5_000);
|
|
200
|
+
(timer as { unref?: () => void }).unref?.();
|
|
201
|
+
});
|
|
151
202
|
await Promise.race([pending, timeout]);
|
|
152
203
|
pendingEmbeds.delete(store);
|
|
153
204
|
}
|
|
@@ -157,8 +208,9 @@ export async function closeStore(store: QMDStore): Promise<void> {
|
|
|
157
208
|
/**
|
|
158
209
|
* Search the wiki with scope and category filtering.
|
|
159
210
|
*
|
|
160
|
-
* Uses
|
|
161
|
-
*
|
|
211
|
+
* Uses vector search when an embedding index is available, merged with BM25
|
|
212
|
+
* lexical search for exact keyword matches. Each result's frontmatter is parsed
|
|
213
|
+
* to apply scope/category/archive filters.
|
|
162
214
|
*/
|
|
163
215
|
export async function searchWiki(
|
|
164
216
|
store: QMDStore,
|
|
@@ -181,13 +233,14 @@ export async function searchWiki(
|
|
|
181
233
|
|
|
182
234
|
const fetchLimit = opts.scope ? limit * 3 : limit;
|
|
183
235
|
|
|
184
|
-
const
|
|
236
|
+
const lexResults = await store.searchLex(query, {
|
|
185
237
|
limit: fetchLimit,
|
|
186
238
|
collection: "wiki",
|
|
187
239
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
188
240
|
graphBoost: opts.graphBoost ?? true,
|
|
189
241
|
});
|
|
190
242
|
|
|
243
|
+
const rawResults = await mergeLexAndVectorResults(store, query, lexResults, fetchLimit);
|
|
191
244
|
const results: WikiSearchResult[] = [];
|
|
192
245
|
|
|
193
246
|
for (const result of rawResults) {
|
|
@@ -220,7 +273,7 @@ export async function searchWiki(
|
|
|
220
273
|
page: {
|
|
221
274
|
category,
|
|
222
275
|
slug,
|
|
223
|
-
title: frontmatter.title || result.title,
|
|
276
|
+
title: frontmatter.title || result.title || slug,
|
|
224
277
|
path: `${category}/${slug}.md`,
|
|
225
278
|
},
|
|
226
279
|
score: result.score,
|
|
@@ -234,6 +287,38 @@ export async function searchWiki(
|
|
|
234
287
|
return results;
|
|
235
288
|
}
|
|
236
289
|
|
|
290
|
+
async function mergeLexAndVectorResults(
|
|
291
|
+
store: QMDStore,
|
|
292
|
+
query: string,
|
|
293
|
+
lexResults: RawQmdSearchResult[],
|
|
294
|
+
limit: number,
|
|
295
|
+
): Promise<RawQmdSearchResult[]> {
|
|
296
|
+
let vecResults: RawQmdSearchResult[] = [];
|
|
297
|
+
try {
|
|
298
|
+
const status = await store.getStatus();
|
|
299
|
+
const llm = (store.internal as unknown as { llm?: unknown }).llm;
|
|
300
|
+
const embedModel = (llm as { embedModelName?: string } | undefined)?.embedModelName;
|
|
301
|
+
if (status.hasVectorIndex && llm && embedModel) {
|
|
302
|
+
vecResults = await store.internal.searchVec(query, embedModel, limit, "wiki", llm as any) as RawQmdSearchResult[];
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// Semantic search is best-effort; lexical BM25 remains the reliable fallback.
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const merged = new Map<string, RawQmdSearchResult>();
|
|
309
|
+
for (const result of lexResults) {
|
|
310
|
+
merged.set(result.displayPath, { ...result, score: result.score * 0.85 });
|
|
311
|
+
}
|
|
312
|
+
for (const result of vecResults) {
|
|
313
|
+
const existing = merged.get(result.displayPath);
|
|
314
|
+
if (!existing || result.score > existing.score) {
|
|
315
|
+
merged.set(result.displayPath, result);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit * 2);
|
|
320
|
+
}
|
|
321
|
+
|
|
237
322
|
/**
|
|
238
323
|
* Re-index the store (fast filesystem scan). Makes new/changed
|
|
239
324
|
* content BM25-searchable immediately. Call after wiki writes.
|
package/src/summarize.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { ProjectScope } from "./scope.js";
|
|
|
16
16
|
import {
|
|
17
17
|
convertToLlm,
|
|
18
18
|
serializeConversation,
|
|
19
|
-
} from "@
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import {
|
|
21
21
|
SUMMARIZE_SYSTEM_PROMPT,
|
|
22
22
|
ITERATIVE_UPDATE_PROMPT,
|
|
@@ -72,7 +72,7 @@ function extractTextContent(content: unknown): string {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Fallback serialization that mirrors pi's serializeConversation format.
|
|
75
|
-
* Used when @
|
|
75
|
+
* Used when @earendil-works/pi-coding-agent is not available.
|
|
76
76
|
*
|
|
77
77
|
* Formats: [User]:, [Assistant]:, [Assistant thinking]:,
|
|
78
78
|
* [Assistant tool calls]:, [Tool result]:
|
|
@@ -143,7 +143,7 @@ function serializeMessagesFallback(messages: unknown[]): string {
|
|
|
143
143
|
* (bashExecution, compactionSummary, branchSummary, etc.). Falls back to
|
|
144
144
|
* a compatible reimplementation for plain user/assistant/toolResult messages.
|
|
145
145
|
*
|
|
146
|
-
* @param messages - AgentMessage[] from @
|
|
146
|
+
* @param messages - AgentMessage[] from @earendil-works/pi-agent-core
|
|
147
147
|
* @returns Serialized text in [User]: / [Assistant]: / [Tool result]: format
|
|
148
148
|
*/
|
|
149
149
|
export function serializeSessionForWiki(
|
|
@@ -246,28 +246,84 @@ export function generateSummary(
|
|
|
246
246
|
* @param scope - Current project scope for context
|
|
247
247
|
* @returns Prompt string for the LLM
|
|
248
248
|
*/
|
|
249
|
+
export interface OverviewPromptOptions {
|
|
250
|
+
depth?: "brief" | "detailed";
|
|
251
|
+
maxChars?: number;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const DEFAULT_BRIEF_OVERVIEW_MAX_CHARS = 28_000;
|
|
255
|
+
const DEFAULT_DETAILED_OVERVIEW_MAX_CHARS = 80_000;
|
|
256
|
+
const BRIEF_EXCERPT_CHARS = 180;
|
|
257
|
+
const DETAILED_BODY_CHARS = 2_000;
|
|
258
|
+
|
|
249
259
|
export function generateOverviewPrompt(
|
|
250
260
|
pages: WikiPage[],
|
|
251
261
|
scope: ProjectScope,
|
|
262
|
+
options: OverviewPromptOptions = {},
|
|
252
263
|
): string {
|
|
264
|
+
const depth = options.depth ?? "brief";
|
|
265
|
+
const maxChars = options.maxChars ?? (depth === "detailed" ? DEFAULT_DETAILED_OVERVIEW_MAX_CHARS : DEFAULT_BRIEF_OVERVIEW_MAX_CHARS);
|
|
253
266
|
const scopeInfo = `Current scope: ${scope.name} (tags: ${scope.include.join(", ")})`;
|
|
254
|
-
|
|
255
|
-
const pageEntries = pages.map((page) => {
|
|
256
|
-
const fm = page.frontmatter;
|
|
257
|
-
return [
|
|
258
|
-
`### ${fm.title} (${page.category}/${page.slug})`,
|
|
259
|
-
`PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
|
|
260
|
-
"",
|
|
261
|
-
page.body,
|
|
262
|
-
].join("\n");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
return [
|
|
267
|
+
const header = [
|
|
266
268
|
OVERVIEW_PROMPT,
|
|
267
269
|
"",
|
|
268
270
|
scopeInfo,
|
|
269
271
|
`Total pages: ${pages.length}`,
|
|
272
|
+
`Input mode: ${depth === "detailed" ? "bounded detailed excerpts" : "compact metadata/excerpts"}`,
|
|
273
|
+
"",
|
|
274
|
+
].join("\n");
|
|
275
|
+
|
|
276
|
+
const entries: string[] = [];
|
|
277
|
+
let usedChars = header.length;
|
|
278
|
+
let omitted = 0;
|
|
279
|
+
|
|
280
|
+
for (const page of pages) {
|
|
281
|
+
const entry = depth === "detailed" ? detailedPageEntry(page) : briefPageEntry(page);
|
|
282
|
+
if (usedChars + entry.length + 2 > maxChars) {
|
|
283
|
+
omitted++;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
entries.push(entry);
|
|
287
|
+
usedChars += entry.length + 2;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const omittedNote = omitted > 0
|
|
291
|
+
? `\n\n[${omitted} page(s) omitted to keep wiki_summarize within the context budget. Use a category, page path, or depth=\"detailed\" on a narrower target for more detail.]`
|
|
292
|
+
: "";
|
|
293
|
+
|
|
294
|
+
return `${header}${entries.join("\n\n")}${omittedNote}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function briefPageEntry(page: WikiPage): string {
|
|
298
|
+
const fm = page.frontmatter;
|
|
299
|
+
const excerpt = firstUsefulExcerpt(page.body, BRIEF_EXCERPT_CHARS);
|
|
300
|
+
return [
|
|
301
|
+
`- ${fm.title} (${page.category}/${page.slug})`,
|
|
302
|
+
` PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ") || "-"} | Updated: ${fm.updated ?? "-"}`,
|
|
303
|
+
excerpt ? ` Excerpt: ${excerpt}` : undefined,
|
|
304
|
+
fm.links?.length ? ` Links: ${fm.links.slice(0, 8).map((link) => `[[${link}]]`).join(", ")}` : undefined,
|
|
305
|
+
].filter(Boolean).join("\n");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function detailedPageEntry(page: WikiPage): string {
|
|
309
|
+
const fm = page.frontmatter;
|
|
310
|
+
return [
|
|
311
|
+
`### ${fm.title} (${page.category}/${page.slug})`,
|
|
312
|
+
`PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
|
|
313
|
+
`Updated: ${fm.updated ?? "-"} | Links: ${(fm.links ?? []).slice(0, 12).map((link) => `[[${link}]]`).join(", ") || "-"}`,
|
|
270
314
|
"",
|
|
271
|
-
|
|
315
|
+
truncateForSummary(page.body, DETAILED_BODY_CHARS),
|
|
272
316
|
].join("\n");
|
|
273
317
|
}
|
|
318
|
+
|
|
319
|
+
function firstUsefulExcerpt(body: string, maxChars: number): string {
|
|
320
|
+
const cleaned = body
|
|
321
|
+
.split(/\r?\n/)
|
|
322
|
+
.map((line) => line.trim())
|
|
323
|
+
.filter((line) => line && !line.startsWith("#") && !/^[-*]\s*\[?\[?/.test(line))
|
|
324
|
+
.join(" ")
|
|
325
|
+
.replace(/\s+/g, " ")
|
|
326
|
+
.trim();
|
|
327
|
+
if (!cleaned) return "";
|
|
328
|
+
return cleaned.length <= maxChars ? cleaned : `${cleaned.slice(0, maxChars - 1)}…`;
|
|
329
|
+
}
|
package/src/templates/prompts.ts
CHANGED
|
@@ -134,7 +134,7 @@ Instructions:
|
|
|
134
134
|
FRESHNESS VERIFICATION (critical):
|
|
135
135
|
- Each result shows an "Updated" timestamp and an age indicator (e.g. "⚠️ STALE — 45 days old"). Treat pages not updated in >14 days with healthy skepticism.
|
|
136
136
|
- When a wiki page makes claims about code, configs, file paths, ports, API endpoints, or architecture — and the page is >14 days old — VERIFY by checking the actual source (read the file, run a command) before trusting the wiki.
|
|
137
|
-
- When you discover a wiki page contains outdated or incorrect information, IMMEDIATELY fix it with
|
|
137
|
+
- When you discover a wiki page contains outdated or incorrect information, IMMEDIATELY fix it with wiki_edit so future queries get correct answers. This is the self-healing loop.
|
|
138
138
|
- If you cannot verify a claim (e.g. no file access, external service), tell the user: "Wiki says X (last updated DATE) — I could not verify this is still current."
|
|
139
139
|
- Never silently pass along stale wiki content as fact. Either verify it or flag it.`;
|
|
140
140
|
|