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.
Files changed (195) hide show
  1. package/ARCHITECTURE.md +160 -0
  2. package/CHANGELOG.md +51 -0
  3. package/CONFIGURATION.md +243 -0
  4. package/README.md +140 -237
  5. package/SETUP.md +196 -112
  6. package/STRUCTURE.md +140 -0
  7. package/package.json +86 -18
  8. package/scripts/build-assets.mjs +29 -0
  9. package/scripts/install.ps1 +55 -0
  10. package/scripts/install.sh +63 -0
  11. package/scripts/smoke-install.mjs +63 -0
  12. package/skills/para/SKILL.md +1 -1
  13. package/skills/setup/SKILL.md +1 -1
  14. package/src/atomic-write.ts +17 -0
  15. package/src/capture.ts +4 -4
  16. package/src/cli-legacy.ts +43 -0
  17. package/src/cli.ts +133 -28
  18. package/src/commands.ts +203 -243
  19. package/src/config.ts +355 -0
  20. package/src/context.ts +29 -14
  21. package/src/credentials.ts +100 -0
  22. package/src/daemon.ts +1 -1
  23. package/src/doctor.ts +209 -0
  24. package/src/gepa/index.ts +1 -1
  25. package/src/index.ts +119 -40
  26. package/src/maintainer.ts +4 -4
  27. package/src/model-resolver.ts +96 -0
  28. package/src/paths.ts +48 -0
  29. package/src/processor.ts +4 -4
  30. package/src/qmd-providers.ts +63 -0
  31. package/src/query.ts +15 -1
  32. package/src/repair.ts +49 -0
  33. package/src/scheduler/controls.ts +125 -0
  34. package/src/scheduler/index.ts +210 -0
  35. package/src/scheduler/leases.ts +98 -0
  36. package/src/scheduler/session-capture.ts +146 -0
  37. package/src/scheduler/state.ts +304 -0
  38. package/src/session-tools.ts +1 -1
  39. package/src/settings.ts +126 -0
  40. package/src/setup.ts +116 -0
  41. package/src/status.ts +115 -0
  42. package/src/store.ts +116 -31
  43. package/src/summarize.ts +72 -16
  44. package/src/templates/prompts.ts +1 -1
  45. package/src/tools.ts +280 -90
  46. package/src/wiki-tool-guidance.ts +84 -0
  47. package/src/wiki.ts +98 -26
  48. package/dist/capture.d.ts +0 -34
  49. package/dist/capture.d.ts.map +0 -1
  50. package/dist/capture.js +0 -461
  51. package/dist/capture.js.map +0 -1
  52. package/dist/cli.d.ts +0 -15
  53. package/dist/cli.d.ts.map +0 -1
  54. package/dist/cli.js +0 -405
  55. package/dist/cli.js.map +0 -1
  56. package/dist/commands.d.ts +0 -15
  57. package/dist/commands.d.ts.map +0 -1
  58. package/dist/commands.js +0 -786
  59. package/dist/commands.js.map +0 -1
  60. package/dist/context.d.ts +0 -61
  61. package/dist/context.d.ts.map +0 -1
  62. package/dist/context.js +0 -289
  63. package/dist/context.js.map +0 -1
  64. package/dist/daemon.d.ts +0 -49
  65. package/dist/daemon.d.ts.map +0 -1
  66. package/dist/daemon.js +0 -226
  67. package/dist/daemon.js.map +0 -1
  68. package/dist/frontmatter.d.ts +0 -59
  69. package/dist/frontmatter.d.ts.map +0 -1
  70. package/dist/frontmatter.js +0 -170
  71. package/dist/frontmatter.js.map +0 -1
  72. package/dist/gepa/dataset.d.ts +0 -36
  73. package/dist/gepa/dataset.d.ts.map +0 -1
  74. package/dist/gepa/dataset.js +0 -148
  75. package/dist/gepa/dataset.js.map +0 -1
  76. package/dist/gepa/evaluator.d.ts +0 -51
  77. package/dist/gepa/evaluator.d.ts.map +0 -1
  78. package/dist/gepa/evaluator.js +0 -317
  79. package/dist/gepa/evaluator.js.map +0 -1
  80. package/dist/gepa/index.d.ts +0 -66
  81. package/dist/gepa/index.d.ts.map +0 -1
  82. package/dist/gepa/index.js +0 -240
  83. package/dist/gepa/index.js.map +0 -1
  84. package/dist/gepa/llm.d.ts +0 -35
  85. package/dist/gepa/llm.d.ts.map +0 -1
  86. package/dist/gepa/llm.js +0 -268
  87. package/dist/gepa/llm.js.map +0 -1
  88. package/dist/gepa/optimized/capture-prompt.txt +0 -65
  89. package/dist/gepa/optimizer.d.ts +0 -29
  90. package/dist/gepa/optimizer.d.ts.map +0 -1
  91. package/dist/gepa/optimizer.js +0 -249
  92. package/dist/gepa/optimizer.js.map +0 -1
  93. package/dist/gepa/targets.d.ts +0 -39
  94. package/dist/gepa/targets.d.ts.map +0 -1
  95. package/dist/gepa/targets.js +0 -326
  96. package/dist/gepa/targets.js.map +0 -1
  97. package/dist/gepa/types.d.ts +0 -168
  98. package/dist/gepa/types.d.ts.map +0 -1
  99. package/dist/gepa/types.js +0 -124
  100. package/dist/gepa/types.js.map +0 -1
  101. package/dist/index.d.ts +0 -21
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -294
  104. package/dist/index.js.map +0 -1
  105. package/dist/ingest.d.ts +0 -88
  106. package/dist/ingest.d.ts.map +0 -1
  107. package/dist/ingest.js +0 -105
  108. package/dist/ingest.js.map +0 -1
  109. package/dist/link-discovery.d.ts +0 -40
  110. package/dist/link-discovery.d.ts.map +0 -1
  111. package/dist/link-discovery.js +0 -122
  112. package/dist/link-discovery.js.map +0 -1
  113. package/dist/link-utils.d.ts +0 -30
  114. package/dist/link-utils.d.ts.map +0 -1
  115. package/dist/link-utils.js +0 -145
  116. package/dist/link-utils.js.map +0 -1
  117. package/dist/lint.d.ts +0 -39
  118. package/dist/lint.d.ts.map +0 -1
  119. package/dist/lint.js +0 -827
  120. package/dist/lint.js.map +0 -1
  121. package/dist/maintainer.d.ts +0 -24
  122. package/dist/maintainer.d.ts.map +0 -1
  123. package/dist/maintainer.js +0 -358
  124. package/dist/maintainer.js.map +0 -1
  125. package/dist/processor.d.ts +0 -14
  126. package/dist/processor.d.ts.map +0 -1
  127. package/dist/processor.js +0 -361
  128. package/dist/processor.js.map +0 -1
  129. package/dist/query.d.ts +0 -69
  130. package/dist/query.d.ts.map +0 -1
  131. package/dist/query.js +0 -107
  132. package/dist/query.js.map +0 -1
  133. package/dist/raw.d.ts +0 -57
  134. package/dist/raw.d.ts.map +0 -1
  135. package/dist/raw.js +0 -219
  136. package/dist/raw.js.map +0 -1
  137. package/dist/redact.d.ts +0 -21
  138. package/dist/redact.d.ts.map +0 -1
  139. package/dist/redact.js +0 -143
  140. package/dist/redact.js.map +0 -1
  141. package/dist/scope.d.ts +0 -54
  142. package/dist/scope.d.ts.map +0 -1
  143. package/dist/scope.js +0 -161
  144. package/dist/scope.js.map +0 -1
  145. package/dist/scripts/bulk-link-fix.d.ts +0 -9
  146. package/dist/scripts/bulk-link-fix.d.ts.map +0 -1
  147. package/dist/scripts/bulk-link-fix.js +0 -106
  148. package/dist/scripts/bulk-link-fix.js.map +0 -1
  149. package/dist/session-tools.d.ts +0 -33
  150. package/dist/session-tools.d.ts.map +0 -1
  151. package/dist/session-tools.js +0 -211
  152. package/dist/session-tools.js.map +0 -1
  153. package/dist/state.d.ts +0 -54
  154. package/dist/state.d.ts.map +0 -1
  155. package/dist/state.js +0 -176
  156. package/dist/state.js.map +0 -1
  157. package/dist/store.d.ts +0 -56
  158. package/dist/store.d.ts.map +0 -1
  159. package/dist/store.js +0 -227
  160. package/dist/store.js.map +0 -1
  161. package/dist/summarize.d.ts +0 -56
  162. package/dist/summarize.d.ts.map +0 -1
  163. package/dist/summarize.js +0 -231
  164. package/dist/summarize.js.map +0 -1
  165. package/dist/tag-registry.d.ts +0 -56
  166. package/dist/tag-registry.d.ts.map +0 -1
  167. package/dist/tag-registry.js +0 -146
  168. package/dist/tag-registry.js.map +0 -1
  169. package/dist/templates/prompts.d.ts +0 -63
  170. package/dist/templates/prompts.d.ts.map +0 -1
  171. package/dist/templates/prompts.js +0 -260
  172. package/dist/templates/prompts.js.map +0 -1
  173. package/dist/tools.d.ts +0 -26
  174. package/dist/tools.d.ts.map +0 -1
  175. package/dist/tools.js +0 -1033
  176. package/dist/tools.js.map +0 -1
  177. package/dist/watcher.d.ts +0 -25
  178. package/dist/watcher.d.ts.map +0 -1
  179. package/dist/watcher.js +0 -84
  180. package/dist/watcher.js.map +0 -1
  181. package/dist/webui/client/dist/index.html +0 -1885
  182. package/dist/webui/server.d.ts +0 -31
  183. package/dist/webui/server.d.ts.map +0 -1
  184. package/dist/webui/server.js +0 -566
  185. package/dist/webui/server.js.map +0 -1
  186. package/dist/wiki.d.ts +0 -103
  187. package/dist/wiki.d.ts.map +0 -1
  188. package/dist/wiki.js +0 -428
  189. package/dist/wiki.js.map +0 -1
  190. package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
  191. package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
  192. package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
  193. package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
  194. package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
  195. 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, runs update() for BM25,
