@phren/cli 0.0.15 → 0.0.16

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.
@@ -7,12 +7,12 @@ import * as querystring from "querystring";
7
7
  import { spawn, execFileSync } from "child_process";
8
8
  import { computePhrenLiveStateToken, getProjectDirs, } from "./shared.js";
9
9
  import { editFinding, readReviewQueue, removeFinding, readFindings, addFinding as addFindingStore, readTasksAcrossProjects, addTask as addTaskStore, completeTask as completeTaskStore, removeTask as removeTaskStore, TASKS_FILENAME, } from "./data-access.js";
10
- import { isValidProjectName, errorMessage, queueFilePath } from "./utils.js";
10
+ import { isValidProjectName, errorMessage, queueFilePath, safeProjectPath } from "./utils.js";
11
11
  import { readInstallPreferences, writeInstallPreferences, writeGovernanceInstallPreferences } from "./init-preferences.js";
12
12
  import { buildGraph, collectProjectsForUI, collectSkillsForUI, getHooksData, isAllowedFilePath, readSyncSnapshot, recentAccepted, recentUsage, } from "./memory-ui-data.js";
13
13
  import { CONSOLIDATION_ENTRY_THRESHOLD } from "./content-validate.js";
14
14
  import { ensureTopicReferenceDoc, getProjectTopicsResponse, listProjectReferenceDocs, pinProjectTopicSuggestion, readReferenceContent, reclassifyLegacyTopicDocs, unpinProjectTopicSuggestion, writeProjectTopics, } from "./project-topics.js";
15
- import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides } from "./governance-policy.js";
15
+ import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides, VALID_TASK_MODES } from "./governance-policy.js";
16
16
  import { updateProjectConfigOverrides } from "./project-config.js";
17
17
  import { findSkill } from "./skill-registry.js";
18
18
  import { setSkillEnabledAndSync } from "./skill-files.js";
