@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,169 @@
|
|
|
1
|
+
import { getPhrenPath } from "./shared.js";
|
|
2
|
+
import { buildIndex, queryRows } from "./shared-index.js";
|
|
3
|
+
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
4
|
+
/**
|
|
5
|
+
* CLI: phren graph [--project <name>] [--limit <n>]
|
|
6
|
+
* Displays the fragment knowledge graph as a table.
|
|
7
|
+
*/
|
|
8
|
+
export async function handleGraphRead(args) {
|
|
9
|
+
const phrenPath = getPhrenPath();
|
|
10
|
+
const profile = resolveRuntimeProfile(phrenPath);
|
|
11
|
+
let project;
|
|
12
|
+
let limit = 20;
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
if ((args[i] === "--project" || args[i] === "-p") && args[i + 1]) {
|
|
15
|
+
project = args[++i];
|
|
16
|
+
}
|
|
17
|
+
else if ((args[i] === "--limit" || args[i] === "-n") && args[i + 1]) {
|
|
18
|
+
limit = Math.min(Math.max(parseInt(args[++i], 10) || 20, 1), 200);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const db = await buildIndex(phrenPath, profile);
|
|
22
|
+
let sql;
|
|
23
|
+
let params;
|
|
24
|
+
if (project) {
|
|
25
|
+
sql = `
|
|
26
|
+
SELECT e.name, e.type, COUNT(el.source_id) as ref_count
|
|
27
|
+
FROM entities e
|
|
28
|
+
JOIN entity_links el ON el.target_id = e.id
|
|
29
|
+
WHERE e.type != 'document' AND el.source_doc LIKE ?
|
|
30
|
+
GROUP BY e.id, e.name, e.type
|
|
31
|
+
ORDER BY ref_count DESC
|
|
32
|
+
LIMIT ?
|
|
33
|
+
`;
|
|
34
|
+
params = [`${project}/%`, limit];
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
sql = `
|
|
38
|
+
SELECT e.name, e.type, COUNT(el.source_id) as ref_count
|
|
39
|
+
FROM entities e
|
|
40
|
+
JOIN entity_links el ON el.target_id = e.id
|
|
41
|
+
WHERE e.type != 'document'
|
|
42
|
+
GROUP BY e.id, e.name, e.type
|
|
43
|
+
ORDER BY ref_count DESC
|
|
44
|
+
LIMIT ?
|
|
45
|
+
`;
|
|
46
|
+
params = [limit];
|
|
47
|
+
}
|
|
48
|
+
const rows = queryRows(db, sql, params);
|
|
49
|
+
if (!rows || rows.length === 0) {
|
|
50
|
+
console.log("No fragments in the knowledge graph.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Header
|
|
54
|
+
const nameW = 30;
|
|
55
|
+
const typeW = 12;
|
|
56
|
+
const refW = 6;
|
|
57
|
+
console.log(`${"Fragment".padEnd(nameW)} ${"Type".padEnd(typeW)} ${"Refs".padStart(refW)}`);
|
|
58
|
+
console.log(`${"─".repeat(nameW)} ${"─".repeat(typeW)} ${"─".repeat(refW)}`);
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
const name = String(row[0]).slice(0, nameW);
|
|
61
|
+
const type = String(row[1]).slice(0, typeW);
|
|
62
|
+
const refs = String(Number(row[2]));
|
|
63
|
+
console.log(`${name.padEnd(nameW)} ${type.padEnd(typeW)} ${refs.padStart(refW)}`);
|
|
64
|
+
}
|
|
65
|
+
console.log(`\n${rows.length} fragments shown.`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* CLI: phren graph link <project> <finding_text> <fragment_name>
|
|
69
|
+
* Links a finding to a fragment manually.
|
|
70
|
+
*/
|
|
71
|
+
export async function handleGraphLink(args) {
|
|
72
|
+
if (args.length < 3) {
|
|
73
|
+
console.error('Usage: phren graph link <project> "<finding text>" "<fragment name>"');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const [project, findingText, fragmentName] = args;
|
|
77
|
+
const phrenPath = getPhrenPath();
|
|
78
|
+
const profile = resolveRuntimeProfile(phrenPath);
|
|
79
|
+
const db = await buildIndex(phrenPath, profile);
|
|
80
|
+
// Check that the finding exists
|
|
81
|
+
const docCheck = queryRows(db, "SELECT content FROM docs WHERE project = ? AND filename = 'FINDINGS.md' LIMIT 1", [project]);
|
|
82
|
+
if (!docCheck || docCheck.length === 0) {
|
|
83
|
+
console.error(`No FINDINGS.md found for project "${project}".`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const content = String(docCheck[0][0]);
|
|
87
|
+
if (!content.toLowerCase().includes(findingText.toLowerCase())) {
|
|
88
|
+
console.error(`Finding text not found in ${project}/FINDINGS.md.`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
// Use the MCP link_findings logic via direct DB operations
|
|
92
|
+
const sourceDoc = `${project}/FINDINGS.md`;
|
|
93
|
+
const normalizedFragment = fragmentName.toLowerCase();
|
|
94
|
+
try {
|
|
95
|
+
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [normalizedFragment, "fragment", new Date().toISOString().slice(0, 10)]);
|
|
96
|
+
}
|
|
97
|
+
catch { /* best effort */ }
|
|
98
|
+
const fragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [normalizedFragment, "fragment"]);
|
|
99
|
+
if (!fragmentResult?.length || !fragmentResult[0]?.values?.length) {
|
|
100
|
+
console.error("Failed to create fragment.");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const targetId = Number(fragmentResult[0].values[0][0]);
|
|
104
|
+
try {
|
|
105
|
+
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [sourceDoc, "document", new Date().toISOString().slice(0, 10)]);
|
|
106
|
+
}
|
|
107
|
+
catch { /* best effort */ }
|
|
108
|
+
const docResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [sourceDoc, "document"]);
|
|
109
|
+
if (!docResult?.length || !docResult[0]?.values?.length) {
|
|
110
|
+
console.error("Failed to create document fragment.");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const sourceId = Number(docResult[0].values[0][0]);
|
|
114
|
+
try {
|
|
115
|
+
db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, "mentions", sourceDoc]);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(`Failed to link: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
// Persist to manual-links.json
|
|
122
|
+
const { runtimeFile } = await import("./shared.js");
|
|
123
|
+
const fs = await import("fs");
|
|
124
|
+
const crypto = await import("crypto");
|
|
125
|
+
const { withFileLock } = await import("./shared-governance.js");
|
|
126
|
+
const manualLinksPath = runtimeFile(phrenPath, "manual-links.json");
|
|
127
|
+
try {
|
|
128
|
+
withFileLock(manualLinksPath, () => {
|
|
129
|
+
let existing = [];
|
|
130
|
+
if (fs.existsSync(manualLinksPath)) {
|
|
131
|
+
try {
|
|
132
|
+
existing = JSON.parse(fs.readFileSync(manualLinksPath, "utf8"));
|
|
133
|
+
}
|
|
134
|
+
catch { /* fresh start */ }
|
|
135
|
+
}
|
|
136
|
+
const newEntry = { entity: normalizedFragment, entityType: "fragment", sourceDoc, relType: "mentions" };
|
|
137
|
+
const alreadyStored = existing.some((e) => e.entity === newEntry.entity && e.entityType === newEntry.entityType && e.sourceDoc === newEntry.sourceDoc && e.relType === newEntry.relType);
|
|
138
|
+
if (!alreadyStored) {
|
|
139
|
+
existing.push(newEntry);
|
|
140
|
+
const tmpPath = manualLinksPath + `.tmp-${crypto.randomUUID()}`;
|
|
141
|
+
fs.writeFileSync(tmpPath, JSON.stringify(existing, null, 2));
|
|
142
|
+
fs.renameSync(tmpPath, manualLinksPath);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error(`Failed to persist manual link: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
console.log(`Linked "${fragmentName}" to ${sourceDoc}.`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* CLI: phren graph <subcommand>
|
|
154
|
+
* Routes graph subcommands.
|
|
155
|
+
*/
|
|
156
|
+
export async function handleGraphNamespace(args) {
|
|
157
|
+
const sub = args[0];
|
|
158
|
+
const rest = args.slice(1);
|
|
159
|
+
switch (sub) {
|
|
160
|
+
case "link":
|
|
161
|
+
return handleGraphLink(rest);
|
|
162
|
+
case undefined:
|
|
163
|
+
case "read":
|
|
164
|
+
return handleGraphRead(rest);
|
|
165
|
+
default:
|
|
166
|
+
// Treat unknown subcommand as flags for read (e.g., --project)
|
|
167
|
+
return handleGraphRead(args);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseCitationComment, validateFindingCitation, } from "./content-citation.js";
|
|
2
|
+
import { capCache } from "./shared.js";
|
|
3
|
+
// ── Citation validation ──────────────────────────────────────────────────────
|
|
4
|
+
const citationValidCache = new Map();
|
|
5
|
+
export function clearCitationValidCache() {
|
|
6
|
+
citationValidCache.clear();
|
|
7
|
+
}
|
|
8
|
+
export function parseCitations(text) {
|
|
9
|
+
const results = [];
|
|
10
|
+
let m;
|
|
11
|
+
const citeRe = /<!--\s*phren:cite\s+(\{[\s\S]*?\})\s*-->/g;
|
|
12
|
+
while ((m = citeRe.exec(text)) !== null) {
|
|
13
|
+
const parsed = parseCitationComment(m[0]);
|
|
14
|
+
if (parsed)
|
|
15
|
+
results.push({ citation: parsed });
|
|
16
|
+
}
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
export function validateCitation(citation) {
|
|
20
|
+
const key = JSON.stringify(citation.citation);
|
|
21
|
+
if (citationValidCache.has(key))
|
|
22
|
+
return citationValidCache.get(key);
|
|
23
|
+
let valid = false;
|
|
24
|
+
if (citation.citation) {
|
|
25
|
+
valid = validateFindingCitation(citation.citation);
|
|
26
|
+
}
|
|
27
|
+
citationValidCache.set(key, valid);
|
|
28
|
+
capCache(citationValidCache);
|
|
29
|
+
return valid;
|
|
30
|
+
}
|
|
31
|
+
export function annotateStale(snippet) {
|
|
32
|
+
const citations = parseCitations(snippet);
|
|
33
|
+
if (citations.length === 0)
|
|
34
|
+
return snippet;
|
|
35
|
+
const annotations = [];
|
|
36
|
+
for (const c of citations) {
|
|
37
|
+
if (!validateCitation(c)) {
|
|
38
|
+
annotations.push("[citation stale]");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (annotations.length === 0)
|
|
42
|
+
return snippet;
|
|
43
|
+
return snippet + " " + annotations.join(" ");
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// cli-hooks-context.ts — HookContext: everything a hook handler needs as plain data + helpers.
|
|
2
|
+
// Centralizes the "resolve state and check guards" pattern so hook handlers don't
|
|
3
|
+
// reach into governance, init, project-config, hooks, init-setup, etc. directly.
|
|
4
|
+
import { appendAuditLog, getPhrenPath, readRootManifest, } from "./shared.js";
|
|
5
|
+
import { updateRuntimeHealth, } from "./shared-governance.js";
|
|
6
|
+
import { detectProject } from "./shared-index.js";
|
|
7
|
+
import { getHooksEnabledPreference } from "./init.js";
|
|
8
|
+
import { isToolHookEnabled } from "./hooks.js";
|
|
9
|
+
import { isProjectHookEnabled } from "./project-config.js";
|
|
10
|
+
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
11
|
+
/** Build a HookContext from the current process environment. */
|
|
12
|
+
export function buildHookContext() {
|
|
13
|
+
const phrenPath = getPhrenPath();
|
|
14
|
+
const profile = resolveRuntimeProfile(phrenPath);
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const hookTool = process.env.PHREN_HOOK_TOOL || "claude";
|
|
17
|
+
const hooksEnabled = getHooksEnabledPreference(phrenPath);
|
|
18
|
+
const toolHookEnabled = hooksEnabled && isToolHookEnabled(phrenPath, hookTool);
|
|
19
|
+
const activeProject = detectProject(phrenPath, cwd, profile);
|
|
20
|
+
const manifest = readRootManifest(phrenPath);
|
|
21
|
+
return { phrenPath, profile, cwd, hookTool, activeProject, hooksEnabled, toolHookEnabled, manifest };
|
|
22
|
+
}
|
|
23
|
+
/** Check common hook guards. Returns a reason string if the hook should NOT run, null if OK. */
|
|
24
|
+
export function checkHookGuard(ctx, event) {
|
|
25
|
+
if (!ctx.hooksEnabled)
|
|
26
|
+
return "disabled";
|
|
27
|
+
if (!ctx.toolHookEnabled)
|
|
28
|
+
return `tool_disabled tool=${ctx.hookTool}`;
|
|
29
|
+
if (!isProjectHookEnabled(ctx.phrenPath, ctx.activeProject, event)) {
|
|
30
|
+
return `project_disabled project=${ctx.activeProject}`;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** Log a guard skip and optionally update runtime health. */
|
|
35
|
+
export function handleGuardSkip(ctx, hookName, reason, healthUpdate) {
|
|
36
|
+
if (healthUpdate)
|
|
37
|
+
updateRuntimeHealth(ctx.phrenPath, healthUpdate);
|
|
38
|
+
appendAuditLog(ctx.phrenPath, hookName, `status=${reason}`);
|
|
39
|
+
}
|
|
40
|
+
// Re-export frequently used functions so hook handlers can import from one place
|
|
41
|
+
export { debugLog, appendAuditLog, getPhrenPath, readRootManifest, sessionMarker, runtimeFile, EXEC_TIMEOUT_MS, getProjectDirs, findProjectNameCaseInsensitive, homePath, } from "./shared.js";
|
|
42
|
+
export { updateRuntimeHealth, getWorkflowPolicy, withFileLock, appendReviewQueue, recordFeedback, getQualityMultiplier, } from "./shared-governance.js";
|
|
43
|
+
export { detectProject } from "./shared-index.js";
|
|
44
|
+
export { isProjectHookEnabled, readProjectConfig, getProjectSourcePath } from "./project-config.js";
|
|
45
|
+
export { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
46
|
+
export { detectProjectDir, ensureLocalGitRepo, isProjectTracked, repairPreexistingInstall, } from "./init-setup.js";
|
|
47
|
+
export { getProactivityLevelForTask, getProactivityLevelForFindings } from "./proactivity.js";
|
|
48
|
+
export { hasExplicitFindingSignal, shouldAutoCaptureFindingsForLevel } from "./proactivity.js";
|
|
49
|
+
export { FINDING_SENSITIVITY_CONFIG } from "./cli-config.js";
|
|
50
|
+
export { isFeatureEnabled, errorMessage } from "./utils.js";
|
|
51
|
+
export { bootstrapPhrenDotEnv } from "./phren-dotenv.js";
|
|
52
|
+
export { finalizeTaskSession } from "./task-lifecycle.js";
|
|
53
|
+
export { appendFindingJournal } from "./finding-journal.js";
|
|
54
|
+
export { getHooksEnabledPreference } from "./init.js";
|
|
55
|
+
export { isToolHookEnabled } from "./hooks.js";
|
|
56
|
+
export { runDoctor } from "./link.js";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { capCache } from "./shared.js";
|
|
4
|
+
// ── Glob matching and project frontmatter ────────────────────────────────────
|
|
5
|
+
const projectGlobCache = new Map();
|
|
6
|
+
export function clearProjectGlobCache() {
|
|
7
|
+
projectGlobCache.clear();
|
|
8
|
+
}
|
|
9
|
+
function parseProjectGlobs(phrenPathLocal, project) {
|
|
10
|
+
if (projectGlobCache.has(project))
|
|
11
|
+
return projectGlobCache.get(project);
|
|
12
|
+
const claudeMdPath = path.join(phrenPathLocal, project, "CLAUDE.md");
|
|
13
|
+
let globs = null;
|
|
14
|
+
try {
|
|
15
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
16
|
+
const raw = fs.readFileSync(claudeMdPath, "utf8");
|
|
17
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
18
|
+
if (fmMatch) {
|
|
19
|
+
const fmBlock = fmMatch[1];
|
|
20
|
+
const globLine = fmBlock.match(/^globs:\s*$/m);
|
|
21
|
+
if (globLine) {
|
|
22
|
+
const lines = fmBlock.split("\n");
|
|
23
|
+
const idx = lines.findIndex((l) => /^globs:\s*$/.test(l));
|
|
24
|
+
if (idx >= 0) {
|
|
25
|
+
const items = [];
|
|
26
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
27
|
+
const m = lines[i].match(/^\s+-\s+(.+)/);
|
|
28
|
+
if (m)
|
|
29
|
+
items.push(m[1].trim().replace(/^["']|["']$/g, ""));
|
|
30
|
+
else
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (items.length > 0)
|
|
34
|
+
globs = items;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const inlineMatch = fmBlock.match(/^globs:\s*\[([^\]]+)\]/m);
|
|
39
|
+
if (inlineMatch) {
|
|
40
|
+
globs = inlineMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (process.env.PHREN_DEBUG)
|
|
48
|
+
process.stderr.write(`[phren] getProjectGlobs: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
49
|
+
}
|
|
50
|
+
projectGlobCache.set(project, globs);
|
|
51
|
+
capCache(projectGlobCache);
|
|
52
|
+
return globs;
|
|
53
|
+
}
|
|
54
|
+
function simpleGlobMatch(pattern, filePath) {
|
|
55
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
56
|
+
const regex = pattern
|
|
57
|
+
.replace(/\\/g, "/")
|
|
58
|
+
.replace(/[.+^${}()|[\]]/g, "\\$&")
|
|
59
|
+
.replace(/\*\*/g, "\0")
|
|
60
|
+
.replace(/\*/g, "[^/]*")
|
|
61
|
+
.replace(/\?/g, "[^/]")
|
|
62
|
+
.replace(/\0/g, ".*");
|
|
63
|
+
return new RegExp(`^${regex}$`).test(normalized) || new RegExp(`(^|/)${regex}$`).test(normalized);
|
|
64
|
+
}
|
|
65
|
+
export function getProjectGlobBoost(phrenPathLocal, project, cwd, changedFiles) {
|
|
66
|
+
const globs = parseProjectGlobs(phrenPathLocal, project);
|
|
67
|
+
if (!globs)
|
|
68
|
+
return 1.0;
|
|
69
|
+
const paths = [];
|
|
70
|
+
if (cwd)
|
|
71
|
+
paths.push(cwd);
|
|
72
|
+
if (changedFiles) {
|
|
73
|
+
for (const f of changedFiles)
|
|
74
|
+
paths.push(f);
|
|
75
|
+
}
|
|
76
|
+
for (const p of paths) {
|
|
77
|
+
for (const glob of globs) {
|
|
78
|
+
if (simpleGlobMatch(glob, p))
|
|
79
|
+
return 1.3;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return 0.7;
|
|
83
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { recordInjection, recordRetrieval, } from "./shared-governance.js";
|
|
2
|
+
import { getDocSourceKey, } from "./shared-index.js";
|
|
3
|
+
import { logImpact, extractFindingIdsFromSnippet, } from "./finding-impact.js";
|
|
4
|
+
import { isFeatureEnabled } from "./utils.js";
|
|
5
|
+
import { annotateStale } from "./cli-hooks-citations.js";
|
|
6
|
+
import { approximateTokens, fileRelevanceBoost, branchMatchBoost } from "./shared-retrieval.js";
|
|
7
|
+
// ── Progressive disclosure helpers ────────────────────────────────────────────
|
|
8
|
+
function buildOneLiner(snippet) {
|
|
9
|
+
const lines = snippet.split("\n").map(l => l.trim()).filter(l => l.length > 0);
|
|
10
|
+
if (!lines.length)
|
|
11
|
+
return "";
|
|
12
|
+
const first = lines[0].replace(/^[-*#>\s]+/, "").trim();
|
|
13
|
+
if (first.length <= 80)
|
|
14
|
+
return first;
|
|
15
|
+
return first.slice(0, 79) + "\u2026";
|
|
16
|
+
}
|
|
17
|
+
function buildCompactIndex(selected, phrenPathLocal) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
for (const { doc, snippet } of selected) {
|
|
20
|
+
const id = `mem:${getDocSourceKey(doc, phrenPathLocal)}`;
|
|
21
|
+
const summary = buildOneLiner(snippet);
|
|
22
|
+
lines.push(`[${id}] ${doc.type}: ${summary}`);
|
|
23
|
+
}
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
26
|
+
// ── Hook output formatting ───────────────────────────────────────────────────
|
|
27
|
+
export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedProject, stage, tokenBudget, phrenPathLocal, sessionId) {
|
|
28
|
+
const projectLabel = detectedProject ? ` \u00b7 ${detectedProject}` : "";
|
|
29
|
+
const resultLabel = selected.length === 1 ? "1 result" : `${selected.length} results`;
|
|
30
|
+
const statusLine = `\u25c6 phren${projectLabel} \u00b7 ${resultLabel}`;
|
|
31
|
+
const parts = [statusLine, "<phren-context>"];
|
|
32
|
+
const impactEntries = [];
|
|
33
|
+
const impactSessionId = sessionId ?? "none";
|
|
34
|
+
const useCompactIndex = isFeatureEnabled("PHREN_FEATURE_PROGRESSIVE_DISCLOSURE", false) && selected.length >= 3;
|
|
35
|
+
if (useCompactIndex) {
|
|
36
|
+
const indexEntries = selected.slice(0, 8);
|
|
37
|
+
const indexLines = buildCompactIndex(indexEntries, phrenPathLocal);
|
|
38
|
+
parts.push("Context index (use get_memory_detail to expand any entry):");
|
|
39
|
+
for (const line of indexLines) {
|
|
40
|
+
parts.push(line);
|
|
41
|
+
}
|
|
42
|
+
parts.push("");
|
|
43
|
+
for (const injected of indexEntries) {
|
|
44
|
+
recordInjection(phrenPathLocal, injected.key, sessionId);
|
|
45
|
+
if (injected.doc.type === "findings") {
|
|
46
|
+
for (const findingId of extractFindingIdsFromSnippet(injected.snippet)) {
|
|
47
|
+
impactEntries.push({
|
|
48
|
+
findingId,
|
|
49
|
+
project: injected.doc.project,
|
|
50
|
+
sessionId: impactSessionId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
recordRetrieval(phrenPathLocal, `${injected.doc.project}/${injected.doc.filename}`, injected.doc.type);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (process.env.PHREN_DEBUG)
|
|
59
|
+
process.stderr.write(`[phren] injectContext recordRetrieval: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Position-aware injection: place most relevant at START and END so the
|
|
65
|
+
// highest-value snippets survive truncation pressure better.
|
|
66
|
+
// Input `selected` is already ranked by relevance (best first).
|
|
67
|
+
// Reorder so: [0] stays first, [1] goes last, middle positions get [2..N-1].
|
|
68
|
+
let ordered = selected;
|
|
69
|
+
if (selected.length >= 3) {
|
|
70
|
+
ordered = [
|
|
71
|
+
selected[0], // most relevant → start
|
|
72
|
+
...selected.slice(2), // remaining → middle
|
|
73
|
+
selected[1], // second most → end
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
// Re-verify token budget after reordering; trim middle items if over budget
|
|
77
|
+
if (ordered.length > 2) {
|
|
78
|
+
let totalTokens = 36; // base overhead
|
|
79
|
+
const keep = ordered.map(() => true);
|
|
80
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
81
|
+
totalTokens += approximateTokens(ordered[i].snippet) + 14;
|
|
82
|
+
}
|
|
83
|
+
// Trim from the middle (indices 1..N-2) if over budget
|
|
84
|
+
if (totalTokens > tokenBudget) {
|
|
85
|
+
for (let i = ordered.length - 2; i >= 1; i--) {
|
|
86
|
+
if (totalTokens <= tokenBudget)
|
|
87
|
+
break;
|
|
88
|
+
totalTokens -= approximateTokens(ordered[i].snippet) + 14;
|
|
89
|
+
keep[i] = false;
|
|
90
|
+
}
|
|
91
|
+
ordered = ordered.filter((_, i) => keep[i]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const injected of ordered) {
|
|
95
|
+
const { doc, snippet, key } = injected;
|
|
96
|
+
recordInjection(phrenPathLocal, key, sessionId);
|
|
97
|
+
if (doc.type === "findings") {
|
|
98
|
+
for (const findingId of extractFindingIdsFromSnippet(snippet)) {
|
|
99
|
+
impactEntries.push({
|
|
100
|
+
findingId,
|
|
101
|
+
project: doc.project,
|
|
102
|
+
sessionId: impactSessionId,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
recordRetrieval(phrenPathLocal, doc.path ?? doc.filename, doc.type);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (process.env.PHREN_DEBUG)
|
|
111
|
+
process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
112
|
+
}
|
|
113
|
+
parts.push(`[${getDocSourceKey(doc, phrenPathLocal)}] (${doc.type})`);
|
|
114
|
+
parts.push(annotateStale(snippet));
|
|
115
|
+
parts.push("");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
logImpact(phrenPathLocal, impactEntries);
|
|
119
|
+
parts.push("<phren-context>");
|
|
120
|
+
const changedCount = gitCtx?.changedFiles.size ?? 0;
|
|
121
|
+
if (gitCtx) {
|
|
122
|
+
const fileHits = selected.filter((r) => fileRelevanceBoost(r.doc.path, gitCtx.changedFiles) > 0).length;
|
|
123
|
+
const branchHits = selected.filter((r) => branchMatchBoost(r.doc.content, gitCtx.branch) > 0).length;
|
|
124
|
+
parts.push(`\u25c6 phren \u00b7 trace: intent=${intent}; reasons=file:${fileHits},branch:${branchHits}; branch=${gitCtx.branch}; changed_files=${changedCount}; tokens\u2248${usedTokens}/${tokenBudget}; stages=index:${stage.indexMs}ms,search:${stage.searchMs}ms,trust:${stage.trustMs}ms,rank:${stage.rankMs}ms,select:${stage.selectMs}ms`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
parts.push(`\u25c6 phren \u00b7 trace: intent=${intent}; reasons=intent-only; tokens\u2248${usedTokens}/${tokenBudget}; stages=index:${stage.indexMs}ms,search:${stage.searchMs}ms,trust:${stage.trustMs}ms,rank:${stage.rankMs}ms,select:${stage.selectMs}ms`);
|
|
128
|
+
}
|
|
129
|
+
return parts;
|
|
130
|
+
}
|