@phnx-labs/agents-cli 1.20.78 → 1.20.82
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/CHANGELOG.md +487 -0
- package/README.md +25 -25
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +65 -0
- package/dist/commands/doctor.js +575 -31
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +24 -5
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +187 -11
- package/dist/commands/routines.js +12 -2
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +100 -32
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.js +32 -3
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +31 -0
- package/dist/commands/sessions.js +141 -16
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +5 -8
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -2
- package/dist/lib/agents.js +72 -63
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +18 -0
- package/dist/lib/daemon.js +47 -6
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.js +35 -8
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +14 -14
- package/dist/lib/git.js +41 -44
- package/dist/lib/hooks.d.ts +42 -0
- package/dist/lib/hooks.js +137 -93
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +20 -0
- package/dist/lib/routines.js +6 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +49 -10
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +16 -0
- package/dist/lib/secrets/agent.js +105 -3
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +11 -8
- package/dist/lib/secrets/bundles.js +31 -23
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +110 -26
- package/dist/lib/session/active.js +161 -50
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +4 -1
- package/dist/lib/session/db.js +65 -8
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +13 -0
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* 'error' listener — without it Node re-throws ENOENT as an uncaught exception and
|
|
17
17
|
* takes the whole daemon down (the exact crash overdue.test.ts guards).
|
|
18
18
|
*/
|
|
19
|
+
import { type ChildProcess } from 'child_process';
|
|
19
20
|
export interface DesktopNotification {
|
|
20
21
|
/** Bold first line. */
|
|
21
22
|
title: string;
|
|
@@ -36,6 +37,20 @@ export interface DesktopNotification {
|
|
|
36
37
|
export declare function buildMenubarNotifyArgs(n: DesktopNotification): string[];
|
|
37
38
|
/** AppleScript for the osascript degradation path. Exported for tests. */
|
|
38
39
|
export declare function buildOsascriptNotifyArgs(n: DesktopNotification): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Spawn one detached, best-effort notifier process with a bounded lifetime.
|
|
42
|
+
*
|
|
43
|
+
* The child is detached + unref'd so it never blocks the daemon, but — unlike a
|
|
44
|
+
* pure fire-and-forget — it is supervised: a watchdog SIGKILLs it after
|
|
45
|
+
* `timeoutMs` so a stalled notifier can never linger (the pile-up this fixes).
|
|
46
|
+
* The common path (a sub-second post + self-exit) clears the watchdog on the
|
|
47
|
+
* child's own 'exit', so the kill only ever fires for a genuinely hung child.
|
|
48
|
+
*
|
|
49
|
+
* `timeoutMs` is injectable so the bounded-lifetime behaviour is testable against
|
|
50
|
+
* a real long-running child without a multi-second wait. Returns the child so a
|
|
51
|
+
* caller/test can observe it; callers in this module ignore it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function spawnDetachedQuiet(command: string, args: string[], timeoutMs?: number): ChildProcess;
|
|
39
54
|
/**
|
|
40
55
|
* Fire a native desktop notification, branded via the MenubarHelper companion on
|
|
41
56
|
* macOS. Best-effort — any failure is swallowed so a notification hiccup never
|
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
import { spawn } from 'child_process';
|
|
20
20
|
import * as os from 'os';
|
|
21
21
|
import { resolveInstalledMenubarExecutable } from './install-menubar.js';
|
|
22
|
+
/**
|
|
23
|
+
* Hard ceiling on a one-shot notifier's lifetime. A notifier posts and exits in
|
|
24
|
+
* well under a second on the happy path; if delivery stalls (locked screen, a
|
|
25
|
+
* WindowServer/XPC hiccup) a detached GUI helper can hang indefinitely and pile
|
|
26
|
+
* up in the menu bar. This is above the Swift one-shot's own 0.6s flush + its 3s
|
|
27
|
+
* self-terminate watchdog, so Node's kill is the last-resort guarantee that runs
|
|
28
|
+
* only if the child never self-exits.
|
|
29
|
+
*/
|
|
30
|
+
const NOTIFY_TIMEOUT_MS = 4000;
|
|
22
31
|
/** Argv for the MenubarHelper one-shot notify mode. Exported for tests. */
|
|
23
32
|
export function buildMenubarNotifyArgs(n) {
|
|
24
33
|
const args = ['--notify', '--title', n.title, '--body', n.body];
|
|
@@ -36,14 +45,41 @@ export function buildOsascriptNotifyArgs(n) {
|
|
|
36
45
|
script += ` subtitle "${esc(n.subtitle)}"`;
|
|
37
46
|
return ['-e', script];
|
|
38
47
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Spawn one detached, best-effort notifier process with a bounded lifetime.
|
|
50
|
+
*
|
|
51
|
+
* The child is detached + unref'd so it never blocks the daemon, but — unlike a
|
|
52
|
+
* pure fire-and-forget — it is supervised: a watchdog SIGKILLs it after
|
|
53
|
+
* `timeoutMs` so a stalled notifier can never linger (the pile-up this fixes).
|
|
54
|
+
* The common path (a sub-second post + self-exit) clears the watchdog on the
|
|
55
|
+
* child's own 'exit', so the kill only ever fires for a genuinely hung child.
|
|
56
|
+
*
|
|
57
|
+
* `timeoutMs` is injectable so the bounded-lifetime behaviour is testable against
|
|
58
|
+
* a real long-running child without a multi-second wait. Returns the child so a
|
|
59
|
+
* caller/test can observe it; callers in this module ignore it.
|
|
60
|
+
*/
|
|
61
|
+
export function spawnDetachedQuiet(command, args, timeoutMs = NOTIFY_TIMEOUT_MS) {
|
|
41
62
|
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
63
|
+
// Bound the lifetime: a stalled notifier is hard-killed after the timeout. The
|
|
64
|
+
// timer is unref'd so it never keeps a short-lived caller's event loop alive.
|
|
65
|
+
const watchdog = setTimeout(() => {
|
|
66
|
+
try {
|
|
67
|
+
child.kill('SIGKILL');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* already gone */
|
|
71
|
+
}
|
|
72
|
+
}, timeoutMs);
|
|
73
|
+
watchdog.unref();
|
|
42
74
|
// A missing binary (headless box with no osascript/notify-send, or a helper
|
|
43
|
-
// that vanished) arrives as an async 'error' event
|
|
44
|
-
//
|
|
45
|
-
|
|
75
|
+
// that vanished) arrives as an async 'error' event — the process never started,
|
|
76
|
+
// so cancel the watchdog. Without a listener Node re-throws ENOENT as an
|
|
77
|
+
// uncaught exception and crashes the daemon; having one swallows it.
|
|
78
|
+
child.on('error', () => clearTimeout(watchdog));
|
|
79
|
+
// Fast, self-driven exit (the common path) — cancel the watchdog.
|
|
80
|
+
child.on('exit', () => clearTimeout(watchdog));
|
|
46
81
|
child.unref();
|
|
82
|
+
return child;
|
|
47
83
|
}
|
|
48
84
|
/**
|
|
49
85
|
* Fire a native desktop notification, branded via the MenubarHelper companion on
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The merged, first-wins DotAgents resource surface — every resource kind, one
|
|
3
|
+
* table, showing the winning layer per row.
|
|
4
|
+
*
|
|
5
|
+
* Ported verbatim from the former `agents resources` command, now surfaced as
|
|
6
|
+
* `agents view --merged`. `agents view agent@version` shows per-version
|
|
7
|
+
* resources; this shows the cross-layer merge across project → user → extras →
|
|
8
|
+
* system. Kept as a shared lib (not inlined into `view.ts`) so `inspect` can
|
|
9
|
+
* reuse it later.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderMergedResources(): void;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The merged, first-wins DotAgents resource surface — every resource kind, one
|
|
3
|
+
* table, showing the winning layer per row.
|
|
4
|
+
*
|
|
5
|
+
* Ported verbatim from the former `agents resources` command, now surfaced as
|
|
6
|
+
* `agents view --merged`. `agents view agent@version` shows per-version
|
|
7
|
+
* resources; this shows the cross-layer merge across project → user → extras →
|
|
8
|
+
* system. Kept as a shared lib (not inlined into `view.ts`) so `inspect` can
|
|
9
|
+
* reuse it later.
|
|
3
10
|
*/
|
|
4
11
|
import * as path from 'path';
|
|
5
12
|
import chalk from 'chalk';
|
|
6
|
-
import { listResources } from '
|
|
7
|
-
import { terminalWidth, truncateToWidth, stringWidth } from '
|
|
13
|
+
import { listResources } from './resources.js';
|
|
14
|
+
import { terminalWidth, truncateToWidth, stringWidth } from './session/width.js';
|
|
8
15
|
const DRILLABLE_KINDS = [
|
|
9
16
|
'skills',
|
|
10
17
|
'commands',
|
|
@@ -25,22 +32,7 @@ const KIND_LABELS = {
|
|
|
25
32
|
workflows: 'Workflows',
|
|
26
33
|
subagents: 'Subagents',
|
|
27
34
|
};
|
|
28
|
-
export function
|
|
29
|
-
program
|
|
30
|
-
.command('resources')
|
|
31
|
-
.description('Show the merged DotAgents resources resolved across project, user, system, and extras')
|
|
32
|
-
.option('--merged', 'Show the merged first-wins resource surface (default)')
|
|
33
|
-
.addHelpText('after', `
|
|
34
|
-
Examples:
|
|
35
|
-
agents resources
|
|
36
|
-
agents resources --merged
|
|
37
|
-
`)
|
|
38
|
-
.action((options) => {
|
|
39
|
-
void options;
|
|
40
|
-
renderMergedResources();
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
function renderMergedResources() {
|
|
35
|
+
export function renderMergedResources() {
|
|
44
36
|
const groups = DRILLABLE_KINDS.map((kind) => ({
|
|
45
37
|
kind,
|
|
46
38
|
rows: listResources(kind),
|
|
@@ -120,19 +120,6 @@ export interface CopilotPermissionsConfig {
|
|
|
120
120
|
allowed_directories?: string[];
|
|
121
121
|
}>;
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Convert canonical permission set to Gemini format.
|
|
125
|
-
* Gemini reads tool allow/deny lists from settings.json under
|
|
126
|
-
* `tools.core` / `tools.exclude`. Bash permissions map to ShellTool(pattern).
|
|
127
|
-
* Blanket Bash grants map to bare "ShellTool" (no command filter).
|
|
128
|
-
* Non-Bash tool patterns are skipped; Gemini uses different tool names.
|
|
129
|
-
*/
|
|
130
|
-
export declare function convertToGeminiFormat(set: PermissionSet): {
|
|
131
|
-
tools: {
|
|
132
|
-
core: string[];
|
|
133
|
-
exclude?: string[];
|
|
134
|
-
};
|
|
135
|
-
};
|
|
136
123
|
export declare function convertToCopilotFormat(set: PermissionSet, location: string): CopilotPermissionsConfig;
|
|
137
124
|
/** Convert canonical Bash rules into Droid's command arrays. */
|
|
138
125
|
export declare function convertToDroidFormat(set: PermissionSet): {
|
|
@@ -154,25 +141,6 @@ export declare function convertToOpenClawFormat(set: PermissionSet): {
|
|
|
154
141
|
alsoAllow: string[];
|
|
155
142
|
deny: string[];
|
|
156
143
|
};
|
|
157
|
-
export type ForgePermission = 'allow' | 'deny' | 'confirm';
|
|
158
|
-
export type ForgeOperation = 'read' | 'write' | 'command' | 'url';
|
|
159
|
-
export type ForgePolicy = {
|
|
160
|
-
permission: ForgePermission;
|
|
161
|
-
rule: Partial<Record<ForgeOperation, string>>;
|
|
162
|
-
};
|
|
163
|
-
/**
|
|
164
|
-
* Convert canonical permissions to ForgeCode's permissions.yaml policy list.
|
|
165
|
-
*
|
|
166
|
-
* ForgeCode gates built-in tools by operation family (`read`, `write`,
|
|
167
|
-
* `command`, `url`) plus glob patterns, optionally scoped by `dir`. The policy
|
|
168
|
-
* file is ignored unless `.forge.toml` has `restricted = true`, and MCP tools
|
|
169
|
-
* bypass permissions.yaml entirely; agents-cli therefore maps only canonical
|
|
170
|
-
* built-in allow/deny rules and does not try to express per-named-MCP-tool
|
|
171
|
-
* grants.
|
|
172
|
-
*/
|
|
173
|
-
export declare function convertToForgeFormat(set: PermissionSet): {
|
|
174
|
-
policies: ForgePolicy[];
|
|
175
|
-
};
|
|
176
144
|
export declare function convertToHermesFormat(set: PermissionSet): {
|
|
177
145
|
command_allowlist: string[];
|
|
178
146
|
approvals: {
|
package/dist/lib/permissions.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as TOML from 'smol-toml';
|
|
|
15
15
|
import { getPermissionsDir, getUserPermissionsDir, ensureAgentsDir } from './state.js';
|
|
16
16
|
import { safeJoin } from './paths.js';
|
|
17
17
|
import { agentConfigDirName } from './agents.js';
|
|
18
|
+
import { supports } from './capabilities.js';
|
|
18
19
|
import { updateGeminiSettings } from './gemini-settings.js';
|
|
19
20
|
const HOME = os.homedir();
|
|
20
21
|
// PERMISSIONS_CAPABLE_AGENTS removed — use `capableAgents('allowlist')`
|
|
@@ -504,43 +505,6 @@ function parseCanonicalPattern(permission) {
|
|
|
504
505
|
}
|
|
505
506
|
/** Blanket-Bash canonical forms that mean "allow any bash command". */
|
|
506
507
|
const BLANKET_BASH_FORMS = new Set(['Bash', 'Bash(*)', 'Bash(**)']);
|
|
507
|
-
/**
|
|
508
|
-
* Convert canonical permission set to Gemini format.
|
|
509
|
-
* Gemini reads tool allow/deny lists from settings.json under
|
|
510
|
-
* `tools.core` / `tools.exclude`. Bash permissions map to ShellTool(pattern).
|
|
511
|
-
* Blanket Bash grants map to bare "ShellTool" (no command filter).
|
|
512
|
-
* Non-Bash tool patterns are skipped; Gemini uses different tool names.
|
|
513
|
-
*/
|
|
514
|
-
export function convertToGeminiFormat(set) {
|
|
515
|
-
const serialize = (permissions) => {
|
|
516
|
-
const tools = new Set();
|
|
517
|
-
for (const perm of permissions) {
|
|
518
|
-
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
519
|
-
tools.add('ShellTool');
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
const parsed = parseCanonicalPattern(perm);
|
|
523
|
-
if (!parsed || parsed.tool !== 'bash')
|
|
524
|
-
continue;
|
|
525
|
-
const command = normalizeBashPattern(parsed.pattern);
|
|
526
|
-
if (command === '*') {
|
|
527
|
-
tools.add('ShellTool');
|
|
528
|
-
}
|
|
529
|
-
else {
|
|
530
|
-
tools.add(`ShellTool(${command})`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return Array.from(tools);
|
|
534
|
-
};
|
|
535
|
-
const core = serialize(set.allow);
|
|
536
|
-
const exclude = serialize(set.deny ?? []);
|
|
537
|
-
return {
|
|
538
|
-
tools: {
|
|
539
|
-
core,
|
|
540
|
-
...(exclude.length ? { exclude } : {}),
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
508
|
/**
|
|
545
509
|
* Strip Claude's `:*` subcommand-wildcard suffix and return a space-glob form.
|
|
546
510
|
* "mq:*" -> "mq *", "git status" -> "git status", "*" -> "*".
|
|
@@ -710,64 +674,6 @@ export function convertToOpenClawFormat(set) {
|
|
|
710
674
|
deny: map(set.deny ?? []),
|
|
711
675
|
};
|
|
712
676
|
}
|
|
713
|
-
const FORGE_OPERATION_BY_CANONICAL = {
|
|
714
|
-
bash: 'command',
|
|
715
|
-
read: 'read',
|
|
716
|
-
grep: 'read',
|
|
717
|
-
glob: 'read',
|
|
718
|
-
write: 'write',
|
|
719
|
-
edit: 'write',
|
|
720
|
-
notebookedit: 'write',
|
|
721
|
-
webfetch: 'url',
|
|
722
|
-
websearch: 'url',
|
|
723
|
-
};
|
|
724
|
-
function canonicalToForgePolicy(perm, permission) {
|
|
725
|
-
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
726
|
-
return { permission, rule: { command: '*' } };
|
|
727
|
-
}
|
|
728
|
-
const parsed = parseCanonicalPattern(perm);
|
|
729
|
-
if (!parsed)
|
|
730
|
-
return null;
|
|
731
|
-
const operation = FORGE_OPERATION_BY_CANONICAL[parsed.tool];
|
|
732
|
-
if (!operation)
|
|
733
|
-
return null;
|
|
734
|
-
const pattern = parsed.tool === 'bash'
|
|
735
|
-
? normalizeBashPattern(parsed.pattern)
|
|
736
|
-
: (parsed.tool === 'webfetch' || parsed.tool === 'websearch') && parsed.pattern.startsWith('domain:')
|
|
737
|
-
? `${parsed.pattern.slice('domain:'.length)}*`
|
|
738
|
-
: parsed.pattern === '**'
|
|
739
|
-
? '**/*'
|
|
740
|
-
: parsed.pattern;
|
|
741
|
-
return { permission, rule: { [operation]: pattern } };
|
|
742
|
-
}
|
|
743
|
-
/**
|
|
744
|
-
* Convert canonical permissions to ForgeCode's permissions.yaml policy list.
|
|
745
|
-
*
|
|
746
|
-
* ForgeCode gates built-in tools by operation family (`read`, `write`,
|
|
747
|
-
* `command`, `url`) plus glob patterns, optionally scoped by `dir`. The policy
|
|
748
|
-
* file is ignored unless `.forge.toml` has `restricted = true`, and MCP tools
|
|
749
|
-
* bypass permissions.yaml entirely; agents-cli therefore maps only canonical
|
|
750
|
-
* built-in allow/deny rules and does not try to express per-named-MCP-tool
|
|
751
|
-
* grants.
|
|
752
|
-
*/
|
|
753
|
-
export function convertToForgeFormat(set) {
|
|
754
|
-
const policies = [];
|
|
755
|
-
const seen = new Set();
|
|
756
|
-
const add = (policy) => {
|
|
757
|
-
if (!policy)
|
|
758
|
-
return;
|
|
759
|
-
const key = `${policy.permission}|${JSON.stringify(policy.rule)}`;
|
|
760
|
-
if (seen.has(key))
|
|
761
|
-
return;
|
|
762
|
-
seen.add(key);
|
|
763
|
-
policies.push(policy);
|
|
764
|
-
};
|
|
765
|
-
for (const perm of set.allow)
|
|
766
|
-
add(canonicalToForgePolicy(perm, 'allow'));
|
|
767
|
-
for (const perm of set.deny ?? [])
|
|
768
|
-
add(canonicalToForgePolicy(perm, 'deny'));
|
|
769
|
-
return { policies };
|
|
770
|
-
}
|
|
771
677
|
export function convertToHermesFormat(set) {
|
|
772
678
|
const commands = (permissions) => {
|
|
773
679
|
const out = new Set();
|
|
@@ -1467,6 +1373,9 @@ function applyPermissionsToAgent(agentId, set, scope = 'user', cwd, merge = true
|
|
|
1467
1373
|
* This writes to {versionHome}/.{agent}/settings.json (or equivalent).
|
|
1468
1374
|
*/
|
|
1469
1375
|
export function applyPermissionsToVersion(agentId, set, versionHome, merge = true, cwd) {
|
|
1376
|
+
if (!supports(agentId, 'allowlist').ok) {
|
|
1377
|
+
return { success: false, error: `Agent '${agentId}' does not support permissions` };
|
|
1378
|
+
}
|
|
1470
1379
|
const configDir = path.join(versionHome, agentConfigDirName(agentId));
|
|
1471
1380
|
try {
|
|
1472
1381
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -1557,38 +1466,6 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1557
1466
|
}
|
|
1558
1467
|
return { success: true };
|
|
1559
1468
|
}
|
|
1560
|
-
if (agentId === 'gemini') {
|
|
1561
|
-
const geminiPerms = convertToGeminiFormat(set);
|
|
1562
|
-
const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
|
|
1563
|
-
updateGeminiSettings(settingsPath, (settings) => {
|
|
1564
|
-
// Remove stale keys written by earlier serializer versions:
|
|
1565
|
-
// top-level `permissions`, and Gemini's pre-core/exclude `tools.allowed`.
|
|
1566
|
-
delete settings.permissions;
|
|
1567
|
-
const tools = (typeof settings.tools === 'object' && settings.tools !== null && !Array.isArray(settings.tools))
|
|
1568
|
-
? settings.tools
|
|
1569
|
-
: {};
|
|
1570
|
-
delete tools.allowed;
|
|
1571
|
-
if (merge) {
|
|
1572
|
-
const existingCore = Array.isArray(tools.core) ? tools.core : [];
|
|
1573
|
-
const existingExclude = Array.isArray(tools.exclude) ? tools.exclude : [];
|
|
1574
|
-
tools.core = Array.from(new Set([...existingCore, ...geminiPerms.tools.core]));
|
|
1575
|
-
const mergedExclude = Array.from(new Set([...existingExclude, ...(geminiPerms.tools.exclude ?? [])]));
|
|
1576
|
-
if (mergedExclude.length)
|
|
1577
|
-
tools.exclude = mergedExclude;
|
|
1578
|
-
else
|
|
1579
|
-
delete tools.exclude;
|
|
1580
|
-
}
|
|
1581
|
-
else {
|
|
1582
|
-
tools.core = geminiPerms.tools.core;
|
|
1583
|
-
if (geminiPerms.tools.exclude?.length)
|
|
1584
|
-
tools.exclude = geminiPerms.tools.exclude;
|
|
1585
|
-
else
|
|
1586
|
-
delete tools.exclude;
|
|
1587
|
-
}
|
|
1588
|
-
settings.tools = tools;
|
|
1589
|
-
});
|
|
1590
|
-
return { success: true };
|
|
1591
|
-
}
|
|
1592
1469
|
if (agentId === 'antigravity') {
|
|
1593
1470
|
const antigravityPerms = convertToAntigravityFormat(set);
|
|
1594
1471
|
const settingsPath = path.join(versionHome, '.gemini', 'antigravity-cli', 'settings.json');
|
|
@@ -1888,36 +1765,6 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1888
1765
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
1889
1766
|
return { success: true };
|
|
1890
1767
|
}
|
|
1891
|
-
if (agentId === 'forge') {
|
|
1892
|
-
const permissionsPath = path.join(versionHome, '.forge', 'permissions.yaml');
|
|
1893
|
-
let config = {};
|
|
1894
|
-
if (fs.existsSync(permissionsPath)) {
|
|
1895
|
-
const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
1896
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1897
|
-
config = parsed;
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
const newPolicies = convertToForgeFormat(set).policies;
|
|
1901
|
-
if (merge) {
|
|
1902
|
-
const existingPolicies = Array.isArray(config.policies) ? config.policies : [];
|
|
1903
|
-
const seen = new Set();
|
|
1904
|
-
config.policies = [...existingPolicies, ...newPolicies].filter((policy) => {
|
|
1905
|
-
if (!policy || typeof policy !== 'object' || Array.isArray(policy))
|
|
1906
|
-
return false;
|
|
1907
|
-
const key = JSON.stringify(policy);
|
|
1908
|
-
if (seen.has(key))
|
|
1909
|
-
return false;
|
|
1910
|
-
seen.add(key);
|
|
1911
|
-
return true;
|
|
1912
|
-
});
|
|
1913
|
-
}
|
|
1914
|
-
else {
|
|
1915
|
-
config.policies = newPolicies;
|
|
1916
|
-
}
|
|
1917
|
-
fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
|
|
1918
|
-
fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
|
|
1919
|
-
return { success: true };
|
|
1920
|
-
}
|
|
1921
1768
|
if (agentId === 'hermes') {
|
|
1922
1769
|
const configPath = path.join(versionHome, '.hermes', 'config.yaml');
|
|
1923
1770
|
let config = {};
|
package/dist/lib/picker.js
CHANGED
|
@@ -214,7 +214,7 @@ export function multiItemPicker(config) {
|
|
|
214
214
|
const theme = makeTheme({});
|
|
215
215
|
const [status, setStatus] = useState('idle');
|
|
216
216
|
const [searchTerm, setSearchTerm] = useState(cfg.initialSearch ?? '');
|
|
217
|
-
const [previewOpen, setPreviewOpen] = useState(
|
|
217
|
+
const [previewOpen, setPreviewOpen] = useState(Boolean(cfg.buildPreview));
|
|
218
218
|
const [selectedKeys, setSelectedKeys] = useState(new Set());
|
|
219
219
|
const [active, setActive] = useState(0);
|
|
220
220
|
const prefix = usePrefix({ status, theme });
|
|
@@ -41,13 +41,24 @@ export declare function backgroundSpawnOptions(opts?: {
|
|
|
41
41
|
windowsHide: boolean;
|
|
42
42
|
};
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Whether `pid` has stopped serving — dead, or a ZOMBIE awaiting reap.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* `isAlive` is a bare `kill(pid, 0)`, which succeeds for a zombie. That matters
|
|
47
|
+
* here because {@link waitForExit} blocks the event loop, so a daemon that is
|
|
48
|
+
* this process's own child can never be reaped while we wait: it would read as
|
|
49
|
+
* alive for the entire timeout and then be hard-killed after it had already
|
|
50
|
+
* exited. A zombie holds no socket and serves no request, so for stop purposes
|
|
51
|
+
* it has exited.
|
|
50
52
|
*/
|
|
53
|
+
export declare function hasExited(pid: number): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Block until `pid` stops serving or `timeoutMs` elapses. Synchronous on purpose:
|
|
56
|
+
* the callers are short-lived CLI/postinstall processes that exit before any
|
|
57
|
+
* async timer would fire, which is exactly how a SIGTERMed daemon used to outlive
|
|
58
|
+
* the `stopDaemon()` that was supposed to have reaped it. Returns true if it is
|
|
59
|
+
* gone.
|
|
60
|
+
*/
|
|
61
|
+
export declare function waitForExit(pid: number, timeoutMs: number): boolean;
|
|
51
62
|
export declare function isAlive(pid: number): boolean;
|
|
52
63
|
/**
|
|
53
64
|
* A stable identifier for the process at `pid` as of when it started, or null if
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { readFileSync } from 'fs';
|
|
6
|
+
import { sleepSync } from '../fs-atomic.js';
|
|
6
7
|
/**
|
|
7
8
|
* Forcefully terminate a process AND its descendant tree.
|
|
8
9
|
*
|
|
@@ -70,6 +71,59 @@ export function backgroundSpawnOptions(opts = {}) {
|
|
|
70
71
|
* permission to signal it), matching the long-standing call sites that treat a
|
|
71
72
|
* throw from `process.kill(pid, 0)` as "not running".
|
|
72
73
|
*/
|
|
74
|
+
/** Poll interval while waiting for a pid to disappear. */
|
|
75
|
+
const EXIT_POLL_MS = 50;
|
|
76
|
+
/**
|
|
77
|
+
* Whether `pid` has stopped serving — dead, or a ZOMBIE awaiting reap.
|
|
78
|
+
*
|
|
79
|
+
* `isAlive` is a bare `kill(pid, 0)`, which succeeds for a zombie. That matters
|
|
80
|
+
* here because {@link waitForExit} blocks the event loop, so a daemon that is
|
|
81
|
+
* this process's own child can never be reaped while we wait: it would read as
|
|
82
|
+
* alive for the entire timeout and then be hard-killed after it had already
|
|
83
|
+
* exited. A zombie holds no socket and serves no request, so for stop purposes
|
|
84
|
+
* it has exited.
|
|
85
|
+
*/
|
|
86
|
+
export function hasExited(pid) {
|
|
87
|
+
if (!isAlive(pid))
|
|
88
|
+
return true;
|
|
89
|
+
if (process.platform === 'win32')
|
|
90
|
+
return false; // no zombie state to unwrap
|
|
91
|
+
try {
|
|
92
|
+
const state = execFileSync('ps', ['-o', 'state=', '-p', String(pid)], { encoding: 'utf-8' }).trim();
|
|
93
|
+
return state.startsWith('Z');
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// `ps` exiting non-zero because the pid is unknown means gone. Any OTHER
|
|
97
|
+
// failure (ps missing from PATH, a permission error) is NOT evidence of
|
|
98
|
+
// death, and isAlive already said this pid is live — so fail CLOSED and keep
|
|
99
|
+
// treating it as alive. Failing open here would clear the pid file under a
|
|
100
|
+
// running daemon and recreate this very bug through a different door.
|
|
101
|
+
if (err?.code === 'ENOENT' && err?.syscall === 'spawnSync ps')
|
|
102
|
+
return false;
|
|
103
|
+
const out = String(err?.stdout ?? '').trim();
|
|
104
|
+
const errOut = String(err?.stderr ?? '').trim();
|
|
105
|
+
if (err?.status === 1 && out === '' && errOut === '')
|
|
106
|
+
return true; // no such pid
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Block until `pid` stops serving or `timeoutMs` elapses. Synchronous on purpose:
|
|
112
|
+
* the callers are short-lived CLI/postinstall processes that exit before any
|
|
113
|
+
* async timer would fire, which is exactly how a SIGTERMed daemon used to outlive
|
|
114
|
+
* the `stopDaemon()` that was supposed to have reaped it. Returns true if it is
|
|
115
|
+
* gone.
|
|
116
|
+
*/
|
|
117
|
+
export function waitForExit(pid, timeoutMs) {
|
|
118
|
+
const deadline = Date.now() + timeoutMs;
|
|
119
|
+
for (;;) {
|
|
120
|
+
if (hasExited(pid))
|
|
121
|
+
return true;
|
|
122
|
+
if (Date.now() >= deadline)
|
|
123
|
+
return false;
|
|
124
|
+
sleepSync(EXIT_POLL_MS); // blocks this thread outright; no busy-loop
|
|
125
|
+
}
|
|
126
|
+
}
|
|
73
127
|
export function isAlive(pid) {
|
|
74
128
|
if (!pid || pid <= 0)
|
|
75
129
|
return false;
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -191,14 +191,6 @@ export declare function resolveOpenCodePluginSources(pluginRoot: string): string
|
|
|
191
191
|
export declare function installOpenCodePlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
|
|
192
192
|
export declare function isOpenCodePluginInstalled(pluginName: string, versionHome: string): boolean;
|
|
193
193
|
export declare function removeOpenCodePlugin(pluginName: string, versionHome: string): boolean;
|
|
194
|
-
/**
|
|
195
|
-
* Gemini CLI extensions live under `$HOME/.gemini/extensions/<name>/` and
|
|
196
|
-
* require a `gemini-extension.json` manifest at the extension root.
|
|
197
|
-
*/
|
|
198
|
-
export declare function geminiExtensionsDir(versionHome: string): string;
|
|
199
|
-
export declare function installGeminiPlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
|
|
200
|
-
export declare function isGeminiPluginInstalled(pluginName: string, versionHome: string): boolean;
|
|
201
|
-
export declare function removeGeminiPlugin(pluginName: string, versionHome: string): boolean;
|
|
202
194
|
/**
|
|
203
195
|
* Goose auto-discovers Open Plugins at `$HOME/.agents/plugins/<name>/`.
|
|
204
196
|
* Under agents-cli version isolation HOME is the version home, so we install to:
|