portable-agent-layer 0.88.1 → 0.89.1

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.
Files changed (87) hide show
  1. package/README.md +8 -5
  2. package/assets/agents/gemini-researcher.md +13 -0
  3. package/assets/agents/grok-researcher.md +13 -0
  4. package/assets/agents/multi-perspective-researcher.md +12 -0
  5. package/assets/agents/perplexity-researcher.md +13 -0
  6. package/assets/agents/skill-author.md +17 -0
  7. package/assets/agents/subagent-author.md +18 -1
  8. package/assets/skills/create-subagent/SKILL.md +3 -2
  9. package/assets/skills/create-subagent/authoring-guide.md +16 -0
  10. package/assets/templates/PAL/MEMORY_SYSTEM.md +1 -1
  11. package/assets/templates/PAL/README.md +6 -5
  12. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +4 -1
  13. package/assets/templates/hooks.antigravity.json +42 -0
  14. package/assets/templates/settings.antigravity.json +44 -0
  15. package/package.json +1 -1
  16. package/src/cli/builtin-tools.ts +28 -23
  17. package/src/cli/doctor/agents.ts +64 -64
  18. package/src/cli/identity.ts +26 -20
  19. package/src/cli/index.ts +109 -312
  20. package/src/cli/knowledge.ts +160 -201
  21. package/src/cli/ledger-command.ts +69 -0
  22. package/src/cli/ledger.ts +20 -101
  23. package/src/cli/personal-context.ts +15 -12
  24. package/src/cli/rule.ts +29 -29
  25. package/src/cli/server.ts +33 -52
  26. package/src/cli/session-agent.ts +7 -9
  27. package/src/cli/skill.ts +73 -64
  28. package/src/cli/subagent.ts +51 -58
  29. package/src/cli/tree.ts +177 -0
  30. package/src/hooks/InvocationContext.ts +26 -0
  31. package/src/hooks/LedgerCommit.ts +4 -2
  32. package/src/hooks/LedgerSnapshot.ts +2 -0
  33. package/src/hooks/SecurityValidator.ts +2 -0
  34. package/src/hooks/StopOrchestrator.ts +12 -2
  35. package/src/hooks/UserPromptOrchestrator.ts +3 -15
  36. package/src/hooks/handlers/context-digests.ts +21 -21
  37. package/src/hooks/handlers/readme-sync.ts +6 -2
  38. package/src/hooks/lib/agent-definition.ts +6 -16
  39. package/src/hooks/lib/agent-registry.ts +88 -0
  40. package/src/hooks/lib/agent.ts +47 -23
  41. package/src/hooks/lib/antigravity-inference.ts +84 -0
  42. package/src/hooks/lib/antigravity-transcript.ts +129 -0
  43. package/src/hooks/lib/claim-check.ts +13 -2
  44. package/src/hooks/lib/claim-log.ts +5 -1
  45. package/src/hooks/lib/claude-md.ts +21 -1
  46. package/src/hooks/lib/context.ts +5 -1
  47. package/src/hooks/lib/export-manifest.ts +23 -0
  48. package/src/hooks/lib/export.ts +1 -24
  49. package/src/hooks/lib/hook-turn.ts +25 -2
  50. package/src/hooks/lib/import-merge.ts +1 -1
  51. package/src/hooks/lib/inference.ts +77 -1
  52. package/src/hooks/lib/invocation-context.ts +99 -0
  53. package/src/hooks/lib/ledger-hook.ts +14 -6
  54. package/src/hooks/lib/models.ts +34 -17
  55. package/src/hooks/lib/paths.ts +39 -6
  56. package/src/hooks/lib/prompt-capture.ts +28 -0
  57. package/src/hooks/lib/readme-sync.ts +3 -31
  58. package/src/hooks/lib/security-gate.ts +10 -3
  59. package/src/hooks/lib/semi-static.ts +13 -3
  60. package/src/hooks/lib/session-context.ts +3 -1
  61. package/src/hooks/lib/stop.ts +1 -0
  62. package/src/hooks/lib/transcript.ts +6 -3
  63. package/src/targets/agent-render.ts +3 -3
  64. package/src/targets/antigravity/install.ts +68 -0
  65. package/src/targets/antigravity/uninstall.ts +66 -0
  66. package/src/targets/lib.ts +81 -64
  67. package/src/targets/opencode/install.ts +2 -0
  68. package/src/targets/opencode/permission.ts +39 -0
  69. package/src/targets/opencode/uninstall.ts +9 -8
  70. package/src/tools/agent/algorithm-reflect.ts +70 -62
  71. package/src/tools/agent/algorithm-synthesize.ts +39 -14
  72. package/src/tools/agent/analyze.ts +31 -36
  73. package/src/tools/agent/handoff-note.ts +51 -54
  74. package/src/tools/agent/interaction.ts +32 -28
  75. package/src/tools/agent/project.ts +300 -287
  76. package/src/tools/agent/relationship-note.ts +40 -36
  77. package/src/tools/agent/relationship-reflect.ts +23 -32
  78. package/src/tools/agent/synthesize.ts +26 -27
  79. package/src/tools/agent/thread.ts +42 -51
  80. package/src/tools/agent/wisdom-frame.ts +45 -38
  81. package/src/tools/control-room/server.ts +0 -2
  82. package/src/tools/control-room/ui/dist/assets/{index-BZMDzWFa.js → index-mFpXpiBj.js} +1 -1
  83. package/src/tools/control-room/ui/dist/index.html +1 -1
  84. package/src/tools/control-room/ui/screens/log.tsx +10 -1
  85. package/src/tools/lib/command.ts +275 -0
  86. package/src/tools/subagent-doctor.ts +74 -9
  87. package/src/tools/token-cost.ts +18 -17
