portable-agent-layer 0.88.0 → 0.89.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.
Files changed (88) 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/inject-retrieval.ts +7 -5
  38. package/src/hooks/handlers/readme-sync.ts +6 -2
  39. package/src/hooks/handlers/session-name.ts +3 -12
  40. package/src/hooks/lib/agent-definition.ts +6 -16
  41. package/src/hooks/lib/agent-registry.ts +88 -0
  42. package/src/hooks/lib/agent.ts +47 -23
  43. package/src/hooks/lib/antigravity-inference.ts +84 -0
  44. package/src/hooks/lib/antigravity-transcript.ts +129 -0
  45. package/src/hooks/lib/claim-check.ts +13 -2
  46. package/src/hooks/lib/claim-log.ts +5 -1
  47. package/src/hooks/lib/claude-md.ts +21 -1
  48. package/src/hooks/lib/context.ts +5 -1
  49. package/src/hooks/lib/hook-turn.ts +25 -2
  50. package/src/hooks/lib/inference.ts +77 -1
  51. package/src/hooks/lib/invocation-context.ts +99 -0
  52. package/src/hooks/lib/ledger-hook.ts +14 -6
  53. package/src/hooks/lib/models.ts +34 -17
  54. package/src/hooks/lib/paths.ts +39 -6
  55. package/src/hooks/lib/prompt-capture.ts +28 -0
  56. package/src/hooks/lib/prompt-text.ts +1 -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/session-names.ts +20 -4
  62. package/src/hooks/lib/stop.ts +1 -0
  63. package/src/hooks/lib/transcript.ts +6 -3
  64. package/src/targets/agent-render.ts +3 -3
  65. package/src/targets/antigravity/install.ts +68 -0
  66. package/src/targets/antigravity/uninstall.ts +66 -0
  67. package/src/targets/lib.ts +81 -64
  68. package/src/targets/opencode/install.ts +2 -0
  69. package/src/targets/opencode/permission.ts +39 -0
  70. package/src/targets/opencode/uninstall.ts +9 -8
  71. package/src/tools/agent/algorithm-reflect.ts +70 -62
  72. package/src/tools/agent/algorithm-synthesize.ts +39 -14
  73. package/src/tools/agent/analyze.ts +31 -36
  74. package/src/tools/agent/handoff-note.ts +51 -54
  75. package/src/tools/agent/interaction.ts +32 -28
  76. package/src/tools/agent/project.ts +300 -287
  77. package/src/tools/agent/relationship-note.ts +40 -36
  78. package/src/tools/agent/relationship-reflect.ts +23 -32
  79. package/src/tools/agent/synthesize.ts +26 -27
  80. package/src/tools/agent/thread.ts +42 -51
  81. package/src/tools/agent/wisdom-frame.ts +45 -38
  82. package/src/tools/control-room/server.ts +0 -2
  83. package/src/tools/control-room/ui/dist/assets/{index-BZMDzWFa.js → index-mFpXpiBj.js} +1 -1
  84. package/src/tools/control-room/ui/dist/index.html +1 -1
  85. package/src/tools/control-room/ui/screens/log.tsx +10 -1
  86. package/src/tools/lib/command.ts +275 -0
  87. package/src/tools/subagent-doctor.ts +74 -9
  88. package/src/tools/token-cost.ts +18 -17
@@ -10,7 +10,7 @@
10
10
  rel="stylesheet"
11
11
  href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600&family=Barlow+Condensed:wght@400;500;600&display=swap"
12
12
  />
13
- <script type="module" crossorigin src="/assets/index-BZMDzWFa.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-mFpXpiBj.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/assets/index-DeglMbfS.css">
15
15
  </head>
16
16
  <body>
@@ -1,4 +1,5 @@
1
1
  import { useId, useState } from "react";
2
+ import type { AgentName } from "../../../../hooks/lib/agent-registry";
2
3
  import { PAGE_OUTCOMES, type PageOutcome } from "../../../ledger/outcomes";
3
4
  import type { LedgerView, LedgerViewRow } from "../../../ledger/view";
4
5
  import { Badge } from "../components/badge";
