portable-agent-layer 0.17.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.
@@ -2,11 +2,11 @@
2
2
 
3
3
  Core: transition from CURRENT STATE to IDEAL STATE using verifiable criteria. Every criterion is atomic, binary testable, and checked off with evidence.
4
4
 
5
- ## The Four Phases
5
+ ## The Five Phases
6
6
 
7
7
  All work happens inside these phases. No work outside the phase structure until the Algorithm completes.
8
8
 
9
- ### ━━━ 👁️ OBSERVE ━━━ 1/4
9
+ ### ━━━ 👁️ OBSERVE ━━━ 1/5
10
10
 
11
11
  Thinking-only. No tool calls except context recovery (Grep/Glob/Read).
12
12
 
@@ -44,7 +44,7 @@ Output:
44
44
  🏹 CAPABILITIES: [list each selected skill/tool and why]
45
45
  ```
46
46
 
47
- ### ━━━ 🧠 PLAN ━━━ 2/4
47
+ ### ━━━ 🧠 PLAN ━━━ 2/5
48
48
 
49
49
  **Pressure test the criteria:**
50
50
 
@@ -59,7 +59,7 @@ Refine criteria if the pressure test reveals gaps. Add criteria for uncovered fa
59
59
  - Decide execution order — what's serial, what can parallelize
60
60
  - If Advanced+ complexity, use EnterPlanMode for user alignment
61
61
 
62
- ### ━━━ ⚡ EXECUTE ━━━ 3/4
62
+ ### ━━━ ⚡ EXECUTE ━━━ 3/5
63
63
 
64
64
  Do the work. Invoke selected capabilities via tool calls.
65
65
 
@@ -67,7 +67,7 @@ Do the work. Invoke selected capabilities via tool calls.
67
67
  - If a criterion can't be met, flag it immediately — don't defer to VERIFY
68
68
  - Make decisions explicit — state why you chose approach A over B
69
69
 
70
- ### ━━━ ✅ VERIFY ━━━ 4/4
70
+ ### ━━━ ✅ VERIFY ━━━ 4/5
71
71
 
72
72
  No rubber-stamping. Each criterion needs specific evidence.
73
73
 
@@ -88,13 +88,30 @@ For EACH criterion:
88
88
 
89
89
  If any criteria failed, fix and re-verify before completing.
90
90
 
91
+ ### ━━━ 📚 LEARN ━━━ 5/5
92
+
93
+ Reflect on the work and capture reusable knowledge. Skip this phase when the work was trivial or purely mechanical.
94
+
95
+ **1. Reflection** (one sentence each):
96
+ - What would I do differently next time?
97
+ - What would a better algorithm have done differently?
98
+
99
+ **2. Wisdom Frame** — if the session produced a genuine, reusable insight:
100
+
101
+ ```bash
102
+ bun ~/.agents/PAL/tools/wisdom-frame.ts --domain <domain> --observation "insight" [--type principle|contextual-rule|anti-pattern|evolution]
103
+ ```
104
+
105
+ Domains: `development`, `workflow`, `communication`, `infrastructure`, `integration`, or any fitting domain.
106
+ Only write if the insight is **genuine and reusable** — not every session produces one. When in doubt, skip.
107
+
91
108
  ## Output Format
92
109
 
93
110
  ```
94
111
  ♻️ ALGORITHM ═══════════════════════════
95
112
  🗒️ TASK: [brief description]
96
113
 
97
- ━━━ 👁️ OBSERVE ━━━ 1/4
114
+ ━━━ 👁️ OBSERVE ━━━ 1/5
98
115
  🔎 REVERSE ENGINEERING:
99
116
  [reverse engineering output]
100
117
 
@@ -103,18 +120,22 @@ If any criteria failed, fix and re-verify before completing.
103
120
 
104
121
  🏹 CAPABILITIES: [selected capabilities]
105
122
 
106
- ━━━ 🧠 PLAN ━━━ 2/4
123
+ ━━━ 🧠 PLAN ━━━ 2/5
107
124
  🧠 RISKS: [risks]
108
125
  🧠 PREMORTEM: [failure modes]
109
126
  📐 APPROACH: [execution plan]
110
127
 
111
- ━━━ ⚡ EXECUTE ━━━ 3/4
128
+ ━━━ ⚡ EXECUTE ━━━ 3/5
112
129
  [work happens here]
113
130
 
