@phren/cli 0.1.12 → 0.1.14
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/dist/cli/hooks-session.d.ts +18 -36
- package/dist/cli/hooks-session.js +21 -1482
- package/dist/cli/namespaces-findings.d.ts +1 -0
- package/dist/cli/namespaces-findings.js +208 -0
- package/dist/cli/namespaces-profile.d.ts +1 -0
- package/dist/cli/namespaces-profile.js +76 -0
- package/dist/cli/namespaces-projects.d.ts +1 -0
- package/dist/cli/namespaces-projects.js +370 -0
- package/dist/cli/namespaces-review.d.ts +1 -0
- package/dist/cli/namespaces-review.js +45 -0
- package/dist/cli/namespaces-skills.d.ts +4 -0
- package/dist/cli/namespaces-skills.js +550 -0
- package/dist/cli/namespaces-store.d.ts +2 -0
- package/dist/cli/namespaces-store.js +367 -0
- package/dist/cli/namespaces-tasks.d.ts +1 -0
- package/dist/cli/namespaces-tasks.js +369 -0
- package/dist/cli/namespaces-utils.d.ts +4 -0
- package/dist/cli/namespaces-utils.js +47 -0
- package/dist/cli/namespaces.d.ts +7 -11
- package/dist/cli/namespaces.js +8 -1991
- package/dist/cli/session-background.d.ts +3 -0
- package/dist/cli/session-background.js +176 -0
- package/dist/cli/session-git.d.ts +17 -0
- package/dist/cli/session-git.js +181 -0
- package/dist/cli/session-metrics.d.ts +2 -0
- package/dist/cli/session-metrics.js +67 -0
- package/dist/cli/session-start.d.ts +3 -0
- package/dist/cli/session-start.js +289 -0
- package/dist/cli/session-stop.d.ts +8 -0
- package/dist/cli/session-stop.js +468 -0
- package/dist/cli/session-tool-hook.d.ts +18 -0
- package/dist/cli/session-tool-hook.js +376 -0
- package/dist/profile-store.js +14 -1
- package/dist/shared/index.js +22 -3
- package/dist/shared/retrieval.js +10 -9
- package/dist/tools/search.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse hook handler and context hook handler.
|
|
3
|
+
* Extracted from hooks-session.ts for modularity.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { buildHookContext, debugLog, appendAuditLog, runtimeFile, sessionMarker, getPhrenPath, getWorkflowPolicy, appendReviewQueue, getProactivityLevelForFindings, FINDING_SENSITIVITY_CONFIG, isProjectHookEnabled, errorMessage, detectProject, } from "./hooks-context.js";
|
|
8
|
+
import { logger } from "../logger.js";
|
|
9
|
+
import { buildIndex, queryRows, } from "../shared/index.js";
|
|
10
|
+
import { filterTaskByPriority } from "../shared/retrieval.js";
|
|
11
|
+
// ── PostToolUse hook ─────────────────────────────────────────────────────────
|
|
12
|
+
const INTERESTING_TOOLS = new Set(["Read", "Write", "Edit", "Bash", "Glob", "Grep"]);
|
|
13
|
+
const COOLDOWN_MS = parseInt(process.env.PHREN_AUTOCAPTURE_COOLDOWN_MS ?? "30000", 10);
|
|
14
|
+
function getSessionCap() {
|
|
15
|
+
if (process.env.PHREN_AUTOCAPTURE_SESSION_CAP) {
|
|
16
|
+
return parseInt(process.env.PHREN_AUTOCAPTURE_SESSION_CAP, 10);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const policy = getWorkflowPolicy(getPhrenPath());
|
|
20
|
+
const sensitivity = policy.findingSensitivity ?? "balanced";
|
|
21
|
+
return FINDING_SENSITIVITY_CONFIG[sensitivity]?.sessionCap ?? 10;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return 10;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function flattenToolResponseText(value, maxChars = 4000) {
|
|
28
|
+
if (typeof value === "string")
|
|
29
|
+
return value;
|
|
30
|
+
const queue = [value];
|
|
31
|
+
const parts = [];
|
|
32
|
+
let length = 0;
|
|
33
|
+
while (queue.length > 0 && length < maxChars) {
|
|
34
|
+
const current = queue.shift();
|
|
35
|
+
if (typeof current === "string") {
|
|
36
|
+
const trimmed = current.trim();
|
|
37
|
+
if (!trimmed)
|
|
38
|
+
continue;
|
|
39
|
+
parts.push(trimmed);
|
|
40
|
+
length += trimmed.length + 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(current)) {
|
|
44
|
+
queue.unshift(...current);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (current && typeof current === "object") {
|
|
48
|
+
queue.unshift(...Object.values(current));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (parts.length > 0)
|
|
52
|
+
return parts.join("\n").slice(0, maxChars);
|
|
53
|
+
return JSON.stringify(value ?? "").slice(0, maxChars);
|
|
54
|
+
}
|
|
55
|
+
export async function handleHookTool() {
|
|
56
|
+
const ctx = buildHookContext();
|
|
57
|
+
if (!ctx.hooksEnabled) {
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const start = Date.now();
|
|
62
|
+
let raw = "";
|
|
63
|
+
if (!process.stdin.isTTY) {
|
|
64
|
+
try {
|
|
65
|
+
raw = fs.readFileSync(0, "utf-8");
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logger.debug("hooks-session", `hookTool stdinRead: ${errorMessage(err)}`);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let data;
|
|
73
|
+
try {
|
|
74
|
+
data = JSON.parse(raw);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
logger.debug("hooks-session", `hookTool stdinParse: ${errorMessage(err)}`);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
const toolName = String(data.tool_name ?? data.tool ?? "");
|
|
81
|
+
if (!INTERESTING_TOOLS.has(toolName)) {
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
const sessionId = data.session_id;
|
|
85
|
+
const input = (data.tool_input ?? {});
|
|
86
|
+
const entry = {
|
|
87
|
+
at: new Date().toISOString(),
|
|
88
|
+
session_id: sessionId,
|
|
89
|
+
tool: toolName,
|
|
90
|
+
};
|
|
91
|
+
if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
|
|
92
|
+
const filePath = input.file_path ?? input.path ?? undefined;
|
|
93
|
+
if (filePath)
|
|
94
|
+
entry.file = String(filePath);
|
|
95
|
+
}
|
|
96
|
+
else if (toolName === "Bash") {
|
|
97
|
+
const cmd = input.command ?? undefined;
|
|
98
|
+
if (cmd)
|
|
99
|
+
entry.command = String(cmd).slice(0, 200);
|
|
100
|
+
}
|
|
101
|
+
else if (toolName === "Glob") {
|
|
102
|
+
const pattern = input.pattern ?? undefined;
|
|
103
|
+
if (pattern)
|
|
104
|
+
entry.file = String(pattern);
|
|
105
|
+
}
|
|
106
|
+
else if (toolName === "Grep") {
|
|
107
|
+
const pattern = input.pattern ?? undefined;
|
|
108
|
+
const searchPath = input.path ?? undefined;
|
|
109
|
+
if (pattern)
|
|
110
|
+
entry.command = `grep ${pattern}${searchPath ? ` in ${searchPath}` : ""}`.slice(0, 200);
|
|
111
|
+
}
|
|
112
|
+
const responseStr = flattenToolResponseText(data.tool_response ?? "");
|
|
113
|
+
if (/(error|exception|failed|no such file|ENOENT)/i.test(responseStr)) {
|
|
114
|
+
entry.error = responseStr.slice(0, 300);
|
|
115
|
+
}
|
|
116
|
+
const cwd = (data.cwd ?? input.cwd ?? undefined);
|
|
117
|
+
let activeProject = cwd ? detectProject(ctx.phrenPath, cwd, ctx.profile) : null;
|
|
118
|
+
if (!isProjectHookEnabled(ctx.phrenPath, activeProject, "PostToolUse")) {
|
|
119
|
+
appendAuditLog(ctx.phrenPath, "hook_tool", `status=project_disabled project=${activeProject}`);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const logFile = runtimeFile(ctx.phrenPath, "tool-log.jsonl");
|
|
124
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
125
|
+
fs.appendFileSync(logFile, JSON.stringify(entry) + "\n");
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
logger.debug("hooks-session", `hookTool toolLog: ${errorMessage(err)}`);
|
|
129
|
+
}
|
|
130
|
+
const cooldownFile = runtimeFile(ctx.phrenPath, "hook-tool-cooldown");
|
|
131
|
+
try {
|
|
132
|
+
if (fs.existsSync(cooldownFile)) {
|
|
133
|
+
const age = Date.now() - fs.statSync(cooldownFile).mtimeMs;
|
|
134
|
+
if (age < COOLDOWN_MS) {
|
|
135
|
+
debugLog(`hook-tool: cooldown active (${Math.round(age / 1000)}s < ${Math.round(COOLDOWN_MS / 1000)}s), skipping extraction`);
|
|
136
|
+
activeProject = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
logger.debug("hooks-session", `hookTool cooldownStat: ${errorMessage(err)}`);
|
|
142
|
+
}
|
|
143
|
+
if (activeProject && sessionId) {
|
|
144
|
+
try {
|
|
145
|
+
const capFile = sessionMarker(ctx.phrenPath, `tool-findings-${sessionId}`);
|
|
146
|
+
let count = 0;
|
|
147
|
+
if (fs.existsSync(capFile)) {
|
|
148
|
+
count = Number.parseInt(fs.readFileSync(capFile, "utf8").trim(), 10) || 0;
|
|
149
|
+
}
|
|
150
|
+
const sessionCap = getSessionCap();
|
|
151
|
+
if (count >= sessionCap) {
|
|
152
|
+
debugLog(`hook-tool: session cap reached (${count}/${sessionCap}), skipping extraction`);
|
|
153
|
+
activeProject = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
logger.debug("hooks-session", `hookTool sessionCapCheck: ${errorMessage(err)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const findingsLevelForTool = getProactivityLevelForFindings(ctx.phrenPath);
|
|
161
|
+
if (activeProject && findingsLevelForTool !== "low") {
|
|
162
|
+
try {
|
|
163
|
+
const candidates = filterToolFindingsForProactivity(extractToolFindings(toolName, input, responseStr), findingsLevelForTool);
|
|
164
|
+
for (const { text, confidence } of candidates) {
|
|
165
|
+
appendReviewQueue(ctx.phrenPath, activeProject, "Review", [text]);
|
|
166
|
+
debugLog(`hook-tool: queued candidate for review (conf=${confidence}): ${text.slice(0, 60)}`);
|
|
167
|
+
}
|
|
168
|
+
if (candidates.length > 0) {
|
|
169
|
+
try {
|
|
170
|
+
fs.writeFileSync(cooldownFile, Date.now().toString());
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
logger.debug("hooks-session", `hookTool cooldownWrite: ${errorMessage(err)}`);
|
|
174
|
+
}
|
|
175
|
+
if (sessionId) {
|
|
176
|
+
try {
|
|
177
|
+
const capFile = sessionMarker(ctx.phrenPath, `tool-findings-${sessionId}`);
|
|
178
|
+
let count = 0;
|
|
179
|
+
try {
|
|
180
|
+
count = Number.parseInt(fs.readFileSync(capFile, "utf8").trim(), 10) || 0;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logger.debug("hooks-session", `hookTool capFileRead: ${errorMessage(err)}`);
|
|
184
|
+
}
|
|
185
|
+
count += candidates.length;
|
|
186
|
+
fs.writeFileSync(capFile, count.toString());
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
logger.debug("hooks-session", `hookTool capFileWrite: ${errorMessage(err)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
debugLog(`hook-tool: finding extraction failed: ${errorMessage(err)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (activeProject) {
|
|
199
|
+
debugLog("hook-tool: skipped because findings proactivity is low");
|
|
200
|
+
}
|
|
201
|
+
const elapsed = Date.now() - start;
|
|
202
|
+
debugLog(`hook-tool: ${toolName} logged in ${elapsed}ms`);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
debugLog(`hook-tool: unhandled error: ${err instanceof Error ? err.stack || err.message : String(err)}`);
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const EXPLICIT_TAG_PATTERN = /\[(pitfall|decision|pattern|tradeoff|architecture|bug)\]\s*(.+)/i;
|
|
211
|
+
export function filterToolFindingsForProactivity(candidates, level = getProactivityLevelForFindings(getPhrenPath())) {
|
|
212
|
+
if (level === "high")
|
|
213
|
+
return candidates;
|
|
214
|
+
if (level === "low")
|
|
215
|
+
return [];
|
|
216
|
+
return candidates.filter((candidate) => candidate.explicit === true);
|
|
217
|
+
}
|
|
218
|
+
export function extractToolFindings(toolName, input, responseStr) {
|
|
219
|
+
const candidates = [];
|
|
220
|
+
const changedContent = (toolName === "Edit" || toolName === "Write")
|
|
221
|
+
? String(input.new_string ?? input.content ?? "")
|
|
222
|
+
: "";
|
|
223
|
+
const explicitSource = changedContent || responseStr;
|
|
224
|
+
const tagMatches = explicitSource.matchAll(new RegExp(EXPLICIT_TAG_PATTERN.source, "gi"));
|
|
225
|
+
for (const m of tagMatches) {
|
|
226
|
+
const tag = m[1].toLowerCase();
|
|
227
|
+
const content = m[2].replace(/\s+/g, " ").trim().slice(0, 200);
|
|
228
|
+
if (content) {
|
|
229
|
+
candidates.push({ text: `[${tag}] ${content}`, confidence: 0.85, explicit: true });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (toolName === "Edit" || toolName === "Write") {
|
|
233
|
+
const filePath = String(input.file_path ?? input.path ?? "unknown");
|
|
234
|
+
const filename = path.basename(filePath);
|
|
235
|
+
if (/\b(TODO|FIXME)\b/.test(changedContent)) {
|
|
236
|
+
const firstLine = changedContent.split("\n").find((l) => /\b(TODO|FIXME)\b/.test(l));
|
|
237
|
+
if (firstLine) {
|
|
238
|
+
candidates.push({
|
|
239
|
+
text: `[pitfall] ${filename}: ${firstLine.trim().slice(0, 150)}`,
|
|
240
|
+
confidence: 0.45,
|
|
241
|
+
explicit: false,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (/\btry\s*\{[\s\S]*?\bcatch\b/.test(changedContent)) {
|
|
246
|
+
const meaningfulLine = changedContent.split("\n").find((l) => l.trim().length > 10 && !/^\s*(try|catch|\{|\})/.test(l));
|
|
247
|
+
if (meaningfulLine) {
|
|
248
|
+
candidates.push({
|
|
249
|
+
text: `[pitfall] ${filename}: error handling added near "${meaningfulLine.trim().slice(0, 100)}"`,
|
|
250
|
+
confidence: 0.45,
|
|
251
|
+
explicit: false,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (toolName === "Bash") {
|
|
257
|
+
const cmd = String(input.command ?? "").slice(0, 30);
|
|
258
|
+
const hasError = /(error|exception|failed|ENOENT|command not found|permission denied)/i.test(responseStr);
|
|
259
|
+
if (hasError && cmd) {
|
|
260
|
+
const firstErrorLine = responseStr.split("\n").find((l) => /(error|exception|failed|ENOENT|command not found|permission denied)/i.test(l));
|
|
261
|
+
if (firstErrorLine) {
|
|
262
|
+
candidates.push({
|
|
263
|
+
text: `[bug] command '${cmd}' failed: ${firstErrorLine.trim().slice(0, 150)}`,
|
|
264
|
+
confidence: 0.55,
|
|
265
|
+
explicit: false,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return candidates;
|
|
271
|
+
}
|
|
272
|
+
// ── Context hook handler ────────────────────────────────────────────────────
|
|
273
|
+
function readStdinJson() {
|
|
274
|
+
if (process.stdin.isTTY)
|
|
275
|
+
return null;
|
|
276
|
+
try {
|
|
277
|
+
return JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
logger.debug("hooks-session", `readStdinJson: ${errorMessage(err)}`);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
export async function handleHookContext() {
|
|
285
|
+
const ctx = buildHookContext();
|
|
286
|
+
if (!ctx.hooksEnabled) {
|
|
287
|
+
process.exit(0);
|
|
288
|
+
}
|
|
289
|
+
let cwd = ctx.cwd;
|
|
290
|
+
const ctxStdin = readStdinJson();
|
|
291
|
+
if (ctxStdin?.cwd)
|
|
292
|
+
cwd = ctxStdin.cwd;
|
|
293
|
+
const project = cwd !== ctx.cwd ? detectProject(ctx.phrenPath, cwd, ctx.profile) : ctx.activeProject;
|
|
294
|
+
if (!isProjectHookEnabled(ctx.phrenPath, project, "UserPromptSubmit")) {
|
|
295
|
+
process.exit(0);
|
|
296
|
+
}
|
|
297
|
+
const db = await buildIndex(ctx.phrenPath, ctx.profile);
|
|
298
|
+
const contextLabel = project ? `\u25c6 phren \u00b7 ${project} \u00b7 context` : `\u25c6 phren \u00b7 context`;
|
|
299
|
+
const parts = [contextLabel, "<phren-context>"];
|
|
300
|
+
if (project) {
|
|
301
|
+
const summaryRow = queryRows(db, "SELECT content FROM docs WHERE project = ? AND type = 'summary'", [project]);
|
|
302
|
+
if (summaryRow) {
|
|
303
|
+
parts.push(`# ${project}`);
|
|
304
|
+
parts.push(summaryRow[0][0]);
|
|
305
|
+
parts.push("");
|
|
306
|
+
}
|
|
307
|
+
const findingsRow = queryRows(db, "SELECT content FROM docs WHERE project = ? AND type = 'findings'", [project]);
|
|
308
|
+
if (findingsRow) {
|
|
309
|
+
const content = findingsRow[0][0];
|
|
310
|
+
const bullets = content.split("\n").filter(l => l.startsWith("- ")).slice(0, 10);
|
|
311
|
+
if (bullets.length > 0) {
|
|
312
|
+
parts.push("## Recent findings");
|
|
313
|
+
parts.push(bullets.join("\n"));
|
|
314
|
+
parts.push("");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Collect pinned tasks across ALL projects (excluding Done)
|
|
318
|
+
const allTaskRows = queryRows(db, "SELECT project, content FROM docs WHERE type = 'task'", []);
|
|
319
|
+
const pinnedFromOtherProjects = [];
|
|
320
|
+
if (allTaskRows) {
|
|
321
|
+
for (const row of allTaskRows) {
|
|
322
|
+
const taskProject = row[0];
|
|
323
|
+
if (taskProject === project)
|
|
324
|
+
continue;
|
|
325
|
+
const content = row[1];
|
|
326
|
+
const pinned = content.split("\n")
|
|
327
|
+
.filter(l => l.startsWith("- [ ] ") && /\[pinned\]/i.test(l))
|
|
328
|
+
.map(l => `[${taskProject}] ${l}`);
|
|
329
|
+
pinnedFromOtherProjects.push(...pinned);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Active project tasks — pinned float to top, exclude Done
|
|
333
|
+
const taskRow = queryRows(db, "SELECT content FROM docs WHERE project = ? AND type = 'task'", [project]);
|
|
334
|
+
const pinnedItems = [];
|
|
335
|
+
const otherItems = [];
|
|
336
|
+
if (taskRow) {
|
|
337
|
+
const content = taskRow[0][0];
|
|
338
|
+
const allItems = content.split("\n").filter(l => l.startsWith("- [ ] "));
|
|
339
|
+
for (const item of allItems) {
|
|
340
|
+
if (/\[pinned\]/i.test(item)) {
|
|
341
|
+
pinnedItems.push(item);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
otherItems.push(item);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const filteredOther = filterTaskByPriority(otherItems);
|
|
349
|
+
const allPinned = [...pinnedItems, ...pinnedFromOtherProjects].slice(0, 10);
|
|
350
|
+
const remaining = Math.max(0, 5 - allPinned.length);
|
|
351
|
+
const trimmedOther = filteredOther.slice(0, remaining);
|
|
352
|
+
if (allPinned.length > 0 || trimmedOther.length > 0) {
|
|
353
|
+
if (allPinned.length > 0) {
|
|
354
|
+
parts.push("## Pinned tasks");
|
|
355
|
+
parts.push(allPinned.join("\n"));
|
|
356
|
+
}
|
|
357
|
+
if (trimmedOther.length > 0) {
|
|
358
|
+
parts.push("## Active tasks");
|
|
359
|
+
parts.push(trimmedOther.join("\n"));
|
|
360
|
+
}
|
|
361
|
+
parts.push("");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
const projectRows = queryRows(db, "SELECT DISTINCT project FROM docs ORDER BY project", []);
|
|
366
|
+
if (projectRows) {
|
|
367
|
+
parts.push("# Phren projects");
|
|
368
|
+
parts.push(projectRows.map(r => `- ${r[0]}`).join("\n"));
|
|
369
|
+
parts.push("");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
parts.push("<phren-context>");
|
|
373
|
+
if (parts.length > 2) {
|
|
374
|
+
console.log(parts.join("\n"));
|
|
375
|
+
}
|
|
376
|
+
}
|
package/dist/profile-store.js
CHANGED
|
@@ -310,8 +310,19 @@ export function listProjectCards(phrenPath, profile) {
|
|
|
310
310
|
const dirs = getProjectDirs(phrenPath, profile).sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
311
311
|
const cards = dirs.map(buildProjectCard);
|
|
312
312
|
const seen = new Set(dirs.map((d) => path.basename(d)));
|
|
313
|
-
// Include projects from team stores
|
|
313
|
+
// Include projects from team stores, filtered by active profile
|
|
314
314
|
try {
|
|
315
|
+
// Resolve the profile's project allow-list (if any)
|
|
316
|
+
let profileProjectNames;
|
|
317
|
+
if (profile) {
|
|
318
|
+
const profiles = listProfiles(phrenPath);
|
|
319
|
+
if (profiles.ok) {
|
|
320
|
+
const active = profiles.data.find((p) => p.name === profile);
|
|
321
|
+
if (active && active.projects.length > 0) {
|
|
322
|
+
profileProjectNames = new Set(active.projects);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
315
326
|
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
316
327
|
if (!fs.existsSync(store.path))
|
|
317
328
|
continue;
|
|
@@ -319,6 +330,8 @@ export function listProjectCards(phrenPath, profile) {
|
|
|
319
330
|
const name = path.basename(dir);
|
|
320
331
|
if (seen.has(name) || name === "global")
|
|
321
332
|
continue;
|
|
333
|
+
if (profileProjectNames && !profileProjectNames.has(name))
|
|
334
|
+
continue;
|
|
322
335
|
seen.add(name);
|
|
323
336
|
cards.push(buildProjectCard(dir));
|
|
324
337
|
}
|
package/dist/shared/index.js
CHANGED
|
@@ -30,12 +30,30 @@ async function refreshStoreProjectDirs(phrenPath, profile) {
|
|
|
30
30
|
try {
|
|
31
31
|
const { getNonPrimaryStores, getStoreProjectDirs } = await import("../store-registry.js");
|
|
32
32
|
const otherStores = getNonPrimaryStores(phrenPath);
|
|
33
|
-
|
|
33
|
+
let dirs = [];
|
|
34
34
|
for (const store of otherStores) {
|
|
35
35
|
if (!fs.existsSync(store.path))
|
|
36
36
|
continue;
|
|
37
37
|
dirs.push(...getStoreProjectDirs(store));
|
|
38
38
|
}
|
|
39
|
+
// Filter by active profile's project list, matching getProjectDirs behavior
|
|
40
|
+
if (profile) {
|
|
41
|
+
const profilePath = path.join(phrenPath, "profiles", `${profile}.yaml`);
|
|
42
|
+
if (fs.existsSync(profilePath)) {
|
|
43
|
+
try {
|
|
44
|
+
const yaml = await import("js-yaml");
|
|
45
|
+
const data = yaml.load(fs.readFileSync(profilePath, "utf-8"), { schema: yaml.CORE_SCHEMA });
|
|
46
|
+
const projects = data?.projects;
|
|
47
|
+
if (Array.isArray(projects)) {
|
|
48
|
+
const allowed = new Set(projects.map(String));
|
|
49
|
+
dirs = dirs.filter(dir => allowed.has(path.basename(dir)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Profile parse error — include all dirs as fallback
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
39
57
|
_cachedStoreProjectDirs = dirs;
|
|
40
58
|
_cachedStorePhrenPath = phrenPath;
|
|
41
59
|
}
|
|
@@ -445,8 +463,9 @@ function globAllFiles(phrenPath, profile) {
|
|
|
445
463
|
const allAbsolutePaths = [];
|
|
446
464
|
for (const dir of projectDirs) {
|
|
447
465
|
const projectName = path.basename(dir);
|
|
448
|
-
const
|
|
449
|
-
const
|
|
466
|
+
const storePath = path.dirname(dir);
|
|
467
|
+
const config = readProjectConfig(storePath, projectName);
|
|
468
|
+
const ownership = getProjectOwnershipMode(storePath, projectName, config);
|
|
450
469
|
const mdFilesSet = new Set();
|
|
451
470
|
for (const pattern of indexPolicy.includeGlobs) {
|
|
452
471
|
const dot = indexPolicy.includeHidden || pattern.startsWith(".") || pattern.includes("/.");
|
package/dist/shared/retrieval.js
CHANGED
|
@@ -495,20 +495,21 @@ export async function searchKnowledgeRows(db, options) {
|
|
|
495
495
|
* Returns an array of results tagged with their source store. Read-only — no mutations.
|
|
496
496
|
*/
|
|
497
497
|
export async function searchFederatedStores(localPhrenPath, options) {
|
|
498
|
-
|
|
498
|
+
// Registered non-primary stores are already included in the main FTS index
|
|
499
|
+
// by buildIndex (via refreshStoreProjectDirs). Only search unregistered
|
|
500
|
+
// federation paths from PHREN_FEDERATION_PATHS to avoid double indexing.
|
|
501
|
+
let registeredStorePaths;
|
|
499
502
|
try {
|
|
500
503
|
const { getNonPrimaryStores } = await import("../store-registry.js");
|
|
501
|
-
|
|
502
|
-
path: s.path, name: s.name, id: s.id,
|
|
503
|
-
}));
|
|
504
|
+
registeredStorePaths = new Set(getNonPrimaryStores(localPhrenPath).map((s) => s.path));
|
|
504
505
|
}
|
|
505
506
|
catch {
|
|
506
|
-
|
|
507
|
-
const raw = process.env.PHREN_FEDERATION_PATHS ?? "";
|
|
508
|
-
nonPrimaryStores = raw.split(":").map((p) => p.trim())
|
|
509
|
-
.filter((p) => p.length > 0 && p !== localPhrenPath && fs.existsSync(p))
|
|
510
|
-
.map((p) => ({ path: p, name: path.basename(p), id: "" }));
|
|
507
|
+
registeredStorePaths = new Set();
|
|
511
508
|
}
|
|
509
|
+
const raw = process.env.PHREN_FEDERATION_PATHS ?? "";
|
|
510
|
+
const nonPrimaryStores = raw.split(":").map((p) => p.trim())
|
|
511
|
+
.filter((p) => p.length > 0 && p !== localPhrenPath && !registeredStorePaths.has(p) && fs.existsSync(p))
|
|
512
|
+
.map((p) => ({ path: p, name: path.basename(p), id: "" }));
|
|
512
513
|
if (nonPrimaryStores.length === 0)
|
|
513
514
|
return [];
|
|
514
515
|
const allRows = [];
|
package/dist/tools/search.js
CHANGED
|
@@ -474,7 +474,7 @@ async function handleGetProjectSummary(ctx, { name }) {
|
|
|
474
474
|
for (const [file, type] of [["summary.md", "summary"], ["CLAUDE.md", "claude"], ["FINDINGS.md", "findings"], ["tasks.md", "task"], ["truths.md", "canonical"]]) {
|
|
475
475
|
const filePath = path.join(projDir, file);
|
|
476
476
|
if (fs.existsSync(filePath)) {
|
|
477
|
-
fsDocs.push({ filename: file, type, content: fs.readFileSync(filePath, "utf8").slice(0, 8000), path: filePath });
|
|
477
|
+
fsDocs.push({ project: lookupName, filename: file, type, content: fs.readFileSync(filePath, "utf8").slice(0, 8000), path: filePath });
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
if (fsDocs.length > 0)
|