portable-agent-layer 0.2.0 → 0.3.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.
- package/README.md +116 -0
- package/package.json +1 -2
- package/src/cli/index.ts +285 -76
- package/src/hooks/StopOrchestrator.ts +12 -0
- package/src/hooks/handlers/readme-sync.ts +61 -0
- package/src/hooks/lib/claude-md.ts +2 -1
- package/src/hooks/lib/readme-sync.ts +129 -0
- package/src/targets/lib.ts +1 -0
- package/bin/pal +0 -24
- package/bin/pal.bat +0 -8
- package/bin/pal.ps1 +0 -30
package/README.md
CHANGED
|
@@ -26,6 +26,116 @@ With PAL, you can:
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
### Prerequisites
|
|
32
|
+
|
|
33
|
+
> **Bun is required.** PAL is built on [Bun](https://bun.sh) and will not work with Node.js or other runtimes. Install it with `curl -fsSL https://bun.sh/install | bash`.
|
|
34
|
+
|
|
35
|
+
- [Bun](https://bun.sh) >= 1.3.0
|
|
36
|
+
- At least one of: [Claude Code](https://claude.ai/code) or [opencode](https://opencode.ai)
|
|
37
|
+
|
|
38
|
+
### Package mode (recommended)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bun add -g portable-agent-layer
|
|
42
|
+
pal cli init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Repo mode (for development / contributors)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/kovrichard/portable-agent-layer.git
|
|
49
|
+
cd portable-agent-layer
|
|
50
|
+
bun install
|
|
51
|
+
bun run install:all
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
In repo mode, add an alias to your shell profile:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
alias pal="bun run ~/path/to/portable-agent-layer/src/cli/index.ts"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pal cli init # scaffold home, install hooks for all targets
|
|
66
|
+
pal # start a Claude session (with session summary on exit)
|
|
67
|
+
pal cli status # check your setup
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Commands
|
|
73
|
+
|
|
74
|
+
| Command | Description |
|
|
75
|
+
|---------|-------------|
|
|
76
|
+
| `pal` | Start a Claude session with session summary on exit |
|
|
77
|
+
| `pal cli init` | Scaffold PAL home directory and install hooks |
|
|
78
|
+
| `pal cli install` | Register hooks/skills for targets |
|
|
79
|
+
| `pal cli uninstall` | Remove hooks/skills for targets |
|
|
80
|
+
| `pal cli export` | Export user state (telos, memory) to a zip |
|
|
81
|
+
| `pal cli import` | Import user state from a zip |
|
|
82
|
+
| `pal cli status` | Show current PAL configuration |
|
|
83
|
+
| `pal cli doctor` | Check prerequisites and system health |
|
|
84
|
+
|
|
85
|
+
### Target flags
|
|
86
|
+
|
|
87
|
+
`init`, `install`, and `uninstall` accept target flags:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pal cli install --claude # Claude Code only
|
|
91
|
+
pal cli install --opencode # opencode only
|
|
92
|
+
pal cli install # both (default)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Environment variables
|
|
98
|
+
|
|
99
|
+
### Required
|
|
100
|
+
|
|
101
|
+
| Variable | Description |
|
|
102
|
+
|----------|-------------|
|
|
103
|
+
| `ANTHROPIC_API_KEY` | Required for PAL's hook inference (sentiment analysis, session naming). Uses Haiku for low-cost background calls. |
|
|
104
|
+
|
|
105
|
+
### Optional
|
|
106
|
+
|
|
107
|
+
| Variable | Description |
|
|
108
|
+
|----------|-------------|
|
|
109
|
+
| `GEMINI_API_KEY` | For YouTube video analysis skill |
|
|
110
|
+
| `PAL_HOME` | Override user state directory (default: `~/.pal` or repo root) |
|
|
111
|
+
| `PAL_PKG` | Override package root |
|
|
112
|
+
| `PAL_CLAUDE_DIR` | Override Claude config dir (default: `~/.claude`) |
|
|
113
|
+
| `PAL_OPENCODE_DIR` | Override opencode config dir (default: `~/.config/opencode`) |
|
|
114
|
+
| `PAL_AGENTS_DIR` | Override agents dir (default: `~/.agents`) |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Skills
|
|
119
|
+
|
|
120
|
+
PAL ships with built-in skills that extend your agent's capabilities:
|
|
121
|
+
|
|
122
|
+
| Skill | Description |
|
|
123
|
+
|-------|-------------|
|
|
124
|
+
| `analyze-pdf` | Download and analyze PDF files |
|
|
125
|
+
| `analyze-youtube` | Analyze YouTube videos using Gemini |
|
|
126
|
+
| `council` | Multi-perspective parallel debate on decisions |
|
|
127
|
+
| `create-skill` | Scaffold a new skill from a description |
|
|
128
|
+
| `extract-entities` | Extract people and companies from content |
|
|
129
|
+
| `extract-wisdom` | Extract structured insights from content |
|
|
130
|
+
| `first-principles` | Break down problems to fundamentals |
|
|
131
|
+
| `fyzz-chat-api` | Query Fyzz Chat conversations via API |
|
|
132
|
+
| `reflect` | Diagnose why a PAL behavior didn't trigger |
|
|
133
|
+
| `research` | Multi-agent parallel research |
|
|
134
|
+
| `review` | Security-focused code review |
|
|
135
|
+
| `summarize` | Structured summarization |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
29
139
|
## Core idea
|
|
30
140
|
|
|
31
141
|
PAL stands for **Portable Agent Layer**.
|
|
@@ -78,3 +188,9 @@ PAL is for people who want:
|
|
|
78
188
|
- to move between machines without rebuilding everything
|
|
79
189
|
- a durable way to store and reuse context
|
|
80
190
|
- an open foundation for portable agent workflows
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"src/",
|
|
11
11
|
"assets/",
|
|
12
|
-
"bin/",
|
|
13
12
|
"README.md",
|
|
14
13
|
"LICENSE"
|
|
15
14
|
],
|
package/src/cli/index.ts
CHANGED
|
@@ -2,23 +2,168 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* PAL CLI — Portable Agent Layer
|
|
4
4
|
*
|
|
5
|
-
* Usage:
|
|
5
|
+
* Usage:
|
|
6
|
+
* pal [claude-args...] Start a Claude session with session summary on exit
|
|
7
|
+
* pal cli <command> [options] Admin commands
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
* init
|
|
9
|
-
* install
|
|
10
|
-
* uninstall
|
|
11
|
-
* export
|
|
12
|
-
* import
|
|
13
|
-
* status
|
|
9
|
+
* Admin commands (pal cli ...):
|
|
10
|
+
* init Scaffold PAL home, install hooks for all targets
|
|
11
|
+
* install [--claude] [--opencode] Register hooks/skills for targets
|
|
12
|
+
* uninstall [--claude] [--opencode] Remove hooks/skills for targets
|
|
13
|
+
* export [path] [--dry-run] Export user state to zip
|
|
14
|
+
* import [path] [--dry-run] Import user state from zip
|
|
15
|
+
* status Show current PAL configuration
|
|
16
|
+
* doctor Check prerequisites and system health
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
|
-
import {
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
17
22
|
import { resolve } from "node:path";
|
|
18
23
|
import { palHome, palPkg, platform } from "../hooks/lib/paths";
|
|
19
24
|
import { log } from "../targets/lib";
|
|
20
25
|
|
|
21
|
-
const
|
|
26
|
+
const allArgs = process.argv.slice(2);
|
|
27
|
+
|
|
28
|
+
// ── Route: pal cli <command> or pal [claude-args] ──
|
|
29
|
+
|
|
30
|
+
if (allArgs[0] === "cli") {
|
|
31
|
+
const [, command, ...args] = allArgs;
|
|
32
|
+
await runCli(command, args);
|
|
33
|
+
} else if (allArgs[0] === "--help" || allArgs[0] === "-h" || allArgs[0] === "help") {
|
|
34
|
+
showHelp();
|
|
35
|
+
} else {
|
|
36
|
+
await session(allArgs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Session: pal [args] ──
|
|
40
|
+
|
|
41
|
+
interface ToolCheck {
|
|
42
|
+
name: string;
|
|
43
|
+
available: boolean;
|
|
44
|
+
version?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkTool(cmd: string, versionArgs: string[] = ["--version"]): ToolCheck {
|
|
48
|
+
try {
|
|
49
|
+
const result = spawnSync(cmd, versionArgs, {
|
|
50
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
+
shell: true,
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
});
|
|
54
|
+
if (result.status === 0) {
|
|
55
|
+
const version = (result.stdout?.toString() || "").trim().split("\n")[0];
|
|
56
|
+
return { name: cmd, available: true, version };
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// not found
|
|
60
|
+
}
|
|
61
|
+
return { name: cmd, available: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectAgent(): string | null {
|
|
65
|
+
if (checkTool("claude").available) return "claude";
|
|
66
|
+
if (checkTool("opencode").available) return "opencode";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function session(sessionArgs: string[]) {
|
|
71
|
+
const agent = detectAgent();
|
|
72
|
+
if (!agent) {
|
|
73
|
+
log.error("No supported agent found. Install Claude Code or opencode.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = spawnSync(agent, sessionArgs, {
|
|
78
|
+
stdio: "inherit",
|
|
79
|
+
shell: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const exitCode = result.status ?? 1;
|
|
83
|
+
|
|
84
|
+
// Session summary (Claude only)
|
|
85
|
+
if (agent !== "claude") process.exit(exitCode);
|
|
86
|
+
try {
|
|
87
|
+
const projectsDir = resolve(homedir(), ".claude", "projects");
|
|
88
|
+
if (!existsSync(projectsDir)) process.exit(exitCode);
|
|
89
|
+
|
|
90
|
+
// Find most recently modified .jsonl file
|
|
91
|
+
let latestFile = "";
|
|
92
|
+
let latestMtime = 0;
|
|
93
|
+
|
|
94
|
+
for (const project of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
95
|
+
if (!project.isDirectory()) continue;
|
|
96
|
+
const dir = resolve(projectsDir, project.name);
|
|
97
|
+
for (const file of readdirSync(dir)) {
|
|
98
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
99
|
+
const filepath = resolve(dir, file);
|
|
100
|
+
const { mtimeMs } = statSync(filepath);
|
|
101
|
+
if (mtimeMs > latestMtime) {
|
|
102
|
+
latestMtime = mtimeMs;
|
|
103
|
+
latestFile = filepath;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (latestFile) {
|
|
109
|
+
const content = readFileSync(latestFile, "utf-8").trim();
|
|
110
|
+
const lastLine = content.split("\n").pop();
|
|
111
|
+
if (lastLine) {
|
|
112
|
+
const sessionId = JSON.parse(lastLine).sessionId;
|
|
113
|
+
if (sessionId) {
|
|
114
|
+
const summaryScript = resolve(palPkg(), "src", "tools", "session-summary.ts");
|
|
115
|
+
spawnSync("bun", ["run", summaryScript, "--", "--session", sessionId], {
|
|
116
|
+
stdio: "inherit",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Silently ignore summary errors
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
process.exit(exitCode);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── CLI dispatcher ──
|
|
129
|
+
|
|
130
|
+
async function runCli(command: string | undefined, args: string[]) {
|
|
131
|
+
switch (command) {
|
|
132
|
+
case "init":
|
|
133
|
+
await init(args);
|
|
134
|
+
break;
|
|
135
|
+
case "install":
|
|
136
|
+
banner();
|
|
137
|
+
await install(resolveTargets(args));
|
|
138
|
+
break;
|
|
139
|
+
case "uninstall":
|
|
140
|
+
await uninstall(args);
|
|
141
|
+
break;
|
|
142
|
+
case "export":
|
|
143
|
+
await exportState(args);
|
|
144
|
+
break;
|
|
145
|
+
case "import":
|
|
146
|
+
await importState(args);
|
|
147
|
+
break;
|
|
148
|
+
case "status":
|
|
149
|
+
await status();
|
|
150
|
+
break;
|
|
151
|
+
case "doctor":
|
|
152
|
+
doctor();
|
|
153
|
+
break;
|
|
154
|
+
case "--help":
|
|
155
|
+
case "-h":
|
|
156
|
+
case "help":
|
|
157
|
+
showHelp();
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
if (command) log.error(`Unknown command: ${command}`);
|
|
161
|
+
showHelp();
|
|
162
|
+
process.exit(command ? 1 : 0);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Helpers ──
|
|
22
167
|
|
|
23
168
|
function banner() {
|
|
24
169
|
console.log("");
|
|
@@ -31,15 +176,19 @@ function banner() {
|
|
|
31
176
|
|
|
32
177
|
function showHelp() {
|
|
33
178
|
console.log(`
|
|
34
|
-
Usage:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
179
|
+
Usage:
|
|
180
|
+
pal [claude-args...] Start a Claude session
|
|
181
|
+
pal cli <command> [options] Admin commands
|
|
182
|
+
|
|
183
|
+
Admin commands:
|
|
184
|
+
pal cli init [--claude] [--opencode] Scaffold and install (default: all)
|
|
185
|
+
pal cli install [--claude] [--opencode] Register hooks for targets
|
|
186
|
+
pal cli uninstall [--claude] [--opencode] Remove hooks for targets
|
|
187
|
+
pal cli export [path] [--dry-run] Export state to zip
|
|
188
|
+
pal cli import [path] [--dry-run] Import state from zip
|
|
189
|
+
pal cli status Show PAL configuration
|
|
190
|
+
pal cli doctor Check prerequisites and health
|
|
191
|
+
|
|
43
192
|
Environment:
|
|
44
193
|
PAL_HOME Override user state directory (default: ~/.pal or repo root)
|
|
45
194
|
PAL_PKG Override package root
|
|
@@ -53,8 +202,6 @@ function parseTargets(args: string[]): {
|
|
|
53
202
|
claude: boolean;
|
|
54
203
|
opencode: boolean;
|
|
55
204
|
} {
|
|
56
|
-
if (args.length === 0) return { claude: true, opencode: true };
|
|
57
|
-
|
|
58
205
|
let claude = false;
|
|
59
206
|
let opencode = false;
|
|
60
207
|
for (const arg of args) {
|
|
@@ -65,24 +212,125 @@ function parseTargets(args: string[]): {
|
|
|
65
212
|
opencode = true;
|
|
66
213
|
}
|
|
67
214
|
}
|
|
68
|
-
// If no target flags, default to all
|
|
69
215
|
if (!claude && !opencode) return { claude: true, opencode: true };
|
|
70
216
|
return { claude, opencode };
|
|
71
217
|
}
|
|
72
218
|
|
|
219
|
+
/** Resolve targets against available agents. Errors if explicitly requested but missing. */
|
|
220
|
+
function resolveTargets(
|
|
221
|
+
args: string[],
|
|
222
|
+
health?: DoctorResult
|
|
223
|
+
): { claude: boolean; opencode: boolean } {
|
|
224
|
+
const requested = parseTargets(args);
|
|
225
|
+
const h = health || doctor(true);
|
|
226
|
+
const explicit = args.some(
|
|
227
|
+
(a) => a === "--claude" || a === "--opencode" || a === "--all"
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (explicit) {
|
|
231
|
+
// User explicitly requested — error if not available
|
|
232
|
+
if (requested.claude && !h.claude.available) {
|
|
233
|
+
log.error("Claude Code is not installed. Run 'pal cli doctor' for details.");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (requested.opencode && !h.opencode.available) {
|
|
237
|
+
log.error("opencode is not installed. Run 'pal cli doctor' for details.");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
return requested;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Default (no flags) — install for available agents only
|
|
244
|
+
const targets = {
|
|
245
|
+
claude: h.claude.available,
|
|
246
|
+
opencode: h.opencode.available,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (!targets.claude) log.info("Skipping Claude Code (not installed)");
|
|
250
|
+
if (!targets.opencode) log.info("Skipping opencode (not installed)");
|
|
251
|
+
|
|
252
|
+
return targets;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Doctor ──
|
|
256
|
+
|
|
257
|
+
interface DoctorResult {
|
|
258
|
+
bun: ToolCheck;
|
|
259
|
+
claude: ToolCheck;
|
|
260
|
+
opencode: ToolCheck;
|
|
261
|
+
hasAgent: boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function doctor(silent = false): DoctorResult {
|
|
265
|
+
// Allow CI/tests to skip agent detection
|
|
266
|
+
if (process.env.PAL_SKIP_DOCTOR === "1") {
|
|
267
|
+
return {
|
|
268
|
+
bun: { name: "bun", available: true, version: Bun.version },
|
|
269
|
+
claude: { name: "claude", available: true },
|
|
270
|
+
opencode: { name: "opencode", available: true },
|
|
271
|
+
hasAgent: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const bun = { name: "bun", available: true, version: Bun.version };
|
|
276
|
+
const claude = checkTool("claude");
|
|
277
|
+
const opencode = checkTool("opencode");
|
|
278
|
+
const hasAgent = claude.available || opencode.available;
|
|
279
|
+
|
|
280
|
+
const home = palHome();
|
|
281
|
+
const isRepo = existsSync(resolve(palPkg(), ".palroot"));
|
|
282
|
+
const telosCount = (() => {
|
|
283
|
+
try {
|
|
284
|
+
return readdirSync(resolve(home, "telos")).filter((f) => f.endsWith(".md")).length;
|
|
285
|
+
} catch {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
|
|
290
|
+
if (!silent) {
|
|
291
|
+
const ok = (msg: string) => log.info(` \u2713 ${msg}`);
|
|
292
|
+
const fail = (msg: string) => log.warn(` \u2717 ${msg}`);
|
|
293
|
+
|
|
294
|
+
console.log("");
|
|
295
|
+
log.info("Doctor");
|
|
296
|
+
ok(`Bun ${bun.version}`);
|
|
297
|
+
claude.available
|
|
298
|
+
? ok(`Claude Code ${claude.version || ""}`.trim())
|
|
299
|
+
: fail("Claude Code — not found");
|
|
300
|
+
opencode.available
|
|
301
|
+
? ok(`opencode ${opencode.version || ""}`.trim())
|
|
302
|
+
: fail("opencode — not found");
|
|
303
|
+
ok(`PAL home: ${home} (${isRepo ? "repo" : "package"} mode)`);
|
|
304
|
+
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
305
|
+
|
|
306
|
+
if (!hasAgent) {
|
|
307
|
+
console.log("");
|
|
308
|
+
log.error("No supported agent found. Install Claude Code or opencode.");
|
|
309
|
+
}
|
|
310
|
+
console.log("");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { bun, claude, opencode, hasAgent };
|
|
314
|
+
}
|
|
315
|
+
|
|
73
316
|
// ── Commands ──
|
|
74
317
|
|
|
75
|
-
async function init() {
|
|
318
|
+
async function init(args: string[]) {
|
|
76
319
|
const { ensureSetupState, isSetupComplete } = await import("../hooks/lib/setup");
|
|
77
320
|
const { scaffoldTelos } = await import("../targets/lib");
|
|
78
321
|
|
|
79
322
|
banner();
|
|
80
323
|
|
|
324
|
+
// Run doctor first — abort if no agents available
|
|
325
|
+
const health = doctor(false);
|
|
326
|
+
if (!health.hasAgent) {
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
81
330
|
const home = palHome();
|
|
82
331
|
const isRepo = existsSync(resolve(palPkg(), ".palroot"));
|
|
83
332
|
|
|
84
333
|
if (!isRepo) {
|
|
85
|
-
// Package mode — scaffold ~/.pal/
|
|
86
334
|
log.info(`Creating PAL home at ${home}`);
|
|
87
335
|
mkdirSync(resolve(home, "telos"), { recursive: true });
|
|
88
336
|
mkdirSync(resolve(home, "memory"), { recursive: true });
|
|
@@ -91,7 +339,8 @@ async function init() {
|
|
|
91
339
|
scaffoldTelos();
|
|
92
340
|
ensureSetupState();
|
|
93
341
|
|
|
94
|
-
|
|
342
|
+
// Auto-detect available targets
|
|
343
|
+
const targets = resolveTargets(args, health);
|
|
95
344
|
await install(targets);
|
|
96
345
|
|
|
97
346
|
console.log("");
|
|
@@ -101,16 +350,14 @@ async function init() {
|
|
|
101
350
|
}
|
|
102
351
|
}
|
|
103
352
|
|
|
104
|
-
async function install(targets
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (t.claude) {
|
|
353
|
+
async function install(targets: { claude: boolean; opencode: boolean }) {
|
|
354
|
+
if (targets.claude) {
|
|
108
355
|
console.log("━━━ Claude Code ━━━");
|
|
109
356
|
await import("../targets/claude/install");
|
|
110
357
|
console.log("");
|
|
111
358
|
}
|
|
112
359
|
|
|
113
|
-
if (
|
|
360
|
+
if (targets.opencode) {
|
|
114
361
|
console.log("━━━ opencode ━━━");
|
|
115
362
|
await import("../targets/opencode/install");
|
|
116
363
|
console.log("");
|
|
@@ -119,7 +366,7 @@ async function install(targets?: { claude: boolean; opencode: boolean }) {
|
|
|
119
366
|
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
120
367
|
}
|
|
121
368
|
|
|
122
|
-
async function uninstall() {
|
|
369
|
+
async function uninstall(args: string[]) {
|
|
123
370
|
const targets = parseTargets(args);
|
|
124
371
|
|
|
125
372
|
if (targets.claude) {
|
|
@@ -139,13 +386,13 @@ async function uninstall() {
|
|
|
139
386
|
);
|
|
140
387
|
}
|
|
141
388
|
|
|
142
|
-
async function exportState() {
|
|
389
|
+
async function exportState(args: string[]) {
|
|
143
390
|
const { collectExportFiles, exportZip, timestamp } = await import(
|
|
144
391
|
"../hooks/lib/export"
|
|
145
392
|
);
|
|
146
393
|
|
|
147
394
|
const dryRun = args.includes("--dry-run");
|
|
148
|
-
const pathArg = args.find((a) => !a.startsWith("-")
|
|
395
|
+
const pathArg = args.find((a) => !a.startsWith("-"));
|
|
149
396
|
const outputPath = pathArg || resolve(palHome(), `pal-export-${timestamp()}.zip`);
|
|
150
397
|
|
|
151
398
|
if (dryRun) {
|
|
@@ -166,14 +413,14 @@ async function exportState() {
|
|
|
166
413
|
}
|
|
167
414
|
}
|
|
168
415
|
|
|
169
|
-
async function importState() {
|
|
170
|
-
const {
|
|
416
|
+
async function importState(args: string[]) {
|
|
417
|
+
const { statSync } = await import("node:fs");
|
|
171
418
|
const { createInterface } = await import("node:readline");
|
|
172
419
|
const AdmZip = (await import("adm-zip")).default;
|
|
173
420
|
|
|
174
421
|
const home = palHome();
|
|
175
422
|
const dryRun = args.includes("--dry-run");
|
|
176
|
-
const pathArg = args.find((a) => !a.startsWith("-")
|
|
423
|
+
const pathArg = args.find((a) => !a.startsWith("-"));
|
|
177
424
|
|
|
178
425
|
function findLatest(): string | null {
|
|
179
426
|
const candidates: string[] = [];
|
|
@@ -214,7 +461,7 @@ async function importState() {
|
|
|
214
461
|
} else {
|
|
215
462
|
const latest = findLatest();
|
|
216
463
|
if (!latest) {
|
|
217
|
-
log.error("No export or backup files found. Provide a path: pal import <path>");
|
|
464
|
+
log.error("No export or backup files found. Provide a path: pal cli import <path>");
|
|
218
465
|
process.exit(1);
|
|
219
466
|
}
|
|
220
467
|
console.log(`Found: ${latest}`);
|
|
@@ -254,13 +501,11 @@ async function importState() {
|
|
|
254
501
|
} else {
|
|
255
502
|
zip.extractAllTo(home, true);
|
|
256
503
|
console.log(`Imported ${entries.length} files → ${home}`);
|
|
257
|
-
log.info("Run 'pal install' to re-register hooks.");
|
|
504
|
+
log.info("Run 'pal cli install' to re-register hooks.");
|
|
258
505
|
}
|
|
259
506
|
}
|
|
260
507
|
|
|
261
508
|
async function status() {
|
|
262
|
-
const { existsSync, readdirSync, readFileSync } = await import("node:fs");
|
|
263
|
-
|
|
264
509
|
const home = palHome();
|
|
265
510
|
const pkg = palPkg();
|
|
266
511
|
const isRepo = existsSync(resolve(pkg, ".palroot"));
|
|
@@ -274,13 +519,11 @@ async function status() {
|
|
|
274
519
|
log.info(`Home: ${home}`);
|
|
275
520
|
console.log("");
|
|
276
521
|
|
|
277
|
-
// Platform dirs
|
|
278
522
|
log.info(`Claude: ${platform.claudeDir()}`);
|
|
279
523
|
log.info(`opencode: ${platform.opencodeDir()}`);
|
|
280
524
|
log.info(`Agents: ${platform.agentsDir()}`);
|
|
281
525
|
console.log("");
|
|
282
526
|
|
|
283
|
-
// Counts
|
|
284
527
|
const count = (dir: string, ext?: string) => {
|
|
285
528
|
try {
|
|
286
529
|
const files = readdirSync(dir);
|
|
@@ -298,7 +541,6 @@ async function status() {
|
|
|
298
541
|
const agentsDir = resolve(platform.claudeDir(), "agents");
|
|
299
542
|
log.info(`Agents: ${count(agentsDir, ".md")} installed`);
|
|
300
543
|
|
|
301
|
-
// Check if hooks are registered
|
|
302
544
|
const settingsPath = resolve(platform.claudeDir(), "settings.json");
|
|
303
545
|
try {
|
|
304
546
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
@@ -309,36 +551,3 @@ async function status() {
|
|
|
309
551
|
}
|
|
310
552
|
console.log("");
|
|
311
553
|
}
|
|
312
|
-
|
|
313
|
-
// ── Dispatch ──
|
|
314
|
-
|
|
315
|
-
switch (command) {
|
|
316
|
-
case "init":
|
|
317
|
-
await init();
|
|
318
|
-
break;
|
|
319
|
-
case "install":
|
|
320
|
-
banner();
|
|
321
|
-
await install();
|
|
322
|
-
break;
|
|
323
|
-
case "uninstall":
|
|
324
|
-
await uninstall();
|
|
325
|
-
break;
|
|
326
|
-
case "export":
|
|
327
|
-
await exportState();
|
|
328
|
-
break;
|
|
329
|
-
case "import":
|
|
330
|
-
await importState();
|
|
331
|
-
break;
|
|
332
|
-
case "status":
|
|
333
|
-
await status();
|
|
334
|
-
break;
|
|
335
|
-
case "--help":
|
|
336
|
-
case "-h":
|
|
337
|
-
case "help":
|
|
338
|
-
showHelp();
|
|
339
|
-
break;
|
|
340
|
-
default:
|
|
341
|
-
if (command) log.error(`Unknown command: ${command}`);
|
|
342
|
-
showHelp();
|
|
343
|
-
process.exit(command ? 1 : 0);
|
|
344
|
-
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Transcript is read from the file at transcript_path, NOT from stdin.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { checkReadmeSync } from "./handlers/readme-sync";
|
|
9
10
|
import { logError } from "./lib/log";
|
|
10
11
|
import { readStdinJSON } from "./lib/stdin";
|
|
11
12
|
import { runStopHandlers } from "./lib/stop";
|
|
@@ -17,6 +18,17 @@ interface StopHookInput {
|
|
|
17
18
|
last_assistant_message?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// Check README sync before anything else — may block the session
|
|
22
|
+
try {
|
|
23
|
+
const decision = checkReadmeSync();
|
|
24
|
+
if (decision.decision === "block") {
|
|
25
|
+
console.log(JSON.stringify(decision));
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
logError("StopOrchestrator:readme-sync", err);
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
const input = await readStdinJSON<StopHookInput>();
|
|
21
33
|
if (!input?.transcript_path) {
|
|
22
34
|
logError("StopOrchestrator", "No transcript_path in hook input");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop handler: check if README.md is out of sync with code.
|
|
3
|
+
*
|
|
4
|
+
* Runs git diff to see if documentable files changed in this session.
|
|
5
|
+
* If they did and README is stale, returns a block decision.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { logDebug } from "../lib/log";
|
|
10
|
+
import { palPkg } from "../lib/paths";
|
|
11
|
+
import { validateReadmeSync, WATCHED_PATHS } from "../lib/readme-sync";
|
|
12
|
+
|
|
13
|
+
/** Check if any watched files have uncommitted changes. */
|
|
14
|
+
function hasDocumentableChanges(): boolean {
|
|
15
|
+
try {
|
|
16
|
+
const diff = execSync("git diff --name-only HEAD", {
|
|
17
|
+
cwd: palPkg(),
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
}).trim();
|
|
20
|
+
|
|
21
|
+
const staged = execSync("git diff --name-only --cached", {
|
|
22
|
+
cwd: palPkg(),
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
}).trim();
|
|
25
|
+
|
|
26
|
+
const changed = `${diff}\n${staged}`.split("\n").filter((f) => f.length > 0);
|
|
27
|
+
|
|
28
|
+
return changed.some((file) =>
|
|
29
|
+
WATCHED_PATHS.some((watched) => file === watched || file.startsWith(`${watched}/`))
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReadmeSyncDecision {
|
|
37
|
+
decision?: "block";
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns a block decision if README is stale, or empty object to allow stop. */
|
|
42
|
+
export function checkReadmeSync(): ReadmeSyncDecision {
|
|
43
|
+
if (!hasDocumentableChanges()) {
|
|
44
|
+
logDebug("readme-sync", "No documentable changes detected");
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logDebug("readme-sync", "Documentable files changed — validating README");
|
|
49
|
+
const result = validateReadmeSync();
|
|
50
|
+
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
logDebug("readme-sync", `README out of sync: ${result.issues.join("; ")}`);
|
|
53
|
+
return {
|
|
54
|
+
decision: "block",
|
|
55
|
+
reason: `README.md is out of date. Please update it before finishing:\n${result.issues.map((i) => `- ${i}`).join("\n")}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
logDebug("readme-sync", "README is in sync");
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { dirname, relative, resolve } from "node:path";
|
|
20
20
|
import { loadTelos } from "./context";
|
|
21
|
-
import { assets, palHome, paths, platform } from "./paths";
|
|
21
|
+
import { assets, ensureDir, palHome, paths, platform } from "./paths";
|
|
22
22
|
import { buildSetupPrompt, readSetupState } from "./setup";
|
|
23
23
|
|
|
24
24
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
@@ -116,6 +116,7 @@ export function regenerateIfNeeded(): boolean {
|
|
|
116
116
|
const { outputPath } = getOutputPaths();
|
|
117
117
|
ensureSymlink();
|
|
118
118
|
if (!needsRebuild()) return false;
|
|
119
|
+
ensureDir(dirname(outputPath));
|
|
119
120
|
writeFileSync(outputPath, buildClaudeMd(), "utf-8");
|
|
120
121
|
return true;
|
|
121
122
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* README sync validation — ensures README.md reflects current code surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Checks CLI commands, environment variables, and skills against README content.
|
|
5
|
+
* Used by tests (CI/pre-commit) and the Stop hook (blocks session if stale).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { palPkg } from "./paths";
|
|
11
|
+
|
|
12
|
+
export interface SyncResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
issues: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Files that, when changed, should trigger a README check. */
|
|
18
|
+
export const WATCHED_PATHS = [
|
|
19
|
+
"src/cli/index.ts",
|
|
20
|
+
"src/hooks/lib/paths.ts",
|
|
21
|
+
"src/hooks/lib/inference.ts",
|
|
22
|
+
"src/tools/youtube-analyze.ts",
|
|
23
|
+
"assets/skills",
|
|
24
|
+
"assets/agents",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Extract CLI command names from the switch statement in index.ts */
|
|
28
|
+
function extractCliCommands(): string[] {
|
|
29
|
+
const pkg = palPkg();
|
|
30
|
+
const cliPath = resolve(pkg, "src", "cli", "index.ts");
|
|
31
|
+
if (!existsSync(cliPath)) return [];
|
|
32
|
+
|
|
33
|
+
const content = readFileSync(cliPath, "utf-8");
|
|
34
|
+
const matches = content.matchAll(/case\s+"([^"]+)":/g);
|
|
35
|
+
const commands: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const match of matches) {
|
|
38
|
+
const cmd = match[1];
|
|
39
|
+
// Skip help aliases and internal routing
|
|
40
|
+
if (["--help", "-h", "help", "cli"].includes(cmd)) continue;
|
|
41
|
+
commands.push(cmd);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...new Set(commands)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Extract PAL_* env var names from paths.ts + API keys from source */
|
|
48
|
+
function extractEnvVars(): string[] {
|
|
49
|
+
const pkg = palPkg();
|
|
50
|
+
const vars: Set<string> = new Set();
|
|
51
|
+
|
|
52
|
+
// PAL_* from paths.ts
|
|
53
|
+
const pathsFile = resolve(pkg, "src", "hooks", "lib", "paths.ts");
|
|
54
|
+
if (existsSync(pathsFile)) {
|
|
55
|
+
const content = readFileSync(pathsFile, "utf-8");
|
|
56
|
+
for (const match of content.matchAll(/process\.env\.(PAL_\w+)/g)) {
|
|
57
|
+
vars.add(match[1]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ANTHROPIC_API_KEY from inference.ts
|
|
62
|
+
const inferenceFile = resolve(pkg, "src", "hooks", "lib", "inference.ts");
|
|
63
|
+
if (existsSync(inferenceFile)) {
|
|
64
|
+
const content = readFileSync(inferenceFile, "utf-8");
|
|
65
|
+
if (content.includes("ANTHROPIC_API_KEY")) {
|
|
66
|
+
vars.add("ANTHROPIC_API_KEY");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// GEMINI_API_KEY from youtube-analyze.ts
|
|
71
|
+
const youtubeFile = resolve(pkg, "src", "tools", "youtube-analyze.ts");
|
|
72
|
+
if (existsSync(youtubeFile)) {
|
|
73
|
+
const content = readFileSync(youtubeFile, "utf-8");
|
|
74
|
+
if (content.includes("GEMINI_API_KEY")) {
|
|
75
|
+
vars.add("GEMINI_API_KEY");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [...vars];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extract skill names from assets/skills/ */
|
|
83
|
+
function extractSkillNames(): string[] {
|
|
84
|
+
const pkg = palPkg();
|
|
85
|
+
const skillsDir = resolve(pkg, "assets", "skills");
|
|
86
|
+
if (!existsSync(skillsDir)) return [];
|
|
87
|
+
|
|
88
|
+
return readdirSync(skillsDir)
|
|
89
|
+
.filter((f) => f.endsWith(".md"))
|
|
90
|
+
.map((f) => f.replace(/\.md$/, ""));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Validate that README.md documents all code surfaces. */
|
|
94
|
+
export function validateReadmeSync(): SyncResult {
|
|
95
|
+
const pkg = palPkg();
|
|
96
|
+
const readmePath = resolve(pkg, "README.md");
|
|
97
|
+
|
|
98
|
+
if (!existsSync(readmePath)) {
|
|
99
|
+
return { ok: false, issues: ["README.md not found"] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const readme = readFileSync(readmePath, "utf-8");
|
|
103
|
+
const issues: string[] = [];
|
|
104
|
+
|
|
105
|
+
// Check CLI commands
|
|
106
|
+
for (const cmd of extractCliCommands()) {
|
|
107
|
+
if (!readme.includes(`pal cli ${cmd}`)) {
|
|
108
|
+
issues.push(`CLI command "${cmd}" exists in code but not documented in README`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check environment variables
|
|
113
|
+
for (const envVar of extractEnvVars()) {
|
|
114
|
+
if (!readme.includes(envVar)) {
|
|
115
|
+
issues.push(
|
|
116
|
+
`Environment variable "${envVar}" used in code but not documented in README`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check skills — just verify the count is mentioned or each name appears
|
|
122
|
+
const skills = extractSkillNames();
|
|
123
|
+
const undocumentedSkills = skills.filter((name) => !readme.includes(name));
|
|
124
|
+
if (undocumentedSkills.length > 0) {
|
|
125
|
+
issues.push(`Skills not documented in README: ${undocumentedSkills.join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { ok: issues.length === 0, issues };
|
|
129
|
+
}
|
package/src/targets/lib.ts
CHANGED
|
@@ -48,6 +48,7 @@ export function scaffoldTelos(): void {
|
|
|
48
48
|
const templatesDir = assets.telosTemplates();
|
|
49
49
|
const telosDir = resolve(palHome(), "telos");
|
|
50
50
|
if (!existsSync(templatesDir)) return;
|
|
51
|
+
mkdirSync(telosDir, { recursive: true });
|
|
51
52
|
|
|
52
53
|
for (const file of readdirSync(templatesDir).filter((f) => f.endsWith(".md"))) {
|
|
53
54
|
const src = resolve(templatesDir, file);
|
package/bin/pal
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Jarvis — Claude Code wrapper with session summary on exit.
|
|
3
|
-
#
|
|
4
|
-
# After Claude exits, finds the most recently modified transcript JSONL
|
|
5
|
-
# in ~/.claude/projects/ and extracts the sessionId from its last line.
|
|
6
|
-
|
|
7
|
-
PAL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
-
|
|
9
|
-
# Run Claude (blocking — keeps the interactive terminal)
|
|
10
|
-
claude "$@"
|
|
11
|
-
EXIT_CODE=$?
|
|
12
|
-
|
|
13
|
-
# Find the most recently modified transcript and extract its session ID
|
|
14
|
-
LATEST=$(find "$HOME/.claude/projects" -name '*.jsonl' -type f -print0 2>/dev/null \
|
|
15
|
-
| xargs -0 ls -t 2>/dev/null | head -1)
|
|
16
|
-
|
|
17
|
-
if [ -n "$LATEST" ]; then
|
|
18
|
-
SESSION_ID=$(tail -1 "$LATEST" | python3 -c "import sys,json; print(json.loads(sys.stdin.readline()).get('sessionId',''))" 2>/dev/null)
|
|
19
|
-
if [ -n "$SESSION_ID" ]; then
|
|
20
|
-
bun run "$PAL_DIR/src/tools/session-summary.ts" -- --session "$SESSION_ID" 2>/dev/null
|
|
21
|
-
fi
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
exit $EXIT_CODE
|
package/bin/pal.bat
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
REM Jarvis — Claude Code wrapper with session summary on exit.
|
|
3
|
-
REM
|
|
4
|
-
REM Uses PowerShell to start Claude, capture its PID, read the session ID
|
|
5
|
-
REM from %USERPROFILE%\.claude\sessions\<PID>.json, then show a cost
|
|
6
|
-
REM summary after Claude exits.
|
|
7
|
-
|
|
8
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0pal.ps1" %*
|
package/bin/pal.ps1
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Jarvis — Claude Code wrapper with session summary on exit.
|
|
2
|
-
#
|
|
3
|
-
# After Claude exits, finds the most recently modified transcript JSONL
|
|
4
|
-
# in ~/.claude/projects/ and extracts the sessionId from its last line.
|
|
5
|
-
|
|
6
|
-
$palDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
7
|
-
|
|
8
|
-
# Run Claude (blocking — keeps the interactive terminal)
|
|
9
|
-
& claude @args
|
|
10
|
-
$exitCode = $LASTEXITCODE
|
|
11
|
-
|
|
12
|
-
# Find the most recently modified transcript and extract its session ID
|
|
13
|
-
$latest = Get-ChildItem "$env:USERPROFILE\.claude\projects\*\*.jsonl" -ErrorAction SilentlyContinue |
|
|
14
|
-
Sort-Object LastWriteTime -Descending |
|
|
15
|
-
Select-Object -First 1
|
|
16
|
-
|
|
17
|
-
if ($latest) {
|
|
18
|
-
$lastLine = Get-Content $latest.FullName -Tail 1 -ErrorAction SilentlyContinue
|
|
19
|
-
if ($lastLine) {
|
|
20
|
-
try {
|
|
21
|
-
$sessionId = ($lastLine | ConvertFrom-Json).sessionId
|
|
22
|
-
if ($sessionId) {
|
|
23
|
-
$summaryScript = Join-Path $palDir "src" "tools" "session-summary.ts"
|
|
24
|
-
& bun run $summaryScript -- --session $sessionId 2>$null
|
|
25
|
-
}
|
|
26
|
-
} catch {}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
exit $exitCode
|