portable-agent-layer 0.16.0 → 0.18.0

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.
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * WisdomFrameUpdater — Update wisdom frames with new observations.
4
+ *
5
+ * Takes a domain and observation, updates the appropriate frame file.
6
+ * Creates the frame if it doesn't exist. Tracks observation count and
7
+ * evolution log. Principles are marked [CRYSTAL: N%] manually when
8
+ * confidence is high enough.
9
+ *
10
+ * Usage:
11
+ * bun run tool:wisdom-frame --domain communication --observation "prefers bullet points"
12
+ * bun run tool:wisdom-frame --domain development --observation "refactoring without tests caused regressions" --type anti-pattern
13
+ * bun run tool:wisdom-frame --domain workflow --observation "always run type-check after edits" --type principle
14
+ *
15
+ * Types: principle, contextual-rule, anti-pattern, evolution (default)
16
+ */
17
+
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { resolve } from "node:path";
20
+ import { parseArgs } from "node:util";
21
+ import { paths } from "../../hooks/lib/paths";
22
+
23
+ // ── Types ──
24
+
25
+ type ObservationType = "principle" | "contextual-rule" | "anti-pattern" | "evolution";
26
+
27
+ interface UpdateResult {
28
+ success: boolean;
29
+ domain: string;
30
+ type: ObservationType;
31
+ message: string;
32
+ framePath: string;
33
+ }
34
+
35
+ // ── Helpers ──
36
+
37
+ function date(): string {
38
+ return new Date().toISOString().slice(0, 10);
39
+ }
40
+
41
+ function parseObservationCount(content: string): number {
42
+ const match = content.match(/\*\*Observation Count:\*\*\s*(\d+)/);
43
+ return match ? parseInt(match[1], 10) : 0;
44
+ }
45
+
46
+ function incrementCount(content: string): string {
47
+ const current = parseObservationCount(content);
48
+ return content.replace(/(\*\*Observation Count:\*\*\s*)\d+/, `$1${current + 1}`);
49
+ }
50
+
51
+ function updateDate(content: string): string {
52
+ return content.replace(/(\*\*Last Updated:\*\*\s*)\S+/, `$1${date()}`);
53
+ }
54
+
55
+ function appendToSection(
56
+ content: string,
57
+ sectionHeader: string,
58
+ entry: string,
59
+ fallbackBefore?: string
60
+ ): string {
61
+ const idx = content.indexOf(sectionHeader);
62
+
63
+ if (idx === -1) {
64
+ // Section doesn't exist — insert before fallback or at end
65
+ const insertAt = fallbackBefore ? content.indexOf(fallbackBefore) : -1;
66
+ const pos = insertAt !== -1 ? insertAt : content.length;
67
+ return `${content.slice(0, pos)}${sectionHeader}\n\n${entry}\n\n${content.slice(pos)}`;
68
+ }
69
+
70
+ // Find end of section (next ## or EOF)
71
+ const afterSection = content.slice(idx + sectionHeader.length);
72
+ const nextSection = afterSection.indexOf("\n## ");
73
+ const insertPoint =
74
+ nextSection === -1 ? content.length : idx + sectionHeader.length + nextSection;
75
+
76
+ return `${content.slice(0, insertPoint)}\n${entry}${content.slice(insertPoint)}`;
77
+ }
78
+
79
+ // ── Core Update ──
80
+
81
+ export function updateFrame(
82
+ domain: string,
83
+ observation: string,
84
+ type: ObservationType = "evolution"
85
+ ): UpdateResult {
86
+ const framesDir = paths.wisdom();
87
+ const framePath = resolve(framesDir, `${domain}.md`);
88
+
89
+ // Create frame if it doesn't exist
90
+ if (!existsSync(framePath)) {
91
+ mkdirSync(framesDir, { recursive: true });
92
+
93
+ const content = `# Frame: ${domain.charAt(0).toUpperCase() + domain.slice(1)}
94
+
95
+ ## Meta
96
+ - **Domain:** ${domain}
97
+ - **Observation Count:** 1
98
+ - **Last Updated:** ${date()}
99
+
100
+ ---
101
+
102
+ ## Core Principles
103
+
104
+ *No crystallized principles yet. Observations accumulating.*
105
+
106
+ ---
107
+
108
+ ## Contextual Rules
109
+
110
+ ${type === "contextual-rule" ? `- ${observation} (${date()})` : "*None yet.*"}
111
+
112
+ ---
113
+
114
+ ## Anti-Patterns
115
+
116
+ ${type === "anti-pattern" ? `### ${observation}\n- **Severity:** Medium\n- **Frequency:** Observed` : "*None yet.*"}
117
+
118
+ ---
119
+
120
+ ## Evolution Log
121
+ - ${date()}: Frame created — ${observation}
122
+ `;
123
+
124
+ writeFileSync(framePath, content);
125
+ return {
126
+ success: true,
127
+ domain,
128
+ type,
129
+ message: `Created new frame "${domain}" with initial observation`,
130
+ framePath,
131
+ };
132
+ }
133
+
134
+ // Update existing frame
135
+ let content = readFileSync(framePath, "utf-8");
136
+ content = incrementCount(content);
137
+ content = updateDate(content);
138
+
139
+ const evolutionEntry = `- ${date()}: ${observation}`;
140
+
141
+ switch (type) {
142
+ case "anti-pattern":
143
+ content = appendToSection(
144
+ content,
145
+ "## Anti-Patterns",
146
+ `\n### ${observation}\n- **Severity:** Medium\n- **Frequency:** Observed`,
147
+ "## Evolution Log"
148
+ );
149
+ content = appendToSection(content, "## Evolution Log", evolutionEntry);
150
+ break;
151
+
152
+ case "contextual-rule":
153
+ content = appendToSection(
154
+ content,
155
+ "## Contextual Rules",
156
+ `- ${observation} (${date()})`,
157
+ "## Anti-Patterns"
158
+ );
159
+ content = appendToSection(content, "## Evolution Log", evolutionEntry);
160
+ break;
161
+
162
+ case "principle":
163
+ // Principles logged for manual crystallization — don't auto-add to Core Principles
164
+ content = appendToSection(
165
+ content,
166
+ "## Evolution Log",
167
+ `- ${date()}: Principle candidate — ${observation}`
168
+ );
169
+ break;
170
+
171
+ case "evolution":
172
+ default:
173
+ content = appendToSection(content, "## Evolution Log", evolutionEntry);
174
+ break;
175
+ }
176
+
177
+ writeFileSync(framePath, content);
178
+
179
+ return {
180
+ success: true,
181
+ domain,
182
+ type,
183
+ message: `Updated "${domain}" frame with ${type}: ${observation}`,
184
+ framePath,
185
+ };
186
+ }
187
+
188
+ // ── CLI ──
189
+
190
+ function run() {
191
+ const { values } = parseArgs({
192
+ args: Bun.argv.slice(2),
193
+ options: {
194
+ domain: { type: "string", short: "d" },
195
+ observation: { type: "string", short: "o" },
196
+ type: { type: "string", short: "t" },
197
+ help: { type: "boolean", short: "h" },
198
+ },
199
+ });
200
+
201
+ if (values.help) {
202
+ console.log(`
203
+ WisdomFrameUpdater — Update wisdom frames with observations
204
+
205
+ Usage:
206
+ bun run tool:wisdom-frame --domain <domain> --observation "text" [--type <type>]
207
+
208
+ Domains:
209
+ development, workflow, communication, infrastructure, integration, or any custom domain
210
+
211
+ Types:
212
+ principle High-confidence pattern (logged for manual crystallization)
213
+ contextual-rule Context-specific behavioral rule
214
+ anti-pattern Something to avoid
215
+ evolution General observation (default)
216
+
217
+ Examples:
218
+ bun run tool:wisdom-frame -d workflow -o "always run type-check after edits"
219
+ bun run tool:wisdom-frame -d development -o "mocking DB hides migration bugs" -t anti-pattern
220
+ bun run tool:wisdom-frame -d communication -o "user prefers terse summaries" -t principle
221
+ `);
222
+ process.exit(0);
223
+ }
224
+
225
+ if (!values.domain || !values.observation) {
226
+ console.error("Required: --domain and --observation");
227
+ process.exit(1);
228
+ }
229
+
230
+ const cliType = (values.type || "evolution") as ObservationType;
231
+ const result = updateFrame(values.domain, values.observation, cliType);
232
+ console.log(JSON.stringify(result, null, 2));
233
+ }
234
+
235
+ if (import.meta.main) run();
@@ -10,25 +10,31 @@ import { resolve } from "node:path";
10
10
  import { collectExportFiles, exportZip, timestamp } from "../hooks/lib/export";