114
- ━━━ ✅ VERIFY ━━━ 4/4
131
+ ━━━ ✅ VERIFY ━━━ 4/5
115
132
  ✅ VERIFICATION:
116
133
  [criterion-by-criterion evidence]
117
134
 
118
135
  🔧 CHANGE: [what changed]
119
136
  🗣️ {{IDENTITY_NAME}}: [summary]
137
+
138
+ ━━━ 📚 LEARN ━━━ 5/5
139
+ 🪞 REFLECT: [what I'd do differently]
140
+ 📝 WISDOM: [frame update if genuine insight, or "No new insight"]
120
141
  ```
@@ -19,7 +19,8 @@
19
19
  "Bash(file //*)",
20
20
  "Bash(stat //*)",
21
21
  "Bash(readlink //*)",
22
- "Bash(bun ~/.agents/skills/*/tools/*.ts *)"
22
+ "Bash(bun ~/.agents/skills/*/tools/*.ts *)",
23
+ "Bash(bun run tool:wisdom-frame *)"
23
24
  ]
24
25
  },
25
26
  "hooks": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,8 @@
44
44
  "prepare": "husky",
45
45
  "install:all": "bun run src/cli/index.ts cli install",
46
46
  "uninstall": "bun run src/cli/index.ts cli uninstall",
47
- "tool:analyze": "bun run src/tools/analyze.ts",
47
+ "tool:analyze": "bun run src/tools/agent/analyze.ts",
48
+ "tool:wisdom-frame": "bun run src/tools/agent/wisdom-frame.ts",
48
49
  "tool:reflect": "bun run src/tools/relationship-reflect.ts",
49
50
  "tool:export": "bun run src/tools/export.ts",
50
51
  "tool:import": "bun run src/tools/import.ts",
@@ -81,5 +81,6 @@ export const assets = {
81
81
  agentsMdTemplate: () => pkg("assets", "templates", "AGENTS.md.template"),
82
82
  claudeSettingsTemplate: () => pkg("assets", "templates", "settings.claude.json"),
83
83
  cursorHooksTemplate: () => pkg("assets", "templates", "hooks.cursor.json"),
84
+ agentTools: () => pkg("src", "tools", "agent"),
84
85
  palDocs: () => pkg("assets", "templates", "PAL"),
85
86
  } as const;
@@ -273,10 +273,11 @@ export function copyPalDocs(): number {
273
273
  count++;
274
274
  }
275
275
 
276
- // Symlink ~/.agents/PAL/telos and ~/.agents/PAL/memory → <palHome>/...
276
+ // Symlink ~/.agents/PAL/{telos,memory,tools}source locations
277
277
  const linkType = process.platform === "win32" ? "junction" : "dir";
278
278
  ensureSymlink(resolve(PAL_DOCS_DIR, "telos"), resolve(palHome(), "telos"), linkType);
279
279
  ensureSymlink(resolve(PAL_DOCS_DIR, "memory"), resolve(palHome(), "memory"), linkType);
280
+ ensureSymlink(resolve(PAL_DOCS_DIR, "tools"), assets.agentTools(), linkType);
280
281
 
281
282
  return count;
282
283
  }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Unified Learning Analysis — graduation patterns + ratings summary.
4
+ *
5
+ * Reads failures and session learnings, finds recurring patterns,
6
+ * summarizes ratings, and generates recommendations.
7
+ *
8
+ * Usage: bun run tool:analyze
9
+ */
10
+
11
+ import { parseArgs } from "node:util";
12
+ import { type AnalysisResult, analyze } from "../../hooks/lib/graduation";
13
+
14
+ // ── ANSI Colors ──
15
+
16
+ const c = {
17
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
18
+ dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
19
+ cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
20
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
21
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
22
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
23
+ magenta: (s: string) => `\x1b[35m${s}\x1b[0m`,
24
+ };
25
+
26
+ export function printReport(result: AnalysisResult): void {
27
+ const hasPatterns = result.candidates.length > 0 || result.emerging.length > 0;
28
+ const hasRatings = result.ratings !== null;
29
+
30
+ if (!hasPatterns && !hasRatings) {
31
+ console.log("\n No patterns or ratings data found.\n");
32
+ return;
33
+ }
34
+
35
+ if (result.ratings) {
36
+ const r = result.ratings;
37
+ const avgColor = r.average >= 7 ? c.green : r.average <= 4 ? c.red : c.yellow;
38
+ console.log(
39
+ `\n ${c.bold("Ratings:")} ${avgColor(`${r.average.toFixed(1)}/10`)} avg (${r.total} total)`
40
+ );
41
+ console.log(
42
+ ` ${c.red(`Low (≤4): ${r.low.count}`)} | ${c.green(`High (≥7): ${r.high.count}`)}`
43
+ );
44
+ }
45
+
46
+ if (result.candidates.length > 0) {
47
+ console.log(
48
+ `\n ${c.bold(c.green(`Graduation Report — ${result.candidates.length} pattern(s) detected`))}\n`
49
+ );
50
+ console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
51
+
52
+ for (const candidate of result.candidates) {
53
+ console.log(
54
+ ` ${c.cyan(`[${candidate.domain}]`)} ${c.bold(`${candidate.entries.length}x`)} occurrences`
55
+ );
56
+ console.log("");
57
+
58
+ for (const entry of candidate.entries) {
59
+ const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
60
+ const tag =
61
+ sourceType === "failure"
62
+ ? c.red(`[${sourceType}]`)
63
+ : c.yellow(`[${sourceType}]`);
64
+ console.log(
65
+ ` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 100)}`
66
+ );
67
+ }
68
+
69
+ console.log(`\n ${c.dim("Files:")}`);
70
+ for (const entry of candidate.entries) {
71
+ console.log(` ${c.dim(entry.path)}`);
72
+ }
73
+
74
+ console.log("");
75
+ console.log(
76
+ ` Target frame: ${c.magenta(`memory/wisdom/frames/${candidate.domain}.md`)}`
77
+ );
78
+ console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
79
+ }
80
+ }
81
+
82
+ if (result.emerging.length > 0) {
83
+ console.log(` ${c.bold(c.yellow("Emerging (2x — one more to graduate)"))}\n`);
84
+ for (const group of result.emerging) {
85
+ console.log(
86
+ ` ${c.cyan(`[${group.domain}]`)} ${c.bold(`${group.entries.length}x`)}`
87
+ );
88
+ for (const entry of group.entries) {
89
+ const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
90
+ const tag =
91
+ sourceType === "failure"
92
+ ? c.red(`[${sourceType}]`)
93
+ : c.yellow(`[${sourceType}]`);
94
+ console.log(
95
+ ` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 80)}`
96
+ );
97
+ }
98
+ console.log(" Files:");
99
+ for (const entry of group.entries) {
100
+ console.log(` ${c.dim(entry.path)}`);
101
+ }
102
+ console.log("");
103
+ }
104
+ }
105
+
106
+ if (result.recommendations.length > 0) {
107
+ console.log(` ${c.bold("Recommendations:")}\n`);
108
+ for (const rec of result.recommendations) {
109
+ console.log(` ${rec}`);
110
+ }
111
+ console.log("");
112
+ }
113
+
114
+ if (result.candidates.length > 0) {
115
+ console.log(` To crystallize: add a line to the wisdom frame file.`);
116
+ console.log(` Format: ${c.green("- Your principle here [CRYSTAL: 85%]")}\n`);
117
+ }
118
+ }
119
+
120
+ async function run() {
121
+ const { values } = parseArgs({
122
+ args: Bun.argv.slice(2),
123
+ options: {
124
+ help: { type: "boolean", short: "h" },
125
+ actionable: { type: "boolean", short: "a" },
126
+ },
127
+ });
128
+
129
+ if (values.help) {
130
+ console.log(`
131
+ PAL Learning Analysis — unified graduation + ratings report
132
+
133
+ Reads all captured failures (rating ≤3) and session learnings,
134
+ groups recurring patterns via Dice similarity on context text,
135
+ and summarizes rating trends.
136
+
137
+ Sections:
138
+ Ratings Overall average, low/high counts
139
+ Graduation Patterns with 3+ occurrences → ready to crystallize
140
+ Emerging Patterns with 2 occurrences → one more to graduate
141
+
142
+ Flags:
143
+ --actionable, -a Generate actionable recommendations via Haiku inference
144
+
145
+ To crystallize a graduated pattern, add it to the target wisdom frame:
146
+ - Your principle here [CRYSTAL: 85%]
147
+
148
+ Usage: bun run tool:analyze [--actionable] [--help]
149
+ `);
150
+ process.exit(0);
151
+ }
152
+
153
+ const result = await analyze({ actionable: values.actionable });
154
+ printReport(result);
155
+ }
156
+
157
+ if (import.meta.main) run();
@@ -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();