portable-agent-layer 0.1.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/LICENSE +21 -0
- package/README.md +80 -0
- package/assets/agents/claude-researcher.md +43 -0
- package/assets/agents/investigative-researcher.md +44 -0
- package/assets/agents/multi-perspective-researcher.md +43 -0
- package/assets/skills/analyze-pdf.md +40 -0
- package/assets/skills/analyze-youtube.md +35 -0
- package/assets/skills/council.md +43 -0
- package/assets/skills/create-skill.md +31 -0
- package/assets/skills/extract-entities.md +63 -0
- package/assets/skills/extract-wisdom.md +18 -0
- package/assets/skills/first-principles.md +17 -0
- package/assets/skills/fyzz-chat-api.md +43 -0
- package/assets/skills/reflect.md +87 -0
- package/assets/skills/research.md +68 -0
- package/assets/skills/review.md +19 -0
- package/assets/skills/summarize.md +15 -0
- package/assets/templates/AGENTS.md.template +45 -0
- package/assets/templates/telos/BELIEFS.md +4 -0
- package/assets/templates/telos/CHALLENGES.md +4 -0
- package/assets/templates/telos/GOALS.md +12 -0
- package/assets/templates/telos/IDEAS.md +4 -0
- package/assets/templates/telos/IDENTITY.md +4 -0
- package/assets/templates/telos/LEARNED.md +4 -0
- package/assets/templates/telos/MISSION.md +4 -0
- package/assets/templates/telos/MODELS.md +4 -0
- package/assets/templates/telos/NARRATIVES.md +4 -0
- package/assets/templates/telos/PROJECTS.md +7 -0
- package/assets/templates/telos/STRATEGIES.md +4 -0
- package/bin/pal +24 -0
- package/bin/pal.bat +8 -0
- package/bin/pal.ps1 +30 -0
- package/package.json +82 -0
- package/src/cli/index.ts +344 -0
- package/src/cli/install.ts +86 -0
- package/src/cli/uninstall.ts +45 -0
- package/src/hooks/LoadContext.ts +41 -0
- package/src/hooks/SecurityValidator.ts +52 -0
- package/src/hooks/SkillGuard.ts +41 -0
- package/src/hooks/StopOrchestrator.ts +35 -0
- package/src/hooks/UserPromptOrchestrator.ts +35 -0
- package/src/hooks/handlers/backup.ts +41 -0
- package/src/hooks/handlers/failure.ts +136 -0
- package/src/hooks/handlers/rating.ts +409 -0
- package/src/hooks/handlers/relationship.ts +113 -0
- package/src/hooks/handlers/session-name.ts +121 -0
- package/src/hooks/handlers/synthesis.ts +109 -0
- package/src/hooks/handlers/tab.ts +8 -0
- package/src/hooks/handlers/update-counts.ts +151 -0
- package/src/hooks/handlers/work-learning.ts +183 -0
- package/src/hooks/handlers/work-session.ts +58 -0
- package/src/hooks/lib/claude-md.ts +121 -0
- package/src/hooks/lib/context.ts +433 -0
- package/src/hooks/lib/entities.ts +304 -0
- package/src/hooks/lib/export.ts +76 -0
- package/src/hooks/lib/inference.ts +91 -0
- package/src/hooks/lib/learning-category.ts +14 -0
- package/src/hooks/lib/log.ts +53 -0
- package/src/hooks/lib/models.ts +16 -0
- package/src/hooks/lib/paths.ts +80 -0
- package/src/hooks/lib/relationship.ts +135 -0
- package/src/hooks/lib/security.ts +122 -0
- package/src/hooks/lib/session-names.ts +247 -0
- package/src/hooks/lib/setup.ts +189 -0
- package/src/hooks/lib/signal-trends.ts +117 -0
- package/src/hooks/lib/signals.ts +37 -0
- package/src/hooks/lib/stdin.ts +18 -0
- package/src/hooks/lib/stop.ts +155 -0
- package/src/hooks/lib/time.ts +19 -0
- package/src/hooks/lib/token-usage.ts +42 -0
- package/src/hooks/lib/transcript.ts +76 -0
- package/src/hooks/lib/wisdom.ts +48 -0
- package/src/hooks/lib/work-tracking.ts +193 -0
- package/src/hooks/setup-check.ts +42 -0
- package/src/targets/claude/install.ts +145 -0
- package/src/targets/claude/uninstall.ts +101 -0
- package/src/targets/lib.ts +337 -0
- package/src/targets/opencode/install.ts +59 -0
- package/src/targets/opencode/plugin.ts +328 -0
- package/src/targets/opencode/uninstall.ts +57 -0
- package/src/tools/entity-save.ts +110 -0
- package/src/tools/export.ts +34 -0
- package/src/tools/fyzz-api.ts +104 -0
- package/src/tools/import.ts +123 -0
- package/src/tools/pattern-synthesis.ts +435 -0
- package/src/tools/pdf-download.ts +102 -0
- package/src/tools/relationship-reflect.ts +362 -0
- package/src/tools/session-summary.ts +206 -0
- package/src/tools/token-cost.ts +301 -0
- package/src/tools/youtube-analyze.ts +105 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* PAL CLI — Portable Agent Layer
|
|
4
|
+
*
|
|
5
|
+
* Usage: pal <command> [options]
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* init Scaffold PAL home, install hooks for all targets
|
|
9
|
+
* install Register hooks/skills for targets
|
|
10
|
+
* uninstall Remove hooks/skills for targets
|
|
11
|
+
* export Export user state (telos, memory) to a zip
|
|
12
|
+
* import Import user state from a zip
|
|
13
|
+
* status Show current PAL configuration
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
import { palHome, palPkg, platform } from "../hooks/lib/paths";
|
|
19
|
+
import { log } from "../targets/lib";
|
|
20
|
+
|
|
21
|
+
const [command, ...args] = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
function banner() {
|
|
24
|
+
console.log("");
|
|
25
|
+
console.log(" ╔═══════════════════════════════════╗");
|
|
26
|
+
console.log(" ║ PAL — Portable Agent Layer ║");
|
|
27
|
+
console.log(" ║ Non-destructive · Modular ║");
|
|
28
|
+
console.log(" ╚═══════════════════════════════════╝");
|
|
29
|
+
console.log("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function showHelp() {
|
|
33
|
+
console.log(`
|
|
34
|
+
Usage: pal <command> [options]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
init [--claude] [--opencode] [--all] Scaffold and install (default: all)
|
|
38
|
+
install [--claude] [--opencode] [--all] Register hooks for targets
|
|
39
|
+
uninstall [--claude] [--opencode] [--all] Remove hooks for targets
|
|
40
|
+
export [path] [--dry-run] Export state to zip
|
|
41
|
+
import [path] [--dry-run] Import state from zip
|
|
42
|
+
status Show PAL configuration
|
|
43
|
+
Environment:
|
|
44
|
+
PAL_HOME Override user state directory (default: ~/.pal or repo root)
|
|
45
|
+
PAL_PKG Override package root
|
|
46
|
+
PAL_CLAUDE_DIR Override Claude config dir (default: ~/.claude)
|
|
47
|
+
PAL_OPENCODE_DIR Override opencode config dir (default: ~/.config/opencode)
|
|
48
|
+
PAL_AGENTS_DIR Override agents dir (default: ~/.agents)
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseTargets(args: string[]): {
|
|
53
|
+
claude: boolean;
|
|
54
|
+
opencode: boolean;
|
|
55
|
+
} {
|
|
56
|
+
if (args.length === 0) return { claude: true, opencode: true };
|
|
57
|
+
|
|
58
|
+
let claude = false;
|
|
59
|
+
let opencode = false;
|
|
60
|
+
for (const arg of args) {
|
|
61
|
+
if (arg === "--claude") claude = true;
|
|
62
|
+
else if (arg === "--opencode") opencode = true;
|
|
63
|
+
else if (arg === "--all") {
|
|
64
|
+
claude = true;
|
|
65
|
+
opencode = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// If no target flags, default to all
|
|
69
|
+
if (!claude && !opencode) return { claude: true, opencode: true };
|
|
70
|
+
return { claude, opencode };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Commands ──
|
|
74
|
+
|
|
75
|
+
async function init() {
|
|
76
|
+
const { ensureSetupState, isSetupComplete } = await import("../hooks/lib/setup");
|
|
77
|
+
const { scaffoldTelos } = await import("../targets/lib");
|
|
78
|
+
|
|
79
|
+
banner();
|
|
80
|
+
|
|
81
|
+
const home = palHome();
|
|
82
|
+
const isRepo = existsSync(resolve(palPkg(), ".palroot"));
|
|
83
|
+
|
|
84
|
+
if (!isRepo) {
|
|
85
|
+
// Package mode — scaffold ~/.pal/
|
|
86
|
+
log.info(`Creating PAL home at ${home}`);
|
|
87
|
+
mkdirSync(resolve(home, "telos"), { recursive: true });
|
|
88
|
+
mkdirSync(resolve(home, "memory"), { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
scaffoldTelos();
|
|
92
|
+
ensureSetupState();
|
|
93
|
+
|
|
94
|
+
const targets = parseTargets(args);
|
|
95
|
+
await install(targets);
|
|
96
|
+
|
|
97
|
+
console.log("");
|
|
98
|
+
const state = ensureSetupState();
|
|
99
|
+
if (!isSetupComplete(state)) {
|
|
100
|
+
log.info("Start a session — PAL will guide you through first-run setup");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function install(targets?: { claude: boolean; opencode: boolean }) {
|
|
105
|
+
const t = targets || parseTargets(args);
|
|
106
|
+
|
|
107
|
+
if (t.claude) {
|
|
108
|
+
console.log("━━━ Claude Code ━━━");
|
|
109
|
+
await import("../targets/claude/install");
|
|
110
|
+
console.log("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (t.opencode) {
|
|
114
|
+
console.log("━━━ opencode ━━━");
|
|
115
|
+
await import("../targets/opencode/install");
|
|
116
|
+
console.log("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function uninstall() {
|
|
123
|
+
const targets = parseTargets(args);
|
|
124
|
+
|
|
125
|
+
if (targets.claude) {
|
|
126
|
+
console.log("━━━ Claude Code ━━━");
|
|
127
|
+
await import("../targets/claude/uninstall");
|
|
128
|
+
console.log("");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (targets.opencode) {
|
|
132
|
+
console.log("━━━ opencode ━━━");
|
|
133
|
+
await import("../targets/opencode/uninstall");
|
|
134
|
+
console.log("");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
log.success(
|
|
138
|
+
`PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function exportState() {
|
|
143
|
+
const { collectExportFiles, exportZip, timestamp } = await import(
|
|
144
|
+
"../hooks/lib/export"
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const dryRun = args.includes("--dry-run");
|
|
148
|
+
const pathArg = args.find((a) => !a.startsWith("-") && a !== "export");
|
|
149
|
+
const outputPath = pathArg || resolve(palHome(), `pal-export-${timestamp()}.zip`);
|
|
150
|
+
|
|
151
|
+
if (dryRun) {
|
|
152
|
+
const files = collectExportFiles();
|
|
153
|
+
if (files.length === 0) {
|
|
154
|
+
console.log("Nothing to export.");
|
|
155
|
+
} else {
|
|
156
|
+
console.log(`Would export ${files.length} files → ${outputPath}\n`);
|
|
157
|
+
for (const f of files) console.log(` ${f}`);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
const count = exportZip(outputPath);
|
|
161
|
+
if (count === 0) {
|
|
162
|
+
console.log("Nothing to export.");
|
|
163
|
+
} else {
|
|
164
|
+
console.log(`Exported ${count} files → ${outputPath}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function importState() {
|
|
170
|
+
const { readdirSync, statSync } = await import("node:fs");
|
|
171
|
+
const { createInterface } = await import("node:readline");
|
|
172
|
+
const AdmZip = (await import("adm-zip")).default;
|
|
173
|
+
|
|
174
|
+
const home = palHome();
|
|
175
|
+
const dryRun = args.includes("--dry-run");
|
|
176
|
+
const pathArg = args.find((a) => !a.startsWith("-") && a !== "import");
|
|
177
|
+
|
|
178
|
+
function findLatest(): string | null {
|
|
179
|
+
const candidates: string[] = [];
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
candidates.push(
|
|
183
|
+
...readdirSync(home)
|
|
184
|
+
.filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
|
|
185
|
+
.map((f) => resolve(home, f))
|
|
186
|
+
);
|
|
187
|
+
} catch {
|
|
188
|
+
/* empty */
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const backupDir = resolve(home, "backups");
|
|
193
|
+
candidates.push(
|
|
194
|
+
...readdirSync(backupDir)
|
|
195
|
+
.filter(
|
|
196
|
+
(f) =>
|
|
197
|
+
(f.startsWith("pal-export-") || f.startsWith("pal-backup-")) &&
|
|
198
|
+
f.endsWith(".zip")
|
|
199
|
+
)
|
|
200
|
+
.map((f) => resolve(backupDir, f))
|
|
201
|
+
);
|
|
202
|
+
} catch {
|
|
203
|
+
/* empty */
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (candidates.length === 0) return null;
|
|
207
|
+
return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let zipPath: string;
|
|
211
|
+
|
|
212
|
+
if (pathArg) {
|
|
213
|
+
zipPath = resolve(pathArg);
|
|
214
|
+
} else {
|
|
215
|
+
const latest = findLatest();
|
|
216
|
+
if (!latest) {
|
|
217
|
+
log.error("No export or backup files found. Provide a path: pal import <path>");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
console.log(`Found: ${latest}`);
|
|
221
|
+
const zip = new AdmZip(latest);
|
|
222
|
+
console.log(
|
|
223
|
+
`Contains ${zip.getEntries().length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const rl = createInterface({
|
|
227
|
+
input: process.stdin,
|
|
228
|
+
output: process.stdout,
|
|
229
|
+
});
|
|
230
|
+
const answer = await new Promise<string>((res) =>
|
|
231
|
+
rl.question("Import this file? [y/N] ", (a) => {
|
|
232
|
+
rl.close();
|
|
233
|
+
res(a);
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
237
|
+
console.log("Cancelled.");
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
240
|
+
zipPath = latest;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const zip = new AdmZip(zipPath);
|
|
244
|
+
const entries = zip.getEntries();
|
|
245
|
+
|
|
246
|
+
if (entries.length === 0) {
|
|
247
|
+
console.log("Archive is empty.");
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (dryRun) {
|
|
252
|
+
console.log(`Would import ${entries.length} files → ${home}\n`);
|
|
253
|
+
for (const e of entries) console.log(` ${e.entryName}`);
|
|
254
|
+
} else {
|
|
255
|
+
zip.extractAllTo(home, true);
|
|
256
|
+
console.log(`Imported ${entries.length} files → ${home}`);
|
|
257
|
+
log.info("Run 'pal install' to re-register hooks.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function status() {
|
|
262
|
+
const { existsSync, readdirSync, readFileSync } = await import("node:fs");
|
|
263
|
+
|
|
264
|
+
const home = palHome();
|
|
265
|
+
const pkg = palPkg();
|
|
266
|
+
const isRepo = existsSync(resolve(pkg, ".palroot"));
|
|
267
|
+
|
|
268
|
+
const pkgJson = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8"));
|
|
269
|
+
|
|
270
|
+
console.log("");
|
|
271
|
+
log.info(`Version: ${pkgJson.version}`);
|
|
272
|
+
log.info(`Mode: ${isRepo ? "repo" : "package"}`);
|
|
273
|
+
log.info(`Package: ${pkg}`);
|
|
274
|
+
log.info(`Home: ${home}`);
|
|
275
|
+
console.log("");
|
|
276
|
+
|
|
277
|
+
// Platform dirs
|
|
278
|
+
log.info(`Claude: ${platform.claudeDir()}`);
|
|
279
|
+
log.info(`opencode: ${platform.opencodeDir()}`);
|
|
280
|
+
log.info(`Agents: ${platform.agentsDir()}`);
|
|
281
|
+
console.log("");
|
|
282
|
+
|
|
283
|
+
// Counts
|
|
284
|
+
const count = (dir: string, ext?: string) => {
|
|
285
|
+
try {
|
|
286
|
+
const files = readdirSync(dir);
|
|
287
|
+
return ext ? files.filter((f) => f.endsWith(ext)).length : files.length;
|
|
288
|
+
} catch {
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
log.info(`TELOS: ${count(resolve(home, "telos"), ".md")} files`);
|
|
294
|
+
|
|
295
|
+
const skillsDir = resolve(platform.agentsDir(), "skills");
|
|
296
|
+
log.info(`Skills: ${count(skillsDir)} installed`);
|
|
297
|
+
|
|
298
|
+
const agentsDir = resolve(platform.claudeDir(), "agents");
|
|
299
|
+
log.info(`Agents: ${count(agentsDir, ".md")} installed`);
|
|
300
|
+
|
|
301
|
+
// Check if hooks are registered
|
|
302
|
+
const settingsPath = resolve(platform.claudeDir(), "settings.json");
|
|
303
|
+
try {
|
|
304
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
305
|
+
const hookCount = Object.values(settings.hooks || {}).flat().length;
|
|
306
|
+
log.info(`Hooks: ${hookCount} registered`);
|
|
307
|
+
} catch {
|
|
308
|
+
log.info(`Hooks: not configured`);
|
|
309
|
+
}
|
|
310
|
+
console.log("");
|
|
311
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — main installer entry point (TypeScript)
|
|
3
|
+
* Usage: bun run install.ts [--claude] [--opencode] [--all]
|
|
4
|
+
* Default: installs for both targets.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ensureSetupState, isSetupComplete } from "../hooks/lib/setup";
|
|
8
|
+
import { log, scaffoldTelos } from "../targets/lib";
|
|
9
|
+
|
|
10
|
+
// --- Parse args ---
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
let installClaude = false;
|
|
13
|
+
let installOpencode = false;
|
|
14
|
+
|
|
15
|
+
if (args.length === 0) {
|
|
16
|
+
installClaude = true;
|
|
17
|
+
installOpencode = true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const arg of args) {
|
|
21
|
+
if (arg === "--claude") installClaude = true;
|
|
22
|
+
else if (arg === "--opencode") installOpencode = true;
|
|
23
|
+
else if (arg === "--all") {
|
|
24
|
+
installClaude = true;
|
|
25
|
+
installOpencode = true;
|
|
26
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
27
|
+
console.log("Usage: bun run install.ts [--claude] [--opencode] [--all]");
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log(" --claude Install hooks/skills for Claude Code");
|
|
30
|
+
console.log(" --opencode Install context/skills for opencode");
|
|
31
|
+
console.log(" --all Install for both (default)");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
} else {
|
|
34
|
+
log.error(`Unknown option: ${arg}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- Check bun ---
|
|
40
|
+
if (installClaude) {
|
|
41
|
+
try {
|
|
42
|
+
Bun.version; // always available in bun
|
|
43
|
+
} catch {
|
|
44
|
+
log.error("bun is required: curl -fsSL https://bun.sh/install | bash");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log(" ╔═══════════════════════════════════╗");
|
|
51
|
+
console.log(" ║ PAL — Portable Agent Layer ║");
|
|
52
|
+
console.log(" ║ Non-destructive · Modular ║");
|
|
53
|
+
console.log(" ╚═══════════════════════════════════╝");
|
|
54
|
+
console.log("");
|
|
55
|
+
|
|
56
|
+
// --- Scaffold TELOS + seed setup state ---
|
|
57
|
+
scaffoldTelos();
|
|
58
|
+
ensureSetupState();
|
|
59
|
+
|
|
60
|
+
// --- Run target installers ---
|
|
61
|
+
if (installClaude) {
|
|
62
|
+
console.log("━━━ Claude Code ━━━");
|
|
63
|
+
await import("../targets/claude/install");
|
|
64
|
+
console.log("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (installOpencode) {
|
|
68
|
+
console.log("━━━ opencode ━━━");
|
|
69
|
+
await import("../targets/opencode/install");
|
|
70
|
+
console.log("");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
74
|
+
console.log("");
|
|
75
|
+
log.info("Next steps:");
|
|
76
|
+
|
|
77
|
+
const state = ensureSetupState();
|
|
78
|
+
if (!isSetupComplete(state)) {
|
|
79
|
+
log.info(" 1. Start a session — PAL will guide you through first-run setup");
|
|
80
|
+
log.info(" 2. Or fill in telos/*.md manually, then re-run install.ts");
|
|
81
|
+
} else {
|
|
82
|
+
log.info(" 1. Fill in telos/*.md with your info (if not already done)");
|
|
83
|
+
log.info(" 2. Re-run install.ts to regenerate context files");
|
|
84
|
+
}
|
|
85
|
+
log.info(" 3. Add skills by dropping .md files into skills/");
|
|
86
|
+
log.info(" 4. Uninstall: bun run uninstall.ts [--claude] [--opencode]");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — main uninstaller entry point (TypeScript)
|
|
3
|
+
* Usage: bun run uninstall.ts [--claude] [--opencode] [--all]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { palHome } from "../hooks/lib/paths";
|
|
7
|
+
import { log } from "../targets/lib";
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
let removeClaude = false;
|
|
11
|
+
let removeOpencode = false;
|
|
12
|
+
|
|
13
|
+
if (args.length === 0) {
|
|
14
|
+
removeClaude = true;
|
|
15
|
+
removeOpencode = true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const arg of args) {
|
|
19
|
+
if (arg === "--claude") removeClaude = true;
|
|
20
|
+
else if (arg === "--opencode") removeOpencode = true;
|
|
21
|
+
else if (arg === "--all") {
|
|
22
|
+
removeClaude = true;
|
|
23
|
+
removeOpencode = true;
|
|
24
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
25
|
+
console.log("Usage: bun run uninstall.ts [--claude] [--opencode] [--all]");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
} else {
|
|
28
|
+
log.error(`Unknown option: ${arg}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (removeClaude) {
|
|
34
|
+
console.log("━━━ Claude Code ━━━");
|
|
35
|
+
await import("../targets/claude/uninstall");
|
|
36
|
+
console.log("");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (removeOpencode) {
|
|
40
|
+
console.log("━━━ opencode ━━━");
|
|
41
|
+
await import("../targets/opencode/uninstall");
|
|
42
|
+
console.log("");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
log.success(`PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: SessionStart — Injects dynamic context + regenerates AGENTS.md if stale.
|
|
3
|
+
*
|
|
4
|
+
* Static context (TELOS, setup prompt) is loaded natively from AGENTS.md / CLAUDE.md.
|
|
5
|
+
* This hook injects dynamic context only: wisdom principles, relationship notes,
|
|
6
|
+
* learning digest, signal trends, failure patterns, active work state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { regenerateIfNeeded } from "./lib/claude-md";
|
|
10
|
+
import { buildGreeting, buildSystemReminder } from "./lib/context";
|
|
11
|
+
import { logDebug, logError } from "./lib/log";
|
|
12
|
+
|
|
13
|
+
// --- Skip heavy context for subagents ---
|
|
14
|
+
const isSubagent =
|
|
15
|
+
process.env.CLAUDE_PROJECT_DIR?.includes("/.claude/Agents/") ||
|
|
16
|
+
process.env.CLAUDE_AGENT_TYPE !== undefined;
|
|
17
|
+
|
|
18
|
+
if (isSubagent) {
|
|
19
|
+
logDebug("LoadContext", "Subagent session — skipping context loading");
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// --- Regenerate CLAUDE.md if telos or setup changed ---
|
|
24
|
+
try {
|
|
25
|
+
const rebuilt = regenerateIfNeeded();
|
|
26
|
+
if (rebuilt) logDebug("LoadContext", "AGENTS.md regenerated");
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logError("LoadContext:regenerate", err);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Visible greeting to stderr ---
|
|
32
|
+
process.stderr.write(`${buildGreeting().join("\n")}\n`);
|
|
33
|
+
|
|
34
|
+
// --- Dynamic system-reminder to stdout (empty = nothing injected) ---
|
|
35
|
+
try {
|
|
36
|
+
const reminder = buildSystemReminder();
|
|
37
|
+
if (reminder) console.log(reminder);
|
|
38
|
+
logDebug("LoadContext", `Reminder injected: ${reminder ? reminder.length : 0} chars`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logError("LoadContext:reminder", err);
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PreToolUse — Guards against dangerous commands.
|
|
3
|
+
* Returns JSON { decision: "block", reason: "..." } to block, or exits silently to allow.
|
|
4
|
+
*
|
|
5
|
+
* Fail-open design: if anything goes wrong, the command is allowed through.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { checkBashCommand, checkFilePath, WARN_COMMANDS } from "./lib/security";
|
|
9
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
10
|
+
|
|
11
|
+
interface ToolUseInput {
|
|
12
|
+
tool_name: string;
|
|
13
|
+
tool_input: {
|
|
14
|
+
command?: string;
|
|
15
|
+
file_path?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const input = await readStdinJSON<ToolUseInput>();
|
|
21
|
+
if (!input) process.exit(0);
|
|
22
|
+
|
|
23
|
+
const { tool_name, tool_input } = input;
|
|
24
|
+
|
|
25
|
+
// Check Bash commands
|
|
26
|
+
if (tool_name === "Bash" && tool_input.command) {
|
|
27
|
+
const reason = checkBashCommand(tool_input.command);
|
|
28
|
+
if (reason) {
|
|
29
|
+
console.log(JSON.stringify({ decision: "block", reason: `Blocked: ${reason}` }));
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const pattern of WARN_COMMANDS) {
|
|
34
|
+
if (pattern.test(tool_input.command)) {
|
|
35
|
+
// Log but don't block — Claude Code's own permission system handles confirmation
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check file path operations (Write, Edit)
|
|
42
|
+
if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path) {
|
|
43
|
+
const fileReason = checkFilePath(tool_input.file_path);
|
|
44
|
+
if (fileReason) {
|
|
45
|
+
console.log(JSON.stringify({ decision: "block", reason: fileReason }));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Fail open
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PreToolUse (Skill) — Blocks false-positive skill invocations.
|
|
3
|
+
*
|
|
4
|
+
* keybindings-help appears first in every skills list and triggers on
|
|
5
|
+
* virtually any ambiguous prompt due to position bias + the Skill tool's
|
|
6
|
+
* aggressive "BLOCKING REQUIREMENT" language. This hook blocks it unless
|
|
7
|
+
* the user explicitly asked for keybinding help.
|
|
8
|
+
*
|
|
9
|
+
* Fail-open: on any error, the skill is allowed through.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
13
|
+
|
|
14
|
+
const BLOCKED_SKILLS = ["keybindings-help"];
|
|
15
|
+
|
|
16
|
+
interface SkillInput {
|
|
17
|
+
tool_name: string;
|
|
18
|
+
tool_input: {
|
|
19
|
+
skill?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const input = await readStdinJSON<SkillInput>();
|
|
25
|
+
if (!input) process.exit(0);
|
|
26
|
+
|
|
27
|
+
const skill = (input.tool_input?.skill || "").toLowerCase().trim();
|
|
28
|
+
|
|
29
|
+
if (BLOCKED_SKILLS.includes(skill)) {
|
|
30
|
+
console.log(
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
decision: "block",
|
|
33
|
+
reason:
|
|
34
|
+
'BLOCKED: "keybindings-help" is a known false-positive triggered by position bias. ' +
|
|
35
|
+
"The user did NOT ask about keybindings. Continue with their ACTUAL request.",
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Fail open
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: Stop — Single entry point for all stop-event handling.
|
|
3
|
+
* Fans out to independent handlers via Promise.allSettled.
|
|
4
|
+
*
|
|
5
|
+
* stdin: JSON object with { session_id, transcript_path, last_assistant_message, ... }
|
|
6
|
+
* Transcript is read from the file at transcript_path, NOT from stdin.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logError } from "./lib/log";
|
|
10
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
11
|
+
import { runStopHandlers } from "./lib/stop";
|
|
12
|
+
import { readTranscriptFile } from "./lib/transcript";
|
|
13
|
+
|
|
14
|
+
interface StopHookInput {
|
|
15
|
+
session_id: string;
|
|
16
|
+
transcript_path: string;
|
|
17
|
+
last_assistant_message?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const input = await readStdinJSON<StopHookInput>();
|
|
21
|
+
if (!input?.transcript_path) {
|
|
22
|
+
logError("StopOrchestrator", "No transcript_path in hook input");
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Read the actual transcript from the file on disk
|
|
27
|
+
const messages = readTranscriptFile(input.transcript_path);
|
|
28
|
+
if (messages.length < 2) process.exit(0);
|
|
29
|
+
|
|
30
|
+
// Serialize and run handlers
|
|
31
|
+
const transcript = JSON.stringify(messages);
|
|
32
|
+
await runStopHandlers(transcript, {
|
|
33
|
+
lastAssistantMessage: input.last_assistant_message,
|
|
34
|
+
sessionId: input.session_id,
|
|
35
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: UserPromptSubmit — Single entry point for all prompt-event handling.
|
|
3
|
+
* Fans out to independent handlers via Promise.allSettled.
|
|
4
|
+
*
|
|
5
|
+
* Handlers:
|
|
6
|
+
* - rating: capture explicit/implicit sentiment ratings
|
|
7
|
+
* - session-name: generate 4-word session headline on first prompt
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { captureRating } from "./handlers/rating";
|
|
11
|
+
import { captureSessionName } from "./handlers/session-name";
|
|
12
|
+
import { logDebug, logError } from "./lib/log";
|
|
13
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
14
|
+
|
|
15
|
+
interface PromptSubmitInput {
|
|
16
|
+
prompt: string;
|
|
17
|
+
session_id?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const input = await readStdinJSON<PromptSubmitInput>();
|
|
21
|
+
logDebug("UserPromptOrchestrator", `Input: ${JSON.stringify(input).slice(0, 200)}`);
|
|
22
|
+
if (!input?.prompt) process.exit(0);
|
|
23
|
+
|
|
24
|
+
const results = await Promise.allSettled([
|
|
25
|
+
captureRating(input.prompt, input.session_id),
|
|
26
|
+
captureSessionName(input.prompt, input.session_id ?? ""),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const handlerNames = ["rating", "session-name"];
|
|
30
|
+
for (let i = 0; i < results.length; i++) {
|
|
31
|
+
const r = results[i];
|
|
32
|
+
if (r.status === "rejected") {
|
|
33
|
+
logError(`UserPromptOrchestrator:${handlerNames[i]}`, r.reason);
|
|
34
|
+
}
|
|
35
|
+
}
|