package/src/cli/ledger.ts CHANGED
@@ -4,14 +4,8 @@
4
4
  * Thin presentation layer over src/tools/ledger/query.ts. Owns formatting and
5
5
  * argv parsing only; every question about the records themselves is answered
6
6
  * there.
7
- *
8
- * Subcommands:
9
- * log [filters] Matching actions, oldest first
10
- * show <id> One action in full, with its change and current standing
11
- * stats [filters] Counts by outcome, runtime, actor, tool and target
12
7
  */
13
8
 
14
- import { parseArgs } from "node:util";
15
9
  import type { LedgerEntry } from "../hooks/lib/ledger";
16
10
  import {
17
11
  type ChainVerdict,
@@ -28,75 +22,22 @@ import {
28
22
  standing,
29
23
  summarize,
30
24
  } from "../tools/ledger/query";
25
+ import { UsageError } from "../tools/lib/command";
31
26
 
32
- const FILTER_OPTIONS = {
33
- project: { type: "string" },
34
- since: { type: "string" },
35
- until: { type: "string" },
36
- actor: { type: "string" },
37
- machine: { type: "string" },
38
- runtime: { type: "string" },
39
- outcome: { type: "string" },
40
- tool: { type: "string" },
41
- target: { type: "string" },
42
- limit: { type: "string" },
43
- json: { type: "boolean" },
44
- } as const;
45
-
46
- export async function runLedger(args: string[]): Promise<number> {
47
- const [sub, ...rest] = args;
48
- switch (sub) {
49
- case "log":
50
- return cmdLog(rest);
51
- case "show":
52
- return cmdShow(rest);
53
- case "stats":
54
- return cmdStats(rest);
55
- case undefined:
56
- case "help":
57
- case "--help":
58
- case "-h":
59
- showHelp();
60
- return 0;
61
- default:
62
- console.error(`Unknown subcommand: ${sub}\n`);
63
- showHelp();
64
- return 1;
65
- }
27
+ export function ledgerLog(values: Record<string, unknown>): number {
28
+ return cmdLog(buildFilter(values), values.json === true);
66
29
  }
67
30
 
68
- function showHelp(): void {
69
- console.log(`
70
- Usage:
71
- pal cli ledger <subcommand> [filters]
72
-
73
- Subcommands:
74
- log [filters] Matching actions, oldest first
75
- show <id> One action in full: change, target, standing
76
- stats [filters] Counts by outcome, runtime, actor, tool, target
77
-
78
- Filters:
79
- --project <slug> Actions against a registered project
80
- --since <7d|2026-09-01> A duration back from now, or a date
81
- --until <date> Upper bound on the timestamp
82
- --actor <id> Who caused it
83
- --machine <id> Which install wrote it
84
- --runtime <agent> claude, cursor, codex, copilot, opencode
85
- --outcome <applied|failed|denied>
86
- --tool <Edit|Write>
87
- --target <substring> Match anywhere in the recorded path
88
- --limit <n> Keep the newest n matches
89
- --json Machine-readable output
31
+ export function ledgerShow(id: string, json: boolean): number {
32
+ return cmdShow(id, json);
33
+ }
90
34
 
91
- Examples:
92
- pal cli ledger log --project portable-agent-layer --since 7d
93
- pal cli ledger log --target memory/ --outcome applied
94
- pal cli ledger stats --since 24h
95
- `);
35
+ export function ledgerStats(values: Record<string, unknown>): number {
36
+ return cmdStats(buildFilter(values), values.json === true);
96
37
  }
97
38
 
98
39
  /** A filter that silently ignored an unparseable window would answer the wrong question. */
99
- function buildFilter(values: Record<string, unknown>): LedgerFilter | string {
40
+ function buildFilter(values: Record<string, unknown>): LedgerFilter {
100
41
  const filter: LedgerFilter = {};
101
42
  for (const key of [
102
43
  "project",
@@ -115,43 +56,27 @@ function buildFilter(values: Record<string, unknown>): LedgerFilter | string {
115
56
  const spec = values[key];
116
57
  if (typeof spec !== "string") continue;
117
58
  const at = parseSince(spec);
118
- if (!at) return `Unrecognised --${key}: ${spec} (use 7d, 24h, or a date)`;
59
+ if (!at)
60
+ throw new UsageError(`Unrecognised --${key}: ${spec} (use 7d, 24h, or a date)`);
119
61
  filter[key] = at;
120
62
  }
121
63
 
122
64
  if (typeof values.limit === "string") {
123
65
  const limit = Number(values.limit);
124
66
  if (!Number.isInteger(limit) || limit < 1)
125
- return `--limit must be a positive integer`;
67
+ throw new UsageError("--limit must be a positive integer");
126
68
  filter.limit = limit;
127
69
  }
128
70
  return filter;
129
71
  }
130
72
 
131
- function parseFilters(args: string[]): { filter: LedgerFilter; json: boolean } | string {
132
- try {
133
- const { values } = parseArgs({
134
- args,
135
- options: FILTER_OPTIONS,
136
- allowPositionals: true,
137
- });
138
- const filter = buildFilter(values);
139
- return typeof filter === "string" ? filter : { filter, json: values.json === true };
140
- } catch (error) {
141
- return error instanceof Error ? error.message : String(error);
142
- }
143
- }
144
-
145
73
  function shortId(id: string): string {
146
74
  return id.slice(0, 11).padEnd(11);
147
75
  }
148
76
 
149
- function cmdLog(args: string[]): number {
150
- const parsed = parseFilters(args);
151
- if (typeof parsed === "string") return fail(parsed);
152
-
153
- const entries = queryLedger(parsed.filter);
154
- if (parsed.json) {
77
+ function cmdLog(filter: LedgerFilter, json: boolean): number {
78
+ const entries = queryLedger(filter);
79
+ if (json) {
155
80
  console.log(JSON.stringify(entries, null, 2));
156
81
  return 0;
157
82
  }
@@ -218,14 +143,11 @@ function printChange(entry: LedgerEntry): void {
218
143
  }
219
144
  }
220
145
 
221
- function cmdShow(args: string[]): number {
222
- const [id, ...rest] = args;
223
- if (!id) return fail("Usage: pal cli ledger show <id>");
224
-
146
+ function cmdShow(id: string, json: boolean): number {
225
147
  const entry = findEntry(id);
226
148
  if (!entry) return fail(`No action with id ${id}`);
227
149
 
228
- if (rest.includes("--json")) {
150
+ if (json) {
229
151
  console.log(
230
152
  JSON.stringify(
231
153
  {
@@ -280,12 +202,9 @@ function printTally(label: string, counts: Record<string, number>): void {
280
202
  console.log(` ${String(count).padStart(6)} ${key}`);
281
203
  }
282
204
 
283
- function cmdStats(args: string[]): number {
284
- const parsed = parseFilters(args);
285
- if (typeof parsed === "string") return fail(parsed);
286
-
287
- const stats = summarize(queryLedger(parsed.filter));
288
- if (parsed.json) {
205
+ function cmdStats(filter: LedgerFilter, json: boolean): number {
206
+ const stats = summarize(queryLedger(filter));
207
+ if (json) {
289
208
  console.log(JSON.stringify(stats, null, 2));
290
209
  return 0;
291
210
  }
@@ -1,10 +1,6 @@
1
1
  /**
2
2
  * pal cli telos / pal cli timezone — the personal context PAL carries about you.
3
3
  *
4
- * pal cli telos Which TELOS topics are answered, in interview order
5
- * pal cli timezone Show the configured timezone
6
- * pal cli timezone <zone> Set it, if Intl recognises the name
7
- *
8
4
  * Both exist for the onboarding skill. pal-settings.json is hook-protected, so
9
5
  * an agent cannot write the timezone itself, and a skill that restated the
10
6
  * answered/unanswered rule in prose would drift from the code that decides it.
@@ -18,14 +14,21 @@ import {
18
14
  import { telosStatus } from "../hooks/lib/telos-topics";
19
15
  import { canonicalTimeZone } from "../hooks/lib/wall-clock";
20
16
  import { log } from "../targets/lib";
17
+ import { leaf } from "../tools/lib/command";
21
18
 
22
- export function runTelos(args: string[]): number {
23
- if (args.length > 0) {
24
- log.error(`Unknown telos argument: ${args[0]}`);
25
- log.info("Usage: pal cli telos");
26
- return 1;
27
- }
19
+ export const telosCommand = leaf({
20
+ summary: "Which TELOS topics are answered, in interview order",
21
+ run: printTelosStatus,
22
+ });
23
+
24
+ export const timezoneCommand = leaf({
25
+ summary: "Show the configured timezone, or set it to an IANA name",
26
+ args: "[zone...]",
27
+ details: "Example: pal cli timezone Europe/Budapest",
28
+ run: ({ positionals }) => timezone(positionals),
29
+ });
28
30
 
31
+ function printTelosStatus(): number {
29
32
  const topics = telosStatus();
30
33
  for (const topic of topics) {
31
34
  const mark = topic.answered ? "answered " : "unanswered";
@@ -39,8 +42,8 @@ export function runTelos(args: string[]): number {
39
42
  return 0;
40
43
  }
41
44
 
42
- export function runTimezone(args: string[]): number {
43
- const requested = args.join(" ").trim();
45
+ function timezone(words: string[]): number {
46
+ const requested = words.join(" ").trim();
44
47
 
45
48
  if (!requested) {
46
49
  const current = identity().principal.timezone;
package/src/cli/rule.ts CHANGED
@@ -14,24 +14,39 @@ import {
14
14
  readRules,
15
15
  type TriggerProof,
16
16
  } from "../hooks/lib/adaptation-rules";
17
+ import { group, leaf, runCommand } from "../tools/lib/command";
17
18
 
18
- const DECISIONS: Record<string, Exclude<RuleStatus, "draft">> = {
19
- approve: "approved",
20
- deny: "denied",
21
- };
19
+ export const ruleCommand = group({
20
+ summary: "Review the rules PAL drafted from your corrections",
21
+ commands: {
22
+ list: leaf({
23
+ summary: "Drafts waiting for a decision",
24
+ options: {
25
+ all: { type: "boolean", description: "Every rule, not only the drafts" },
26
+ json: { type: "boolean", description: "Machine-readable output" },
27
+ },
28
+ run: ({ values }) => cmdList(values.all === true, values.json === true),
29
+ }),
30
+ approve: leaf({
31
+ summary: "Approve a draft so it steers",
32
+ args: "<id>",
33
+ run: ({ positionals }) => cmdDecide(positionals[0], "approved"),
34
+ }),
35
+ deny: leaf({
36
+ summary: "Deny a draft; it stays on record so it is not drafted again",
37
+ args: "<id>",
38
+ run: ({ positionals }) => cmdDecide(positionals[0], "denied"),
39
+ }),
40
+ },
41
+ });
22
42
 
23
- export async function runRule(args: string[]): Promise<number> {
24
- const [sub, ...rest] = args;
25
- if (sub === "list") return cmdList(rest);
26
- if (sub && sub in DECISIONS) return cmdDecide(rest[0], DECISIONS[sub]);
27
- showHelp();
28
- return sub === undefined || sub === "help" ? 0 : 1;
43
+ export function runRule(args: string[]): Promise<number> {
44
+ return runCommand(ruleCommand, args, ["pal", "cli", "rule"]);
29
45
  }
30
46
 
31
- function cmdList(args: string[]): number {
32
- const all = args.includes("--all");
47
+ function cmdList(all: boolean, json: boolean): number {
33
48
  const rules = readRules().filter((r) => all || r.status === "draft");
34
- if (args.includes("--json")) {
49
+ if (json) {
35
50
  console.log(JSON.stringify(rules, null, 2));
36
51
  return 0;
37
52
  }
@@ -64,14 +79,7 @@ function decisionLine(id: string, decision: string, rule: AdaptationRule): strin
64
79
  return `Rule ${rule.id} now triggers on: ${rule.trigger.pattern} (widened by ${id})`;
65
80
  }
66
81
 
67
- function cmdDecide(
68
- id: string | undefined,
69
- decision: Exclude<RuleStatus, "draft">
70
- ): number {
71
- if (!id) {
72
- console.error("Give the rule id: pal cli rule approve|deny <id>");
73
- return 1;
74
- }
82
+ function cmdDecide(id: string, decision: Exclude<RuleStatus, "draft">): number {
75
83
  const result = decideRule(id, decision);
76
84
  if (!result.ok) {
77
85
  console.error(result.reason);
@@ -80,11 +88,3 @@ function cmdDecide(
80
88
  console.log(decisionLine(id, decision, result.rule));
81
89
  return 0;
82
90
  }
83
-
84
- function showHelp(): void {
85
- console.log(`pal cli rule — review adaptation rules drafted from your corrections
86
-
87
- pal cli rule list [--all] [--json] Drafts waiting for a decision
88
- pal cli rule approve <id> Approve a draft so it steers
89
- pal cli rule deny <id> Deny a draft; it is not drafted again`);
90
- }
package/src/cli/server.ts CHANGED
@@ -8,11 +8,12 @@
8
8
 
9
9
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
- import { parseArgs } from "node:util";
12
11
  import { spawnDetachedInference } from "../hooks/lib/detached-inference";
13
12
  import { paths } from "../hooks/lib/paths";
14
- import { DEFAULT_PORT, LOOPBACK, type ServerStatus } from "../tools/control-room/server";
13
+ import type { ServerStatus } from "../tools/control-room/server";
14
+ import { DEFAULT_PORT, LOOPBACK } from "../tools/control-room/server-config";
15
15
  import { BUILD_COMMAND, buildPage, isBuilt } from "../tools/control-room/static";
16
+ import { group, leaf, UsageError } from "../tools/lib/command";
16
17
 
17
18
  interface ServerState {
18
19
  pid: number;
@@ -30,44 +31,29 @@ const SERVER_SCRIPT = resolve(
30
31
  const STARTUP_TIMEOUT_MS = 3000;
31
32
  const PROBE_TIMEOUT_MS = 500;
32
33
 
33
- export async function runServer(args: string[]): Promise<number> {
34
- const [sub, ...rest] = args;
35
- switch (sub) {
36
- case "start":
37
- return cmdStart(rest);
38
- case "stop":
39
- return cmdStop();
40
- case "restart":
41
- return cmdRestart();
42
- case "status":
43
- return cmdStatus();
44
- case undefined:
45
- case "help":
46
- case "--help":
47
- case "-h":
48
- showHelp();
49
- return 0;
50
- default:
51
- console.error(`Unknown subcommand: ${sub}\n`);
52
- showHelp();
53
- return 1;
54
- }
55
- }
56
-
57
- function showHelp(): void {
58
- console.log(`
59
- Usage:
60
- pal cli server <subcommand>
61
-
62
- Subcommands:
63
- start [--port <n>] Start the control room in the background (default port ${DEFAULT_PORT})
64
- stop Stop it
65
- restart Replace a running one — the API only changes when the process does
66
- status Show whether it is running, and where
67
-
68
- The page listens on ${LOOPBACK} only.
69
- `);
70
- }
34
+ export const serverCommand = group({
35
+ summary: "Start, stop and inspect the local control room",
36
+ details: `The page listens on ${LOOPBACK} only.`,
37
+ commands: {
38
+ start: leaf({
39
+ summary: "Start the control room in the background",
40
+ options: {
41
+ port: {
42
+ type: "string",
43
+ value: "<n>",
44
+ description: `Port to listen on (default ${DEFAULT_PORT})`,
45
+ },
46
+ },
47
+ run: ({ values }) => cmdStart(parsePort(values.port)),
48
+ }),
49
+ stop: leaf({ summary: "Stop it", run: cmdStop }),
50
+ restart: leaf({
51
+ summary: "Replace a running one — the API only changes when the process does",
52
+ run: cmdRestart,
53
+ }),
54
+ status: leaf({ summary: "Show whether it is running, and where", run: cmdStatus }),
55
+ },
56
+ });
71
57
 
72
58
  function url(port: number): string {
73
59
  return `http://${LOOPBACK}:${port}/`;
@@ -122,19 +108,14 @@ async function waitUntilAnswering(port: number): Promise<ServerStatus | null> {
122
108
  return null;
123
109
  }
124
110
 
125
- function parsePort(args: string[]): number | string {
126
- const { values } = parseArgs({ args, options: { port: { type: "string" } } });
127
- if (values.port === undefined) return DEFAULT_PORT;
128
- const port = Number(values.port);
129
- return Number.isInteger(port) && port > 0 && port < 65536
130
- ? port
131
- : `--port must be a port number, got ${values.port}`;
111
+ function parsePort(value: string | undefined): number {
112
+ if (value === undefined) return DEFAULT_PORT;
113
+ const port = Number(value);
114
+ if (Number.isInteger(port) && port > 0 && port < 65536) return port;
115
+ throw new UsageError(`--port must be a port number, got ${value}`);
132
116
  }
133
117
 
134
- async function cmdStart(args: string[]): Promise<number> {
135
- const port = parsePort(args);
136
- if (typeof port === "string") return fail(port);
137
-
118
+ async function cmdStart(port: number): Promise<number> {
138
119
  const running = await runningServer();
139
120
  if (running) {
140
121
  console.log(`Already running at ${url(running.port)} (pid ${running.pid})`);
@@ -171,7 +152,7 @@ async function cmdRestart(): Promise<number> {
171
152
  return 0;
172
153
  }
173
154
  await cmdStop();
174
- return cmdStart(["--port", String(running.port)]);
155
+ return cmdStart(running.port);
175
156
  }
176
157
 
177
158
  /**
@@ -1,14 +1,12 @@
1
+ import { AGENT_REGISTRY, LAUNCH_PRIORITY } from "../hooks/lib/agent-registry";
1
2
  import { findBinaryOnPath } from "../hooks/lib/which";
2
3
 
3
- const SESSION_AGENT_BINARIES = [
4
- "claude",
5
- "codex",
6
- "cursor-agent",
7
- "copilot",
8
- "opencode",
9
- ] as const;
4
+ export type SessionAgent =
5
+ (typeof AGENT_REGISTRY)[(typeof LAUNCH_PRIORITY)[number]]["binary"];
10
6
 
11
- export type SessionAgent = (typeof SESSION_AGENT_BINARIES)[number];
7
+ const SESSION_AGENT_BINARIES: readonly SessionAgent[] = LAUNCH_PRIORITY.map(
8
+ (agent) => AGENT_REGISTRY[agent].binary
9
+ );
12
10
 
13
11
  export function findSessionAgent(): SessionAgent | null {
14
12
  return (
@@ -17,4 +15,4 @@ export function findSessionAgent(): SessionAgent | null {
17
15
  }
18
16
 
19
17
  export const NO_SESSION_AGENT_MESSAGE =
20
- "No supported agent found. Install Claude Code, Codex, Cursor CLI, Copilot CLI or opencode.";
18
+ "No supported agent found. Install Claude Code, Codex, Cursor CLI, Copilot CLI, opencode or Antigravity CLI.";
package/src/cli/skill.ts CHANGED
@@ -1,17 +1,7 @@
1
1
  /**
2
- * pal cli skill — manage personal skills under ~/.pal/skills/.
3
- *
4
- * pal cli skill link <name> Link an existing ~/.pal/skills/<name>/ into
5
- * every installed agent so it is discoverable.
6
- * pal cli skill doctor <name> Evaluate ~/.pal/skills/<name>/ against the
7
- * skill-authoring best practices.
8
- * pal cli skill doctor --all Evaluate every installed skill, one line each.
9
- * pal cli skill author-model Print the flagship model configured to author
10
- * skills for the active agent (empty if none).
11
- * pal cli skill run <skill> <tool> [-- args]
12
- * Run ~/.pal/skills/<skill>/tools/<tool>, so a
13
- * SKILL.md can name the tool instead of a path
14
- * with a tilde no Windows shell expands.
2
+ * pal cli skill — manage personal skills under ~/.pal/skills/. `skill run`
3
+ * exists so a SKILL.md can name a tool instead of a path with a tilde no
4
+ * Windows shell expands.
15
5
  */
16
6
 
17
7
  import { spawnSync } from "node:child_process";
@@ -22,6 +12,7 @@ import { flagshipAuthorModel } from "../hooks/lib/models";
22
12
  import { withPalEnv } from "../hooks/lib/pal-env";
23
13
  import { isInside, palHome, palPkg } from "../hooks/lib/paths";
24
14
  import { linkPersonalSkill, log } from "../targets/lib";
15
+ import { group, leaf, UsageError } from "../tools/lib/command";
25
16
  import {
26
17
  formatReport,
27
18
  formatSummary,
@@ -105,60 +96,78 @@ function isShippedTool(path: string): boolean {
105
96
  return existsSync(shipped) && isInside(realpathSync(shipped), realpathSync(path));
106
97
  }
107
98
 
108
- export async function runSkill(args: string[]): Promise<number> {
109
- const [sub, name] = args;
110
-
111
- if (sub === "run") {
112
- const [, skill, tool, ...rest] = args;
113
- if (!skill || !tool) {
114
- log.error("Usage: pal cli skill run <skill> <tool> [-- args]");
115
- return 1;
116
- }
117
- return runSkillTool(skill, tool, rest[0] === "--" ? rest.slice(1) : rest);
118
- }
119
-
120
- if (sub === "author-model") {
121
- const model = flagshipAuthorModel(getActiveAgent());
122
- if (model) console.log(model);
123
- return 0;
124
- }
99
+ function doctorOne(name: string): number {
100
+ const report = lintSkill(resolveSkillDir(name));
101
+ console.log(formatReport(report));
102
+ return report.errors > 0 ? 1 : 0;
103
+ }
125
104
 
126
- if (sub === "doctor") {
127
- if (name === "--all") return doctorAll();
128
- if (!name) {
129
- log.error("Usage: pal cli skill doctor <skill-dir-or-name|--all>");
130
- return 1;
131
- }
132
- const report = lintSkill(resolveSkillDir(name));
133
- console.log(formatReport(report));
134
- return report.errors > 0 ? 1 : 0;
135
- }
105
+ function doctor(name: string | undefined, all: boolean): number {
106
+ if (all && name) throw new UsageError("give a skill name or --all, not both");
107
+ if (all) return doctorAll();
108
+ if (!name) throw new UsageError("missing <name> (or --all)");
109
+ return doctorOne(name);
110
+ }
136
111
 
137
- if (sub === "link") {
138
- if (!name) {
139
- log.error("Usage: pal cli skill link <name>");
140
- return 1;
141
- }
142
- try {
143
- const linked = linkPersonalSkill(name);
144
- if (linked.length === 0) {
145
- log.warn(
146
- `'${name}' linked to no per-skill agents (none installed). ` +
147
- "It is still discoverable by opencode via ~/.pal/skills/."
148
- );
149
- } else {
150
- log.success(
151
- `Linked '${name}' into: ${linked.join(", ")} ` +
152
- "(opencode: auto via ~/.pal/skills/)"
153
- );
154
- }
155
- return 0;
156
- } catch (e) {
157
- log.error(e instanceof Error ? e.message : String(e));
158
- return 1;
112
+ function link(name: string): number {
113
+ try {
114
+ const linked = linkPersonalSkill(name);
115
+ if (linked.length === 0) {
116
+ log.warn(
117
+ `'${name}' linked to no per-skill agents (none installed). ` +
118
+ "It is still discoverable by opencode via ~/.pal/skills/."
119
+ );
120
+ } else {
121
+ log.success(
122
+ `Linked '${name}' into: ${linked.join(", ")} ` +
123
+ "(opencode: auto via ~/.pal/skills/)"
124
+ );
159
125
  }
126
+ return 0;
127
+ } catch (e) {
128
+ log.error(e instanceof Error ? e.message : String(e));
129
+ return 1;
160
130
  }
131
+ }
161
132
 
162
- log.error("Usage: pal cli skill <run|link|doctor|author-model> [name|--all]");
163
- return 1;
133
+ export function printAuthorModel(): number {
134
+ const model = flagshipAuthorModel(getActiveAgent());
135
+ if (model) console.log(model);
136
+ return 0;
164
137
  }
138
+
139
+ export const skillCommand = group({
140
+ summary: "Manage personal skills under ~/.pal/skills/",
141
+ commands: {
142
+ run: leaf({
143
+ summary:
144
+ "Run ~/.pal/skills/<skill>/tools/<tool>; every later argument goes to the tool",
145
+ args: "<skill> <tool>",
146
+ passThrough: true,
147
+ details:
148
+ "Arguments after <tool> reach the tool untouched; '--' before them is optional.",
149
+ run: ({ positionals, passedThrough }) =>
150
+ runSkillTool(positionals[0], positionals[1], passedThrough),
151
+ }),
152
+ link: leaf({
153
+ summary: "Link ~/.pal/skills/<name>/ into every installed agent",
154
+ args: "<name>",
155
+ run: ({ positionals }) => link(positionals[0]),
156
+ }),
157
+ doctor: leaf({
158
+ summary: "Check a skill against the skill-authoring best practices",
159
+ args: "[name]",
160
+ options: {
161
+ all: {
162
+ type: "boolean",
163
+ description: "Check every installed skill, one line each",
164
+ },
165
+ },
166
+ run: ({ positionals, values }) => doctor(positionals[0], values.all === true),
167
+ }),
168
+ "author-model": leaf({
169
+ summary: "Print the flagship model that authors skills for the active agent",
170
+ run: printAuthorModel,
171
+ }),
172
+ },
173
+ });