@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,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
+ }