@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.
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- 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
|
+
}
|