portable-agent-layer 0.78.2 → 0.78.3

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.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { spawn } from "node:child_process";
11
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
11
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
13
  import { SIMILARITY_THRESHOLD } from "./graduation";
14
14
  import { readFailures, readReflections } from "./learning-store";
@@ -197,14 +197,21 @@ export function readIndex(): RetrievalIndex | null {
197
197
  }
198
198
 
199
199
  /** True if any source directory was modified after the index was built. */
200
+ function newestFileMtime(dir: string): number {
201
+ let newest = 0;
202
+ for (const entry of readdirSync(dir, { recursive: true, withFileTypes: true })) {
203
+ if (!entry.isFile()) continue;
204
+ newest = Math.max(newest, statSync(resolve(entry.parentPath, entry.name)).mtimeMs);
205
+ }
206
+ return newest;
207
+ }
208
+
200
209
  export function isStale(index: RetrievalIndex): boolean {
201
210
  try {
202
211
  const builtMs = new Date(index.builtAt).getTime();
203
- for (const dir of [paths.failures(), paths.wisdom(), paths.reflections()]) {
204
- if (!existsSync(dir)) continue;
205
- if (statSync(dir).mtimeMs > builtMs) return true;
206
- }
207
- return false;
212
+ return [paths.failures(), paths.wisdom(), paths.reflections()].some(
213
+ (dir) => newestFileMtime(dir) > builtMs
214
+ );
208
215
  } catch {
209
216
  return false;
210
217
  }
@@ -79,9 +79,14 @@ const WIN_ELEVATED_THREATS: [RegExp, string][] = [
79
79
  [/\bdiskpart\b/i, "Disk partitioning"],
80
80
  ];
81
81
 
82
+ const POSIX_ROOT_TARGET = String.raw`["']?(?:(?:~|\$HOME|\$\{HOME\})/?|/)\*?["']?(?=\s|[;&|]|$)`;
83
+
82
84
  /** Dangerous command patterns — always blocked */
83
85
  const BLOCKED_COMMANDS: [RegExp, string][] = [
84
- [/rm\s+-rf\s+[/~]/, "Recursive delete of root or home"],
86
+ [
87
+ new RegExp(String.raw`rm\s+-rf\s+${POSIX_ROOT_TARGET}`),
88
+ "Recursive delete of root or home",
89
+ ],
85
90
  [/mkfs\./, "Filesystem format"],
86
91
  [/dd\s+if=.*of=\/dev\//, "Raw disk write"],
87
92
  [/>\s*\/dev\/sd/, "Direct device write"],
@@ -232,6 +237,28 @@ function managedReasonInSegment(segment: string): string | null {
232
237
  const READ_ONLY_COMMANDS =
233
238
  /^\s*(?:cat|head|tail|less|more|grep|rg|wc|diff|stat|file|ls|dir|find|git\s+(?:log|diff|blame|show|status)|bat)\b/;
234
239
 
240
+ const COMMAND_SEPARATORS = new Set(["|", ";", "&"]);
241
+
242
+ function unquotedSegments(cmd: string): string[] {
243
+ const segments: string[] = [];
244
+ let current = "";
245
+ let quote: string | null = null;
246
+ for (const char of cmd) {
247
+ if (quote) {
248
+ if (char === quote) quote = null;
249
+ } else if (char === "'" || char === '"') {
250
+ quote = char;
251
+ } else if (COMMAND_SEPARATORS.has(char)) {
252
+ segments.push(current.trim());
253
+ current = "";
254
+ continue;
255
+ }
256
+ current += char;
257
+ }
258
+ segments.push(current.trim());
259
+ return segments;
260
+ }
261
+
235
262
  /** Check a bash command against blocked patterns. Returns reason string or null. */
236
263
  export function checkBashCommand(cmd: string): string | null {
237
264
  for (const [pattern, reason] of BLOCKED_COMMANDS) {
@@ -242,8 +269,7 @@ export function checkBashCommand(cmd: string): string | null {
242
269
  if (pattern.test(cmd)) return reason;
243
270
  }
244
271
  }
245
- const segments = cmd.split(/[|;&]/).map((s) => s.trim());
246
- for (const segment of segments) {
272
+ for (const segment of unquotedSegments(cmd)) {
247
273
  const reason = managedReasonInSegment(segment);
248
274
  if (reason && !READ_ONLY_COMMANDS.test(segment)) return reason;
249
275
  }
@@ -7,10 +7,11 @@
7
7
  */
8
8
 
9
9
  import { existsSync, readFileSync } from "node:fs";
10
- import { resolve } from "node:path";
10
+ import { basename, resolve } from "node:path";
11
11
  import { type FailureEntry, readFailures } from "./learning-store";
12
12
  import { loadOpinionContext } from "./opinions";
13
13
  import { palHome, paths } from "./paths";
14
+ import { type ProjectProgress, readAllProjects, resolveProjectFromCwd } from "./projects";
14
15
  import { readFramePrinciples } from "./wisdom";
15
16
 
16
17
  /** A single semi-static context source — built at session stop, loaded natively at session start. */
@@ -67,6 +68,12 @@ export function rankFailures(
67
68
  .slice(0, limit);
68
69
  }
69
70
 
71
+ function projectTag(cwd: string, projects: ProjectProgress[]): string {
72
+ if (!cwd) return "";
73
+ const name = resolveProjectFromCwd(cwd, projects)?.name ?? basename(cwd);
74
+ return `[${name}]`;
75
+ }
76
+
70
77
  /** Build the failure avoid-list, prioritized by relevance to the current project. */
71
78
  export function loadFailurePatterns(): string {
72
79
  try {
@@ -74,11 +81,11 @@ export function loadFailurePatterns(): string {
74
81
  const all = readFailures(paths.failures());
75
82
  if (all.length === 0) return "";
76
83
 
84
+ const projects = readAllProjects();
77
85
  const lines = rankFailures(all, cwd).map((e) => {
78
86
  const label = e.rating ? `[${e.rating}/10]` : "";
79
- const tag = e.cwd === cwd ? "[project]" : "[other]";
80
87
  const text = e.principle || e.context;
81
- return `- ${label} ${tag} ${text}`.trim();
88
+ return ["-", label, projectTag(e.cwd, projects), text].filter(Boolean).join(" ");
82
89
  });
83
90
 
84
91
  return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
@@ -54,28 +54,27 @@ const IDENTITY_DEFAULTS: Identity = {
54
54
 
55
55
  // ── Singleton ──
56
56
 
57
- let cached: PalSettingsData | null = null;
57
+ let cached: { path: string; data: PalSettingsData } | null = null;
58
58
 
59
59
  function settingsPath(): string {
60
60
  return resolve(paths.memory(), "pal-settings.json");
61
61
  }
62
62
 
63
- function load(): PalSettingsData {
64
- if (cached) return cached;
65
- const p = settingsPath();
66
- if (!existsSync(p)) {
67
- cached = {};
68
- return cached;
69
- }
63
+ function readSettingsFile(p: string): PalSettingsData {
64
+ if (!existsSync(p)) return {};
70
65
  try {
71
- cached = JSON.parse(readFileSync(p, "utf-8")) as PalSettingsData;
72
- return cached;
66
+ return JSON.parse(readFileSync(p, "utf-8")) as PalSettingsData;
73
67
  } catch {
74
- cached = {};
75
- return cached;
68
+ return {};
76
69
  }
77
70
  }
78
71
 
72
+ function load(): PalSettingsData {
73
+ const p = settingsPath();
74
+ if (cached?.path !== p) cached = { path: p, data: readSettingsFile(p) };
75
+ return cached.data;
76
+ }
77
+
79
78
  /** Force re-read from disk (useful after writes) */
80
79
  export function reload(): PalSettingsData {
81
80
  cached = null;
@@ -8,17 +8,17 @@
8
8
  * - Generate a summary report
9
9
  *
10
10
  * Usage:
11
- * bun run tool:reflect # Reflect on last 7 days
12
- * bun run tool:reflect -- --month # Reflect on last 30 days
13
- * bun run tool:reflect -- --dry-run # Preview without writing
11
+ * pal cli relationship-reflect # Reflect on last 7 days
12
+ * pal cli relationship-reflect --month # Reflect on last 30 days
13
+ * pal cli relationship-reflect --dry-run # Preview without writing
14
14
  */
15
15
 
16
16
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
17
17
  import { resolve } from "node:path";
18
18
  import { parseArgs } from "node:util";
19
- import { readOpinions, saveOpinion, setLastReflectDate } from "../hooks/lib/opinions";
20
- import { palHome } from "../hooks/lib/paths";
21
- import { emit } from "./lib/emit";
19
+ import { readOpinions, saveOpinion, setLastReflectDate } from "../../hooks/lib/opinions";
20
+ import { palHome } from "../../hooks/lib/paths";
21
+ import { emit } from "../lib/emit";
22
22
  import {
23
23
  consoleLines,
24
24
  formatReport,
@@ -27,7 +27,8 @@ import {
27
27
  loadRatings,
28
28
  planPromotions,
29
29
  reportPath,
30
- } from "./lib/relationship-reflect";
30
+ } from "../lib/relationship-reflect";
31
+ import { scriptArgs } from "../lib/script-args";
31
32
 
32
33
  const HELP = `
33
34
  RelationshipReflect — Periodic reflection + opinion promotion
@@ -36,9 +37,9 @@ Reads recent relationship notes and ratings. Promotes recurring
36
37
  observations (O type) into tracked opinions with confidence scoring.
37
38
 
38
39
  Usage:
39
- bun run tool:reflect Reflect on last 7 days (default)
40
- bun run tool:reflect -- --month Reflect on last 30 days
41
- bun run tool:reflect -- --dry-run Preview without writing
40
+ pal cli relationship-reflect Reflect on last 7 days (default)
41
+ pal cli relationship-reflect --month Reflect on last 30 days
42
+ pal cli relationship-reflect --dry-run Preview without writing
42
43
 
43
44
  Output:
44
45
  - Updates memory/relationship/opinions.json (confidence tracking)
@@ -56,9 +57,9 @@ function saveReport(report: string, period: string): string {
56
57
  return filepath;
57
58
  }
58
59
 
59
- function run() {
60
+ export function run(argv: string[] = scriptArgs()) {
60
61
  const { values } = parseArgs({
61
- args: Bun.argv.slice(2),
62
+ args: argv,
62
63
  options: {
63
64
  month: { type: "boolean" },
64
65
  "dry-run": { type: "boolean" },