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