@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,352 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { addTask, addFinding, loadShellState, saveShellState, } from "./data-access.js";
|
|
4
|
+
import { style } from "./shell-render.js";
|
|
5
|
+
import { MAX_UNDO_STACK, TAB_ICONS, } from "./shell-types.js";
|
|
6
|
+
import { resultMsg, defaultRunHooks, defaultRunUpdate, defaultRunRelink, } from "./shell-palette.js";
|
|
7
|
+
import { runDoctor } from "./link.js";
|
|
8
|
+
import { renderShell, } from "./shell-view.js";
|
|
9
|
+
import { executePalette, completeInput as completeInputFn, getListItems, handleNavigateKey, } from "./shell-input.js";
|
|
10
|
+
import { errorMessage } from "./utils.js";
|
|
11
|
+
// ── Shell class ──────────────────────────────────────────────────────────────
|
|
12
|
+
export class PhrenShell {
|
|
13
|
+
phrenPath;
|
|
14
|
+
profile;
|
|
15
|
+
deps;
|
|
16
|
+
state;
|
|
17
|
+
message = ` ${style.boldCyan("←→")} ${style.dim("tabs")} ${style.boldCyan("↑↓")} ${style.dim("move")} ${style.boldCyan("↵")} ${style.dim("activate")} ${style.boldCyan("?")} ${style.dim("help")}`;
|
|
18
|
+
healthCache;
|
|
19
|
+
prevHealthView;
|
|
20
|
+
showHelp = false;
|
|
21
|
+
pendingConfirm;
|
|
22
|
+
undoStack = [];
|
|
23
|
+
navMode = "navigate";
|
|
24
|
+
inputBuf = "";
|
|
25
|
+
inputCtx = "";
|
|
26
|
+
inputMqId = "";
|
|
27
|
+
cursorMap = {};
|
|
28
|
+
viewScrollMap = {};
|
|
29
|
+
healthLineCount = 0;
|
|
30
|
+
_subsectionsCache = null;
|
|
31
|
+
get mode() { return this.navMode; }
|
|
32
|
+
get inputBuffer() { return this.inputBuf; }
|
|
33
|
+
get filter() { return this.state.filter; }
|
|
34
|
+
constructor(phrenPath, profile, deps = {
|
|
35
|
+
runDoctor,
|
|
36
|
+
runRelink: defaultRunRelink,
|
|
37
|
+
runHooks: defaultRunHooks,
|
|
38
|
+
runUpdate: defaultRunUpdate,
|
|
39
|
+
}) {
|
|
40
|
+
this.phrenPath = phrenPath;
|
|
41
|
+
this.profile = profile;
|
|
42
|
+
this.deps = deps;
|
|
43
|
+
this.state = loadShellState(phrenPath);
|
|
44
|
+
this.state.view = "Projects";
|
|
45
|
+
this.message = this.state.project
|
|
46
|
+
? ` Dashboard ready — active context ${style.boldCyan(this.state.project)}`
|
|
47
|
+
: ` Dashboard ready — choose a project with ${style.boldCyan("↵")} or stay global`;
|
|
48
|
+
}
|
|
49
|
+
close() { saveShellState(this.phrenPath, this.state); }
|
|
50
|
+
setMessage(msg) { this.message = msg; }
|
|
51
|
+
confirmThen(label, action) {
|
|
52
|
+
this.pendingConfirm = { label, action };
|
|
53
|
+
this.setMessage(`${label} ${style.boldCyan("y")} ${style.dim("confirm")} ${style.boldCyan("n")} ${style.dim("cancel")}`);
|
|
54
|
+
}
|
|
55
|
+
setView(view) {
|
|
56
|
+
this.state.view = view;
|
|
57
|
+
this.viewScrollMap[view] = 0;
|
|
58
|
+
saveShellState(this.phrenPath, this.state);
|
|
59
|
+
}
|
|
60
|
+
setFilter(value) {
|
|
61
|
+
this.state.filter = value.trim() || undefined;
|
|
62
|
+
saveShellState(this.phrenPath, this.state);
|
|
63
|
+
this.setMessage(this.state.filter ? ` Filter: ${style.yellow(this.state.filter)}` : " Filter cleared.");
|
|
64
|
+
}
|
|
65
|
+
snapshotForUndo(label, file) {
|
|
66
|
+
try {
|
|
67
|
+
if (fs.existsSync(file)) {
|
|
68
|
+
const content = fs.readFileSync(file, "utf8");
|
|
69
|
+
this.undoStack.push({ label, file, content });
|
|
70
|
+
if (this.undoStack.length > MAX_UNDO_STACK)
|
|
71
|
+
this.undoStack.shift();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
76
|
+
process.stderr.write(`[phren] shell pushUndo: ${errorMessage(err)}\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
popUndo() {
|
|
80
|
+
const entry = this.undoStack.pop();
|
|
81
|
+
if (!entry)
|
|
82
|
+
return "Nothing to undo.";
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(entry.file, entry.content);
|
|
85
|
+
return `Undid: ${entry.label}`;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return `Undo failed: ${errorMessage(err)}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
ensureProjectSelected() {
|
|
92
|
+
if (!this.state.project) {
|
|
93
|
+
this.setMessage("No project selected — open one from Projects view (↵) or use :open <project>");
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return this.state.project;
|
|
97
|
+
}
|
|
98
|
+
invalidateSubsectionsCache() { this._subsectionsCache = null; }
|
|
99
|
+
// ── Cursor management ────────────────────────────────────────────────────
|
|
100
|
+
currentCursor() { return this.cursorMap[this.state.view] ?? 0; }
|
|
101
|
+
setCursor(n) {
|
|
102
|
+
const count = this.getListItems().length;
|
|
103
|
+
this.cursorMap[this.state.view] = count > 0 ? Math.max(0, Math.min(n, count - 1)) : 0;
|
|
104
|
+
}
|
|
105
|
+
moveCursor(delta) { this.setCursor(this.currentCursor() + delta); }
|
|
106
|
+
currentScroll() { return this.viewScrollMap[this.state.view] ?? 0; }
|
|
107
|
+
setScroll(n) { this.viewScrollMap[this.state.view] = Math.max(0, n); }
|
|
108
|
+
getListItems() {
|
|
109
|
+
return getListItems(this.phrenPath, this.profile, this.state, this.healthLineCount);
|
|
110
|
+
}
|
|
111
|
+
startInput(ctx, initial) { this.navMode = "input"; this.inputCtx = ctx; this.inputBuf = initial; }
|
|
112
|
+
cancelInput() { this.navMode = "navigate"; this.inputBuf = ""; this.inputCtx = ""; this.setMessage(" Cancelled."); }
|
|
113
|
+
async submitInput() {
|
|
114
|
+
const buf = this.inputBuf;
|
|
115
|
+
const ctx = this.inputCtx;
|
|
116
|
+
this.navMode = "navigate";
|
|
117
|
+
this.inputBuf = "";
|
|
118
|
+
this.inputCtx = "";
|
|
119
|
+
if (!buf.trim() && ctx !== "command") {
|
|
120
|
+
this.setMessage(" Nothing entered.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
switch (ctx) {
|
|
124
|
+
case "filter":
|
|
125
|
+
this.setFilter(buf);
|
|
126
|
+
break;
|
|
127
|
+
case "command":
|
|
128
|
+
await this.runPalette(buf.startsWith(":") ? buf.slice(1) : buf);
|
|
129
|
+
break;
|
|
130
|
+
case "add": {
|
|
131
|
+
const p = this.ensureProjectSelected();
|
|
132
|
+
if (!p)
|
|
133
|
+
return;
|
|
134
|
+
this.setMessage(` ${resultMsg(addTask(this.phrenPath, p, buf))}`);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "learn-add": {
|
|
138
|
+
const p = this.ensureProjectSelected();
|
|
139
|
+
if (!p)
|
|
140
|
+
return;
|
|
141
|
+
this.setMessage(` ${resultMsg(addFinding(this.phrenPath, p, buf))}`);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "skill-add": {
|
|
145
|
+
const p = this.ensureProjectSelected();
|
|
146
|
+
if (!p)
|
|
147
|
+
return;
|
|
148
|
+
const name = buf.trim().replace(/\.md$/i, "").replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
149
|
+
if (!name) {
|
|
150
|
+
this.setMessage(" No name entered.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const destDir = path.join(this.phrenPath, p, "skills");
|
|
154
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
155
|
+
const dest = path.join(destDir, `${name}.md`);
|
|
156
|
+
if (fs.existsSync(dest)) {
|
|
157
|
+
this.setMessage(` Skill "${name}" already exists.`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const template = `# ${name}\n\nDescribe what this skill does.\n\n## Usage\n\n\`\`\`\nExample usage here\n\`\`\`\n`;
|
|
161
|
+
fs.writeFileSync(dest, template, "utf8");
|
|
162
|
+
this.setMessage(` Created skill "${name}" — edit ${dest}`);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "mq-edit": {
|
|
166
|
+
this.setMessage(" Queue editing has been removed. The review queue is now read-only.");
|
|
167
|
+
this.inputMqId = "";
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ── Raw key handling ───────────────────────────────────────────────────
|
|
173
|
+
async handleRawKey(key) {
|
|
174
|
+
if (key === "\x03" || key === "\x04")
|
|
175
|
+
return false;
|
|
176
|
+
if (this.pendingConfirm) {
|
|
177
|
+
const pending = this.pendingConfirm;
|
|
178
|
+
this.pendingConfirm = undefined;
|
|
179
|
+
if (key.toLowerCase() === "y") {
|
|
180
|
+
pending.action();
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
this.setMessage(" Cancelled.");
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (this.showHelp) {
|
|
188
|
+
this.showHelp = false;
|
|
189
|
+
this.setMessage(` ${style.boldCyan("←→")} ${style.dim("tabs")} ${style.boldCyan("↑↓")} ${style.dim("move")} ${style.boldCyan("↵")} ${style.dim("activate")} ${style.boldCyan("?")} ${style.dim("help")}`);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return this.navMode === "input" ? this.handleInputKey(key) : handleNavigateKey(this.asNavigationHost(), key);
|
|
193
|
+
}
|
|
194
|
+
async handleInputKey(key) {
|
|
195
|
+
if (key === "\x03")
|
|
196
|
+
return false;
|
|
197
|
+
if (key === "\x1b") {
|
|
198
|
+
this.cancelInput();
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (key === "\r" || key === "\n") {
|
|
202
|
+
await this.submitInput();
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (key === "\x7f" || key === "\x08") {
|
|
206
|
+
this.inputBuf = this.inputBuf.slice(0, -1);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (key.startsWith("\x1b["))
|
|
210
|
+
return true;
|
|
211
|
+
if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
212
|
+
this.inputBuf += key;
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
// ── Doctor snapshot ────────────────────────────────────────────────────
|
|
218
|
+
async doctorSnapshot() {
|
|
219
|
+
if (this.healthCache && Date.now() - this.healthCache.at < 10_000)
|
|
220
|
+
return this.healthCache.result;
|
|
221
|
+
const result = await this.deps.runDoctor(this.phrenPath, false);
|
|
222
|
+
this.healthCache = { at: Date.now(), result };
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
// ── Render (delegates to shell-view.ts) ────────────────────────────────
|
|
226
|
+
async render() {
|
|
227
|
+
const ctx = {
|
|
228
|
+
phrenPath: this.phrenPath, profile: this.profile, state: this.state,
|
|
229
|
+
currentCursor: () => this.currentCursor(), currentScroll: () => this.currentScroll(),
|
|
230
|
+
setScroll: (n) => this.setScroll(n),
|
|
231
|
+
};
|
|
232
|
+
return renderShell(ctx, this.navMode, this.inputCtx, this.inputBuf, this.showHelp, this.message, () => this.doctorSnapshot(), this._subsectionsCache, (n) => { this.healthLineCount = n; }, (c) => { this._subsectionsCache = c; });
|
|
233
|
+
}
|
|
234
|
+
// ── Navigation host adapter ────────────────────────────────────────────
|
|
235
|
+
asNavigationHost() {
|
|
236
|
+
const self = this;
|
|
237
|
+
return {
|
|
238
|
+
phrenPath: this.phrenPath, profile: this.profile, state: this.state, deps: this.deps,
|
|
239
|
+
get showHelp() { return self.showHelp; }, set showHelp(v) { self.showHelp = v; },
|
|
240
|
+
get healthCache() { return self.healthCache; }, set healthCache(v) { self.healthCache = v; },
|
|
241
|
+
get prevHealthView() { return self.prevHealthView; }, set prevHealthView(v) { self.prevHealthView = v; },
|
|
242
|
+
get filter() { return self.state.filter; },
|
|
243
|
+
get inputMqId() { return self.inputMqId; }, set inputMqId(v) { self.inputMqId = v; },
|
|
244
|
+
setMessage: (msg) => this.setMessage(msg), setView: (view) => this.setView(view),
|
|
245
|
+
setFilter: (value) => this.setFilter(value),
|
|
246
|
+
confirmThen: (label, action) => this.confirmThen(label, action),
|
|
247
|
+
snapshotForUndo: (label, file) => this.snapshotForUndo(label, file),
|
|
248
|
+
ensureProjectSelected: () => this.ensureProjectSelected(),
|
|
249
|
+
invalidateSubsectionsCache: () => this.invalidateSubsectionsCache(),
|
|
250
|
+
popUndo: () => this.popUndo(),
|
|
251
|
+
currentCursor: () => this.currentCursor(),
|
|
252
|
+
setCursor: (n) => this.setCursor(n),
|
|
253
|
+
moveCursor: (delta) => this.moveCursor(delta),
|
|
254
|
+
getListItems: () => this.getListItems(),
|
|
255
|
+
startInput: (ctx, initial) => this.startInput(ctx, initial),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// ── Palette (delegates to shell-input.ts) ──────────────────────────────
|
|
259
|
+
async runPalette(input) {
|
|
260
|
+
await executePalette(this.asNavigationHost(), input);
|
|
261
|
+
}
|
|
262
|
+
async handleInput(raw) {
|
|
263
|
+
const input = raw.trim();
|
|
264
|
+
if (this.pendingConfirm) {
|
|
265
|
+
const pending = this.pendingConfirm;
|
|
266
|
+
this.pendingConfirm = undefined;
|
|
267
|
+
if (input.toLowerCase() === "y") {
|
|
268
|
+
pending.action();
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
this.setMessage(" Cancelled.");
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (this.showHelp) {
|
|
276
|
+
this.showHelp = false;
|
|
277
|
+
this.setMessage(` ${style.boldCyan("←→")} ${style.dim("tabs")} ${style.boldCyan("↑↓")} ${style.dim("move")} ${style.boldCyan("↵")} ${style.dim("activate")} ${style.boldCyan("?")} ${style.dim("help")}`);
|
|
278
|
+
if (!input)
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (!input)
|
|
282
|
+
return true;
|
|
283
|
+
if (["q", "quit", ":q", ":quit", ":exit"].includes(input.toLowerCase()))
|
|
284
|
+
return false;
|
|
285
|
+
if (input === "p") {
|
|
286
|
+
this.setView("Projects");
|
|
287
|
+
this.setMessage(` ${TAB_ICONS.Projects} Projects`);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if (input === "b") {
|
|
291
|
+
if (!this.state.project) {
|
|
292
|
+
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
this.setView("Tasks");
|
|
296
|
+
this.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (input === "l") {
|
|
300
|
+
if (!this.state.project) {
|
|
301
|
+
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
this.setView("Findings");
|
|
305
|
+
this.setMessage(` ${TAB_ICONS.Findings} Fragments`);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
if (input === "m") {
|
|
309
|
+
if (!this.state.project) {
|
|
310
|
+
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
this.setView("Review Queue");
|
|
314
|
+
this.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (input === "s") {
|
|
318
|
+
if (!this.state.project) {
|
|
319
|
+
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
this.setView("Skills");
|
|
323
|
+
this.setMessage(` ${TAB_ICONS.Skills} Skills`);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
if (input === "k") {
|
|
327
|
+
this.setView("Hooks");
|
|
328
|
+
this.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (input === "h") {
|
|
332
|
+
this.healthCache = undefined;
|
|
333
|
+
this.setView("Health");
|
|
334
|
+
this.setMessage(` ${TAB_ICONS.Health} Health`);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
if (input.startsWith("/")) {
|
|
338
|
+
this.setFilter(input.slice(1));
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
if (input.startsWith(":")) {
|
|
342
|
+
await this.runPalette(input.slice(1));
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
await this.runPalette(input);
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
completeInput(line) {
|
|
349
|
+
return completeInputFn(line, this.phrenPath, this.profile, this.state);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export { startShell } from "./shell-entry.js";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { homePath } from "./shared.js";
|
|
4
|
+
import { findProjectDir } from "./project-locator.js";
|
|
5
|
+
import { buildSkillManifest } from "./skill-registry.js";
|
|
6
|
+
import { setSkillEnabled } from "./skill-state.js";
|
|
7
|
+
import { errorMessage } from "./utils.js";
|
|
8
|
+
function normalizeSkillRemovalTarget(skillPath) {
|
|
9
|
+
if (!skillPath)
|
|
10
|
+
return skillPath;
|
|
11
|
+
if (path.basename(skillPath).toLowerCase() === "skill.md") {
|
|
12
|
+
return path.dirname(skillPath);
|
|
13
|
+
}
|
|
14
|
+
return skillPath;
|
|
15
|
+
}
|
|
16
|
+
function symlinkManagedSkill(src, dest, managedRoot) {
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs.lstatSync(dest);
|
|
19
|
+
if (stat.isSymbolicLink()) {
|
|
20
|
+
const currentTarget = fs.readlinkSync(dest);
|
|
21
|
+
const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
|
|
22
|
+
const managedPrefix = path.resolve(managedRoot) + path.sep;
|
|
23
|
+
if (resolvedTarget === path.resolve(src))
|
|
24
|
+
return;
|
|
25
|
+
if (!resolvedTarget.startsWith(managedPrefix))
|
|
26
|
+
return;
|
|
27
|
+
fs.unlinkSync(dest);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err.code !== "ENOENT")
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
38
|
+
fs.symlinkSync(src, dest);
|
|
39
|
+
}
|
|
40
|
+
function removeManagedSkillLink(dest, managedRoot) {
|
|
41
|
+
try {
|
|
42
|
+
const stat = fs.lstatSync(dest);
|
|
43
|
+
if (!stat.isSymbolicLink())
|
|
44
|
+
return;
|
|
45
|
+
const currentTarget = fs.readlinkSync(dest);
|
|
46
|
+
const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
|
|
47
|
+
const managedPrefix = path.resolve(managedRoot) + path.sep;
|
|
48
|
+
if (!resolvedTarget.startsWith(managedPrefix))
|
|
49
|
+
return;
|
|
50
|
+
fs.unlinkSync(dest);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err.code !== "ENOENT" && (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG)) {
|
|
54
|
+
process.stderr.write(`[phren] removeManagedSkillLink: ${errorMessage(err)}\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function writeSkillArtifacts(destDir, manifest) {
|
|
59
|
+
const parentDir = path.dirname(destDir);
|
|
60
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
61
|
+
fs.writeFileSync(path.join(parentDir, "skill-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
62
|
+
fs.writeFileSync(path.join(parentDir, "skill-commands.json"), `${JSON.stringify({
|
|
63
|
+
scope: manifest.scope,
|
|
64
|
+
project: manifest.project,
|
|
65
|
+
generatedAt: manifest.generatedAt,
|
|
66
|
+
commands: manifest.commands.filter((command) => command.registered),
|
|
67
|
+
problems: manifest.problems,
|
|
68
|
+
}, null, 2)}\n`);
|
|
69
|
+
}
|
|
70
|
+
export function syncScopeSkillsToDir(phrenPath, scope, destDir) {
|
|
71
|
+
const manifest = buildSkillManifest(phrenPath, "", scope, destDir);
|
|
72
|
+
const expectedNames = new Set();
|
|
73
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
74
|
+
for (const skill of manifest.skills) {
|
|
75
|
+
const destName = skill.format === "folder" ? skill.name : path.basename(skill.path);
|
|
76
|
+
const destPath = path.join(destDir, destName);
|
|
77
|
+
if (!skill.visibleToAgents) {
|
|
78
|
+
removeManagedSkillLink(destPath, phrenPath);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
expectedNames.add(destName);
|
|
82
|
+
symlinkManagedSkill(skill.root, destPath, phrenPath);
|
|
83
|
+
}
|
|
84
|
+
for (const entry of fs.readdirSync(destDir)) {
|
|
85
|
+
if (expectedNames.has(entry))
|
|
86
|
+
continue;
|
|
87
|
+
removeManagedSkillLink(path.join(destDir, entry), phrenPath);
|
|
88
|
+
}
|
|
89
|
+
writeSkillArtifacts(destDir, manifest);
|
|
90
|
+
return manifest;
|
|
91
|
+
}
|
|
92
|
+
export function syncSkillLinksForScope(phrenPath, scope) {
|
|
93
|
+
if (scope.toLowerCase() === "global") {
|
|
94
|
+
return syncScopeSkillsToDir(phrenPath, "global", homePath(".claude", "skills"));
|
|
95
|
+
}
|
|
96
|
+
const projectDir = findProjectDir(scope);
|
|
97
|
+
if (!projectDir)
|
|
98
|
+
return null;
|
|
99
|
+
return syncScopeSkillsToDir(phrenPath, scope, path.join(projectDir, ".claude", "skills"));
|
|
100
|
+
}
|
|
101
|
+
export function setSkillEnabledAndSync(phrenPath, scope, name, enabled) {
|
|
102
|
+
setSkillEnabled(phrenPath, scope, name, enabled);
|
|
103
|
+
syncSkillLinksForScope(phrenPath, scope);
|
|
104
|
+
}
|
|
105
|
+
export function removeSkillPath(skillPath) {
|
|
106
|
+
const target = normalizeSkillRemovalTarget(skillPath);
|
|
107
|
+
if (!target || !fs.existsSync(target))
|
|
108
|
+
return target;
|
|
109
|
+
const stat = fs.lstatSync(target);
|
|
110
|
+
if (stat.isDirectory()) {
|
|
111
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
fs.unlinkSync(target);
|
|
115
|
+
}
|
|
116
|
+
return target;
|
|
117
|
+
}
|