portable-agent-layer 0.75.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/package.json +1 -1
- package/src/hooks/lib/agent.ts +34 -3
- package/src/hooks/lib/which.ts +19 -12
package/package.json
CHANGED
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";
|
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;
|