83
- * and schedules embed() in the background.
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
- // Read providers from ~/.config/qmd/index.yml so external API providers
87
- // (embed, chat, rerank) are used instead of local GGUF models.
88
- const providers = loadQmdProviders();
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
- ...(providers ? { providers: providers as CollectionConfig["providers"] } : {}),
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
- // Schedule embedding in background (non-blocking).
121
- // BM25 search works immediately; hybrid search improves once embed completes.
122
- // Suppress console.error from qmd's internal embed failures.
123
- const origError = console.error;
124
- const embedPromise = store.embed().then(() => {
125
- pendingEmbeds.delete(store);
126
- }).catch(() => {
127
- // Embedding failures are non-fatal — hybrid search degrades to BM25
128
- pendingEmbeds.delete(store);
129
- }).finally(() => {
130
- console.error = origError;
131
- });
132
- // Temporarily suppress embed error noise during background embed
133
- console.error = (...args: unknown[]) => {
134
- const msg = String(args[0] ?? "");
135
- if (msg.includes("embed error") || msg.includes("fetch failed")) return;
136
- origError(...args);
137
- };
138
- pendingEmbeds.set(store, embedPromise);
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) => setTimeout(resolve, 5_000));
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 BM25 search (searchLex) which requires no LLM. Each result's
161
- * frontmatter is parsed to apply scope/category/archive filters.
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 rawResults = await store.searchLex(query, {
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 "@mariozechner/pi-coding-agent";
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 @mariozechner/pi-coding-agent is not available.
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 @mariozechner/pi-agent-core
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
- ...pageEntries,
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
+ }
@@ -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 wiki_write(mode: "edit") so future queries get correct answers. This is the self-healing loop.
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