@phren/cli 0.0.34 → 0.0.36

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.
@@ -113,6 +113,7 @@ export function readFindings(phrenPath, project, opts = {}) {
113
113
  let date = "unknown";
114
114
  let index = 1;
115
115
  let inArchiveBlock = false;
116
+ let headingTag;
116
117
  const includeArchived = opts.includeArchived ?? false;
117
118
  for (let i = 0; i < lines.length; i++) {
118
119
  const line = lines[i];
@@ -134,6 +135,39 @@ export function readFindings(phrenPath, project, opts = {}) {
134
135
  date = extractedDate;
135
136
  continue;
136
137
  }
138
+ // Support heading-based findings: ## topic / ### title / paragraph
139
+ const h2TagMatch = line.match(/^##\s+([a-z_-]+)\s*$/i);
140
+ if (h2TagMatch && !line.match(/^##\s+\d{4}/)) {
141
+ // Track topic heading (but not date headings like ## 2026-03-22)
142
+ headingTag = h2TagMatch[1].toLowerCase();
143
+ continue;
144
+ }
145
+ const h3Match = line.match(/^###\s+(.+)$/);
146
+ if (h3Match && headingTag) {
147
+ let body = "";
148
+ for (let j = i + 1; j < lines.length; j++) {
149
+ const next = lines[j].trim();
150
+ if (!next)
151
+ continue;
152
+ if (next.startsWith("#") || next.startsWith("- "))
153
+ break;
154
+ body = next;
155
+ break;
156
+ }
157
+ const title = h3Match[1].trim();
158
+ const syntheticText = body ? `[${headingTag}] ${title} — ${body}` : `[${headingTag}] ${title}`;
159
+ items.push({
160
+ id: `L${index}`,
161
+ date,
162
+ text: syntheticText,
163
+ source: "unknown",
164
+ status: "active",
165
+ archived: inArchiveBlock,
166
+ tier: inArchiveBlock ? "archived" : "current",
167
+ });
168
+ index++;
169
+ continue;
170
+ }
137
171
  if (!line.startsWith("- "))
138
172
  continue;
139
173
  const next = lines[i + 1] || "";
package/mcp/dist/hooks.js CHANGED
@@ -218,12 +218,15 @@ export function installPhrenCliWrapper(phrenPath) {
218
218
  if (!existing.includes("PHREN_CLI_WRAPPER"))
219
219
  return false;
220
220
  }
221
- catch { /* unreadable — skip */ }
221
+ catch {
222
+ // File exists but unreadable — don't overwrite, could be a real binary
223
+ return false;
224
+ }
222
225
  }
223
226
  const content = `#!/bin/sh
224
227
  # PHREN_CLI_WRAPPER — managed by phren init; safe to delete
225
228
  set -u
226
- PHREN_PATH="\${PHREN_PATH:-${shellEscape(phrenPath)}}"
229
+ PHREN_PATH="\${PHREN_PATH:-${phrenPath}}"
227
230
  export PHREN_PATH
228
231
  exec node ${shellEscape(entry)} "$@"
229
232
  `;
@@ -118,7 +118,7 @@ export function parseMcpMode(raw) {
118
118
  function normalizedBootstrapProjectName(projectPath) {
119
119
  return path.basename(projectPath).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
120
120
  }
121
- function getPendingBootstrapTarget(phrenPath, opts) {
121
+ function getPendingBootstrapTarget(phrenPath, _opts) {
122
122
  const cwdProject = detectProjectDir(process.cwd(), phrenPath);
123
123
  if (!cwdProject)
124
124
  return null;
@@ -970,13 +970,12 @@ function configureHooksIfEnabled(phrenPath, hooksEnabled, verb) {
970
970
  log(` Hooks are disabled by preference (run: npx phren hooks-mode on)`);
971
971
  }
972
972
  // Install phren CLI wrapper at ~/.local/bin/phren so the bare command works
973
- try {
974
- if (installPhrenCliWrapper(phrenPath)) {
975
- log(` ${verb} CLI wrapper: ~/.local/bin/phren`);
976
- }
973
+ const wrapperInstalled = installPhrenCliWrapper(phrenPath);
974
+ if (wrapperInstalled) {
975
+ log(` ${verb} CLI wrapper: ~/.local/bin/phren`);
977
976
  }
978
- catch (err) {
979
- debugLog(`installPhrenCliWrapper failed: ${errorMessage(err)}`);
977
+ else {
978
+ log(` Note: phren CLI wrapper not installed (existing non-managed binary, or no entry script found)`);
980
979
  }
981
980
  }
982
981
  export async function runInit(opts = {}) {
@@ -6,6 +6,7 @@ import * as path from "path";
6
6
  import * as os from "os";
7
7
  import * as yaml from "js-yaml";
8
8
  import { atomicWriteText, debugLog, findProjectNameCaseInsensitive, hookConfigPath, EXEC_TIMEOUT_QUICK_MS, readRootManifest, sessionsDir, runtimeHealthFile, isRecord, } from "../shared.js";
9
+ import { homePath } from "../phren-paths.js";
9
10
  import { addProjectToProfile, listProfiles, resolveActiveProfile, setMachineProfile } from "../profile-store.js";
10
11
  import { getMachineName } from "../machine-identity.js";
11
12
  import { execFileSync } from "child_process";
@@ -1271,6 +1272,25 @@ export function runPostInitVerify(phrenPath) {
1271
1272
  fix: ftsOk ? undefined : "Create a project: `cd ~/your-project && phren add`",
1272
1273
  });
1273
1274
  checks.push(getHookEntrypointCheck());
1275
+ // Check that the CLI wrapper at ~/.local/bin/phren exists and is executable (soft check — not mandatory)
1276
+ const cliWrapperPath = homePath(".local", "bin", "phren");
1277
+ let cliWrapperOk = false;
1278
+ try {
1279
+ const stat = fs.statSync(cliWrapperPath);
1280
+ cliWrapperOk = stat.isFile();
1281
+ if (cliWrapperOk)
1282
+ fs.accessSync(cliWrapperPath, fs.constants.X_OK);
1283
+ }
1284
+ catch {
1285
+ cliWrapperOk = false;
1286
+ }
1287
+ checks.push({
1288
+ name: "cli-wrapper",
1289
+ ok: true, // always pass — wrapper is optional (global install or npx work too)
1290
+ detail: cliWrapperOk
1291
+ ? `CLI wrapper exists: ${cliWrapperPath}`
1292
+ : `CLI wrapper not found (optional — use 'npm i -g @phren/cli' or 'npx @phren/cli' instead)`,
1293
+ });
1274
1294
  const ok = checks.every((c) => c.ok);
1275
1295
  return { ok, checks };
1276
1296
  }
@@ -232,7 +232,7 @@ function parseUserDefinedFragments(phrenPath, project) {
232
232
  }
233
233
  }
234
234
  /** Clear the user fragment cache (call between index builds). */
235
- function clearUserFragmentCache() {
235
+ function _clearUserFragmentCache() {
236
236
  _userFragmentCache.clear();
237
237
  _buildUserFragmentCache.clear();
238
238
  _activeBuildCacheKeyPrefix = null;
@@ -63,7 +63,7 @@ async function playStartupIntro(phrenPath, plan = resolveStartupIntroPlan(phrenP
63
63
  // Start animated phren during loading
64
64
  const animator = createPhrenAnimator({ facing: "right" });
65
65
  animator.start();
66
- const cols = process.stdout.columns || 80;
66
+ const _cols = process.stdout.columns || 80;
67
67
  const tagline = style.dim("local memory for working agents");
68
68
  const versionBadge = badge(`v${VERSION}`, style.boldBlue);
69
69
  const logoLines = [
@@ -456,7 +456,6 @@ async function handleSearchKnowledge(ctx, { query, limit, project, type, tag, si
456
456
  }
457
457
  }
458
458
  async function handleGetProjectSummary(ctx, { name }) {
459
- const { phrenPath } = ctx;
460
459
  const db = ctx.db();
461
460
  const docs = queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE project = ?", [name]);
462
461
  if (!docs) {
@@ -194,8 +194,62 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
194
194
  const MAX_UNTAGGED = isFocused ? Infinity : 100;
195
195
  let taggedCount = 0;
196
196
  let untaggedAdded = 0;
197
- for (const line of lines) {
198
- // Support legacy tagged findings like [decision], [pitfall], etc.
197
+ // Support heading-based findings: ## topic / ### title / paragraph
198
+ let currentHeadingTag;
199
+ let _currentHeadingTitle;
200
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
201
+ const line = lines[lineIdx];
202
+ // Track heading context for heading-based findings
203
+ const h2Match = line.match(/^##\s+([a-z_-]+)\s*$/i);
204
+ if (h2Match) {
205
+ currentHeadingTag = h2Match[1].toLowerCase();
206
+ _currentHeadingTitle = undefined;
207
+ continue;
208
+ }
209
+ const h3Match = line.match(/^###\s+(.+)$/);
210
+ if (h3Match && currentHeadingTag) {
211
+ // Read the next non-empty line as the body
212
+ let body = "";
213
+ for (let j = lineIdx + 1; j < lines.length; j++) {
214
+ const next = lines[j].trim();
215
+ if (!next)
216
+ continue;
217
+ if (next.startsWith("#"))
218
+ break;
219
+ body = next;
220
+ break;
221
+ }
222
+ const title = h3Match[1].trim();
223
+ const text = body ? `${title} — ${body}` : title;
224
+ if (text.length >= 10) {
225
+ if (taggedCount >= MAX_TAGGED)
226
+ continue;
227
+ const topic = classifyTopicForText(`[${currentHeadingTag}] ${text}`, projectTopics);
228
+ const scoreKey = entryScoreKey(project, "FINDINGS.md", `[${currentHeadingTag}] ${text}`);
229
+ const nodeId = stableId("finding", scoreKey);
230
+ taggedCount++;
231
+ nodes.push({
232
+ id: nodeId,
233
+ label: text.length > 55 ? `${text.slice(0, 52)}...` : text,
234
+ fullLabel: text,
235
+ group: `topic:${topic.slug}`,
236
+ refCount: taggedCount,
237
+ project,
238
+ tagged: true,
239
+ scoreKey,
240
+ scoreKeys: [scoreKey],
241
+ refDocs: [{ doc: `${project}/FINDINGS.md`, project, scoreKey }],
242
+ topicSlug: topic.slug,
243
+ topicLabel: topic.label,
244
+ });
245
+ links.push({ source: project, target: nodeId });
246
+ for (const other of exactProjectMentions(text, projectSet, project)) {
247
+ links.push({ source: project, target: other });
248
+ }
249
+ }
250
+ continue;
251
+ }
252
+ // Standard bullet-based findings: - [tag] text
199
253
  const tagMatch = line.match(/^-\s+\[([a-z_-]+)\]\s+(.+?)(?:\s*<!--.*-->)?$/);
200
254
  if (tagMatch) {
201
255
  if (taggedCount >= MAX_TAGGED)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {