@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,943 @@
1
+ /**
2
+ * Command palette and input handling for the phren interactive shell.
3
+ * Extracted from shell.ts to keep the orchestrator under 300 lines.
4
+ */
5
+ import { execFileSync } from "child_process";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import { addTask, addFinding, addProjectToProfile, completeTask, listProjectCards, pinTask, readTasks, readFindings, readReviewQueue, removeFinding, removeProjectFromProfile, resetShellState, saveShellState, setMachineProfile, tidyDoneTasks, canonicalTaskFilePath, unpinTask, updateTask, workNextTask, loadShellState, resolveTaskFilePath, } from "./data-access.js";
9
+ import { runtimeFile } from "./shared.js";
10
+ import { handleGovernMemories } from "./cli-govern.js";
11
+ import { runSearch } from "./cli-search.js";
12
+ import { consolidateProjectFindings } from "./governance-policy.js";
13
+ import { style } from "./shell-render.js";
14
+ import { SUB_VIEWS, TAB_ICONS } from "./shell-types.js";
15
+ import { getProjectSkills, getHookEntries, writeInstallPreferences } from "./shell-view.js";
16
+ import { removeSkillPath, setSkillEnabledAndSync } from "./skill-files.js";
17
+ import { resultMsg, editDistance, tokenize, expandIds, normalizeSection, tasksByFilter, queueByFilter, } from "./shell-palette.js";
18
+ import { errorMessage } from "./utils.js";
19
+ function taskFileForProject(phrenPath, project) {
20
+ return resolveTaskFilePath(phrenPath, project)
21
+ ?? canonicalTaskFilePath(phrenPath, project)
22
+ ?? path.join(phrenPath, project, "tasks.md");
23
+ }
24
+ export async function executePalette(host, input) {
25
+ const trimmed = input.trim();
26
+ if (!trimmed)
27
+ return;
28
+ const parts = tokenize(trimmed);
29
+ const command = (parts[0] || "").toLowerCase();
30
+ if (command === "help") {
31
+ host.showHelp = true;
32
+ host.setMessage(" Showing help — press any key to dismiss");
33
+ return;
34
+ }
35
+ if (command === "projects") {
36
+ host.setView("Projects");
37
+ host.setMessage(` ${TAB_ICONS.Projects} Projects`);
38
+ return;
39
+ }
40
+ if (command === "tasks" || command === "task") {
41
+ host.setView("Tasks");
42
+ host.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
43
+ return;
44
+ }
45
+ if (command === "learnings" || command === "findings" || command === "fragments") {
46
+ host.setView("Findings");
47
+ host.setMessage(` ${TAB_ICONS.Findings} Fragments`);
48
+ return;
49
+ }
50
+ if (command === "memory") {
51
+ host.setView("Review Queue");
52
+ host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
53
+ return;
54
+ }
55
+ if (command === "machines") {
56
+ host.setView("Machines/Profiles");
57
+ host.setMessage(" Machines/Profiles");
58
+ return;
59
+ }
60
+ if (command === "health") {
61
+ host.healthCache = undefined;
62
+ host.setView("Health");
63
+ host.setMessage(` ${TAB_ICONS.Health} Health`);
64
+ return;
65
+ }
66
+ if (command === "open") {
67
+ const project = parts[1];
68
+ if (!project) {
69
+ host.setMessage(" Usage: :open <project>");
70
+ return;
71
+ }
72
+ const cards = listProjectCards(host.phrenPath, host.profile);
73
+ if (!cards.some((c) => c.name === project)) {
74
+ host.setMessage(` Unknown project: ${project}`);
75
+ return;
76
+ }
77
+ host.state.project = project;
78
+ saveShellState(host.phrenPath, host.state);
79
+ host.setMessage(` ${style.green("●")} ${style.boldCyan(project)} — project context set`);
80
+ return;
81
+ }
82
+ if (command === "search") {
83
+ const query = trimmed.slice("search".length).trim();
84
+ if (!query) {
85
+ host.setMessage(" Usage: :search <query>");
86
+ return;
87
+ }
88
+ host.setMessage(" Searching…");
89
+ try {
90
+ const result = await runSearch({
91
+ query,
92
+ limit: 6,
93
+ project: host.state.project,
94
+ }, host.phrenPath, host.profile);
95
+ host.setMessage(result.lines.slice(0, 14).join("\n") || " No results.");
96
+ }
97
+ catch (err) {
98
+ host.setMessage(` Search failed: ${errorMessage(err)}`);
99
+ }
100
+ return;
101
+ }
102
+ if (command === "intro") {
103
+ const modeRaw = (parts[1] || "").toLowerCase();
104
+ const mode = modeRaw === "always" || modeRaw === "off" ? modeRaw : modeRaw === "once" ? "once-per-version" : modeRaw;
105
+ if (!["always", "once-per-version", "off"].includes(mode)) {
106
+ host.setMessage(" Usage: :intro always|once-per-version|off");
107
+ return;
108
+ }
109
+ host.state.introMode = mode;
110
+ saveShellState(host.phrenPath, host.state);
111
+ host.setMessage(` Intro mode: ${style.boldCyan(mode)}`);
112
+ return;
113
+ }
114
+ if (command === "add") {
115
+ const project = host.ensureProjectSelected();
116
+ if (!project)
117
+ return;
118
+ const text = trimmed.slice("add".length).trim();
119
+ if (!text) {
120
+ host.setMessage(" Usage: :add <task>");
121
+ return;
122
+ }
123
+ host.setMessage(` ${resultMsg(addTask(host.phrenPath, project, text))}`);
124
+ return;
125
+ }
126
+ if (command === "complete") {
127
+ const project = host.ensureProjectSelected();
128
+ if (!project)
129
+ return;
130
+ const match = parts.slice(1).join(" ").trim();
131
+ if (!match) {
132
+ host.setMessage(" Usage: :complete <id|match>");
133
+ return;
134
+ }
135
+ const ids = expandIds(match);
136
+ if (ids.length > 1) {
137
+ host.confirmThen(`Complete ${ids.length} items (${ids.join(", ")})?`, () => {
138
+ const file = taskFileForProject(host.phrenPath, project);
139
+ host.snapshotForUndo(`complete ${ids.length} items`, file);
140
+ host.setMessage(ids.map((id) => resultMsg(completeTask(host.phrenPath, project, id))).join("; "));
141
+ });
142
+ }
143
+ else {
144
+ host.confirmThen(`Complete "${match}"?`, () => {
145
+ const file = taskFileForProject(host.phrenPath, project);
146
+ host.snapshotForUndo(`complete "${match}"`, file);
147
+ host.setMessage(` ${resultMsg(completeTask(host.phrenPath, project, match))}`);
148
+ });
149
+ }
150
+ return;
151
+ }
152
+ if (command === "move") {
153
+ const project = host.ensureProjectSelected();
154
+ if (!project)
155
+ return;
156
+ if (parts.length < 3) {
157
+ host.setMessage(" Usage: :move <id|match> <active|queue|done>");
158
+ return;
159
+ }
160
+ const section = normalizeSection(parts[parts.length - 1]);
161
+ if (!section) {
162
+ host.setMessage(" Target section must be active|queue|done");
163
+ return;
164
+ }
165
+ const match = parts.slice(1, -1).join(" ");
166
+ const ids = expandIds(match);
167
+ if (ids.length > 1) {
168
+ const file = taskFileForProject(host.phrenPath, project);
169
+ host.snapshotForUndo(`move ${ids.length} items to ${section}`, file);
170
+ host.setMessage(ids.map((id) => resultMsg(updateTask(host.phrenPath, project, id, { section }))).join("; "));
171
+ }
172
+ else {
173
+ host.setMessage(` ${resultMsg(updateTask(host.phrenPath, project, match, { section }))}`);
174
+ }
175
+ return;
176
+ }
177
+ if (command === "reprioritize") {
178
+ const project = host.ensureProjectSelected();
179
+ if (!project)
180
+ return;
181
+ if (parts.length < 3) {
182
+ host.setMessage(" Usage: :reprioritize <id|match> <high|medium|low>");
183
+ return;
184
+ }
185
+ const priorityRaw = parts[parts.length - 1].toLowerCase();
186
+ if (!["high", "medium", "low"].includes(priorityRaw)) {
187
+ host.setMessage(" Priority must be high|medium|low");
188
+ return;
189
+ }
190
+ const priority = priorityRaw;
191
+ const match = parts.slice(1, -1).join(" ");
192
+ host.setMessage(` ${resultMsg(updateTask(host.phrenPath, project, match, { priority }))}`);
193
+ return;
194
+ }
195
+ if (command === "context") {
196
+ const project = host.ensureProjectSelected();
197
+ if (!project)
198
+ return;
199
+ if (parts.length < 3) {
200
+ host.setMessage(" Usage: :context <id|match> <text>");
201
+ return;
202
+ }
203
+ const match = parts[1];
204
+ const context = parts.slice(2).join(" ");
205
+ host.setMessage(` ${resultMsg(updateTask(host.phrenPath, project, match, { context }))}`);
206
+ return;
207
+ }
208
+ if (command === "pin") {
209
+ const project = host.ensureProjectSelected();
210
+ if (!project)
211
+ return;
212
+ if (parts.length < 2) {
213
+ host.setMessage(" Usage: :pin <id|match>");
214
+ return;
215
+ }
216
+ host.setMessage(` ${resultMsg(pinTask(host.phrenPath, project, parts.slice(1).join(" ")))}`);
217
+ return;
218
+ }
219
+ if (command === "unpin") {
220
+ const project = host.ensureProjectSelected();
221
+ if (!project)
222
+ return;
223
+ if (parts.length < 2) {
224
+ host.setMessage(" Usage: :unpin <id|match>");
225
+ return;
226
+ }
227
+ host.setMessage(` ${resultMsg(unpinTask(host.phrenPath, project, parts.slice(1).join(" ")))}`);
228
+ return;
229
+ }
230
+ if (command === "work" && parts[1]?.toLowerCase() === "next") {
231
+ const project = host.ensureProjectSelected();
232
+ if (!project)
233
+ return;
234
+ host.setMessage(` ${resultMsg(workNextTask(host.phrenPath, project))}`);
235
+ return;
236
+ }
237
+ if (command === "tidy") {
238
+ const project = host.ensureProjectSelected();
239
+ if (!project)
240
+ return;
241
+ const keep = parts[1] ? Number.parseInt(parts[1], 10) : 30;
242
+ const file = taskFileForProject(host.phrenPath, project);
243
+ host.snapshotForUndo("tidy", file);
244
+ host.setMessage(` ${resultMsg(tidyDoneTasks(host.phrenPath, project, Number.isNaN(keep) ? 30 : keep))}`);
245
+ return;
246
+ }
247
+ if (command === "learn" || command === "find") {
248
+ const project = host.ensureProjectSelected();
249
+ if (!project)
250
+ return;
251
+ const action = (parts[1] || "").toLowerCase();
252
+ if (action === "add") {
253
+ const text = trimmed.split(/\s+/).slice(2).join(" ").trim();
254
+ if (!text) {
255
+ host.setMessage(" Usage: :find add <text>");
256
+ return;
257
+ }
258
+ host.setMessage(` ${resultMsg(addFinding(host.phrenPath, project, text))}`);
259
+ return;
260
+ }
261
+ if (action === "remove") {
262
+ const match = parts.slice(2).join(" ").trim();
263
+ if (!match) {
264
+ host.setMessage(" Usage: :find remove <id|match>");
265
+ return;
266
+ }
267
+ host.confirmThen(`Remove finding "${match}"?`, () => {
268
+ const file = path.join(host.phrenPath, project, "FINDINGS.md");
269
+ host.snapshotForUndo(`find remove "${match}"`, file);
270
+ host.setMessage(` ${resultMsg(removeFinding(host.phrenPath, project, match))}`);
271
+ });
272
+ return;
273
+ }
274
+ host.setMessage(" Usage: :find add <text> | :find remove <id|match>");
275
+ return;
276
+ }
277
+ if (command === "mq") {
278
+ const project = host.ensureProjectSelected();
279
+ if (!project)
280
+ return;
281
+ const action = (parts[1] || "").toLowerCase();
282
+ host.setMessage(" Queue approve/reject/edit have been removed. The review queue is now read-only.");
283
+ return;
284
+ }
285
+ if (command === "machine" && parts[1]?.toLowerCase() === "map") {
286
+ if (parts.length < 4) {
287
+ host.setMessage(" Usage: :machine map <hostname> <profile>");
288
+ return;
289
+ }
290
+ host.setMessage(` ${resultMsg(setMachineProfile(host.phrenPath, parts[2], parts[3]))}`);
291
+ return;
292
+ }
293
+ if (command === "profile") {
294
+ const action = (parts[1] || "").toLowerCase();
295
+ const profileName = parts[2];
296
+ const project = parts[3];
297
+ if (!profileName || !project) {
298
+ host.setMessage(" Usage: :profile add-project|remove-project <profile> <project>");
299
+ return;
300
+ }
301
+ if (action === "add-project") {
302
+ host.setMessage(` ${resultMsg(addProjectToProfile(host.phrenPath, profileName, project))}`);
303
+ return;
304
+ }
305
+ if (action === "remove-project") {
306
+ host.setMessage(` ${resultMsg(removeProjectFromProfile(host.phrenPath, profileName, project))}`);
307
+ return;
308
+ }
309
+ host.setMessage(" Usage: :profile add-project|remove-project <profile> <project>");
310
+ return;
311
+ }
312
+ if ((command === "run" && parts[1]?.toLowerCase() === "fix") || command === "doctor") {
313
+ const t0 = Date.now();
314
+ const doctor = await host.deps.runDoctor(host.phrenPath, true);
315
+ host.healthCache = undefined;
316
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
317
+ host.setMessage(` doctor --fix: ${doctor.ok ? style.green("ok") : style.red("issues remain")} (${elapsed}s)`);
318
+ return;
319
+ }
320
+ if (command === "relink") {
321
+ const t0 = Date.now();
322
+ const r = await host.deps.runRelink(host.phrenPath);
323
+ host.setMessage(` ${r} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
324
+ return;
325
+ }
326
+ if (command === "rerun" && parts[1]?.toLowerCase() === "hooks") {
327
+ const t0 = Date.now();
328
+ const r = await host.deps.runHooks(host.phrenPath);
329
+ host.healthCache = undefined;
330
+ host.setMessage(` ${r} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
331
+ return;
332
+ }
333
+ if (command === "update") {
334
+ const t0 = Date.now();
335
+ const r = await host.deps.runUpdate();
336
+ host.setMessage(` ${r} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
337
+ return;
338
+ }
339
+ if (command === "govern") {
340
+ const project = host.ensureProjectSelected();
341
+ if (!project)
342
+ return;
343
+ try {
344
+ const t0 = Date.now();
345
+ const summary = await handleGovernMemories(project, true);
346
+ host.setMessage(` Governed memories: stale=${summary.staleCount}, conflicts=${summary.conflictCount}, review=${summary.reviewCount}` +
347
+ ` (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
348
+ }
349
+ catch (err) {
350
+ host.setMessage(` Governance failed: ${errorMessage(err)}`);
351
+ }
352
+ return;
353
+ }
354
+ if (command === "consolidate") {
355
+ const project = host.ensureProjectSelected();
356
+ if (!project)
357
+ return;
358
+ try {
359
+ const t0 = Date.now();
360
+ const backupPath = path.join(host.phrenPath, project, "FINDINGS.md.bak");
361
+ const backupBefore = fs.existsSync(backupPath) ? fs.statSync(backupPath).mtimeMs : undefined;
362
+ const result = consolidateProjectFindings(host.phrenPath, project);
363
+ const backupAfter = fs.existsSync(backupPath) ? fs.statSync(backupPath).mtimeMs : undefined;
364
+ const backupNote = result.ok && backupAfter !== undefined && backupAfter !== backupBefore
365
+ ? `; Updated backup: ${path.relative(host.phrenPath, backupPath).replace(/\\/g, "/")}`
366
+ : "";
367
+ host.setMessage(` ${resultMsg(result)}${backupNote} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
368
+ }
369
+ catch (err) {
370
+ host.setMessage(` Consolidation failed: ${errorMessage(err)}`);
371
+ }
372
+ return;
373
+ }
374
+ if (command === "conflicts") {
375
+ try {
376
+ const lines = [];
377
+ try {
378
+ const conflicted = execFileSync("git", ["diff", "--name-only", "--diff-filter=U"], {
379
+ cwd: host.phrenPath, encoding: "utf8", timeout: 10_000,
380
+ stdio: ["ignore", "pipe", "ignore"],
381
+ }).trim();
382
+ if (conflicted) {
383
+ lines.push(style.boldRed(" Unresolved conflicts:"));
384
+ for (const f of conflicted.split("\n").filter(Boolean)) {
385
+ lines.push(` ${style.red("!")} ${f}`);
386
+ }
387
+ }
388
+ }
389
+ catch (err) {
390
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
391
+ process.stderr.write(`[phren] shell status gitStatus: ${errorMessage(err)}\n`);
392
+ }
393
+ const auditPathNew = runtimeFile(host.phrenPath, "audit.log");
394
+ const auditPathLegacy = path.join(host.phrenPath, ".governance", "audit.log");
395
+ const auditPath = fs.existsSync(auditPathNew) ? auditPathNew : auditPathLegacy;
396
+ if (fs.existsSync(auditPath)) {
397
+ const auditLines = fs.readFileSync(auditPath, "utf8").split("\n")
398
+ .filter((l) => l.includes("auto_merge"))
399
+ .slice(-10);
400
+ if (auditLines.length) {
401
+ lines.push(` ${style.bold("Recent auto-merges:")}`);
402
+ for (const l of auditLines)
403
+ lines.push(` ${style.dim(l)}`);
404
+ }
405
+ }
406
+ const project = host.state.project;
407
+ if (project) {
408
+ const queueResult = readReviewQueue(host.phrenPath, project);
409
+ if (queueResult.ok) {
410
+ const conflictItems = queueResult.data.filter((q) => q.section === "Conflicts");
411
+ if (conflictItems.length) {
412
+ lines.push(` ${style.yellow(`${conflictItems.length} conflict(s) in Review Queue`)} (:mq approve|reject)`);
413
+ }
414
+ }
415
+ }
416
+ host.setMessage(lines.length ? lines.join("\n") : " No conflicts found.");
417
+ }
418
+ catch (err) {
419
+ host.setMessage(` Conflict check failed: ${errorMessage(err)}`);
420
+ }
421
+ return;
422
+ }
423
+ if (command === "undo") {
424
+ host.setMessage(` ${host.popUndo()}`);
425
+ return;
426
+ }
427
+ if (command === "diff") {
428
+ const project = host.ensureProjectSelected();
429
+ if (!project)
430
+ return;
431
+ try {
432
+ const projectDir = path.join(host.phrenPath, project);
433
+ const diff = execFileSync("git", ["diff", "--no-color", "--", projectDir], {
434
+ cwd: host.phrenPath, encoding: "utf8", timeout: 10_000,
435
+ stdio: ["ignore", "pipe", "ignore"],
436
+ }).trim();
437
+ if (!diff) {
438
+ const staged = execFileSync("git", ["diff", "--cached", "--no-color", "--", projectDir], {
439
+ cwd: host.phrenPath, encoding: "utf8", timeout: 10_000,
440
+ stdio: ["ignore", "pipe", "ignore"],
441
+ }).trim();
442
+ host.setMessage(staged || " No uncommitted changes.");
443
+ }
444
+ else {
445
+ const lines = diff.split("\n").slice(0, 30);
446
+ if (diff.split("\n").length > 30)
447
+ lines.push(style.dim(`... (${diff.split("\n").length - 30} more lines)`));
448
+ host.setMessage(lines.join("\n"));
449
+ }
450
+ }
451
+ catch {
452
+ host.setMessage(" Not a git repository or git not available.");
453
+ }
454
+ return;
455
+ }
456
+ if (command === "reset") {
457
+ host.setMessage(` ${resultMsg(resetShellState(host.phrenPath))}`);
458
+ const newState = loadShellState(host.phrenPath);
459
+ Object.assign(host.state, newState);
460
+ const cards = listProjectCards(host.phrenPath, host.profile);
461
+ host.state.project = cards[0]?.name;
462
+ return;
463
+ }
464
+ const suggestion = suggestCommand(command);
465
+ if (suggestion) {
466
+ host.setMessage(` Unknown: ${trimmed} — did you mean :${suggestion}?`);
467
+ }
468
+ else {
469
+ host.setMessage(` Unknown: ${trimmed} — press ${style.boldCyan("?")} for help`);
470
+ }
471
+ }
472
+ function suggestCommand(input) {
473
+ const known = [
474
+ "help", "projects", "tasks", "task", "findings", "review queue", "machines", "health",
475
+ "open", "search", "add", "complete", "move", "reprioritize", "pin", "unpin", "context",
476
+ "work next", "tidy", "find add", "find remove", "mq approve", "mq reject",
477
+ "mq edit", "machine map", "profile add-project", "profile remove-project",
478
+ "run fix", "relink", "rerun hooks", "update", "govern", "consolidate",
479
+ "undo", "diff", "conflicts", "reset",
480
+ ];
481
+ let best;
482
+ let bestDist = Infinity;
483
+ for (const cmd of known) {
484
+ const d = editDistance(input.toLowerCase(), cmd);
485
+ if (d < bestDist && d <= 2) {
486
+ bestDist = d;
487
+ best = cmd;
488
+ }
489
+ }
490
+ return best;
491
+ }
492
+ export function completeInput(line, phrenPath, profile, state) {
493
+ const commands = [
494
+ ":projects", ":tasks", ":task", ":findings", ":review queue", ":machines", ":health",
495
+ ":open", ":search", ":add", ":complete", ":move", ":reprioritize", ":pin",
496
+ ":unpin", ":context", ":work next", ":tidy", ":find add", ":find remove",
497
+ ":mq approve", ":mq reject", ":mq edit", ":machine map",
498
+ ":profile add-project", ":profile remove-project",
499
+ ":run fix", ":relink", ":rerun hooks", ":update", ":govern", ":consolidate",
500
+ ":undo", ":diff", ":conflicts", ":reset", ":help",
501
+ ];
502
+ const trimmed = line.trimStart();
503
+ if (!trimmed.startsWith(":"))
504
+ return [];
505
+ const after = trimmed.slice(1);
506
+ const parts = tokenize(after);
507
+ const endsWithSpace = /\s$/.test(trimmed);
508
+ if (parts.length === 0)
509
+ return commands;
510
+ if (parts.length === 1 && !endsWithSpace) {
511
+ const prefix = `:${parts[0].toLowerCase()}`;
512
+ return commands.filter((c) => c.startsWith(prefix));
513
+ }
514
+ const cmd = parts[0].toLowerCase();
515
+ if (cmd === "open") {
516
+ return listProjectCards(phrenPath, profile).map((c) => `:open ${c.name}`);
517
+ }
518
+ if (["complete", "move", "reprioritize", "context", "pin", "unpin"].includes(cmd)) {
519
+ const project = state.project;
520
+ if (!project)
521
+ return [];
522
+ const result = readTasks(phrenPath, project);
523
+ if (!result.ok)
524
+ return [];
525
+ return [
526
+ ...result.data.items.Active,
527
+ ...result.data.items.Queue,
528
+ ...result.data.items.Done,
529
+ ].map((item) => `:${cmd} ${item.id}`);
530
+ }
531
+ if (cmd === "mq" && ["approve", "reject", "edit"].includes((parts[1] || "").toLowerCase())) {
532
+ const project = state.project;
533
+ if (!project)
534
+ return [];
535
+ const result = readReviewQueue(phrenPath, project);
536
+ if (!result.ok)
537
+ return [];
538
+ return result.data.map((item) => `:mq ${parts[1].toLowerCase()} ${item.id}`);
539
+ }
540
+ if (cmd === "find" && (parts[1] || "").toLowerCase() === "remove") {
541
+ const project = state.project;
542
+ if (!project)
543
+ return [];
544
+ const r = readFindings(phrenPath, project);
545
+ if (!r.ok)
546
+ return [];
547
+ return r.data.map((item) => `:find remove ${item.id}`);
548
+ }
549
+ return commands;
550
+ }
551
+ // ── List items for each view ──────────────────────────────────────────────────
552
+ export function getListItems(phrenPath, profile, state, healthLineCount) {
553
+ switch (state.view) {
554
+ case "Projects": {
555
+ const cards = listProjectCards(phrenPath, profile);
556
+ return state.filter
557
+ ? cards.filter((c) => `${c.name} ${c.summary} ${c.docs.join(" ")}`.toLowerCase().includes(state.filter.toLowerCase()))
558
+ : cards;
559
+ }
560
+ case "Tasks": {
561
+ if (!state.project)
562
+ return [];
563
+ const result = readTasks(phrenPath, state.project);
564
+ if (!result.ok)
565
+ return [];
566
+ const active = state.filter ? tasksByFilter(result.data.items.Active, state.filter) : result.data.items.Active;
567
+ const queue = state.filter ? tasksByFilter(result.data.items.Queue, state.filter) : result.data.items.Queue;
568
+ return [...active, ...queue];
569
+ }
570
+ case "Findings": {
571
+ if (!state.project)
572
+ return [];
573
+ const result = readFindings(phrenPath, state.project);
574
+ if (!result.ok)
575
+ return [];
576
+ return state.filter
577
+ ? result.data.filter((i) => `${i.id} ${i.date} ${i.text}`.toLowerCase().includes(state.filter.toLowerCase()))
578
+ : result.data;
579
+ }
580
+ case "Review Queue": {
581
+ if (!state.project)
582
+ return [];
583
+ const result = readReviewQueue(phrenPath, state.project);
584
+ if (!result.ok)
585
+ return [];
586
+ return state.filter ? queueByFilter(result.data, state.filter) : result.data;
587
+ }
588
+ case "Skills": {
589
+ if (!state.project)
590
+ return [];
591
+ const allSkills = getProjectSkills(phrenPath, state.project).map((s) => ({ name: s.name, text: `${s.enabled ? "enabled" : "disabled"} · ${s.path}` }));
592
+ return state.filter
593
+ ? allSkills.filter((s) => `${s.name} ${s.text}`.toLowerCase().includes(state.filter.toLowerCase()))
594
+ : allSkills;
595
+ }
596
+ case "Hooks": {
597
+ return getHookEntries(phrenPath).map((e) => ({ name: e.event, text: e.enabled ? "active" : "inactive" }));
598
+ }
599
+ case "Health":
600
+ return Array.from({ length: Math.max(1, healthLineCount) }, (_, i) => ({ id: String(i) }));
601
+ default:
602
+ return [];
603
+ }
604
+ }
605
+ // ── Activation (Enter key) ────────────────────────────────────────────────────
606
+ async function activateSelected(host) {
607
+ const cursor = host.currentCursor();
608
+ const items = host.getListItems();
609
+ const item = items[cursor];
610
+ if (!item)
611
+ return;
612
+ switch (host.state.view) {
613
+ case "Projects":
614
+ if (item.name) {
615
+ host.state.project = item.name;
616
+ saveShellState(host.phrenPath, host.state);
617
+ host.setView("Tasks");
618
+ host.setMessage(` ${style.green("●")} ${style.boldCyan(item.name)}`);
619
+ }
620
+ break;
621
+ case "Tasks":
622
+ if (item.id) {
623
+ const project = host.ensureProjectSelected();
624
+ if (!project)
625
+ return;
626
+ const file = taskFileForProject(host.phrenPath, project);
627
+ host.confirmThen(`Complete ${style.dim(item.id)} "${item.line}"?`, () => {
628
+ host.snapshotForUndo(`complete ${item.id}`, file);
629
+ const r = completeTask(host.phrenPath, project, item.id);
630
+ host.invalidateSubsectionsCache();
631
+ host.setMessage(` ${resultMsg(r)}`);
632
+ host.setCursor(Math.max(0, cursor - 1));
633
+ });
634
+ }
635
+ break;
636
+ case "Findings":
637
+ if (item.text) {
638
+ host.setMessage(` ${style.dim(item.id ?? "")} ${item.text}`);
639
+ }
640
+ break;
641
+ case "Review Queue":
642
+ if (item.text) {
643
+ host.setMessage(` ${style.dim(item.id ?? "")} ${item.text} ${style.dim("[ a approve · d reject ]")}`);
644
+ }
645
+ break;
646
+ case "Skills":
647
+ if (item.name) {
648
+ host.setMessage(` ${style.bold(item.name)} ${style.dim(item.text ?? "")}`);
649
+ }
650
+ break;
651
+ case "Hooks":
652
+ if (item.name) {
653
+ host.setMessage(` ${item.text === "active" ? style.boldGreen("active") : style.dim("inactive")} ${style.bold(item.name)}`);
654
+ }
655
+ break;
656
+ }
657
+ }
658
+ // ── View-specific action keys ─────────────────────────────────────────────────
659
+ async function doViewAction(host, key) {
660
+ const cursor = host.currentCursor();
661
+ const items = host.getListItems();
662
+ const item = items[cursor];
663
+ const project = host.state.project;
664
+ switch (host.state.view) {
665
+ case "Tasks":
666
+ if (key === "a") {
667
+ host.startInput("add", "");
668
+ }
669
+ else if (key === "d" && item?.id) {
670
+ if (!project) {
671
+ host.setMessage("Select a project first.");
672
+ return;
673
+ }
674
+ const file = taskFileForProject(host.phrenPath, project);
675
+ const taskResult = readTasks(host.phrenPath, project);
676
+ const isActive = taskResult.ok && taskResult.data.items.Active.some((i) => i.id === item.id);
677
+ const targetSection = isActive ? "Queue" : "Active";
678
+ host.snapshotForUndo(`move ${item.id} → ${targetSection.toLowerCase()}`, file);
679
+ const r = updateTask(host.phrenPath, project, item.id, { section: targetSection });
680
+ host.invalidateSubsectionsCache();
681
+ host.setMessage(` ${resultMsg(r)}`);
682
+ }
683
+ break;
684
+ case "Findings":
685
+ if (key === "a") {
686
+ host.startInput("learn-add", "");
687
+ }
688
+ else if ((key === "d" || key === "\x7f") && item?.text) {
689
+ if (!project) {
690
+ host.setMessage("Select a project first.");
691
+ return;
692
+ }
693
+ host.confirmThen(`Delete finding ${style.dim(item.id ?? "")}?`, () => {
694
+ const file = path.join(host.phrenPath, project, "FINDINGS.md");
695
+ host.snapshotForUndo(`remove finding ${item.id ?? ''}`, file);
696
+ const r = removeFinding(host.phrenPath, project, item.text);
697
+ host.setMessage(` ${resultMsg(r)}`);
698
+ host.setCursor(Math.max(0, cursor - 1));
699
+ });
700
+ }
701
+ break;
702
+ case "Review Queue":
703
+ host.setMessage(" Review queue is read-only.");
704
+ break;
705
+ case "Skills":
706
+ if ((key === "d" || key === "\x7f") && item?.name) {
707
+ if (!project) {
708
+ host.setMessage("Select a project first.");
709
+ return;
710
+ }
711
+ const skillPath = item.text;
712
+ host.confirmThen(`Remove skill "${item.name}"?`, () => {
713
+ try {
714
+ removeSkillPath(skillPath.split("·").slice(-1)[0].trim());
715
+ host.setMessage(` Removed ${item.name}`);
716
+ host.setCursor(Math.max(0, cursor - 1));
717
+ }
718
+ catch (err) {
719
+ host.setMessage(` Failed: ${errorMessage(err)}`);
720
+ }
721
+ });
722
+ }
723
+ else if (key === "t" && item?.name) {
724
+ if (!project) {
725
+ host.setMessage("Select a project first.");
726
+ return;
727
+ }
728
+ const isEnabled = !item.text?.startsWith("disabled");
729
+ setSkillEnabledAndSync(host.phrenPath, project, item.name, !isEnabled);
730
+ host.setMessage(` ${!isEnabled ? "Enabled" : "Disabled"} ${item.name}`);
731
+ }
732
+ else if (key === "a") {
733
+ if (!project) {
734
+ host.setMessage("Select a project first.");
735
+ return;
736
+ }
737
+ host.startInput("skill-add", "");
738
+ }
739
+ break;
740
+ case "Hooks":
741
+ if (key === "a" || key === "d") {
742
+ const enable = key === "a";
743
+ writeInstallPreferences(host.phrenPath, { hooksEnabled: enable });
744
+ host.setMessage(` Hooks ${enable ? style.boldGreen("enabled") : style.dim("disabled")} — takes effect next session`);
745
+ }
746
+ break;
747
+ }
748
+ }
749
+ // ── Cursor position display ───────────────────────────────────────────────────
750
+ function showCursorPosition(host) {
751
+ const items = host.getListItems();
752
+ const count = items.length;
753
+ if (count === 0)
754
+ return;
755
+ const cursor = host.currentCursor();
756
+ const item = items[cursor];
757
+ const label = item?.name ?? item?.line ?? item?.text ?? "";
758
+ const short = label.length > 50 ? label.slice(0, 48) + "…" : label;
759
+ host.setMessage(` ${style.dim(`${cursor + 1} / ${count}`)}${short ? ` ${style.dimItalic(short)}` : ""}`);
760
+ }
761
+ // ── Navigate-mode key handler ─────────────────────────────────────────────────
762
+ export async function handleNavigateKey(host, key) {
763
+ if (key === "\x1b[A") {
764
+ host.moveCursor(-1);
765
+ showCursorPosition(host);
766
+ return true;
767
+ }
768
+ if (key === "\x1b[B") {
769
+ host.moveCursor(1);
770
+ showCursorPosition(host);
771
+ return true;
772
+ }
773
+ if (key === "\x1b[D") {
774
+ if (host.state.view === "Projects") {
775
+ host.setMessage(` ${style.dim("Projects is the dashboard landing screen")}`);
776
+ }
777
+ else {
778
+ prevTab(host);
779
+ }
780
+ return true;
781
+ }
782
+ if (key === "\x1b[C") {
783
+ if (host.state.view === "Projects") {
784
+ host.setMessage(` ${style.dim("Press ↵ to enter the selected project's tasks")}`);
785
+ }
786
+ else {
787
+ nextTab(host);
788
+ }
789
+ return true;
790
+ }
791
+ if (key === "\x1b[5~") {
792
+ host.moveCursor(-10);
793
+ showCursorPosition(host);
794
+ return true;
795
+ }
796
+ if (key === "\x1b[6~") {
797
+ host.moveCursor(10);
798
+ showCursorPosition(host);
799
+ return true;
800
+ }
801
+ if (key === "\x1b[H" || key === "\x1b[1~") {
802
+ host.setCursor(0);
803
+ showCursorPosition(host);
804
+ return true;
805
+ }
806
+ if (key === "\x1b[F" || key === "\x1b[4~") {
807
+ host.setCursor(host.getListItems().length - 1);
808
+ showCursorPosition(host);
809
+ return true;
810
+ }
811
+ if (key === "\t") {
812
+ nextTab(host);
813
+ return true;
814
+ }
815
+ if (key === "\x1b[Z") {
816
+ prevTab(host);
817
+ return true;
818
+ }
819
+ if (key === "q" || key === "Q")
820
+ return false;
821
+ if (key === "\r" || key === "\n") {
822
+ await activateSelected(host);
823
+ return true;
824
+ }
825
+ if (key === "?") {
826
+ host.showHelp = !host.showHelp;
827
+ host.setMessage(host.showHelp ? " Showing help — press any key to dismiss" : ` ${style.boldCyan("←→")} ${style.dim("tabs")} ${style.boldCyan("↑↓")} ${style.dim("move")} ${style.boldCyan("↵")} ${style.dim("activate")} ${style.boldCyan("?")} ${style.dim("help")}`);
828
+ return true;
829
+ }
830
+ if (key === "/") {
831
+ host.startInput("filter", host.filter || "");
832
+ return true;
833
+ }
834
+ if (key === ":") {
835
+ host.startInput("command", "");
836
+ return true;
837
+ }
838
+ if (key === "\x1b") {
839
+ if (host.filter) {
840
+ host.setFilter("");
841
+ }
842
+ else if (host.state.view === "Health") {
843
+ const returnTo = host.prevHealthView ?? "Projects";
844
+ host.setView(returnTo);
845
+ host.prevHealthView = undefined;
846
+ host.setMessage(` ${TAB_ICONS[returnTo] ?? TAB_ICONS.Projects} ${returnTo}`);
847
+ }
848
+ else if (host.state.view !== "Projects") {
849
+ host.setView("Projects");
850
+ host.setMessage(` ${TAB_ICONS.Projects} ${style.dim("dashboard")}`);
851
+ }
852
+ else {
853
+ host.setMessage(` ${style.dim("press")} ${style.boldCyan("q")} ${style.dim("to quit")}`);
854
+ }
855
+ return true;
856
+ }
857
+ if (key === "p") {
858
+ host.setView("Projects");
859
+ host.setMessage(` ${TAB_ICONS.Projects} Projects`);
860
+ return true;
861
+ }
862
+ if (key === "b") {
863
+ if (!host.state.project) {
864
+ host.setMessage(style.dim(" Select a project first (↵)"));
865
+ return true;
866
+ }
867
+ host.setView("Tasks");
868
+ host.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
869
+ return true;
870
+ }
871
+ if (key === "l") {
872
+ if (!host.state.project) {
873
+ host.setMessage(style.dim(" Select a project first (↵)"));
874
+ return true;
875
+ }
876
+ host.setView("Findings");
877
+ host.setMessage(` ${TAB_ICONS.Findings} Fragments`);
878
+ return true;
879
+ }
880
+ if (key === "m") {
881
+ if (!host.state.project) {
882
+ host.setMessage(style.dim(" Select a project first (↵)"));
883
+ return true;
884
+ }
885
+ host.setView("Review Queue");
886
+ host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
887
+ return true;
888
+ }
889
+ if (key === "s") {
890
+ if (!host.state.project) {
891
+ host.setMessage(style.dim(" Select a project first (↵)"));
892
+ return true;
893
+ }
894
+ host.setView("Skills");
895
+ host.setMessage(` ${TAB_ICONS.Skills} Skills`);
896
+ return true;
897
+ }
898
+ if (key === "k") {
899
+ host.setView("Hooks");
900
+ host.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
901
+ return true;
902
+ }
903
+ if (key === "h") {
904
+ host.prevHealthView = host.state.view === "Health" ? host.prevHealthView : host.state.view;
905
+ host.healthCache = undefined;
906
+ host.setView("Health");
907
+ host.setMessage(` ${TAB_ICONS.Health} Health ${style.dim("(esc to return)")}`);
908
+ return true;
909
+ }
910
+ if (key === "i" && host.state.view === "Projects") {
911
+ const next = host.state.introMode === "always" ? "once-per-version" : host.state.introMode === "off" ? "always" : "off";
912
+ host.state.introMode = next;
913
+ saveShellState(host.phrenPath, host.state);
914
+ host.setMessage(` Intro mode: ${style.boldCyan(next)}`);
915
+ return true;
916
+ }
917
+ if (["a", "d", "e", "t", "\x7f"].includes(key)) {
918
+ await doViewAction(host, key);
919
+ return true;
920
+ }
921
+ return true;
922
+ }
923
+ // ── Tab switching ─────────────────────────────────────────────────────────────
924
+ function nextTab(host) {
925
+ if (host.state.view === "Projects" || host.state.view === "Health")
926
+ return;
927
+ const idx = SUB_VIEWS.indexOf(host.state.view);
928
+ const next = SUB_VIEWS[(idx + 1) % SUB_VIEWS.length];
929
+ if (next) {
930
+ host.setView(next);
931
+ host.setMessage(` ${TAB_ICONS[next]} ${next}`);
932
+ }
933
+ }
934
+ function prevTab(host) {
935
+ if (host.state.view === "Projects" || host.state.view === "Health")
936
+ return;
937
+ const idx = SUB_VIEWS.indexOf(host.state.view);
938
+ const prev = SUB_VIEWS[(idx - 1 + SUB_VIEWS.length) % SUB_VIEWS.length];
939
+ if (prev) {
940
+ host.setView(prev);
941
+ host.setMessage(` ${TAB_ICONS[prev]} ${prev}`);
942
+ }
943
+ }