@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,387 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { parseMcpMode, runInit } from "./init.js";
4
+ import { errorMessage } from "./utils.js";
5
+ import { defaultPhrenPath, findPhrenPath } from "./shared.js";
6
+ import { addProjectFromPath } from "./core-project.js";
7
+ import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, parseProjectOwnershipMode, } from "./project-config.js";
8
+ const HELP_TEXT = `phren - He remembers so your agent doesn't have to
9
+
10
+ Usage:
11
+ phren Open interactive shell
12
+ phren quickstart Quick setup: init + project scaffold
13
+ phren add [path] [--ownership <mode>] Add current directory (or path) as a project
14
+ phren init [--mode shared|project-local] [--machine <n>] [--profile <n>] [--mcp on|off] [--template <t>] [--dry-run] [-y]
15
+ Set up phren and offer to add the current project directory
16
+ phren projects list List all tracked projects
17
+ phren projects configure <name> [--ownership <mode>] [--hooks on|off]
18
+ Update per-project enrollment and hook settings
19
+ phren projects remove <name> Remove a project (asks for confirmation)
20
+ phren detect-skills [--import] Find untracked skills in ~/.claude/skills/
21
+ phren skills list List installed skills
22
+ phren skills add <project> <path> Link or copy a skill file into one project
23
+ phren skills resolve <project|global> Print the resolved skill manifest for one scope
24
+ phren skills doctor <project|global> Diagnose resolved skill visibility + mirror state
25
+ phren skills sync <project|global> Regenerate the resolved mirror for one scope
26
+ phren skills remove <project> <name> Remove a project skill by name
27
+ phren hooks list [--project <name>] Show hook tool preferences and optional project overrides
28
+ phren hooks enable <tool> Enable hooks for one tool
29
+ phren hooks disable <tool> Disable hooks for one tool
30
+ phren status Health, active project, stats
31
+ phren search <query> [--project <n>] [--type <t>] [--limit <n>]
32
+ Search what phren remembers
33
+ phren add-finding <project> "..." Tell phren what you learned
34
+ phren pin <project> "..." Pin a canonical memory
35
+ phren tasks Cross-project task view
36
+ phren skill-list List installed skills
37
+ phren doctor [--fix] [--check-data] [--agents]
38
+ Health check and self-heal (--agents: show agent integrations only)
39
+ phren web-ui [--port=3499] [--no-open] Memory web UI
40
+ phren debug-injection --prompt "..." Preview hook-prompt injection output
41
+ phren inspect-index [--project <n>] Inspect FTS index contents for debugging
42
+ phren update [--refresh-starter] Update to latest version
43
+ phren graph [--project <n>] [--limit <n>]
44
+ Show the fragment knowledge graph
45
+ phren graph link <project> "finding" "fragment"
46
+ Link a finding to a fragment manually
47
+
48
+ Configuration:
49
+ phren config policy [get|set ...] Retention, TTL, confidence, decay
50
+ phren config workflow [get|set ...] Risky-memory thresholds
51
+ phren config index [get|set ...] Indexer include/exclude globs
52
+ phren config synonyms [list|add|remove] ...
53
+ Manage project learned synonyms
54
+ phren config project-ownership [mode] Default ownership for future project enrollments
55
+ phren config machines Registered machines
56
+ phren config profiles Profiles and projects
57
+
58
+ Maintenance:
59
+ phren maintain govern [project] Queue stale/low-value memories for review
60
+ phren maintain prune [project] Delete expired entries
61
+ phren maintain consolidate [project] Deduplicate FINDINGS.md
62
+ phren maintain extract [project] Mine git/GitHub signals
63
+
64
+ Setup:
65
+ phren mcp-mode [on|off|status] Toggle MCP integration
66
+ phren hooks-mode [on|off|status] Toggle hook execution
67
+ phren verify Check init completed OK
68
+ phren uninstall Remove phren config and hooks
69
+
70
+ Environment:
71
+ PHREN_PATH Override phren directory (default: ~/.phren)
72
+ PHREN_PROFILE Active profile name (otherwise phren uses machines.yaml when available)
73
+ PHREN_DEBUG Enable debug logging (set to 1)
74
+ PHREN_OLLAMA_URL Ollama base URL (default: http://localhost:11434; set to 'off' to disable)
75
+ PHREN_EMBEDDING_API_URL OpenAI-compatible /embeddings endpoint (cloud alternative to Ollama)
76
+ PHREN_EMBEDDING_API_KEY API key for PHREN_EMBEDDING_API_URL
77
+ PHREN_EMBEDDING_MODEL Embedding model (default: nomic-embed-text)
78
+ PHREN_EXTRACT_MODEL Ollama model for memory extraction (default: llama3.2)
79
+ PHREN_EMBEDDING_PROVIDER Set to 'api' to use OpenAI API for search_knowledge embeddings
80
+ PHREN_FEATURE_AUTO_CAPTURE=1 Extract insights from conversations at session end
81
+ PHREN_FEATURE_SEMANTIC_DEDUP=1 LLM-based dedup on add_finding
82
+ PHREN_FEATURE_SEMANTIC_CONFLICT=1 LLM-based conflict detection on add_finding
83
+ PHREN_FEATURE_HYBRID_SEARCH=0 Disable TF-IDF cosine fallback in search_knowledge
84
+ PHREN_FEATURE_AUTO_EXTRACT=0 Disable automatic memory extraction on each prompt
85
+ PHREN_FEATURE_PROGRESSIVE_DISCLOSURE=1 Compact memory index injection
86
+ PHREN_LLM_MODEL LLM model for semantic dedup/conflict (default: gpt-4o-mini)
87
+ PHREN_LLM_ENDPOINT OpenAI-compatible /chat/completions base URL for dedup/conflict
88
+ PHREN_LLM_KEY API key for PHREN_LLM_ENDPOINT
89
+ PHREN_CONTEXT_TOKEN_BUDGET Max tokens injected per hook-prompt (default: 550)
90
+ PHREN_CONTEXT_SNIPPET_LINES Max lines per injected snippet (default: 6)
91
+ PHREN_CONTEXT_SNIPPET_CHARS Max chars per injected snippet (default: 520)
92
+ PHREN_MAX_INJECT_TOKENS Hard cap on total injected tokens (default: 2000)
93
+ PHREN_TASK_PRIORITY Priorities to include in task injection: high,medium,low (default: high,medium)
94
+ PHREN_MEMORY_TTL_DAYS Override memory TTL for trust filtering
95
+ PHREN_HOOK_TIMEOUT_MS Hook subprocess timeout in ms (default: 14000)
96
+ PHREN_FINDINGS_CAP Max findings per date section before consolidation (default: 20)
97
+ PHREN_GH_PR_LIMIT/RUN_LIMIT/ISSUE_LIMIT GitHub extraction limits (defaults: 40/25/25)
98
+
99
+ Examples:
100
+ phren search "rate limiting" Search across all projects
101
+ phren search "auth" --project my-api Search within one project
102
+ phren add-finding my-app "Redis connections need explicit close in finally blocks"
103
+ phren doctor --fix Fix common config issues
104
+ phren config policy set --ttlDays=90 Change memory retention to 90 days
105
+ phren config project-ownership detached
106
+ phren maintain govern my-app Queue stale memories for review
107
+ phren status Quick health check
108
+ `;
109
+ const CLI_COMMANDS = [
110
+ "search",
111
+ "shell",
112
+ "update",
113
+ "config",
114
+ "maintain",
115
+ "hook-prompt",
116
+ "hook-session-start",
117
+ "hook-stop",
118
+ "hook-context",
119
+ "hook-tool",
120
+ "add-finding",
121
+ "pin",
122
+ "doctor",
123
+ "debug-injection",
124
+ "inspect-index",
125
+ "web-ui",
126
+ "quality-feedback",
127
+ "skill-list",
128
+ "skills",
129
+ "hooks",
130
+ "detect-skills",
131
+ "task",
132
+ "tasks",
133
+ "finding",
134
+ "quickstart",
135
+ "background-maintenance",
136
+ "background-sync",
137
+ "projects",
138
+ "extract-memories",
139
+ "govern-memories",
140
+ "prune-memories",
141
+ "consolidate-memories",
142
+ "index-policy",
143
+ "policy",
144
+ "workflow",
145
+ "access",
146
+ ];
147
+ async function flushTopLevelOutput() {
148
+ await Promise.all([
149
+ new Promise((resolve) => process.stdout.write("", () => resolve())),
150
+ new Promise((resolve) => process.stderr.write("", () => resolve())),
151
+ ]);
152
+ }
153
+ async function finish(exitCode) {
154
+ if (exitCode !== undefined)
155
+ process.exitCode = exitCode;
156
+ await flushTopLevelOutput();
157
+ return true;
158
+ }
159
+ function getOptionValue(args, name) {
160
+ const exactIdx = args.indexOf(name);
161
+ if (exactIdx !== -1)
162
+ return args[exactIdx + 1];
163
+ const prefixed = args.find((arg) => arg.startsWith(`${name}=`));
164
+ return prefixed ? prefixed.slice(name.length + 1) : undefined;
165
+ }
166
+ function getPositionalArgs(args, optionNamesWithValues) {
167
+ const positions = [];
168
+ for (let i = 0; i < args.length; i += 1) {
169
+ const arg = args[i];
170
+ if (optionNamesWithValues.includes(arg)) {
171
+ i += 1;
172
+ continue;
173
+ }
174
+ if (optionNamesWithValues.some((name) => arg.startsWith(`${name}=`))) {
175
+ continue;
176
+ }
177
+ if (!arg.startsWith("--"))
178
+ positions.push(arg);
179
+ }
180
+ return positions;
181
+ }
182
+ function parseTaskModeFlag(raw) {
183
+ if (!raw)
184
+ return undefined;
185
+ const normalized = raw.trim().toLowerCase();
186
+ return ["off", "manual", "suggest", "auto"].includes(normalized)
187
+ ? normalized
188
+ : undefined;
189
+ }
190
+ function parseProactivityFlag(raw) {
191
+ if (!raw)
192
+ return undefined;
193
+ const normalized = raw.trim().toLowerCase();
194
+ return ["high", "medium", "low"].includes(normalized)
195
+ ? normalized
196
+ : undefined;
197
+ }
198
+ async function promptProjectOwnership(phrenPath, fallback) {
199
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
200
+ return fallback;
201
+ const readline = await import("readline");
202
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
203
+ return new Promise((resolve) => {
204
+ rl.question(`Project ownership [${PROJECT_OWNERSHIP_MODES.join("/")}] (${fallback}): `, (input) => {
205
+ rl.close();
206
+ resolve(parseProjectOwnershipMode(input.trim()) ?? fallback);
207
+ });
208
+ });
209
+ }
210
+ export async function runTopLevelCommand(argv) {
211
+ const argvCommand = argv[0];
212
+ if (argvCommand === "--help" || argvCommand === "-h" || argvCommand === "help") {
213
+ console.log(HELP_TEXT);
214
+ return finish();
215
+ }
216
+ if (argvCommand === "add") {
217
+ const positional = getPositionalArgs(argv.slice(1), ["--ownership"]);
218
+ const targetPath = positional[0] || process.cwd();
219
+ const ownershipArg = getOptionValue(argv.slice(1), "--ownership");
220
+ const phrenPath = defaultPhrenPath();
221
+ const profile = (process.env.PHREN_PROFILE) || undefined;
222
+ if (!fs.existsSync(phrenPath) || !fs.existsSync(path.join(phrenPath, ".governance"))) {
223
+ console.log("phren is not set up yet. Run: npx phren init");
224
+ return finish(1);
225
+ }
226
+ const ownership = ownershipArg
227
+ ? parseProjectOwnershipMode(ownershipArg)
228
+ : await promptProjectOwnership(phrenPath, getProjectOwnershipDefault(phrenPath));
229
+ if (ownershipArg && !ownership) {
230
+ console.error(`Invalid --ownership value "${ownershipArg}". Use one of: ${PROJECT_OWNERSHIP_MODES.join(", ")}`);
231
+ return finish(1);
232
+ }
233
+ try {
234
+ const added = addProjectFromPath(phrenPath, path.resolve(targetPath), profile, ownership);
235
+ if (!added.ok) {
236
+ console.error(added.error);
237
+ return finish(1);
238
+ }
239
+ console.log(`Added project "${added.data.project}" (${added.data.ownership})`);
240
+ if (added.data.files.claude)
241
+ console.log(` ${added.data.files.claude}`);
242
+ console.log(` ${added.data.files.findings}`);
243
+ console.log(` ${added.data.files.task}`);
244
+ console.log(` ${added.data.files.summary}`);
245
+ }
246
+ catch (e) {
247
+ console.error(`Could not add project: ${e instanceof Error ? e.message : String(e)}`);
248
+ return finish(1);
249
+ }
250
+ return finish();
251
+ }
252
+ if (argvCommand === "init") {
253
+ const initArgs = argv.slice(1);
254
+ const machineIdx = initArgs.indexOf("--machine");
255
+ const profileIdx = initArgs.indexOf("--profile");
256
+ const mcpIdx = initArgs.indexOf("--mcp");
257
+ const templateIdx = initArgs.indexOf("--template");
258
+ const modeArg = getOptionValue(initArgs, "--mode");
259
+ if (modeArg && !["shared", "project-local"].includes(modeArg)) {
260
+ console.error(`Invalid --mode value "${modeArg}". Use "shared" or "project-local".`);
261
+ return finish(1);
262
+ }
263
+ const ownershipMode = parseProjectOwnershipMode(getOptionValue(initArgs, "--project-ownership"));
264
+ const taskMode = parseTaskModeFlag(getOptionValue(initArgs, "--task-mode"));
265
+ const findingsProactivity = parseProactivityFlag(getOptionValue(initArgs, "--findings-proactivity"));
266
+ const taskProactivity = parseProactivityFlag(getOptionValue(initArgs, "--task-proactivity"));
267
+ const mcpMode = mcpIdx !== -1 ? parseMcpMode(initArgs[mcpIdx + 1]) : undefined;
268
+ if (mcpIdx !== -1 && !mcpMode) {
269
+ console.error(`Invalid --mcp value "${initArgs[mcpIdx + 1] || ""}". Use "on" or "off".`);
270
+ return finish(1);
271
+ }
272
+ const ownershipArg = getOptionValue(initArgs, "--project-ownership");
273
+ if (ownershipArg && !ownershipMode) {
274
+ console.error(`Invalid --project-ownership value "${ownershipArg}". Use one of: ${PROJECT_OWNERSHIP_MODES.join(", ")}`);
275
+ return finish(1);
276
+ }
277
+ const taskModeArg = getOptionValue(initArgs, "--task-mode");
278
+ if (taskModeArg && !taskMode) {
279
+ console.error(`Invalid --task-mode value "${taskModeArg}". Use one of: off, manual, suggest, auto.`);
280
+ return finish(1);
281
+ }
282
+ const findingsArg = getOptionValue(initArgs, "--findings-proactivity");
283
+ if (findingsArg && !findingsProactivity) {
284
+ console.error(`Invalid --findings-proactivity value "${findingsArg}". Use one of: high, medium, low.`);
285
+ return finish(1);
286
+ }
287
+ const taskArg = getOptionValue(initArgs, "--task-proactivity");
288
+ if (taskArg && !taskProactivity) {
289
+ console.error(`Invalid --task-proactivity value "${taskArg}". Use one of: high, medium, low.`);
290
+ return finish(1);
291
+ }
292
+ await runInit({
293
+ mode: modeArg,
294
+ machine: machineIdx !== -1 ? initArgs[machineIdx + 1] : undefined,
295
+ profile: profileIdx !== -1 ? initArgs[profileIdx + 1] : undefined,
296
+ mcp: mcpMode,
297
+ projectOwnershipDefault: ownershipMode,
298
+ taskMode,
299
+ findingsProactivity,
300
+ taskProactivity,
301
+ template: templateIdx !== -1 ? initArgs[templateIdx + 1] : undefined,
302
+ applyStarterUpdate: initArgs.includes("--apply-starter-update"),
303
+ dryRun: initArgs.includes("--dry-run"),
304
+ yes: initArgs.includes("--yes") || initArgs.includes("-y"),
305
+ });
306
+ return finish();
307
+ }
308
+ if (argvCommand === "uninstall") {
309
+ const { runUninstall } = await import("./init.js");
310
+ await runUninstall();
311
+ return finish();
312
+ }
313
+ if (argvCommand === "status") {
314
+ const { runStatus } = await import("./status.js");
315
+ await runStatus();
316
+ return finish();
317
+ }
318
+ if (argvCommand === "verify") {
319
+ const { runPostInitVerify, getVerifyOutcomeNote } = await import("./init.js");
320
+ const { getWorkflowPolicy } = await import("./shared-governance.js");
321
+ const phrenPath = findPhrenPath() || defaultPhrenPath();
322
+ const result = runPostInitVerify(phrenPath);
323
+ console.log(`phren verify: ${result.ok ? "ok" : "issues found"}`);
324
+ console.log(` tasks: ${getWorkflowPolicy(phrenPath).taskMode} mode`);
325
+ for (const check of result.checks) {
326
+ console.log(` ${check.ok ? "pass" : "FAIL"} ${check.name}: ${check.detail}`);
327
+ if (!check.ok && check.fix) {
328
+ console.log(` fix: ${check.fix}`);
329
+ }
330
+ }
331
+ if (!result.ok) {
332
+ const note = getVerifyOutcomeNote(phrenPath, result.checks);
333
+ if (note)
334
+ console.log(`\nNote: ${note}`);
335
+ console.log(`\nRun \`npx phren init\` to fix setup issues.`);
336
+ }
337
+ return finish(result.ok ? 0 : 1);
338
+ }
339
+ if (argvCommand === "mcp-mode") {
340
+ const { runMcpMode } = await import("./init.js");
341
+ try {
342
+ await runMcpMode(argv[1]);
343
+ return finish();
344
+ }
345
+ catch (err) {
346
+ console.error(err instanceof Error ? err.message : String(err));
347
+ return finish(1);
348
+ }
349
+ }
350
+ if (argvCommand === "hooks-mode") {
351
+ const { runHooksMode } = await import("./init.js");
352
+ try {
353
+ await runHooksMode(argv[1]);
354
+ return finish();
355
+ }
356
+ catch (err) {
357
+ console.error(err instanceof Error ? err.message : String(err));
358
+ return finish(1);
359
+ }
360
+ }
361
+ if (argvCommand === "link") {
362
+ console.error("`phren link` has been removed. Use `npx phren init` instead.");
363
+ return finish(1);
364
+ }
365
+ if (argvCommand === "--health") {
366
+ return finish();
367
+ }
368
+ if (!argvCommand && process.stdin.isTTY && process.stdout.isTTY) {
369
+ const { runCliCommand } = await import("./cli.js");
370
+ await runCliCommand("shell", []);
371
+ return finish();
372
+ }
373
+ if (argvCommand && CLI_COMMANDS.includes(argvCommand)) {
374
+ const { runCliCommand } = await import("./cli.js");
375
+ try {
376
+ const { trackCliCommand } = await import("./telemetry.js");
377
+ trackCliCommand(defaultPhrenPath(), argvCommand);
378
+ }
379
+ catch (err) {
380
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
381
+ process.stderr.write(`[phren] cli trackCliCommand: ${errorMessage(err)}\n`);
382
+ }
383
+ await runCliCommand(argvCommand, argv.slice(1));
384
+ return finish();
385
+ }
386
+ return false;
387
+ }
@@ -0,0 +1,172 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { safeProjectPath } from "./utils.js";
4
+ import { resolveTaskFilePath } from "./data-tasks.js";
5
+ const ACTIVE_HEADINGS = new Set(["active", "in progress", "in-progress", "current", "wip"]);
6
+ const QUEUE_HEADINGS = new Set(["queue", "queued", "task", "todo", "upcoming", "next"]);
7
+ const DONE_HEADINGS = new Set(["done", "completed", "finished", "archived"]);
8
+ const BID_PATTERN = /\s*<!--\s*bid:([a-z0-9]{8})\s*-->/;
9
+ function stripBulletPrefix(line) {
10
+ return line
11
+ .replace(/^-\s*\[[ xX]\]\s+/, "")
12
+ .replace(/^-\s+/, "")
13
+ .trim();
14
+ }
15
+ function stripBid(text) {
16
+ const match = text.match(BID_PATTERN);
17
+ if (!match)
18
+ return { clean: text.trimEnd() };
19
+ return {
20
+ clean: text.replace(BID_PATTERN, "").trimEnd(),
21
+ bid: match[1],
22
+ };
23
+ }
24
+ function normalizePriority(text) {
25
+ const match = text.replace(/\s*\[pinned\]/gi, "").match(/\[(high|medium|low)\]\s*$/i);
26
+ if (!match)
27
+ return undefined;
28
+ return match[1].toLowerCase();
29
+ }
30
+ function detectPinned(text) {
31
+ return /\[pinned\]/i.test(text);
32
+ }
33
+ function parseTaskItems(taskPath) {
34
+ if (!fs.existsSync(taskPath))
35
+ return [];
36
+ const lines = fs.readFileSync(taskPath, "utf8").split("\n");
37
+ let section = "Queue";
38
+ const counters = { Active: 0, Queue: 0, Done: 0 };
39
+ const items = [];
40
+ for (const line of lines) {
41
+ const heading = line.trim().match(/^##\s+(.+?)[\s]*$/);
42
+ if (heading) {
43
+ const token = heading[1].replace(/\s+/g, " ").trim().toLowerCase();
44
+ if (ACTIVE_HEADINGS.has(token))
45
+ section = "Active";
46
+ else if (QUEUE_HEADINGS.has(token))
47
+ section = "Queue";
48
+ else if (DONE_HEADINGS.has(token))
49
+ section = "Done";
50
+ continue;
51
+ }
52
+ if (!line.startsWith("- "))
53
+ continue;
54
+ counters[section] += 1;
55
+ const itemId = `${section === "Active" ? "A" : section === "Queue" ? "Q" : "D"}${counters[section]}`;
56
+ const stripped = stripBulletPrefix(line);
57
+ const { clean, bid } = stripBid(stripped);
58
+ items.push({
59
+ id: itemId,
60
+ stableId: bid,
61
+ section,
62
+ line: clean,
63
+ priority: normalizePriority(clean),
64
+ pinned: detectPinned(clean) || undefined,
65
+ });
66
+ }
67
+ return items;
68
+ }
69
+ function resolveTaskItemMatch(items, match) {
70
+ const needle = match.trim().toLowerCase();
71
+ if (!needle)
72
+ return { error: "task reference must not be empty." };
73
+ const bidNeedle = needle.replace(/^bid:/, "");
74
+ if (/^[a-f0-9]{8}$/.test(bidNeedle)) {
75
+ const stable = items.find((item) => item.stableId === bidNeedle);
76
+ if (stable?.stableId)
77
+ return { stableId: stable.stableId };
78
+ }
79
+ const byId = items.find((item) => item.id.toLowerCase() === needle);
80
+ if (byId?.stableId)
81
+ return { stableId: byId.stableId };
82
+ const exact = items.filter((item) => item.line.trim().toLowerCase() === needle);
83
+ if (exact.length === 1) {
84
+ if (!exact[0].stableId)
85
+ return { error: `Task "${match}" does not have a stable ID yet.` };
86
+ return { stableId: exact[0].stableId };
87
+ }
88
+ if (exact.length > 1) {
89
+ return { error: `Task "${match}" is ambiguous (${exact.length} exact matches). Use item ID or stable ID.` };
90
+ }
91
+ const partial = items.filter((item) => item.line.toLowerCase().includes(needle));
92
+ if (partial.length === 1) {
93
+ if (!partial[0].stableId)
94
+ return { error: `Task "${match}" does not have a stable ID yet.` };
95
+ return { stableId: partial[0].stableId };
96
+ }
97
+ if (partial.length > 1) {
98
+ return { error: `Task "${match}" is ambiguous (${partial.length} partial matches). Use item ID or stable ID.` };
99
+ }
100
+ return { error: `No task matching "${match}" in project tasks.` };
101
+ }
102
+ export function resolveFindingTaskReference(phrenPath, project, match) {
103
+ const projectDir = safeProjectPath(phrenPath, project);
104
+ if (!projectDir)
105
+ return { error: `Invalid project name: "${project}".` };
106
+ const taskPath = resolveTaskFilePath(phrenPath, project);
107
+ const items = parseTaskItems(taskPath ?? path.join(projectDir, "tasks.md"));
108
+ return resolveTaskItemMatch(items, match);
109
+ }
110
+ export function resolveAutoFindingTaskItem(phrenPath, project) {
111
+ const projectDir = safeProjectPath(phrenPath, project);
112
+ if (!projectDir)
113
+ return undefined;
114
+ const taskPath = resolveTaskFilePath(phrenPath, project);
115
+ const active = parseTaskItems(taskPath ?? path.join(projectDir, "tasks.md")).filter((item) => item.section === "Active" && item.stableId);
116
+ if (active.length === 1)
117
+ return active[0].stableId;
118
+ const pinned = active.filter((item) => item.pinned);
119
+ if (pinned.length === 1)
120
+ return pinned[0].stableId;
121
+ const high = active.filter((item) => item.priority === "high");
122
+ if (high.length === 1)
123
+ return high[0].stableId;
124
+ return undefined;
125
+ }
126
+ function sessionsDir(phrenPath) {
127
+ return path.join(phrenPath, ".runtime", "sessions");
128
+ }
129
+ function listActiveSessions(phrenPath) {
130
+ const dir = sessionsDir(phrenPath);
131
+ if (!fs.existsSync(dir))
132
+ return [];
133
+ const sessions = [];
134
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
135
+ if (!entry.isFile() || !entry.name.startsWith("session-") || !entry.name.endsWith(".json"))
136
+ continue;
137
+ const fullPath = path.join(dir, entry.name);
138
+ try {
139
+ const parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
140
+ if (!parsed.sessionId || parsed.endedAt)
141
+ continue;
142
+ sessions.push(parsed);
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ }
148
+ return sessions;
149
+ }
150
+ function sessionSortValue(state) {
151
+ if (state.startedAt) {
152
+ const parsed = Date.parse(state.startedAt);
153
+ if (!Number.isNaN(parsed))
154
+ return parsed;
155
+ }
156
+ return 0;
157
+ }
158
+ export function resolveFindingSessionId(phrenPath, project, explicitSessionId) {
159
+ const trimmed = explicitSessionId?.trim();
160
+ if (trimmed)
161
+ return trimmed;
162
+ const active = listActiveSessions(phrenPath);
163
+ if (active.length === 0)
164
+ return undefined;
165
+ const matchingProject = active
166
+ .filter((session) => session.project === project)
167
+ .sort((a, b) => sessionSortValue(b) - sessionSortValue(a));
168
+ if (matchingProject.length > 0)
169
+ return matchingProject[0].sessionId;
170
+ const sorted = active.sort((a, b) => sessionSortValue(b) - sessionSortValue(a));
171
+ return sorted[0]?.sessionId;
172
+ }