11
11
  import { palHome } from "../hooks/lib/paths";
12
12
 
13
- const args = process.argv.slice(2);
14
- const dryRun = args.includes("--dry-run");
15
- const pathArg = args.find((a) => a !== "--dry-run");
13
+ export { collectExportFiles, exportZip };
16
14
 
17
- const outputPath = pathArg || resolve(palHome(), `pal-export-${timestamp()}.zip`);
15
+ function run() {
16
+ const args = process.argv.slice(2);
17
+ const dryRun = args.includes("--dry-run");
18
+ const pathArg = args.find((a) => a !== "--dry-run");
18
19
 
19
- if (dryRun) {
20
- const files = collectExportFiles();
21
- if (files.length === 0) {
22
- console.log("Nothing to export — no gitignored personal files found.");
23
- } else {
24
- console.log(`Would export ${files.length} files → ${outputPath}\n`);
25
- for (const f of files) console.log(` ${f}`);
26
- }
27
- } else {
28
- const count = exportZip(outputPath);
29
- if (count === 0) {
30
- console.log("Nothing to export — no gitignored personal files found.");
20
+ const outputPath = pathArg || resolve(palHome(), `pal-export-${timestamp()}.zip`);
21
+
22
+ if (dryRun) {
23
+ const files = collectExportFiles();
24
+ if (files.length === 0) {
25
+ console.log("Nothing to export no gitignored personal files found.");
26
+ } else {
27
+ console.log(`Would export ${files.length} files → ${outputPath}\n`);
28
+ for (const f of files) console.log(` ${f}`);
29
+ }
31
30
  } else {
32
- console.log(`Exported ${count} files → ${outputPath}`);
31
+ const count = exportZip(outputPath);
32
+ if (count === 0) {
33
+ console.log("Nothing to export — no gitignored personal files found.");
34
+ } else {
35
+ console.log(`Exported ${count} files → ${outputPath}`);
36
+ }
33
37
  }
34
38
  }
39
+
40
+ if (import.meta.main) run();
@@ -13,53 +13,21 @@ import { createInterface } from "node:readline";
13
13
  import AdmZip from "adm-zip";
14
14
  import { palHome } from "../hooks/lib/paths";
15
15
 
16
- const repoRoot = palHome();
17
- const args = process.argv.slice(2);
18
- const dryRun = args.includes("--dry-run");
19
- const pathArg = args.find((a) => a !== "--dry-run");
16
+ export function findLatestExport(root: string): string | null {
17
+ const candidates: string[] = [];
20
18
 
21
- async function confirm(message: string): Promise<boolean> {
22
- const rl = createInterface({ input: process.stdin, output: process.stdout });
23
- return new Promise((res) => {
24
- rl.question(`${message} [y/N] `, (answer) => {
25
- rl.close();
26
- res(answer.trim().toLowerCase() === "y");
27
- });
28
- });
29
- }
30
-
31
- function findLatestExport(): string | null {
32
- const files = readdirSync(repoRoot)
33
- .filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
34
- .sort()
35
- .reverse();
36
-
37
- // Also check backups/
38
19
  try {
39
- const backupDir = resolve(repoRoot, "backups");
40
- const backups = readdirSync(backupDir)
41
- .filter(
42
- (f) =>
43
- (f.startsWith("pal-export-") || f.startsWith("pal-backup-")) &&
44
- f.endsWith(".zip")
45
- )
46
- .map((f) => ({ name: f, path: resolve(backupDir, f) }))
47
- .sort((a, b) => b.name.localeCompare(a.name));
48
- if (backups.length > 0) files.push(backups[0].name);
20
+ candidates.push(
21
+ ...readdirSync(root)
22
+ .filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
23
+ .map((f) => resolve(root, f))
24
+ );
49
25
  } catch {
50
- // No backups dir
26
+ /* empty */
51
27
  }
52
28
 
53
- if (files.length === 0) return null;
54
-
55
- // Find the most recent by mtime across both locations
56
- const candidates = [
57
- ...readdirSync(repoRoot)
58
- .filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
59
- .map((f) => resolve(repoRoot, f)),
60
- ];
61
29
  try {
62
- const backupDir = resolve(repoRoot, "backups");
30
+ const backupDir = resolve(root, "backups");
63
31
  candidates.push(
64
32
  ...readdirSync(backupDir)
65
33
  .filter(
@@ -70,54 +38,74 @@ function findLatestExport(): string | null {
70
38
  .map((f) => resolve(backupDir, f))
71
39
  );
72
40
  } catch {
73
- // No backups dir
41
+ /* empty */
74
42
  }
75
43
 
76
44
  if (candidates.length === 0) return null;
77
45
  return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
78
46
  }
79
47
 
80
- // Resolve zip path
81
- let zipPath: string;
48
+ export function importZip(zipPath: string, targetDir: string, dryRun: boolean): number {
49
+ const zip = new AdmZip(zipPath);
50
+ const entries = zip.getEntries();
82
51
 
83
- if (pathArg) {
84
- zipPath = resolve(pathArg);
85
- } else {
86
- const latest = findLatestExport();
87
- if (!latest) {
88
- console.error(
89
- "No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
90
- );
91
- process.exit(1);
52
+ if (entries.length === 0) {
53
+ console.log("Archive is empty — nothing to import.");
54
+ return 0;
92
55
  }
93
- console.log(`Found: ${latest}`);
94
- const zip = new AdmZip(latest);
95
- const entries = zip.getEntries();
96
- console.log(
97
- `Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
98
- );
99
56
 
100
- if (!(await confirm("Import this file?"))) {
101
- console.log("Cancelled.");
102
- process.exit(0);
57
+ if (dryRun) {
58
+ console.log(`Would import ${entries.length} files → ${targetDir}\n`);
59
+ for (const e of entries) console.log(` ${e.entryName}`);
60
+ return entries.length;
103
61
  }
104
- zipPath = latest;
62
+
63
+ zip.extractAllTo(targetDir, true);
64
+ console.log(`Imported ${entries.length} files → ${targetDir}`);
65
+ console.log("\nRun 'bun run install:all' to re-create symlinks and hooks.");
66
+ return entries.length;
105
67
  }
106
68
 
107
- // Import
108
- const zip = new AdmZip(zipPath);
109
- const entries = zip.getEntries();
69
+ async function run() {
70
+ const repoRoot = palHome();
71
+ const args = process.argv.slice(2);
72
+ const dryRun = args.includes("--dry-run");
73
+ const pathArg = args.find((a) => a !== "--dry-run");
110
74
 
111
- if (entries.length === 0) {
112
- console.log("Archive is empty — nothing to import.");
113
- process.exit(0);
114
- }
75
+ let zipPath: string;
115
76
 
116
- if (dryRun) {
117
- console.log(`Would import ${entries.length} files → ${repoRoot}\n`);
118
- for (const e of entries) console.log(` ${e.entryName}`);
119
- } else {
120
- zip.extractAllTo(repoRoot, true);
121
- console.log(`Imported ${entries.length} files → ${repoRoot}`);
122
- console.log("\nRun 'bun run install:all' to re-create symlinks and hooks.");
77
+ if (pathArg) {
78
+ zipPath = resolve(pathArg);
79
+ } else {
80
+ const latest = findLatestExport(repoRoot);
81
+ if (!latest) {
82
+ console.error(
83
+ "No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
84
+ );
85
+ process.exit(1);
86
+ }
87
+ console.log(`Found: ${latest}`);
88
+ const zip = new AdmZip(latest);
89
+ const entries = zip.getEntries();
90
+ console.log(
91
+ `Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
92
+ );
93
+
94
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
95
+ const answer = await new Promise<string>((res) => {
96
+ rl.question("Import this file? [y/N] ", (a) => {
97
+ rl.close();
98
+ res(a);
99
+ });
100
+ });
101
+ if (answer.trim().toLowerCase() !== "y") {
102
+ console.log("Cancelled.");
103
+ process.exit(0);
104
+ }
105
+ zipPath = latest;
106
+ }
107
+
108
+ importZip(zipPath, repoRoot, dryRun);
123
109
  }
110
+
111
+ if (import.meta.main) run();