@@ -193,6 +193,8 @@ function readFormBody(req, res) {
193
193
  req.on("data", (chunk) => {
194
194
  received += chunk.length;
195
195
  if (received > MAX_FORM_BODY_BYTES) {
196
+ res.writeHead(413, { "Content-Type": "application/json" });
197
+ res.end(JSON.stringify({ ok: false, error: "Request body too large" }));
196
198
  req.destroy();
197
199
  resolve(null);
198
200
  return;
@@ -508,7 +510,12 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
508
510
  res.end(JSON.stringify({ ok: false, error: `File not allowed: ${file}` }));
509
511
  return;
510
512
  }
511
- const filePath = path.join(phrenPath, project, file);
513
+ const filePath = safeProjectPath(phrenPath, project, file);
514
+ if (!filePath) {
515
+ res.writeHead(400, { "content-type": "application/json" });
516
+ res.end(JSON.stringify({ ok: false, error: "Invalid project or file path" }));
517
+ return;
518
+ }
512
519
  if (!fs.existsSync(filePath)) {
513
520
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
514
521
  res.end(JSON.stringify({ ok: false, error: `File not found: ${file}` }));
@@ -1024,7 +1031,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
1024
1031
  if (!requireCsrf(res, parsed, csrfTokens, true))
1025
1032
  return;
1026
1033
  const value = String(parsed.value || "").trim().toLowerCase();
1027
- const valid = ["off", "manual", "auto"];
1034
+ const valid = VALID_TASK_MODES;
1028
1035
  if (!valid.includes(value)) {
1029
1036
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1030
1037
  res.end(JSON.stringify({ ok: false, error: `Invalid task mode: "${value}". Must be one of: ${valid.join(", ")}` }));
@@ -1161,7 +1168,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
1161
1168
  });
1162
1169
  const merged = mergeConfig(phrenPath, project);
1163
1170
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1164
- res.end(JSON.stringify({ ok: true, config: merged }));
1171
+ res.end(JSON.stringify({ ok: true, config: merged, ...(registrationWarning ? { warning: registrationWarning } : {}) }));
1165
1172
  }
1166
1173
  catch (err) {
1167
1174
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
@@ -305,6 +305,23 @@ export function findProjectNameCaseInsensitive(phrenPath, name) {
305
305
  }
306
306
  return null;
307
307
  }
308
+ export function findArchivedProjectNameCaseInsensitive(phrenPath, name) {
309
+ const needle = name.toLowerCase();
310
+ try {
311
+ for (const entry of fs.readdirSync(phrenPath, { withFileTypes: true })) {
312
+ if (!entry.isDirectory() || !entry.name.endsWith(".archived"))
313
+ continue;
314
+ const archivedName = entry.name.slice(0, -".archived".length);
315
+ if (archivedName.toLowerCase() === needle)
316
+ return archivedName;
317
+ }
318
+ }
319
+ catch (err) {
320
+ if ((process.env.PHREN_DEBUG))
321
+ process.stderr.write(`[phren] findArchivedProjectNameCaseInsensitive: ${errorMessage(err)}\n`);
322
+ }
323
+ return null;
324
+ }
308
325
  function getLocalProjectDirs(phrenPath, manifest) {
309
326
  const primaryProject = manifest.primaryProject;
310
327
  if (!primaryProject || !isValidProjectName(primaryProject))
@@ -4,7 +4,7 @@ import { debugLog, runtimeFile } from "./phren-paths.js";
4
4
  import { errorMessage } from "./utils.js";
5
5
  export { HOOK_TOOL_NAMES, hookConfigPath } from "./provider-adapters.js";
6
6
  export { EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, PhrenError, phrenOk, phrenErr, forwardErr, parsePhrenErrorCode, isRecord, withDefaults, FINDING_TYPES, FINDING_TAGS, KNOWN_OBSERVATION_TAGS, DOC_TYPES, capCache, RESERVED_PROJECT_DIR_NAMES, } from "./phren-core.js";
7
- export { ROOT_MANIFEST_FILENAME, homeDir, homePath, expandHomePath, defaultPhrenPath, rootManifestPath, readRootManifest, writeRootManifest, resolveInstallContext, findNearestPhrenPath, isProjectLocalMode, runtimeDir, tryUnlink, sessionsDir, runtimeFile, installPreferencesFile, runtimeHealthFile, shellStateFile, sessionMetricsFile, memoryScoresFile, memoryUsageLogFile, sessionMarker, debugLog, appendIndexEvent, resolveFindingsPath, findPhrenPath, ensurePhrenPath, findPhrenPathWithArg, normalizeProjectNameForCreate, findProjectNameCaseInsensitive, getProjectDirs, collectNativeMemoryFiles, computePhrenLiveStateToken, getPhrenPath, qualityMarkers, atomicWriteText, } from "./phren-paths.js";
7
+ export { ROOT_MANIFEST_FILENAME, homeDir, homePath, expandHomePath, defaultPhrenPath, rootManifestPath, readRootManifest, writeRootManifest, resolveInstallContext, findNearestPhrenPath, isProjectLocalMode, runtimeDir, tryUnlink, sessionsDir, runtimeFile, installPreferencesFile, runtimeHealthFile, shellStateFile, sessionMetricsFile, memoryScoresFile, memoryUsageLogFile, sessionMarker, debugLog, appendIndexEvent, resolveFindingsPath, findPhrenPath, ensurePhrenPath, findPhrenPathWithArg, normalizeProjectNameForCreate, findProjectNameCaseInsensitive, findArchivedProjectNameCaseInsensitive, getProjectDirs, collectNativeMemoryFiles, computePhrenLiveStateToken, getPhrenPath, qualityMarkers, atomicWriteText, } from "./phren-paths.js";
8
8
  export { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, hasExplicitFindingSignal, hasExplicitTaskSignal, hasExecutionIntent, hasDiscoveryIntent, shouldAutoCaptureFindingsForLevel, shouldAutoCaptureTaskForLevel, } from "./proactivity.js";
9
9
  const MEMORY_SCOPE_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
10
10
  export function normalizeMemoryScope(scope) {
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import { getProjectDirs } from "./shared.js";
4
4
  import { parseSkillFrontmatter } from "./link-skills.js";
5
5
  import { isSkillEnabled } from "./skill-state.js";
6
+ import { safeProjectPath } from "./utils.js";
6
7
  function normalizeCommand(raw, fallbackName) {
7
8
  const value = typeof raw === "string" && raw.trim() ? raw.trim() : `/${fallbackName}`;
8
9
  return value.startsWith("/") ? value : `/${value}`;
@@ -28,13 +29,35 @@ function collectSkills(phrenPath, root, sourceLabel, scopeType, sourceKind, seen
28
29
  if (!fs.existsSync(root))
29
30
  return [];
30
31
  const results = [];
32
+ const realRoot = (() => {
33
+ try {
34
+ return fs.realpathSync(root);
35
+ }
36
+ catch {
37
+ return path.resolve(root);
38
+ }
39
+ })();
31
40
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
32
41
  const isFolder = entry.isDirectory();
33
- const filePath = isFolder
42
+ const candidatePath = isFolder
34
43
  ? path.join(root, entry.name, "SKILL.md")
35
44
  : entry.name.endsWith(".md") ? path.join(root, entry.name) : null;
36
- if (!filePath || seen.has(filePath) || !fs.existsSync(filePath))
45
+ if (!candidatePath)
46
+ continue;
47
+ const safeCandidate = safeProjectPath(root, path.relative(root, candidatePath));
48
+ if (!safeCandidate || seen.has(safeCandidate) || !fs.existsSync(safeCandidate))
37
49
  continue;
50
+ try {
51
+ if (fs.lstatSync(safeCandidate).isSymbolicLink())
52
+ continue;
53
+ const realCandidate = fs.realpathSync(safeCandidate);
54
+ if (realCandidate !== realRoot && !realCandidate.startsWith(realRoot + path.sep))
55
+ continue;
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ const filePath = safeCandidate;
38
61
  seen.add(filePath);
39
62
  const name = isFolder ? entry.name : entry.name.replace(/\.md$/, "");
40
63
  const { frontmatter } = parseSkillFrontmatter(fs.readFileSync(filePath, "utf8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {