portable-agent-layer 0.61.4 → 0.62.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.
- package/assets/schema/pal-settings.schema.json +111 -0
- package/assets/templates/hooks.copilot.json +4 -0
- package/assets/templates/hooks.cursor.json +4 -0
- package/assets/templates/pal-settings.json +8 -3
- package/assets/templates/settings.claude.json +23 -0
- package/package.json +1 -1
- package/src/cli/index.ts +16 -1
- package/src/hooks/RtkWrap.ts +52 -0
- package/src/hooks/UserPromptOrchestrator.ts +3 -3
- package/src/hooks/handlers/inject-retrieval.ts +18 -6
- package/src/hooks/lib/inference.ts +2 -41
- package/src/hooks/lib/models.ts +65 -10
- package/src/hooks/lib/settings.ts +5 -0
- package/src/hooks/lib/steering.ts +162 -0
- package/src/hooks/lib/which.ts +41 -0
- package/src/targets/opencode/plugin.ts +35 -4
- package/src/tools/agent/algorithm-reflect.ts +2 -1
- package/src/tools/agent/handoff-note.ts +3 -2
- package/src/tools/agent/project.ts +42 -2
- package/src/tools/agent/relationship-note.ts +6 -11
- package/src/tools/agent/thread.ts +6 -9
- package/src/tools/agent/wisdom-frame.ts +2 -1
- package/src/tools/lib/emit.ts +31 -0
- package/src/tools/relationship-reflect.ts +14 -13
- package/src/tools/session-summary.ts +8 -11
- package/src/tools/token-cost.ts +53 -33
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/kovrichard/portable-agent-layer/main/assets/schema/pal-settings.schema.json",
|
|
4
|
+
"title": "PAL settings",
|
|
5
|
+
"description": "Configuration for the Portable Agent Layer. Lives at ~/.pal/memory/pal-settings.json. All sections are optional; PAL falls back to defaults for anything absent.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "JSON Schema reference — enables editor autocomplete, validation, and hover docs."
|
|
12
|
+
},
|
|
13
|
+
"identity": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"description": "Who the assistant is and who it serves.",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"properties": {
|
|
18
|
+
"ai": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "The assistant's persona.",
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"properties": {
|
|
23
|
+
"name": { "type": "string", "description": "Short name (e.g. the name you call the assistant)." },
|
|
24
|
+
"fullName": { "type": "string", "description": "Full/formal name." },
|
|
25
|
+
"displayName": { "type": "string", "description": "Uppercase label used in headers." },
|
|
26
|
+
"catchphrase": { "type": "string", "description": "Greeting line. '{name}' is substituted with the ai.name value." }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"principal": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"description": "The human the assistant works for.",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"properties": {
|
|
34
|
+
"name": { "type": "string", "description": "The principal's name." },
|
|
35
|
+
"timezone": { "type": "string", "description": "IANA timezone (e.g. Europe/Budapest)." }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"loadAtStartup": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"description": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"properties": {
|
|
45
|
+
"files": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"description": "Absolute or PAL-relative file paths to inject at session start.",
|
|
48
|
+
"items": { "type": "string" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dynamicContext": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"description": "Dynamic context sections injected at session start. Set any key to false to disable it; absent keys default to enabled.",
|
|
55
|
+
"additionalProperties": { "type": "boolean" },
|
|
56
|
+
"properties": {
|
|
57
|
+
"wisdom": { "type": "boolean", "description": "Crystallized wisdom principles." },
|
|
58
|
+
"opinions": { "type": "boolean", "description": "Tracked user opinions." },
|
|
59
|
+
"relationship": { "type": "boolean", "description": "Recent relationship notes." },
|
|
60
|
+
"learningDigest": { "type": "boolean", "description": "Lessons-from-failures digest." },
|
|
61
|
+
"failurePatterns": { "type": "boolean", "description": "Recurring failure patterns." },
|
|
62
|
+
"projectHistory": { "type": "boolean", "description": "Per-project session history." },
|
|
63
|
+
"projects": { "type": "boolean", "description": "Active projects list." },
|
|
64
|
+
"sessionIntelligence": { "type": "boolean", "description": "Rating trends and session signals." },
|
|
65
|
+
"handoff": { "type": "boolean", "description": "Handoff note from the previous session." },
|
|
66
|
+
"selfModel": { "type": "boolean", "description": "The assistant's self-model synthesis." },
|
|
67
|
+
"contextualSteering": { "type": "boolean", "description": "Prompt-time steering self-checks (see the steering section)." },
|
|
68
|
+
"steeringTestReport": { "type": "boolean", "description": "When true, the assistant notes to the user which steering self-check fired. For the dual-live test period; ships false." }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"attribution": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"description": "Git co-author attribution opt-in.",
|
|
74
|
+
"additionalProperties": false,
|
|
75
|
+
"properties": {
|
|
76
|
+
"enabled": { "type": "boolean", "description": "Add the PAL co-author trailer to commits." },
|
|
77
|
+
"decided": { "type": "boolean", "description": "Gates the one-time attribution prompt." }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"steering": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"description": "Contextual steering: PAL injects a short self-check into your prompt when it matches a task type. Toggle the whole feature via dynamicContext.contextualSteering.",
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"properties": {
|
|
85
|
+
"disable": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"description": "Shipped steering rules to suppress, by tag.",
|
|
88
|
+
"items": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"enum": ["debugging", "destructive", "refactor", "planning", "testing", "committing", "secrets"]
|
|
91
|
+
},
|
|
92
|
+
"uniqueItems": true
|
|
93
|
+
},
|
|
94
|
+
"rules": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"description": "Personal steering rules appended to the shipped set. A malformed entry (missing field or bad regex) is skipped, never fatal.",
|
|
97
|
+
"items": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"required": ["tag", "pattern", "snippet"],
|
|
101
|
+
"properties": {
|
|
102
|
+
"tag": { "type": "string", "description": "Short identifier for this rule (also used to dedupe matches)." },
|
|
103
|
+
"pattern": { "type": "string", "description": "JavaScript regex source, matched case-insensitively against the prompt." },
|
|
104
|
+
"snippet": { "type": "string", "description": "The self-check text injected when the pattern matches." }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/kovrichard/portable-agent-layer/main/assets/schema/pal-settings.schema.json",
|
|
2
3
|
"identity": {
|
|
3
4
|
"ai": {
|
|
4
5
|
"name": "",
|
|
@@ -12,11 +13,9 @@
|
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"loadAtStartup": {
|
|
15
|
-
"_docs": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
|
|
16
16
|
"files": []
|
|
17
17
|
},
|
|
18
18
|
"dynamicContext": {
|
|
19
|
-
"_docs": "Dynamic context sections injected at session start. Set to false to disable.",
|
|
20
19
|
"wisdom": true,
|
|
21
20
|
"opinions": true,
|
|
22
21
|
"relationship": true,
|
|
@@ -26,6 +25,12 @@
|
|
|
26
25
|
"projects": true,
|
|
27
26
|
"sessionIntelligence": true,
|
|
28
27
|
"handoff": true,
|
|
29
|
-
"selfModel": true
|
|
28
|
+
"selfModel": true,
|
|
29
|
+
"contextualSteering": true,
|
|
30
|
+
"steeringTestReport": false
|
|
31
|
+
},
|
|
32
|
+
"steering": {
|
|
33
|
+
"disable": [],
|
|
34
|
+
"rules": []
|
|
30
35
|
}
|
|
31
36
|
}
|
|
@@ -18,6 +18,20 @@
|
|
|
18
18
|
"Bash(stat //*)",
|
|
19
19
|
"Bash(readlink //*)",
|
|
20
20
|
"Bash(lsof *)",
|
|
21
|
+
"Bash(rtk read *)",
|
|
22
|
+
"Bash(rtk ls *)",
|
|
23
|
+
"Bash(rtk tree *)",
|
|
24
|
+
"Bash(rtk find *)",
|
|
25
|
+
"Bash(rtk grep *)",
|
|
26
|
+
"Bash(rtk rg *)",
|
|
27
|
+
"Bash(rtk wc *)",
|
|
28
|
+
"Bash(rtk diff *)",
|
|
29
|
+
"Bash(rtk json *)",
|
|
30
|
+
"Bash(rtk log *)",
|
|
31
|
+
"Bash(rtk deps *)",
|
|
32
|
+
"Bash(rtk env *)",
|
|
33
|
+
"Bash(rtk smart *)",
|
|
34
|
+
"Bash(rtk gain *)",
|
|
21
35
|
"Bash(bun ~/.pal/skills/*/tools/*.ts *)",
|
|
22
36
|
"Bash(bun ~/.pal/tools/*.ts *)",
|
|
23
37
|
"Bash(node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts *)"
|
|
@@ -108,6 +122,15 @@
|
|
|
108
122
|
"command": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
|
|
109
123
|
}
|
|
110
124
|
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"matcher": "Bash",
|
|
128
|
+
"hooks": [
|
|
129
|
+
{
|
|
130
|
+
"type": "command",
|
|
131
|
+
"command": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
|
|
132
|
+
}
|
|
133
|
+
]
|
|
111
134
|
}
|
|
112
135
|
],
|
|
113
136
|
"Stop": [
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -622,6 +622,13 @@ function nodeInstallHint(): string {
|
|
|
622
622
|
return "install Node ≥ 22.6 (see https://nodejs.org or your package manager)";
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
+
function rtkInstallHint(): string {
|
|
626
|
+
if (process.platform === "win32")
|
|
627
|
+
return "download rtk.exe from https://github.com/rtk-ai/rtk/releases and add it to PATH";
|
|
628
|
+
if (process.platform === "darwin") return "`brew install rtk`";
|
|
629
|
+
return "`curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh`";
|
|
630
|
+
}
|
|
631
|
+
|
|
625
632
|
function checkHookHealth(home: string): HookHealth {
|
|
626
633
|
const stateDir = resolve(home, "memory", "state");
|
|
627
634
|
// Read current + all rotated logs (`.1`..`.5`) + legacy `.prev` so a recent
|
|
@@ -671,6 +678,7 @@ interface DoctorResult {
|
|
|
671
678
|
cursor: ToolCheck;
|
|
672
679
|
copilot: ToolCheck;
|
|
673
680
|
codex: ToolCheck;
|
|
681
|
+
rtk: ToolCheck;
|
|
674
682
|
hasAgent: boolean;
|
|
675
683
|
}
|
|
676
684
|
|
|
@@ -684,6 +692,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
684
692
|
cursor: { name: "cursor", available: true },
|
|
685
693
|
copilot: { name: "copilot", available: true },
|
|
686
694
|
codex: { name: "codex", available: true },
|
|
695
|
+
rtk: { name: "rtk", available: true },
|
|
687
696
|
hasAgent: true,
|
|
688
697
|
};
|
|
689
698
|
}
|
|
@@ -694,6 +703,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
694
703
|
const cursor = checkTool("cursor");
|
|
695
704
|
const copilot = checkTool("copilot", ["version"]);
|
|
696
705
|
const codex = checkTool("codex");
|
|
706
|
+
const rtk = checkTool("rtk");
|
|
697
707
|
const hasAgent =
|
|
698
708
|
claude.available ||
|
|
699
709
|
opencode.available ||
|
|
@@ -751,6 +761,11 @@ function doctor(silent = false): DoctorResult {
|
|
|
751
761
|
: fail(
|
|
752
762
|
"Playwright Chromium — not found (run 'pal cli install' or 'bunx playwright install chromium')"
|
|
753
763
|
);
|
|
764
|
+
rtk.available
|
|
765
|
+
? ok(rtk.version || "rtk")
|
|
766
|
+
: info(
|
|
767
|
+
`rtk — not installed (optional; enables Bash output compression — ${rtkInstallHint()})`
|
|
768
|
+
);
|
|
754
769
|
|
|
755
770
|
console.log("");
|
|
756
771
|
log.info("PAL state");
|
|
@@ -1023,7 +1038,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
1023
1038
|
console.log("");
|
|
1024
1039
|
}
|
|
1025
1040
|
|
|
1026
|
-
return { bun, claude, opencode, cursor, copilot, codex, hasAgent };
|
|
1041
|
+
return { bun, claude, opencode, cursor, copilot, codex, rtk, hasAgent };
|
|
1027
1042
|
}
|
|
1028
1043
|
|
|
1029
1044
|
// ── Commands ──
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PreToolUse — transparently wraps Bash commands in `rtk` to compress
|
|
3
|
+
* tool output before it reaches the model's context.
|
|
4
|
+
*
|
|
5
|
+
* PAL owns the wiring + presence-gating; rtk owns the rewrite. We do NOT
|
|
6
|
+
* reimplement rtk's selectivity or per-agent JSON shape — we forward the hook's
|
|
7
|
+
* stdin to `rtk hook <agent>` and pass its stdout straight back. rtk emits the
|
|
8
|
+
* correct rewrite object for each agent (Claude/Cursor/Copilot all differ), or
|
|
9
|
+
* nothing when a command isn't worth rewriting.
|
|
10
|
+
*
|
|
11
|
+
* Fail-open by construction: if rtk isn't installed, the agent can't rewrite,
|
|
12
|
+
* or anything throws, we emit nothing and exit 0 — the original command runs
|
|
13
|
+
* unchanged. rtk absent must never block or alter a Bash call.
|
|
14
|
+
*
|
|
15
|
+
* Codex (allow/deny only) and opencode (plugin path) have no `rtk hook`
|
|
16
|
+
* subcommand and are handled elsewhere — this hook no-ops for them.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { getActiveAgent } from "./lib/agent";
|
|
20
|
+
import { findBinaryOnPath } from "./lib/which";
|
|
21
|
+
|
|
22
|
+
const RTK_HOOK_SUBCOMMAND: Partial<Record<ReturnType<typeof getActiveAgent>, string>> = {
|
|
23
|
+
claude: "claude",
|
|
24
|
+
cursor: "cursor",
|
|
25
|
+
copilot: "copilot",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function run(): Promise<void> {
|
|
29
|
+
const subcommand = RTK_HOOK_SUBCOMMAND[getActiveAgent()];
|
|
30
|
+
if (!subcommand) return; // codex/opencode — no rtk hook processor
|
|
31
|
+
|
|
32
|
+
const rtk = findBinaryOnPath("rtk");
|
|
33
|
+
if (!rtk) return; // fail-open: rtk not installed → command runs unchanged
|
|
34
|
+
|
|
35
|
+
const stdin = await Bun.stdin.text();
|
|
36
|
+
const proc = Bun.spawn([rtk, "hook", subcommand], {
|
|
37
|
+
stdin: Buffer.from(stdin),
|
|
38
|
+
stdout: "pipe",
|
|
39
|
+
stderr: "ignore", // drop rtk's "no hook installed" stderr nag
|
|
40
|
+
});
|
|
41
|
+
const out = await new Response(proc.stdout).text();
|
|
42
|
+
await proc.exited;
|
|
43
|
+
if (proc.exitCode === 0 && out) {
|
|
44
|
+
process.stdout.write(out);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await run();
|
|
50
|
+
} catch {
|
|
51
|
+
// Fail open — emit nothing, allow the original command.
|
|
52
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - session-name: generate 4-word session headline on first prompt
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { injectPromptContext } from "./handlers/inject-retrieval";
|
|
11
11
|
import { captureRating } from "./handlers/rating";
|
|
12
12
|
import { captureSessionName } from "./handlers/session-name";
|
|
13
13
|
import { logDebug, logError, logPromptSnapshot } from "./lib/log";
|
|
@@ -30,8 +30,8 @@ logDebug("UserPromptOrchestrator", `Input: ${JSON.stringify(input).slice(0, 200)
|
|
|
30
30
|
if (!input?.prompt) process.exit(0);
|
|
31
31
|
|
|
32
32
|
const sessionId = input.session_id ?? input.sessionId ?? input.conversation_id;
|
|
33
|
-
const
|
|
34
|
-
logPromptSnapshot(input.prompt,
|
|
33
|
+
const injected = await injectPromptContext(input.prompt);
|
|
34
|
+
logPromptSnapshot(input.prompt, injected);
|
|
35
35
|
|
|
36
36
|
const results = await Promise.allSettled([
|
|
37
37
|
captureRating(input.prompt, sessionId),
|
|
@@ -12,6 +12,7 @@ import { logDebug, logError } from "../lib/log";
|
|
|
12
12
|
import { runRetrieval } from "../lib/retrieval";
|
|
13
13
|
import { ensureIndex } from "../lib/retrieval-index";
|
|
14
14
|
import { isEnabled } from "../lib/settings";
|
|
15
|
+
import { getSteeringReminder } from "../lib/steering";
|
|
15
16
|
|
|
16
17
|
const TIMEOUT_MS = 250;
|
|
17
18
|
|
|
@@ -51,12 +52,11 @@ export async function getRetrievalReminder(prompt: string): Promise<string | nul
|
|
|
51
52
|
return result.reminder;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
/** Write
|
|
55
|
+
/** Write a reminder to stdout in the correct format for the current agent.
|
|
55
56
|
* Claude Code: plain text. Cursor: { additional_context }. Codex: hookSpecificOutput JSON.
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!reminder) return null;
|
|
57
|
+
* MUST be called at most once per hook run — Cursor/Codex expect a single JSON
|
|
58
|
+
* object on stdout, so all prompt-time context is merged before this call. */
|
|
59
|
+
function writeForAgent(reminder: string): void {
|
|
60
60
|
if (isCursor()) {
|
|
61
61
|
process.stdout.write(JSON.stringify({ additional_context: reminder }));
|
|
62
62
|
} else if (isCodex()) {
|
|
@@ -71,5 +71,17 @@ export async function injectRetrieval(prompt: string): Promise<string | null> {
|
|
|
71
71
|
} else {
|
|
72
72
|
process.stdout.write(`${reminder}\n`);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Gather all prompt-time context — prior-lesson retrieval + contextual steering —
|
|
77
|
+
* merge into a single payload, and do the one per-agent write. Returns the combined
|
|
78
|
+
* reminder that was injected, or null if there was nothing to inject. */
|
|
79
|
+
export async function injectPromptContext(prompt: string): Promise<string | null> {
|
|
80
|
+
const retrieval = await getRetrievalReminder(prompt);
|
|
81
|
+
const steering = getSteeringReminder(prompt);
|
|
82
|
+
const parts = [steering, retrieval].filter((p): p is string => Boolean(p));
|
|
83
|
+
if (parts.length === 0) return null;
|
|
84
|
+
const combined = parts.join("\n\n");
|
|
85
|
+
writeForAgent(combined);
|
|
86
|
+
return combined;
|
|
75
87
|
}
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
* yet wired and currently fall through to the API path.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
24
|
-
import { basename, delimiter, resolve as resolvePath } from "node:path";
|
|
23
|
+
import { basename } from "node:path";
|
|
25
24
|
import {
|
|
26
25
|
getActiveAgent,
|
|
27
26
|
isClaude,
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
import { logDebug } from "./log";
|
|
34
33
|
import { HAIKU_MODEL } from "./models";
|
|
35
34
|
import { buildSpawnGuardEnv, getInferenceDepth, SPAWN_GUARD_ENV } from "./spawn-guard";
|
|
35
|
+
import { findBinaryOnPath } from "./which";
|
|
36
36
|
|
|
37
37
|
export function hasApiKey(): boolean {
|
|
38
38
|
return !!process.env.PAL_ANTHROPIC_API_KEY;
|
|
@@ -213,45 +213,6 @@ let opencodeBinaryCache: string | null | undefined;
|
|
|
213
213
|
let copilotBinaryCache: string | null | undefined;
|
|
214
214
|
let cursorBinaryCache: string | null | undefined;
|
|
215
215
|
|
|
216
|
-
/**
|
|
217
|
-
* Resolve a binary on PATH to its full absolute path.
|
|
218
|
-
*
|
|
219
|
-
* Manual PATH walk (instead of Bun.which / `which` subprocess) because:
|
|
220
|
-
* 1. Ubuntu 24.04 dropped the `which` binary entirely.
|
|
221
|
-
* 2. Windows has no `which` at all.
|
|
222
|
-
* 3. Bun.which snapshots PATH at startup and ignores mid-test mutations.
|
|
223
|
-
* 4. Bun.spawn on Windows is inconsistent at resolving PATHEXT for bare
|
|
224
|
-
* names — passing the full `.cmd`/`.exe` path bypasses that fragility.
|
|
225
|
-
*
|
|
226
|
-
* Returns the resolved absolute path or null.
|
|
227
|
-
*/
|
|
228
|
-
function findBinaryOnPath(name: string): string | null {
|
|
229
|
-
const PATH = process.env.PATH;
|
|
230
|
-
if (!PATH) return null;
|
|
231
|
-
const exts =
|
|
232
|
-
process.platform === "win32"
|
|
233
|
-
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";")
|
|
234
|
-
: [""];
|
|
235
|
-
for (const dir of PATH.split(delimiter)) {
|
|
236
|
-
if (!dir) continue;
|
|
237
|
-
for (const ext of exts) {
|
|
238
|
-
const candidate = resolvePath(dir, name + ext);
|
|
239
|
-
try {
|
|
240
|
-
if (process.platform === "win32") {
|
|
241
|
-
// Windows has no executable bit — existence in PATHEXT is enough.
|
|
242
|
-
if (existsSync(candidate)) return candidate;
|
|
243
|
-
} else {
|
|
244
|
-
accessSync(candidate, constants.X_OK);
|
|
245
|
-
return candidate;
|
|
246
|
-
}
|
|
247
|
-
} catch {
|
|
248
|
-
/* not here — try next */
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
216
|
function getClaudeBinary(): string | null {
|
|
256
217
|
if (claudeBinaryCache !== undefined) return claudeBinaryCache;
|
|
257
218
|
claudeBinaryCache = findBinaryOnPath("claude");
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -26,17 +26,24 @@ export function flagshipAuthorModel(agent: AgentType): string | undefined {
|
|
|
26
26
|
return FLAGSHIP_AUTHOR_MODEL[agent];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface ModelPricing {
|
|
30
|
+
input: number;
|
|
31
|
+
output: number;
|
|
32
|
+
cacheWrite5m: number;
|
|
33
|
+
cacheWrite1h: number;
|
|
34
|
+
cacheRead: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TokenUsage {
|
|
38
|
+
input: number;
|
|
39
|
+
output: number;
|
|
40
|
+
cacheWrite5m: number;
|
|
41
|
+
cacheWrite1h: number;
|
|
42
|
+
cacheRead: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
/** Pricing per million tokens (USD) — from https://platform.claude.com/docs/en/about-claude/pricing */
|
|
30
|
-
export const MODEL_PRICING: Record<
|
|
31
|
-
string,
|
|
32
|
-
{
|
|
33
|
-
input: number;
|
|
34
|
-
output: number;
|
|
35
|
-
cacheWrite5m: number;
|
|
36
|
-
cacheWrite1h: number;
|
|
37
|
-
cacheRead: number;
|
|
38
|
-
}
|
|
39
|
-
> = {
|
|
46
|
+
export const MODEL_PRICING: Record<string, ModelPricing> = {
|
|
40
47
|
[HAIKU_MODEL]: {
|
|
41
48
|
input: 1,
|
|
42
49
|
output: 5,
|
|
@@ -51,6 +58,13 @@ export const MODEL_PRICING: Record<
|
|
|
51
58
|
cacheWrite1h: 20,
|
|
52
59
|
cacheRead: 1,
|
|
53
60
|
},
|
|
61
|
+
"claude-opus-5": {
|
|
62
|
+
input: 5,
|
|
63
|
+
output: 25,
|
|
64
|
+
cacheWrite5m: 6.25,
|
|
65
|
+
cacheWrite1h: 10,
|
|
66
|
+
cacheRead: 0.5,
|
|
67
|
+
},
|
|
54
68
|
"claude-opus-4-8": {
|
|
55
69
|
input: 5,
|
|
56
70
|
output: 25,
|
|
@@ -72,6 +86,13 @@ export const MODEL_PRICING: Record<
|
|
|
72
86
|
cacheWrite1h: 10,
|
|
73
87
|
cacheRead: 0.5,
|
|
74
88
|
},
|
|
89
|
+
"claude-opus-4-5": {
|
|
90
|
+
input: 5,
|
|
91
|
+
output: 25,
|
|
92
|
+
cacheWrite5m: 6.25,
|
|
93
|
+
cacheWrite1h: 10,
|
|
94
|
+
cacheRead: 0.5,
|
|
95
|
+
},
|
|
75
96
|
// Claude Sonnet 5 — introductory pricing through 2026-08-31; standard (3/15/3.75/6/0.30) applies from 2026-09-01.
|
|
76
97
|
"claude-sonnet-5": {
|
|
77
98
|
input: 2,
|
|
@@ -95,3 +116,37 @@ export const MODEL_PRICING: Record<
|
|
|
95
116
|
cacheRead: 0.3,
|
|
96
117
|
},
|
|
97
118
|
};
|
|
119
|
+
|
|
120
|
+
function longestPrefixKey(model: string): string | null {
|
|
121
|
+
let best: string | null = null;
|
|
122
|
+
for (const key of Object.keys(MODEL_PRICING)) {
|
|
123
|
+
if (model.startsWith(key) && (best === null || key.length > best.length)) best = key;
|
|
124
|
+
}
|
|
125
|
+
return best;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Rates for a model ID. Transcripts carry variants of the same model — dated
|
|
130
|
+
* (`claude-opus-5-20260115`) and context-tagged (`claude-opus-5[1m]`) — so an
|
|
131
|
+
* exact miss falls back to the longest table key the ID starts with.
|
|
132
|
+
*/
|
|
133
|
+
export function pricingFor(model: string): ModelPricing | null {
|
|
134
|
+
const exact = MODEL_PRICING[model];
|
|
135
|
+
if (exact) return exact;
|
|
136
|
+
const prefix = longestPrefixKey(model);
|
|
137
|
+
return prefix ? MODEL_PRICING[prefix] : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** USD cost of a token usage record. Unpriced models bill as 0. */
|
|
141
|
+
export function costOfUsage(model: string, usage: TokenUsage): number {
|
|
142
|
+
const p = pricingFor(model);
|
|
143
|
+
if (!p) return 0;
|
|
144
|
+
return (
|
|
145
|
+
(usage.input * p.input +
|
|
146
|
+
usage.output * p.output +
|
|
147
|
+
usage.cacheWrite5m * p.cacheWrite5m +
|
|
148
|
+
usage.cacheWrite1h * p.cacheWrite1h +
|
|
149
|
+
usage.cacheRead * p.cacheRead) /
|
|
150
|
+
1_000_000
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -25,6 +25,11 @@ export interface PalSettingsData {
|
|
|
25
25
|
dynamicContext?: Record<string, boolean>;
|
|
26
26
|
/** Git co-author attribution opt-in. `decided` gates the one-time prompt. */
|
|
27
27
|
attribution?: { enabled?: boolean; decided?: boolean };
|
|
28
|
+
/** Contextual-steering user extension: personal rules + shipped rules to suppress by tag. */
|
|
29
|
+
steering?: {
|
|
30
|
+
disable?: string[];
|
|
31
|
+
rules?: Array<{ tag: string; pattern: string; snippet: string }>;
|
|
32
|
+
};
|
|
28
33
|
[key: string]: unknown;
|
|
29
34
|
}
|
|
30
35
|
|