@phren/cli 0.0.1

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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,253 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execFileSync } from "child_process";
4
+ import { EXEC_TIMEOUT_MS, findProjectNameCaseInsensitive, getPhrenPath, normalizeProjectNameForCreate, } from "./shared.js";
5
+ import { isValidProjectName, errorMessage } from "./utils.js";
6
+ import { readTasksAcrossProjects, TASKS_FILENAME } from "./data-access.js";
7
+ import { applyGravity } from "./data-tasks.js";
8
+ import { buildIndex, queryRows } from "./shared-index.js";
9
+ import { resolveSubprocessArgs } from "./cli-hooks.js";
10
+ import { listAllSessions, getSessionArtifacts } from "./mcp-session.js";
11
+ export function handleTaskView(profile) {
12
+ const docs = readTasksAcrossProjects(getPhrenPath(), profile);
13
+ if (!docs.length) {
14
+ console.log("No tasks found.");
15
+ return;
16
+ }
17
+ let totalActive = 0;
18
+ let totalQueue = 0;
19
+ for (const doc of docs) {
20
+ const activeCount = doc.items.Active.length;
21
+ const queueCount = doc.items.Queue.length;
22
+ if (activeCount === 0 && queueCount === 0)
23
+ continue;
24
+ totalActive += activeCount;
25
+ totalQueue += queueCount;
26
+ console.log(`\n## ${doc.project}`);
27
+ if (activeCount > 0) {
28
+ console.log(" Active:");
29
+ const activeWithGravity = applyGravity(doc.items.Active).sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0));
30
+ for (const item of activeWithGravity) {
31
+ const rankTag = item.rank !== undefined ? ` [#${item.rank}]` : "";
32
+ const github = item.githubIssue ? ` [gh:#${item.githubIssue}]` : item.githubUrl ? " [gh]" : "";
33
+ console.log(` - ${item.line}${rankTag}${github}`);
34
+ }
35
+ }
36
+ if (queueCount > 0) {
37
+ console.log(" Queue:");
38
+ const queueWithGravity = applyGravity(doc.items.Queue).sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0));
39
+ for (const item of queueWithGravity) {
40
+ const rankTag = item.rank !== undefined ? ` [#${item.rank}]` : "";
41
+ const github = item.githubIssue ? ` [gh:#${item.githubIssue}]` : item.githubUrl ? " [gh]" : "";
42
+ console.log(` - ${item.line}${rankTag}${github}`);
43
+ }
44
+ }
45
+ }
46
+ if (totalActive === 0 && totalQueue === 0) {
47
+ console.log("All tasks are empty.");
48
+ return;
49
+ }
50
+ console.log(`\n${totalActive} active, ${totalQueue} queued across ${docs.length} project(s).`);
51
+ }
52
+ export function handleSessionsView(args) {
53
+ const phrenPath = getPhrenPath();
54
+ const sessionId = args[0];
55
+ if (sessionId) {
56
+ // Drill into a specific session
57
+ const sessions = listAllSessions(phrenPath, 200);
58
+ const session = sessions.find(s => s.sessionId === sessionId || s.sessionId.startsWith(sessionId));
59
+ if (!session) {
60
+ console.error(`Session "${sessionId}" not found.`);
61
+ process.exit(1);
62
+ }
63
+ const artifacts = getSessionArtifacts(phrenPath, session.sessionId);
64
+ console.log(`Session: ${session.sessionId.slice(0, 8)}`);
65
+ console.log(`Project: ${session.project ?? "—"}`);
66
+ console.log(`Started: ${session.startedAt.slice(0, 16).replace("T", " ")}`);
67
+ console.log(`Duration: ${session.durationMins ?? 0} min`);
68
+ console.log(`Status: ${session.status}`);
69
+ if (session.summary)
70
+ console.log(`Summary: ${session.summary}`);
71
+ if (artifacts.findings.length > 0) {
72
+ console.log(`\nFindings (${artifacts.findings.length}):`);
73
+ for (const f of artifacts.findings)
74
+ console.log(` - [${f.project}] ${f.text}`);
75
+ }
76
+ if (artifacts.tasks.length > 0) {
77
+ console.log(`\nTasks (${artifacts.tasks.length}):`);
78
+ for (const t of artifacts.tasks)
79
+ console.log(` - [${t.project}/${t.section}] ${t.text}`);
80
+ }
81
+ return;
82
+ }
83
+ // List all sessions
84
+ const sessions = listAllSessions(phrenPath, 50);
85
+ if (sessions.length === 0) {
86
+ console.log("No sessions found.");
87
+ return;
88
+ }
89
+ console.log("Sessions (newest first):\n");
90
+ for (const s of sessions) {
91
+ const id = s.sessionId.slice(0, 8);
92
+ const proj = s.project ?? "—";
93
+ const dur = s.durationMins != null ? `${s.durationMins}m` : "?";
94
+ const status = s.status === "active" ? " ●" : "";
95
+ const findings = s.findingsAdded > 0 ? ` ${s.findingsAdded}f` : "";
96
+ const date = s.startedAt.slice(0, 16).replace("T", " ");
97
+ const summary = s.summary ? ` ${s.summary.slice(0, 50)}` : "";
98
+ console.log(` ${id}${status} ${date} ${dur}${findings} ${proj}${summary}`);
99
+ }
100
+ console.log(`\n${sessions.length} session(s). Use \`phren sessions <id>\` to drill into one.`);
101
+ }
102
+ export async function handleQuickstart() {
103
+ const { runInit } = await import("./init.js");
104
+ const { runLink } = await import("./link.js");
105
+ const dirBasename = path.basename(process.cwd());
106
+ const readline = await import("readline");
107
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
108
+ const projectName = await new Promise((resolve) => {
109
+ rl.question(`Project name [${dirBasename}]: `, (answer) => {
110
+ rl.close();
111
+ resolve(answer.trim() || dirBasename);
112
+ });
113
+ });
114
+ const normalizedProjectName = normalizeProjectNameForCreate(projectName);
115
+ if (!isValidProjectName(normalizedProjectName)) {
116
+ console.error(`Error: invalid project name "${projectName}". Use lowercase letters, numbers, hyphens, or underscores.`);
117
+ return;
118
+ }
119
+ console.log(`\nInitializing phren for "${normalizedProjectName}"...\n`);
120
+ await runInit({ yes: true });
121
+ const phrenPath = getPhrenPath();
122
+ await runLink(phrenPath, {});
123
+ const existingProject = findProjectNameCaseInsensitive(phrenPath, normalizedProjectName);
124
+ if (existingProject && existingProject !== normalizedProjectName) {
125
+ console.error(`Error: project "${existingProject}" already exists with different casing. Refusing to create "${normalizedProjectName}" because it would split the same project on case-sensitive filesystems.`);
126
+ return;
127
+ }
128
+ const projectDir = path.join(phrenPath, normalizedProjectName);
129
+ if (!fs.existsSync(projectDir)) {
130
+ fs.mkdirSync(projectDir, { recursive: true });
131
+ fs.writeFileSync(path.join(projectDir, "FINDINGS.md"), `# ${normalizedProjectName} Findings\n`);
132
+ fs.writeFileSync(path.join(projectDir, TASKS_FILENAME), `# ${normalizedProjectName} Tasks\n\n## Active\n\n## Queue\n\n## Done\n`);
133
+ }
134
+ console.log(`\n\u2713 phren ready. Project: ${normalizedProjectName}. Try: phren search 'your query'`);
135
+ }
136
+ export async function handleDebugInjection(args, profile) {
137
+ let cwd = process.cwd();
138
+ let sessionId = `debug-${Date.now()}`;
139
+ const promptParts = [];
140
+ for (let i = 0; i < args.length; i++) {
141
+ const arg = args[i];
142
+ if (arg === "--cwd") {
143
+ cwd = args[++i] || cwd;
144
+ continue;
145
+ }
146
+ if (arg === "--session") {
147
+ sessionId = args[++i] || sessionId;
148
+ continue;
149
+ }
150
+ if (arg === "--prompt") {
151
+ promptParts.push(args[++i] || "");
152
+ continue;
153
+ }
154
+ promptParts.push(arg);
155
+ }
156
+ const prompt = promptParts.join(" ").trim();
157
+ if (!prompt) {
158
+ console.error('Usage: phren debug-injection --prompt "your prompt here" [--cwd <path>] [--session <id>]');
159
+ process.exit(1);
160
+ }
161
+ const subprocessArgs = resolveSubprocessArgs("hook-prompt");
162
+ if (!subprocessArgs) {
163
+ console.error("Could not resolve phren entrypoint for debug-injection.");
164
+ process.exit(1);
165
+ }
166
+ const payload = JSON.stringify({
167
+ prompt,
168
+ cwd,
169
+ session_id: sessionId,
170
+ });
171
+ try {
172
+ const out = execFileSync(process.execPath, subprocessArgs, {
173
+ cwd: process.cwd(),
174
+ encoding: "utf8",
175
+ stdio: ["pipe", "pipe", "pipe"],
176
+ input: payload,
177
+ env: {
178
+ ...process.env,
179
+ PHREN_PATH: getPhrenPath(),
180
+ PHREN_PROFILE: profile,
181
+ },
182
+ timeout: EXEC_TIMEOUT_MS,
183
+ }).trim();
184
+ if (!out) {
185
+ console.log("(no context injected)");
186
+ return;
187
+ }
188
+ console.log(out);
189
+ }
190
+ catch (err) {
191
+ const stderr = err instanceof Error && "stderr" in err ? String(err.stderr || "").trim() : "";
192
+ if (stderr)
193
+ console.error(stderr);
194
+ console.error(`debug-injection failed: ${errorMessage(err)}`);
195
+ process.exit(1);
196
+ }
197
+ }
198
+ export async function handleInspectIndex(args, profile) {
199
+ let project;
200
+ let type;
201
+ let limit = 20;
202
+ for (let i = 0; i < args.length; i++) {
203
+ const arg = args[i];
204
+ if (arg === "--project") {
205
+ project = args[++i];
206
+ continue;
207
+ }
208
+ if (arg === "--type") {
209
+ type = args[++i];
210
+ continue;
211
+ }
212
+ if (arg === "--limit") {
213
+ const parsed = Number.parseInt(args[++i] || "", 10);
214
+ if (!Number.isNaN(parsed) && parsed > 0)
215
+ limit = Math.min(parsed, 200);
216
+ continue;
217
+ }
218
+ if (arg === "--help" || arg === "-h") {
219
+ console.log("Usage: phren inspect-index [--project <name>] [--type <doc-type>] [--limit <n>]");
220
+ return;
221
+ }
222
+ }
223
+ const db = await buildIndex(getPhrenPath(), profile);
224
+ const where = [];
225
+ const params = [];
226
+ if (project) {
227
+ where.push("project = ?");
228
+ params.push(project);
229
+ }
230
+ if (type) {
231
+ where.push("type = ?");
232
+ params.push(type);
233
+ }
234
+ const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
235
+ const totalRows = queryRows(db, `SELECT count(*) FROM docs ${whereSql}`, params);
236
+ const total = Number(totalRows?.[0]?.[0] ?? 0);
237
+ console.log(`FTS index docs: ${total}`);
238
+ if (project)
239
+ console.log(`Project filter: ${project}`);
240
+ if (type)
241
+ console.log(`Type filter: ${type}`);
242
+ const sample = queryRows(db, `SELECT project, filename, type, path FROM docs ${whereSql} ORDER BY project, type, filename LIMIT ?`, [...params, limit]);
243
+ if (!sample || sample.length === 0) {
244
+ console.log("No rows for current filter.");
245
+ return;
246
+ }
247
+ console.log("");
248
+ for (const row of sample) {
249
+ const [proj, filename, docType, filePath] = row;
250
+ console.log(`- ${proj}/${filename} (${docType})`);
251
+ console.log(` ${filePath}`);
252
+ }
253
+ }
@@ -0,0 +1,407 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { runtimeFile } from "./shared.js";
4
+ import { buildIndex, extractSnippet, queryDocRows, queryRows, queryEntityLinks, queryDocBySourceKey, logEntityMiss } from "./shared-index.js";
5
+ import { buildFtsQueryVariants, errorMessage, isValidProjectName } from "./utils.js";
6
+ import { keywordFallbackSearch } from "./core-search.js";
7
+ const MAX_HISTORY = 20;
8
+ const SEARCH_TYPE_ALIASES = {
9
+ skills: "skill",
10
+ };
11
+ const SEARCH_TYPES = new Set([
12
+ "claude",
13
+ "summary",
14
+ "findings",
15
+ "reference",
16
+ "task",
17
+ "changelog",
18
+ "canonical",
19
+ "memory-queue",
20
+ "skill",
21
+ "other",
22
+ ]);
23
+ function historyFile(phrenPath) {
24
+ return runtimeFile(phrenPath, "search-history.jsonl");
25
+ }
26
+ export function readSearchHistory(phrenPath) {
27
+ const file = historyFile(phrenPath);
28
+ if (!fs.existsSync(file))
29
+ return [];
30
+ try {
31
+ return fs.readFileSync(file, "utf8")
32
+ .split("\n")
33
+ .filter(Boolean)
34
+ .map((line) => JSON.parse(line));
35
+ }
36
+ catch (err) {
37
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
38
+ process.stderr.write(`[phren] readSearchHistory: ${errorMessage(err)}\n`);
39
+ return [];
40
+ }
41
+ }
42
+ function printSearchUsage() {
43
+ console.error("Usage:");
44
+ console.error(" phren search <query> [--project <name>] [--type <type>] [--limit <n>] [--all]");
45
+ console.error(" phren search --project <name> [--type <type>] [--limit <n>] [--all]");
46
+ console.error(" phren search --history Show recent searches");
47
+ console.error(" phren search --from-history <n> Re-run search #n from history");
48
+ console.error(" type: claude|summary|findings|reference|task|changelog|canonical|memory-queue|skill|other");
49
+ }
50
+ function validateAndNormalizeSearchOptions(phrenPath, queryParts, project, type, limit, showHistory, fromHistory, searchAll) {
51
+ if (showHistory) {
52
+ return { query: "", limit, showHistory: true };
53
+ }
54
+ if (fromHistory !== undefined) {
55
+ const history = readSearchHistory(phrenPath);
56
+ if (fromHistory > history.length || fromHistory < 1) {
57
+ console.error(`No search at position ${fromHistory}. History has ${history.length} entries.`);
58
+ process.exit(1);
59
+ }
60
+ const entry = history[fromHistory - 1];
61
+ return {
62
+ query: entry.query,
63
+ limit,
64
+ project: entry.project,
65
+ type: entry.type,
66
+ };
67
+ }
68
+ if (project && !isValidProjectName(project)) {
69
+ console.error(`Invalid project name: "${project}"`);
70
+ process.exit(1);
71
+ }
72
+ let normalizedType;
73
+ if (type) {
74
+ normalizedType = SEARCH_TYPE_ALIASES[type.toLowerCase()] || type.toLowerCase();
75
+ if (!SEARCH_TYPES.has(normalizedType)) {
76
+ console.error(`Invalid --type value: "${type}"`);
77
+ printSearchUsage();
78
+ process.exit(1);
79
+ }
80
+ }
81
+ const query = queryParts.join(" ").trim();
82
+ if (!query && !project) {
83
+ console.error("Provide a query, or pass --project to browse a project's indexed docs.");
84
+ printSearchUsage();
85
+ process.exit(1);
86
+ }
87
+ return {
88
+ query,
89
+ limit,
90
+ project,
91
+ type: normalizedType,
92
+ searchAll,
93
+ };
94
+ }
95
+ export function parseSearchArgs(phrenPath, args) {
96
+ const queryParts = [];
97
+ let project;
98
+ let type;
99
+ let limit = 10;
100
+ let showHistory = false;
101
+ let fromHistory;
102
+ let searchAll = false;
103
+ for (let i = 0; i < args.length; i++) {
104
+ const arg = args[i];
105
+ if (arg === "--help" || arg === "-h") {
106
+ printSearchUsage();
107
+ return null;
108
+ }
109
+ if (arg === "--history") {
110
+ showHistory = true;
111
+ continue;
112
+ }
113
+ if (arg === "--all") {
114
+ limit = 100;
115
+ searchAll = true;
116
+ continue;
117
+ }
118
+ const [flag, inlineValue] = arg.startsWith("--") ? arg.split("=", 2) : [arg, undefined];
119
+ const readValue = () => {
120
+ if (inlineValue !== undefined)
121
+ return inlineValue;
122
+ const next = args[i + 1];
123
+ if (!next || next.startsWith("-")) {
124
+ console.error(`Missing value for ${flag}`);
125
+ printSearchUsage();
126
+ process.exit(1);
127
+ }
128
+ i++;
129
+ return next;
130
+ };
131
+ if (flag === "--project") {
132
+ project = readValue();
133
+ continue;
134
+ }
135
+ if (flag === "--type") {
136
+ type = readValue();
137
+ continue;
138
+ }
139
+ if (flag === "--limit") {
140
+ const parsed = Number.parseInt(readValue(), 10);
141
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 200) {
142
+ console.error("Invalid --limit value. Use an integer between 1 and 200.");
143
+ process.exit(1);
144
+ }
145
+ limit = parsed;
146
+ continue;
147
+ }
148
+ if (flag === "--from-history") {
149
+ const parsed = Number.parseInt(readValue(), 10);
150
+ if (Number.isNaN(parsed) || parsed < 1) {
151
+ console.error("Invalid --from-history value. Use a positive integer.");
152
+ process.exit(1);
153
+ }
154
+ fromHistory = parsed;
155
+ continue;
156
+ }
157
+ if (arg.startsWith("-")) {
158
+ console.error(`Unknown search flag: ${arg}`);
159
+ printSearchUsage();
160
+ process.exit(1);
161
+ }
162
+ queryParts.push(arg);
163
+ }
164
+ return validateAndNormalizeSearchOptions(phrenPath, queryParts, project, type, limit, showHistory, fromHistory, searchAll);
165
+ }
166
+ function recordSearchQuery(phrenPath, opts) {
167
+ if (!opts.query)
168
+ return;
169
+ const file = historyFile(phrenPath);
170
+ fs.mkdirSync(path.dirname(file), { recursive: true });
171
+ const entry = {
172
+ query: opts.query,
173
+ ...(opts.project && { project: opts.project }),
174
+ ...(opts.type && { type: opts.type }),
175
+ ts: new Date().toISOString(),
176
+ };
177
+ let entries = readSearchHistory(phrenPath);
178
+ entries.push(entry);
179
+ if (entries.length > MAX_HISTORY)
180
+ entries = entries.slice(-MAX_HISTORY);
181
+ fs.writeFileSync(file, entries.map((item) => JSON.stringify(item)).join("\n") + "\n");
182
+ }
183
+ function formatSearchHistoryLines(phrenPath) {
184
+ const entries = readSearchHistory(phrenPath);
185
+ if (!entries.length)
186
+ return ["No search history."];
187
+ const lines = ["Recent searches:", ""];
188
+ entries.forEach((entry, index) => {
189
+ const scope = [
190
+ entry.project ? `--project ${entry.project}` : "",
191
+ entry.type ? `--type ${entry.type}` : "",
192
+ ].filter(Boolean).join(" ");
193
+ const ts = entry.ts.slice(0, 16).replace("T", " ");
194
+ lines.push(` ${index + 1}. "${entry.query}"${scope ? " " + scope : ""} (${ts})`);
195
+ });
196
+ return lines;
197
+ }
198
+ export async function runSearch(opts, phrenPath, profile) {
199
+ if (opts.showHistory) {
200
+ return { lines: formatSearchHistoryLines(phrenPath), exitCode: 0 };
201
+ }
202
+ recordSearchQuery(phrenPath, opts);
203
+ const db = await buildIndex(phrenPath, profile);
204
+ try {
205
+ let sql = "SELECT project, filename, type, content, path FROM docs";
206
+ const where = [];
207
+ const params = [];
208
+ let queryVariants = [];
209
+ if (opts.query) {
210
+ queryVariants = buildFtsQueryVariants(opts.query, opts.project, phrenPath);
211
+ const safeQuery = queryVariants[0] ?? "";
212
+ if (!safeQuery) {
213
+ return { lines: ["Query empty after sanitization."], exitCode: 1 };
214
+ }
215
+ where.push("docs MATCH ?");
216
+ params.push(safeQuery);
217
+ }
218
+ if (opts.project) {
219
+ where.push("project = ?");
220
+ params.push(opts.project);
221
+ }
222
+ if (opts.type) {
223
+ where.push("type = ?");
224
+ params.push(opts.type);
225
+ }
226
+ if (where.length > 0) {
227
+ sql += ` WHERE ${where.join(" AND ")}`;
228
+ }
229
+ sql += opts.query ? " ORDER BY rank LIMIT ?" : " ORDER BY project, type, filename LIMIT ?";
230
+ params.push(opts.limit);
231
+ let rows = queryDocRows(db, sql, params);
232
+ if ((!rows || rows.length === 0) && queryVariants.length > 1) {
233
+ for (const variant of queryVariants.slice(1)) {
234
+ const relaxedParams = [...params];
235
+ relaxedParams[0] = variant;
236
+ rows = queryDocRows(db, sql, relaxedParams);
237
+ if (rows?.length)
238
+ break;
239
+ }
240
+ }
241
+ const lines = [];
242
+ if (!rows && opts.query) {
243
+ const fallbackRows = keywordFallbackSearch(db, opts.query, { project: opts.project, type: opts.type, limit: opts.limit });
244
+ if (fallbackRows) {
245
+ rows = fallbackRows;
246
+ lines.push("(keyword fallback)");
247
+ }
248
+ }
249
+ if (!rows) {
250
+ if (opts.query) {
251
+ try {
252
+ const { logSearchMiss } = await import("./mcp-search.js");
253
+ logSearchMiss(phrenPath, opts.query, opts.project);
254
+ }
255
+ catch (err) {
256
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
257
+ process.stderr.write(`[phren] search logSearchMiss: ${errorMessage(err)}\n`);
258
+ }
259
+ }
260
+ const scope = [
261
+ opts.query ? `query "${opts.query}"` : undefined,
262
+ opts.project ? `project "${opts.project}"` : undefined,
263
+ opts.type ? `type "${opts.type}"` : undefined,
264
+ ].filter(Boolean).join(", ");
265
+ return { lines: [scope ? `No results found for ${scope}.` : "No results found."], exitCode: 0 };
266
+ }
267
+ if (opts.project && !opts.query) {
268
+ lines.push(`Browsing ${rows.length} document(s) in project "${opts.project}"`);
269
+ if (opts.type)
270
+ lines.push(`Type filter: ${opts.type}`);
271
+ lines.push("");
272
+ }
273
+ for (const row of rows) {
274
+ const snippet = extractSnippet(row.content, opts.query, 7);
275
+ lines.push(`[${row.project}/${row.filename}] (${row.type})`);
276
+ lines.push(snippet);
277
+ lines.push("");
278
+ }
279
+ return { lines, exitCode: 0 };
280
+ }
281
+ catch (err) {
282
+ return { lines: [`Search error: ${errorMessage(err)}`], exitCode: 1 };
283
+ }
284
+ }
285
+ // ── Fragment search (parity with MCP search_fragments) ────────────────────
286
+ export async function runFragmentSearch(query, phrenPath, profile, opts) {
287
+ if (!query.trim()) {
288
+ return { lines: ["Usage: phren search-fragments <query> [--project <name>] [--limit <n>]"], exitCode: 1 };
289
+ }
290
+ const db = await buildIndex(phrenPath, profile);
291
+ const max = opts.limit ?? 10;
292
+ const pattern = `%${query.toLowerCase()}%`;
293
+ let sql;
294
+ let params;
295
+ if (opts.project) {
296
+ sql = `
297
+ SELECT e.name, e.type, COUNT(el.source_id) as ref_count
298
+ FROM entities e
299
+ LEFT JOIN entity_links el ON el.target_id = e.id
300
+ WHERE e.name LIKE ? AND el.source_doc LIKE ?
301
+ GROUP BY e.id, e.name, e.type
302
+ ORDER BY ref_count DESC
303
+ LIMIT ?
304
+ `;
305
+ params = [pattern, `${opts.project}/%`, max];
306
+ }
307
+ else {
308
+ sql = `
309
+ SELECT e.name, e.type, COUNT(el.source_id) as ref_count
310
+ FROM entities e
311
+ LEFT JOIN entity_links el ON el.target_id = e.id
312
+ WHERE e.name LIKE ?
313
+ GROUP BY e.id, e.name, e.type
314
+ ORDER BY ref_count DESC
315
+ LIMIT ?
316
+ `;
317
+ params = [pattern, max];
318
+ }
319
+ const rows = queryRows(db, sql, params);
320
+ if (!rows || rows.length === 0) {
321
+ logEntityMiss(phrenPath, query, "cli_search_fragments", opts.project);
322
+ return { lines: [`No fragments matching "${query}".`], exitCode: 0 };
323
+ }
324
+ const lines = [`Fragments matching "${query}" (${rows.length} result(s)):\n`];
325
+ for (const r of rows) {
326
+ const name = String(r[0]);
327
+ const type = String(r[1]);
328
+ const refCount = Number(r[2]);
329
+ lines.push(` ${name} (${type}) — ${refCount} reference(s)`);
330
+ }
331
+ return { lines, exitCode: 0 };
332
+ }
333
+ export function parseFragmentSearchArgs(args) {
334
+ const queryParts = [];
335
+ let project;
336
+ let limit;
337
+ for (let i = 0; i < args.length; i++) {
338
+ const arg = args[i];
339
+ if (arg === "--help" || arg === "-h") {
340
+ console.error("Usage: phren search-fragments <query> [--project <name>] [--limit <n>]");
341
+ return null;
342
+ }
343
+ if (arg === "--project" && args[i + 1]) {
344
+ project = args[++i];
345
+ continue;
346
+ }
347
+ if (arg === "--limit" && args[i + 1]) {
348
+ limit = Number.parseInt(args[++i], 10);
349
+ continue;
350
+ }
351
+ if (!arg.startsWith("-")) {
352
+ queryParts.push(arg);
353
+ }
354
+ }
355
+ return { query: queryParts.join(" "), project, limit };
356
+ }
357
+ // ── Related docs (parity with MCP get_related_docs) ───────────────────────
358
+ export async function runRelatedDocs(entity, phrenPath, profile, opts) {
359
+ if (!entity.trim()) {
360
+ return { lines: ["Usage: phren related-docs <fragment-name> [--project <name>] [--limit <n>]"], exitCode: 1 };
361
+ }
362
+ const db = await buildIndex(phrenPath, profile);
363
+ const max = opts.limit ?? 10;
364
+ const links = queryEntityLinks(db, entity.toLowerCase());
365
+ let relatedDocs = links.related.filter(r => r.includes("/"));
366
+ if (opts.project) {
367
+ relatedDocs = relatedDocs.filter(d => d.startsWith(`${opts.project}/`));
368
+ }
369
+ relatedDocs = relatedDocs.slice(0, max);
370
+ if (relatedDocs.length === 0) {
371
+ logEntityMiss(phrenPath, entity, "cli_related_docs", opts.project);
372
+ return { lines: [`No docs found referencing fragment "${entity}".`], exitCode: 0 };
373
+ }
374
+ const lines = [`Docs referencing "${entity}" (${relatedDocs.length} result(s)):\n`];
375
+ for (const doc of relatedDocs) {
376
+ const docRow = queryDocBySourceKey(db, phrenPath, doc);
377
+ const snippet = docRow?.content ? docRow.content.slice(0, 120).replace(/\n/g, " ").trim() : "";
378
+ lines.push(` [${doc}]`);
379
+ if (snippet)
380
+ lines.push(` ${snippet}...`);
381
+ }
382
+ return { lines, exitCode: 0 };
383
+ }
384
+ export function parseRelatedDocsArgs(args) {
385
+ const entityParts = [];
386
+ let project;
387
+ let limit;
388
+ for (let i = 0; i < args.length; i++) {
389
+ const arg = args[i];
390
+ if (arg === "--help" || arg === "-h") {
391
+ console.error("Usage: phren related-docs <fragment-name> [--project <name>] [--limit <n>]");
392
+ return null;
393
+ }
394
+ if (arg === "--project" && args[i + 1]) {
395
+ project = args[++i];
396
+ continue;
397
+ }
398
+ if (arg === "--limit" && args[i + 1]) {
399
+ limit = Number.parseInt(args[++i], 10);
400
+ continue;
401
+ }
402
+ if (!arg.startsWith("-")) {
403
+ entityParts.push(arg);
404
+ }
405
+ }
406
+ return { entity: entityParts.join(" "), project, limit };
407
+ }