portable-agent-layer 0.74.0 → 0.75.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/README.md +1 -0
- package/package.json +1 -1
- package/src/cli/index.ts +22 -7
- package/src/hooks/lib/agent.ts +34 -3
- package/src/hooks/lib/detached-inference.ts +12 -1
- package/src/hooks/lib/readme-sync.ts +9 -2
- package/src/hooks/lib/which.ts +19 -12
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@ pal cli status # check your setup
|
|
|
97
97
|
| `pal cli subagent link <name>` | Install a personal `~/.pal/agents/<name>.md` (merged multi-platform definition) into every installed agent, split per platform |
|
|
98
98
|
| `pal cli subagent doctor <name>` | Evaluate a subagent definition against the authoring best practices (name/file match, per-platform blocks, model/tools/permission shape, shipped-name collision) |
|
|
99
99
|
| `pal cli subagent list` | List the user-authored subagents in `~/.pal/agents/` |
|
|
100
|
+
| `pal cli version` | Print the installed PAL version. `-v` and `--version` do the same |
|
|
100
101
|
|
|
101
102
|
### Target flags
|
|
102
103
|
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* subagent link <name> Install a personal ~/.pal/agents/<name>.md into installed agents
|
|
24
24
|
* subagent doctor <name> Evaluate a subagent against the authoring best practices
|
|
25
25
|
* debug [on|off] Enable / disable verbose hook debug logging
|
|
26
|
+
* version | -v Print the installed PAL version
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
29
|
import { spawnSync } from "node:child_process";
|
|
@@ -280,6 +281,11 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
280
281
|
case "debug":
|
|
281
282
|
cliDebug(args);
|
|
282
283
|
break;
|
|
284
|
+
case "version":
|
|
285
|
+
case "-v":
|
|
286
|
+
case "--version":
|
|
287
|
+
showVersion();
|
|
288
|
+
break;
|
|
283
289
|
case "--help":
|
|
284
290
|
case "-h":
|
|
285
291
|
case "help":
|
|
@@ -351,6 +357,7 @@ function showHelp() {
|
|
|
351
357
|
pal cli subagent list List the user-authored subagents in ~/.pal/agents/
|
|
352
358
|
pal cli subagent author-model Print the flagship model that authors subagents for the active agent
|
|
353
359
|
pal cli debug [on|off] Enable/disable verbose hook debug logging (persisted)
|
|
360
|
+
pal cli version | -v Print the installed PAL version
|
|
354
361
|
|
|
355
362
|
Environment:
|
|
356
363
|
PAL_HOME Override user state directory (default: ~/.pal or repo root)
|
|
@@ -1617,21 +1624,29 @@ function cliDebug(args: string[]) {
|
|
|
1617
1624
|
}
|
|
1618
1625
|
}
|
|
1619
1626
|
|
|
1620
|
-
|
|
1621
|
-
const home = palHome();
|
|
1622
|
-
const pkg = palPkg();
|
|
1623
|
-
|
|
1624
|
-
let pkgJson: { version: string };
|
|
1627
|
+
function packageVersion(): string {
|
|
1625
1628
|
try {
|
|
1626
|
-
pkgJson = JSON.parse(
|
|
1629
|
+
const pkgJson = JSON.parse(
|
|
1630
|
+
readFileSync(resolve(palPkg(), "package.json"), "utf-8")
|
|
1631
|
+
) as {
|
|
1627
1632
|
version: string;
|
|
1628
1633
|
};
|
|
1634
|
+
return pkgJson.version;
|
|
1629
1635
|
} catch (e) {
|
|
1630
1636
|
throw new Error(`Failed to read package.json: ${e}`);
|
|
1631
1637
|
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function showVersion() {
|
|
1641
|
+
console.log(packageVersion());
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
async function status() {
|
|
1645
|
+
const home = palHome();
|
|
1646
|
+
const pkg = palPkg();
|
|
1632
1647
|
|
|
1633
1648
|
console.log("");
|
|
1634
|
-
log.info(`Version: ${
|
|
1649
|
+
log.info(`Version: ${packageVersion()}`);
|
|
1635
1650
|
log.info(`Package: ${pkg}`);
|
|
1636
1651
|
log.info(`Home: ${home}`);
|
|
1637
1652
|
console.log("");
|
package/src/hooks/lib/agent.ts
CHANGED
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* Detection reads PAL_AGENT first (set in-process by
|
|
10
10
|
* `src/targets/opencode/plugin.ts`), then the host's own environment, and only
|
|
11
11
|
* then the `--agent=` flag the install templates in `assets/templates/*` put
|
|
12
|
-
* on the hook command line. See declaredAgent for why that order.
|
|
12
|
+
* on the hook command line. See declaredAgent for why that order. When nothing
|
|
13
|
+
* declares an agent at all, getActiveAgent falls back to the agent CLIs found
|
|
14
|
+
* on PATH rather than assuming claude.
|
|
13
15
|
*/
|
|
14
16
|
|
|
17
|
+
import { findBinaryOnPath } from "./which";
|
|
18
|
+
|
|
15
19
|
export type AgentType = "claude" | "cursor" | "codex" | "copilot" | "opencode" | "vscode";
|
|
16
20
|
|
|
17
21
|
const KNOWN_AGENTS: ReadonlySet<AgentType> = new Set([
|
|
@@ -109,9 +113,36 @@ export function declaredAgent(): AgentType | undefined {
|
|
|
109
113
|
return agentFromRuntimeEnv() ?? flag;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* The CLI each agent spawns for inference, in the order inference.ts routes
|
|
118
|
+
* them. vscode is absent because it has no CLI of its own — it runs Claude's.
|
|
119
|
+
*/
|
|
120
|
+
const AGENT_BINARIES: ReadonlyArray<readonly [AgentType, string]> = [
|
|
121
|
+
["claude", "claude"],
|
|
122
|
+
["codex", "codex"],
|
|
123
|
+
["opencode", "opencode"],
|
|
124
|
+
["copilot", "copilot"],
|
|
125
|
+
["cursor", "cursor-agent"],
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Which agent this machine actually has, for the case where nothing declared
|
|
130
|
+
* one — a plain terminal running `pal cli`, a cron child, a detached spawn.
|
|
131
|
+
* Assuming claude there names an agent that may not be installed, which routes
|
|
132
|
+
* inference to a binary that isn't on PATH and reports a host that isn't there.
|
|
133
|
+
*/
|
|
134
|
+
function agentFromInstalledBinary(): AgentType | undefined {
|
|
135
|
+
return AGENT_BINARIES.find(([, binary]) => findBinaryOnPath(binary) !== null)?.[0];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Which agent's conventions to follow.
|
|
140
|
+
*
|
|
141
|
+
* A declaration always wins; claude remains the last resort so a machine with
|
|
142
|
+
* no agent CLI at all behaves as it always has.
|
|
143
|
+
*/
|
|
113
144
|
export function getActiveAgent(): AgentType {
|
|
114
|
-
return declaredAgent() ?? "claude";
|
|
145
|
+
return declaredAgent() ?? agentFromInstalledBinary() ?? "claude";
|
|
115
146
|
}
|
|
116
147
|
|
|
117
148
|
export const isClaude = () => getActiveAgent() === "claude";
|
|
@@ -18,15 +18,26 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
|
+
import { getActiveAgent } from "./agent";
|
|
21
22
|
import { logDebug, logError } from "./log";
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A hook run by Cursor, Copilot or Codex knows its agent only from the
|
|
26
|
+
* `--agent=` flag its own config put on the command line, and argv does not
|
|
27
|
+
* cross a spawn. Re-declaring it here is what stops the child from taking the
|
|
28
|
+
* "claude" default and spawning the wrong CLI for its inference.
|
|
29
|
+
*/
|
|
30
|
+
function withAgentDeclaration(args: string[]): string[] {
|
|
31
|
+
return [...args, `--agent=${getActiveAgent()}`];
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
export function spawnDetachedInference(
|
|
24
35
|
scriptPath: string,
|
|
25
36
|
args: string[],
|
|
26
37
|
scope: string
|
|
27
38
|
): void {
|
|
28
39
|
try {
|
|
29
|
-
const child = spawn("bun", [scriptPath, ...args], {
|
|
40
|
+
const child = spawn("bun", [scriptPath, ...withAgentDeclaration(args)], {
|
|
30
41
|
detached: true,
|
|
31
42
|
stdio: "ignore",
|
|
32
43
|
env: { ...process.env, CLAUDECODE: undefined },
|
|
@@ -24,6 +24,14 @@ export const WATCHED_PATHS = [
|
|
|
24
24
|
"assets/agents",
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A flag spelling of a command the README documents under its canonical name,
|
|
29
|
+
* or the `cli` prefix the dispatcher consumes before a command is ever named.
|
|
30
|
+
*/
|
|
31
|
+
function isAliasOrInternalRoute(cmd: string): boolean {
|
|
32
|
+
return ["--help", "-h", "help", "-v", "--version", "cli"].includes(cmd);
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
/** Extract CLI command names from the switch statement in index.ts */
|
|
28
36
|
function extractCliCommands(): string[] {
|
|
29
37
|
const pkg = palPkg();
|
|
@@ -36,8 +44,7 @@ function extractCliCommands(): string[] {
|
|
|
36
44
|
|
|
37
45
|
for (const match of matches) {
|
|
38
46
|
const cmd = match[1];
|
|
39
|
-
|
|
40
|
-
if (["--help", "-h", "help", "cli"].includes(cmd)) continue;
|
|
47
|
+
if (isAliasOrInternalRoute(cmd)) continue;
|
|
41
48
|
commands.push(cmd);
|
|
42
49
|
}
|
|
43
50
|
|
package/src/hooks/lib/which.ts
CHANGED
|
@@ -9,9 +9,26 @@
|
|
|
9
9
|
* names — passing the full `.cmd`/`.exe` path bypasses that fragility.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { accessSync, constants,
|
|
12
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
13
13
|
import { delimiter, resolve as resolvePath } from "node:path";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A directory carries the execute bit as "traversable", so X_OK alone accepts
|
|
17
|
+
* a folder that happens to share a CLI's name. statSync follows symlinks, so a
|
|
18
|
+
* linked binary still resolves. Windows has no executable bit — being a file
|
|
19
|
+
* under a PATHEXT extension is all it can offer.
|
|
20
|
+
*/
|
|
21
|
+
function isExecutableFile(candidate: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
if (!statSync(candidate).isFile()) return false;
|
|
24
|
+
if (process.platform === "win32") return true;
|
|
25
|
+
accessSync(candidate, constants.X_OK);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
/** Resolve a binary on PATH to its full absolute path, or null if absent. */
|
|
16
33
|
export function findBinaryOnPath(name: string): string | null {
|
|
17
34
|
const PATH = process.env.PATH;
|
|
@@ -24,17 +41,7 @@ export function findBinaryOnPath(name: string): string | null {
|
|
|
24
41
|
if (!dir) continue;
|
|
25
42
|
for (const ext of exts) {
|
|
26
43
|
const candidate = resolvePath(dir, name + ext);
|
|
27
|
-
|
|
28
|
-
if (process.platform === "win32") {
|
|
29
|
-
// Windows has no executable bit — existence in PATHEXT is enough.
|
|
30
|
-
if (existsSync(candidate)) return candidate;
|
|
31
|
-
} else {
|
|
32
|
-
accessSync(candidate, constants.X_OK);
|
|
33
|
-
return candidate;
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
36
|
-
/* not here — try next */
|
|
37
|
-
}
|
|
44
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
return null;
|