@@ -42,7 +43,15 @@ export const LOG_KEYS = [
42
43
  export type LogFilter = Record<(typeof LOG_KEYS)[number], string>;
43
44
 
44
45
  const AUTHORITIES = ["user", "agent"] as const;
45
- const RUNTIMES = ["claude", "cursor", "codex", "copilot", "opencode", "unknown"] as const;
46
+ const RUNTIMES = Object.keys({
47
+ claude: true,
48
+ cursor: true,
49
+ codex: true,
50
+ copilot: true,
51
+ opencode: true,
52
+ antigravity: true,
53
+ unknown: true,
54
+ } satisfies Record<AgentName | "unknown", true>);
46
55
 
47
56
  const EMPTY_FILTER: LogFilter = {
48
57
  project: "",
@@ -0,0 +1,275 @@
1
+ /**
2
+ * The `pal cli` command tree. A command declares its arguments and flags once;
3
+ * that declaration drives the parser, the `--help` page and the error a wrong
4
+ * call gets, so the three cannot drift apart.
5
+ */
6
+
7
+ import { type ParseArgsOptionDescriptor, parseArgs } from "node:util";
8
+
9
+ export interface OptionSpec extends ParseArgsOptionDescriptor {
10
+ description: string;
11
+ value?: string;
12
+ }
13
+
14
+ export type OptionSpecs = Record<string, OptionSpec>;
15
+
16
+ type ParsedValues<O extends OptionSpecs> = ReturnType<
17
+ typeof parseArgs<{ options: O; allowPositionals: true; strict: true }>
18
+ >["values"];
19
+
20
+ export interface CommandInput<O extends OptionSpecs = OptionSpecs> {
21
+ values: ParsedValues<O>;
22
+ positionals: string[];
23
+ passedThrough: string[];
24
+ argv: string[];
25
+ }
26
+
27
+ export interface LeafSpec<O extends OptionSpecs> {
28
+ summary: string;
29
+ args?: string;
30
+ options?: O;
31
+ details?: string;
32
+ passThrough?: boolean;
33
+ run(input: CommandInput<O>): unknown;
34
+ }
35
+
36
+ export interface Leaf extends Omit<LeafSpec<OptionSpecs>, "run"> {
37
+ kind: "leaf";
38
+ run(input: CommandInput): unknown;
39
+ }
40
+
41
+ export interface Group {
42
+ kind: "group";
43
+ summary: string;
44
+ commands: Record<string, Command>;
45
+ aliases?: Record<string, string>;
46
+ fallback?: string;
47
+ details?: string;
48
+ }
49
+
50
+ export type Command = Leaf | Group;
51
+
52
+ export class UsageError extends Error {}
53
+
54
+ export function leaf<const O extends OptionSpecs = Record<never, OptionSpec>>(
55
+ spec: LeafSpec<O>
56
+ ): Leaf {
57
+ return { kind: "leaf", ...spec } as unknown as Leaf;
58
+ }
59
+
60
+ export function group(spec: Omit<Group, "kind">): Group {
61
+ return { kind: "group", ...spec };
62
+ }
63
+
64
+ const HELP_FLAGS = new Set(["-h", "--help"]);
65
+ const HELP_OPTION: OptionSpec = {
66
+ type: "boolean",
67
+ short: "h",
68
+ description: "Show this help",
69
+ };
70
+
71
+ export async function runCommand(
72
+ command: Command,
73
+ args: string[],
74
+ path: string[]
75
+ ): Promise<number> {
76
+ return command.kind === "group"
77
+ ? runGroup(command, args, path)
78
+ : runLeaf(command, args, path);
79
+ }
80
+
81
+ async function runGroup(command: Group, args: string[], path: string[]): Promise<number> {
82
+ const [first, ...rest] = args;
83
+ const name = first === undefined ? command.fallback : first;
84
+ if (name === undefined || name === "help" || HELP_FLAGS.has(name)) {
85
+ console.log(helpText(command, path));
86
+ return 0;
87
+ }
88
+ const resolved = command.aliases?.[name] ?? name;
89
+ const sub = command.commands[resolved];
90
+ if (!sub) return usageFailure(`unknown command '${name}'`, command, path);
91
+ return runCommand(sub, rest, [...path, resolved]);
92
+ }
93
+
94
+ async function runLeaf(command: Leaf, args: string[], path: string[]): Promise<number> {
95
+ const { head, passedThrough } = splitPassThrough(command, args);
96
+ if (head.some((arg) => HELP_FLAGS.has(arg))) {
97
+ console.log(helpText(command, path));
98
+ return 0;
99
+ }
100
+ try {
101
+ const { values, positionals } = parseWithTextShielded(head, command.options ?? {});
102
+ checkPositionals(command.args, positionals);
103
+ return exitCode(
104
+ await command.run({ values, positionals, passedThrough, argv: head })
105
+ );
106
+ } catch (error) {
107
+ if (!isUsageError(error)) throw error;
108
+ const message =
109
+ error instanceof UsageError ? error.message : firstSentence(error.message);
110
+ return usageFailure(message, command, path);
111
+ }
112
+ }
113
+
114
+ const SHIELD = "\u0000text:";
115
+
116
+ function isFreeText(arg: string): boolean {
117
+ return /\s/.test(arg);
118
+ }
119
+
120
+ function parseWithTextShielded(args: string[], options: OptionSpecs) {
121
+ const unshield = (value: string) =>
122
+ value.startsWith(SHIELD)
123
+ ? (args[Number(value.slice(SHIELD.length))] ?? value)
124
+ : value;
125
+ const unshieldValue = (value: unknown): unknown => {
126
+ if (Array.isArray(value)) return value.map(unshieldValue);
127
+ return typeof value === "string" ? unshield(value) : value;
128
+ };
129
+ const parsed = parseArgs({
130
+ args: args.map((arg, index) => (isFreeText(arg) ? `${SHIELD}${index}` : arg)),
131
+ options,
132
+ allowPositionals: true,
133
+ strict: true,
134
+ });
135
+ const values = Object.fromEntries(
136
+ Object.entries(parsed.values).map(([key, value]) => [key, unshieldValue(value)])
137
+ ) as typeof parsed.values;
138
+ return { values, positionals: parsed.positionals.map(unshield) };
139
+ }
140
+
141
+ function exitCode(result: unknown): number {
142
+ return typeof result === "number" ? result : 0;
143
+ }
144
+
145
+ function firstSentence(message: string): string {
146
+ return message.split(/\.\s/)[0] ?? message;
147
+ }
148
+
149
+ function splitPassThrough(
150
+ command: Leaf,
151
+ args: string[]
152
+ ): { head: string[]; passedThrough: string[] } {
153
+ const at = command.passThrough
154
+ ? passThroughStart(args, argSlots(command.args))
155
+ : args.length;
156
+ const tail = args.slice(at);
157
+ return {
158
+ head: args.slice(0, at),
159
+ passedThrough: tail[0] === "--" ? tail.slice(1) : tail,
160
+ };
161
+ }
162
+
163
+ function passThroughStart(args: string[], slots: ArgSlot[]): number {
164
+ let filled = 0;
165
+ for (const [index, arg] of args.entries()) {
166
+ if (arg === "--") return index;
167
+ if (!arg.startsWith("-")) filled += 1;
168
+ if (filled === slots.length) return index + 1;
169
+ }
170
+ return args.length;
171
+ }
172
+
173
+ function isUsageError(error: unknown): error is Error {
174
+ if (error instanceof UsageError) return true;
175
+ const code = (error as { code?: unknown } | null)?.code;
176
+ return typeof code === "string" && code.startsWith("ERR_PARSE_ARGS_");
177
+ }
178
+
179
+ function usageFailure(message: string, command: Command, path: string[]): number {
180
+ console.error(`error: ${message}\n\n${helpText(command, path)}`);
181
+ return 1;
182
+ }
183
+
184
+ interface ArgSlot {
185
+ name: string;
186
+ required: boolean;
187
+ variadic: boolean;
188
+ }
189
+
190
+ function argSlots(synopsis: string | undefined): ArgSlot[] {
191
+ return (synopsis ?? "")
192
+ .split(/\s+/)
193
+ .filter(Boolean)
194
+ .map((token) => ({
195
+ name: token,
196
+ required: token.startsWith("<"),
197
+ variadic: token.includes("..."),
198
+ }));
199
+ }
200
+
201
+ function checkPositionals(synopsis: string | undefined, positionals: string[]): void {
202
+ const slots = argSlots(synopsis);
203
+ const missing = slots.filter((slot) => slot.required)[positionals.length];
204
+ if (missing) throw new UsageError(`missing ${missing.name}`);
205
+ if (slots.some((slot) => slot.variadic)) return;
206
+ const extra = positionals[slots.length];
207
+ if (extra !== undefined) throw new UsageError(`unexpected argument '${extra}'`);
208
+ }
209
+
210
+ export function helpText(command: Command, path: string[]): string {
211
+ return command.kind === "group" ? groupHelp(command, path) : leafHelp(command, path);
212
+ }
213
+
214
+ function leafHelp(command: Leaf, path: string[]): string {
215
+ const options = { ...command.options, help: HELP_OPTION };
216
+ const synopsis = [
217
+ ...path,
218
+ ...(command.args ? [command.args] : []),
219
+ ...(command.options && Object.keys(command.options).length ? ["[options]"] : []),
220
+ ...(command.passThrough ? ["[<passed-on>...]"] : []),
221
+ ].join(" ");
222
+ const rows = Object.entries(options).map(
223
+ ([name, spec]) => [optionLabel(name, spec), spec.description] as const
224
+ );
225
+ return [
226
+ `Usage: ${synopsis}`,
227
+ "",
228
+ ` ${command.summary}`,
229
+ "",
230
+ "Options:",
231
+ ...table(rows),
232
+ ...(command.details ? ["", command.details.trimEnd()] : []),
233
+ ].join("\n");
234
+ }
235
+
236
+ function optionLabel(name: string, spec: OptionSpec): string {
237
+ const short = spec.short ? `-${spec.short}, ` : "";
238
+ const value = spec.type === "string" ? ` ${spec.value ?? "<value>"}` : "";
239
+ return `${short}--${name}${value}`;
240
+ }
241
+
242
+ function groupHelp(command: Group, path: string[]): string {
243
+ const placeholder = command.fallback ? "[<command>]" : "<command>";
244
+ const rows = Object.entries(command.commands).map(
245
+ ([name, sub]) => [commandLabel(name, sub, command), sub.summary] as const
246
+ );
247
+ const fallbackNote = command.fallback
248
+ ? [`With no command, runs '${command.fallback}'.`]
249
+ : [];
250
+ return [
251
+ `Usage: ${[...path, placeholder].join(" ")}`,
252
+ "",
253
+ ` ${command.summary}`,
254
+ "",
255
+ "Commands:",
256
+ ...table(rows),
257
+ "",
258
+ ...fallbackNote,
259
+ `Run '${path.join(" ")} <command> --help' for its arguments and options.`,
260
+ ...(command.details ? ["", command.details.trimEnd()] : []),
261
+ ].join("\n");
262
+ }
263
+
264
+ function commandLabel(name: string, sub: Command, parent: Group): string {
265
+ const aliases = Object.entries(parent.aliases ?? {})
266
+ .filter(([, target]) => target === name)
267
+ .map(([alias]) => alias);
268
+ const names = [name, ...aliases].join(" | ");
269
+ return sub.kind === "leaf" && sub.args ? `${names} ${sub.args}` : names;
270
+ }
271
+
272
+ function table(rows: readonly (readonly [string, string])[]): string[] {
273
+ const width = Math.max(...rows.map(([label]) => label.length));
274
+ return rows.map(([label, text]) => ` ${label.padEnd(width)} ${text}`);
275
+ }
@@ -2,7 +2,7 @@
2
2
  * subagent-doctor — static evaluator for a personal subagent definition
3
3
  * (~/.pal/agents/<name>.md) with merged multi-platform frontmatter. Checks only
4
4
  * what is mechanically verifiable: name/description constraints, per-platform
5
- * blocks (claude/opencode/cursor/copilot/codex), model/tools/permission shape, body
5
+ * blocks (one per agent), model/tools/permission shape, body
6
6
  * length, and absolute-path portability.
7
7
  *
8
8
  * Library: import { lintSubagent, formatSubagentReport } from ".../subagent-doctor"
@@ -13,11 +13,8 @@
13
13
 
14
14
  import { existsSync, readdirSync, readFileSync } from "node:fs";
15
15
  import { basename, resolve } from "node:path";
16
- import {
17
- AGENT_PLATFORMS,
18
- type AgentPlatform,
19
- parseAgentDefinition,
20
- } from "../hooks/lib/agent-definition";
16
+ import { parseAgentDefinition } from "../hooks/lib/agent-definition";
17
+ import { AGENT_NAMES, type AgentName } from "../hooks/lib/agent-registry";
21
18
  import { assets, namesAPath, palHome, toPath } from "../hooks/lib/paths";
22
19
 
23
20
  type Level = "pass" | "warn" | "error";
@@ -42,7 +39,7 @@ interface ParsedSubagent {
42
39
  description: string | null;
43
40
  descriptionQuoted: boolean;
44
41
  global: string[];
45
- platforms: Partial<Record<AgentPlatform, string[]>>;
42
+ platforms: Partial<Record<AgentName, string[]>>;
46
43
  body: string;
47
44
  }
48
45
 
@@ -58,6 +55,29 @@ const OPENCODE_MODES = ["subagent", "primary", "all"];
58
55
  const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
59
56
  const CODEX_TABLE_KEYS = ["tools", "skills"];
60
57
  const SHARED_FIELDS = ["name", "description"];
58
+ const ANTIGRAVITY_MODELS = ["inherit", "flash", "pro"];
59
+ const ANTIGRAVITY_TOOLS = [
60
+ "view_file",
61
+ "write_to_file",
62
+ "replace_file_content",
63
+ "multi_replace_file_content",
64
+ "list_dir",
65
+ "find_by_name",
66
+ "grep_search",
67
+ "search_web",
68
+ "read_url_content",
69
+ "run_command",
70
+ "manage_task",
71
+ "schedule",
72
+ "list_permissions",
73
+ "ask_permission",
74
+ "invoke_subagent",
75
+ "define_subagent",
76
+ "send_message",
77
+ "manage_subagents",
78
+ "ask_question",
79
+ "generate_image",
80
+ ];
61
81
 
62
82
  /** Machine/user-specific absolute paths that will not survive an export. */
63
83
  const ABSOLUTE_PATH_RE =
@@ -200,6 +220,49 @@ function checkCodexBlock(lines: string[], add: AddFinding): void {
200
220
  }
201
221
  }
202
222
 
223
+ function checkAntigravityBlock(lines: string[], add: AddFinding): void {
224
+ const model = fieldValue(lines, "model");
225
+ if (model !== null && !ANTIGRAVITY_MODELS.includes(model)) {
226
+ add(
227
+ "warn",
228
+ "antigravity.model",
229
+ `model "${model}" — expected one of ${ANTIGRAVITY_MODELS.join(", ")}`
230
+ );
231
+ }
232
+ checkAntigravityTools(antigravityTools(lines), add);
233
+ }
234
+
235
+ function antigravityTools(lines: string[]): unknown {
236
+ try {
237
+ return (Bun.YAML.parse(lines.join("\n")) as { tools?: unknown } | null)?.tools;
238
+ } catch {
239
+ return "unparseable";
240
+ }
241
+ }
242
+
243
+ function checkAntigravityTools(tools: unknown, add: AddFinding): void {
244
+ if (tools === undefined || tools === null || (Array.isArray(tools) && !tools.length)) {
245
+ add(
246
+ "warn",
247
+ "antigravity.tools",
248
+ "no `tools` — Antigravity gives the subagent no tools at all; list the ones it needs"
249
+ );
250
+ return;
251
+ }
252
+ if (!Array.isArray(tools) || !tools.every((t) => typeof t === "string")) {
253
+ add("error", "antigravity.tools", "`tools` must be a YAML list of tool names");
254
+ return;
255
+ }
256
+ const unknown = tools.filter((t) => !ANTIGRAVITY_TOOLS.includes(t));
257
+ unknown.length > 0
258
+ ? add(
259
+ "error",
260
+ "antigravity.tools",
261
+ `unknown tool(s) ${unknown.join(", ")} — an unmapped tool name can hang the subagent; use Antigravity names such as run_command, view_file, search_web`
262
+ )
263
+ : add("pass", "antigravity.tools", `${tools.length} known tool(s)`);
264
+ }
265
+
203
266
  function validateModel(model: string, platform: string, add: AddFinding): void {
204
267
  const known = KNOWN_MODELS.includes(model) || /^(claude-|gpt-|o\d|gemini-)/.test(model);
205
268
  if (!known) {
@@ -321,13 +384,14 @@ export function lintSubagent(file: string): SubagentReport {
321
384
  }
322
385
 
323
386
  // ── platform blocks ──
324
- const present = AGENT_PLATFORMS.filter((p) => (platforms[p]?.length ?? 0) > 0);
387
+ const present = AGENT_NAMES.filter((p) => (platforms[p]?.length ?? 0) > 0);
388
+ const blockKeys = AGENT_NAMES.map((p) => p.concat(":")).join("/");
325
389
  present.length > 0
326
390
  ? add("pass", "platforms", `defines block(s): ${present.join(", ")}`)
327
391
  : add(
328
392
  "warn",
329
393
  "platforms",
330
- "no platform block — the subagent installs with only name/description; add a claude:/opencode:/cursor:/copilot:/codex: block to set model, tools, mode"
394
+ `no platform block — the subagent installs with only name/description; add a ${blockKeys} block to set model, tools, mode`
331
395
  );
332
396
 
333
397
  if (platforms.claude) checkClaudeBlock(platforms.claude, add);
@@ -335,6 +399,7 @@ export function lintSubagent(file: string): SubagentReport {
335
399
  if (platforms.cursor) checkCursorBlock(platforms.cursor, add);
336
400
  if (platforms.copilot) checkCopilotBlock(platforms.copilot, add);
337
401
  if (platforms.codex) checkCodexBlock(platforms.codex, add);
402
+ if (platforms.antigravity) checkAntigravityBlock(platforms.antigravity, add);
338
403
 
339
404
  // ── skills field: Claude preloads it; the others have no such field ──
340
405
  for (const p of ["opencode", "cursor", "copilot"] as const) {
@@ -4,16 +4,15 @@
4
4
  * Reads from two sources:
5
5
  * 1. Claude Code session transcripts (~/.claude/projects/)
6
6
  * 2. PAL Haiku inference logs (memory/signals/token-usage.jsonl)
7
- *
8
- * Invoked via `pal cli usage [--today|--week|--month|--all] [--project <name>]`.
9
7
  */
10
8
 
11
9
  import { spawnSync } from "node:child_process";
12
10
  import { homedir } from "node:os";
13
11
  import { resolve } from "node:path";
14
- import { parseArgs } from "node:util";
15
12
  import { palHome } from "../hooks/lib/paths";
16
13
  import { findBinaryOnPath } from "../hooks/lib/which";
14
+ import { leaf, runCommand } from "./lib/command";
15
+ import { scriptArgs } from "./lib/script-args";
17
16
  import { parseRtkSummary, type RtkGain, usageLines } from "./lib/token-report";
18
17
  import { readClaudeCode, readPalInference } from "./lib/usage-buckets";
19
18
 
@@ -30,24 +29,26 @@ function rtkGain(): RtkGain {
30
29
  };
31
30
  }
32
31
 
33
- export function usage() {
34
- const { values } = parseArgs({
35
- options: {
36
- today: { type: "boolean", default: false },
37
- week: { type: "boolean", default: false },
38
- month: { type: "boolean", default: false },
39
- all: { type: "boolean", default: false },
40
- project: { type: "string" },
41
- },
42
- strict: false,
43
- });
44
-
32
+ function printUsage(project: string | undefined): undefined {
45
33
  const lines = usageLines(
46
- readClaudeCode(resolve(homedir(), ".claude", "projects"), values.project as string),
34
+ readClaudeCode(resolve(homedir(), ".claude", "projects"), project),
47
35
  readPalInference(resolve(palHome(), "memory", "signals", "token-usage.jsonl")),
48
36
  rtkGain()
49
37
  );
50
38
  for (const line of lines) console.log(line);
51
39
  }
52
40
 
53
- if (import.meta.main) usage();
41
+ export const usageCommand = leaf({
42
+ summary: "Summarize token usage and estimated cost for today, 7 and 30 days",
43
+ options: {
44
+ project: {
45
+ type: "string",
46
+ value: "<name>",
47
+ description: "Only Claude Code sessions of this project",
48
+ },
49
+ },
50
+ run: ({ values }) => printUsage(values.project),
51
+ });
52
+
53
+ if (import.meta.main)
54
+ process.exit(await runCommand(usageCommand, scriptArgs(), ["pal", "cli", "usage"]));