@phnx-labs/agents-cli 1.20.82 → 1.20.84
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 +138 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +2 -1
- package/dist/commands/doctor.js +25 -6
- package/dist/commands/menubar.js +12 -0
- package/dist/commands/routines.d.ts +8 -1
- package/dist/commands/routines.js +229 -75
- package/dist/commands/sessions.d.ts +84 -0
- package/dist/commands/sessions.js +60 -25
- package/dist/index.js +47 -0
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.js +34 -3
- package/dist/lib/hooks.d.ts +29 -0
- package/dist/lib/hooks.js +109 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +24 -0
- package/dist/lib/menubar/install-menubar.js +52 -2
- package/dist/lib/permissions.d.ts +21 -0
- package/dist/lib/permissions.js +57 -2
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +116 -0
- package/dist/lib/scheduler.js +16 -4
- 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/session/active.d.ts +33 -0
- package/dist/lib/session/active.js +49 -4
- package/dist/lib/state.js +57 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -656,6 +656,41 @@ function registerJobsCronAliasCommand(p, alias) {
|
|
|
656
656
|
await program.parseAsync(['node', 'agents', ...args]);
|
|
657
657
|
});
|
|
658
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Removed `check` command (RUSH-1234) — re-parses as `doctor --check`, forwarding
|
|
661
|
+
* any remaining flags so `check --quiet` / `check --json` / `check --devices` keep
|
|
662
|
+
* working and the drift-gate exit code survives the rename. The notice goes to
|
|
663
|
+
* stderr so `--json` stdout stays clean for CI parsers.
|
|
664
|
+
*/
|
|
665
|
+
function registerCheckTombstoneCommand(p) {
|
|
666
|
+
p.command('check', { hidden: true })
|
|
667
|
+
.allowUnknownOption()
|
|
668
|
+
.allowExcessArguments()
|
|
669
|
+
.action(async () => {
|
|
670
|
+
console.error(chalk.yellow('Deprecated: "agents check" is now "agents doctor --check". Running that for you.\n'));
|
|
671
|
+
const args = process.argv.slice(2);
|
|
672
|
+
args[0] = 'doctor';
|
|
673
|
+
args.splice(1, 0, '--check');
|
|
674
|
+
await program.parseAsync(['node', 'agents', ...args]);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Removed `resources` command (RUSH-1234) — re-parses as `view --merged` (the
|
|
679
|
+
* cross-layer, first-wins resource table now lives there; `agents inspect <target>`
|
|
680
|
+
* covers per-agent / per-repo detail). Forwards remaining flags like `--json`.
|
|
681
|
+
*/
|
|
682
|
+
function registerResourcesTombstoneCommand(p) {
|
|
683
|
+
p.command('resources', { hidden: true })
|
|
684
|
+
.allowUnknownOption()
|
|
685
|
+
.allowExcessArguments()
|
|
686
|
+
.action(async () => {
|
|
687
|
+
console.error(chalk.yellow('Deprecated: "agents resources" is now "agents view --merged" (use "agents inspect <target>" for per-agent/per-repo detail). Running that for you.\n'));
|
|
688
|
+
const args = process.argv.slice(2);
|
|
689
|
+
args[0] = 'view';
|
|
690
|
+
args.splice(1, 0, '--merged');
|
|
691
|
+
await program.parseAsync(['node', 'agents', ...args]);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
659
694
|
/** Self-upgrade command (`agents upgrade [version]`). */
|
|
660
695
|
function registerUpgradeCommand(p) {
|
|
661
696
|
p.command('upgrade')
|
|
@@ -739,6 +774,16 @@ async function registerEagerForRequest(name) {
|
|
|
739
774
|
registerJobsCronAliasCommand(program, name);
|
|
740
775
|
await reg(loadRoutines);
|
|
741
776
|
return true;
|
|
777
|
+
case 'check':
|
|
778
|
+
// The action re-parses as `doctor --check`, so doctor must exist too.
|
|
779
|
+
registerCheckTombstoneCommand(program);
|
|
780
|
+
await reg(loadDoctor);
|
|
781
|
+
return true;
|
|
782
|
+
case 'resources':
|
|
783
|
+
// The action re-parses as `view --merged`, so view must exist too.
|
|
784
|
+
registerResourcesTombstoneCommand(program);
|
|
785
|
+
await reg(loadView);
|
|
786
|
+
return true;
|
|
742
787
|
case 'upgrade':
|
|
743
788
|
registerUpgradeCommand(program);
|
|
744
789
|
return true;
|
|
@@ -759,6 +804,7 @@ async function registerEagerForRequest(name) {
|
|
|
759
804
|
*/
|
|
760
805
|
async function registerAllEagerCommands() {
|
|
761
806
|
await reg(loadView);
|
|
807
|
+
registerResourcesTombstoneCommand(program);
|
|
762
808
|
await reg(loadShare);
|
|
763
809
|
await reg(loadSend);
|
|
764
810
|
await reg(loadInspect);
|
|
@@ -791,6 +837,7 @@ async function registerAllEagerCommands() {
|
|
|
791
837
|
await reg(loadTrash);
|
|
792
838
|
await reg(loadRestore);
|
|
793
839
|
await reg(loadDoctor);
|
|
840
|
+
registerCheckTombstoneCommand(program);
|
|
794
841
|
await reg(loadApply);
|
|
795
842
|
await reg(loadStatus);
|
|
796
843
|
registerExecAliasCommand(program);
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -262,6 +262,13 @@ export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
|
|
|
262
262
|
* return false resume via the universal Tier-2 `/continue` replay instead.
|
|
263
263
|
*/
|
|
264
264
|
export declare function nativeResume(agent: AgentId): boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Build the `-c` value that adds `dir` to codex's workspace-write writable
|
|
267
|
+
* roots. Codex parses the value as TOML, so it's a single-element TOML array of
|
|
268
|
+
* one quoted string. Used on codex resume forms, which reject `--add-dir` and
|
|
269
|
+
* only accept `-c` config overrides.
|
|
270
|
+
*/
|
|
271
|
+
export declare function codexWritableRootsConfig(dir: string): string;
|
|
265
272
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
266
273
|
export declare function buildExecCommand(options: ExecOptions): string[];
|
|
267
274
|
/** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
|
package/dist/lib/exec.js
CHANGED
|
@@ -16,7 +16,7 @@ import { resolveModel, buildReasoningFlags } from './models.js';
|
|
|
16
16
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
17
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
18
18
|
import { resolveActor, actorEnv } from './actor.js';
|
|
19
|
-
import { getShimsDir, getHistoryDir } from './state.js';
|
|
19
|
+
import { getShimsDir, getHistoryDir, getUserAgentsDir } from './state.js';
|
|
20
20
|
import { resolveCodexHome } from './codex-home.js';
|
|
21
21
|
import { readCodexConfiguredModel } from './shims.js';
|
|
22
22
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
@@ -573,6 +573,15 @@ export const AGENT_COMMANDS = {
|
|
|
573
573
|
export function nativeResume(agent) {
|
|
574
574
|
return AGENT_COMMANDS[agent]?.resume !== undefined;
|
|
575
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Build the `-c` value that adds `dir` to codex's workspace-write writable
|
|
578
|
+
* roots. Codex parses the value as TOML, so it's a single-element TOML array of
|
|
579
|
+
* one quoted string. Used on codex resume forms, which reject `--add-dir` and
|
|
580
|
+
* only accept `-c` config overrides.
|
|
581
|
+
*/
|
|
582
|
+
export function codexWritableRootsConfig(dir) {
|
|
583
|
+
return `sandbox_workspace_write.writable_roots=[${JSON.stringify(dir)}]`;
|
|
584
|
+
}
|
|
576
585
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
577
586
|
export function buildExecCommand(options) {
|
|
578
587
|
const template = AGENT_COMMANDS[options.agent];
|
|
@@ -658,6 +667,18 @@ export function buildExecCommand(options) {
|
|
|
658
667
|
// AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
|
|
659
668
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
660
669
|
}
|
|
670
|
+
// Codex's workspace-write sandbox blocks $HOME (verified against the live CLI
|
|
671
|
+
// and OpenAI's sandbox docs: writable roots extend scope "without removing the
|
|
672
|
+
// sandbox entirely"). But the model routinely shells out to `agents ...`, whose
|
|
673
|
+
// runtime state lives under ~/.agents — the SSH askpass shim
|
|
674
|
+
// (~/.agents/.cache/devices/askpass.sh), the device/stats cache, secrets,
|
|
675
|
+
// session writes, config tunings. Without ~/.agents as a writable root those
|
|
676
|
+
// inner writes fail with EROFS (e.g. `agents ssh` dies before connecting),
|
|
677
|
+
// which is why a remote `agents run codex` couldn't reach the fleet. Grant it
|
|
678
|
+
// implicitly whenever codex runs workspace-write — far narrower than --mode skip
|
|
679
|
+
// (danger-full-access). Fresh runs take --add-dir (below); resume forms reject
|
|
680
|
+
// --add-dir, so they take the same root via -c writable_roots here.
|
|
681
|
+
const codexWorkspaceWrite = options.agent === 'codex' && resolvedMode === 'edit';
|
|
661
682
|
if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
662
683
|
if (resolvedMode === 'skip') {
|
|
663
684
|
// skip = yolo on resume too; both `codex resume` (TUI) and
|
|
@@ -667,6 +688,8 @@ export function buildExecCommand(options) {
|
|
|
667
688
|
else if (interactive) {
|
|
668
689
|
// `codex resume` (TUI) accepts the same -s/--sandbox flags as a fresh run.
|
|
669
690
|
cmd.push(...modeFlags);
|
|
691
|
+
if (codexWorkspaceWrite)
|
|
692
|
+
cmd.push('-c', codexWritableRootsConfig(getUserAgentsDir()));
|
|
670
693
|
}
|
|
671
694
|
else {
|
|
672
695
|
// `codex exec resume` rejects `--sandbox <mode>` (verified against
|
|
@@ -676,6 +699,7 @@ export function buildExecCommand(options) {
|
|
|
676
699
|
cmd.push('-c', `sandbox_mode=${resolvedMode === 'plan' ? 'read-only' : 'workspace-write'}`);
|
|
677
700
|
if (resolvedMode !== 'plan') {
|
|
678
701
|
cmd.push('-c', 'sandbox_workspace_write.network_access=true');
|
|
702
|
+
cmd.push('-c', codexWritableRootsConfig(getUserAgentsDir()));
|
|
679
703
|
}
|
|
680
704
|
}
|
|
681
705
|
}
|
|
@@ -779,9 +803,16 @@ export function buildExecCommand(options) {
|
|
|
779
803
|
// claude-only, silently dropped for codex and masked by edit mode carrying
|
|
780
804
|
// the approval/sandbox bypass. Codex's resume forms reject --add-dir, so
|
|
781
805
|
// skip it there (claude's flag-based resume accepts it).
|
|
782
|
-
|
|
806
|
+
//
|
|
807
|
+
// On top of any user-supplied dirs, a fresh codex workspace-write run
|
|
808
|
+
// implicitly gets ~/.agents (deduped) so the CLI's own tooling — askpass,
|
|
809
|
+
// secrets, sessions, tunings — can write from inside the sandbox. Resume forms
|
|
810
|
+
// get the same root via -c writable_roots above (see codexWorkspaceWrite).
|
|
811
|
+
const codexImplicitDirs = codexWorkspaceWrite && !options.resume ? [getUserAgentsDir()] : [];
|
|
812
|
+
const addDirs = [...new Set([...(options.addDirs ?? []), ...codexImplicitDirs])];
|
|
813
|
+
if (addDirs.length > 0 &&
|
|
783
814
|
(options.agent === 'claude' || (options.agent === 'codex' && !options.resume))) {
|
|
784
|
-
for (const dir of
|
|
815
|
+
for (const dir of addDirs) {
|
|
785
816
|
cmd.push('--add-dir', dir);
|
|
786
817
|
}
|
|
787
818
|
}
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -21,12 +21,41 @@ export declare function resolveHookScriptPath(script: string): string | null;
|
|
|
21
21
|
* os.homedir()/path.sep at the call site.
|
|
22
22
|
*/
|
|
23
23
|
export declare function toPortableCommand(absPath: string, home?: string, sep?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Collapse version-scoped registrations by logical resource identity rather
|
|
26
|
+
* than absolute command path. When the same resource is present in several
|
|
27
|
+
* homes, the active home's command wins even if it appears later. Commands
|
|
28
|
+
* outside version homes are user-owned and remain untouched.
|
|
29
|
+
*/
|
|
30
|
+
export declare function deduplicateVersionHookCommands(commands: string[], activeVersionHome: string): string[];
|
|
24
31
|
import type { AgentId, InstalledHook, ManifestHook } from './types.js';
|
|
25
32
|
export type HookEntry = {
|
|
26
33
|
name: string;
|
|
27
34
|
scriptPath: string;
|
|
28
35
|
dataFile?: string;
|
|
29
36
|
};
|
|
37
|
+
export interface VersionHookCopy {
|
|
38
|
+
agent: AgentId;
|
|
39
|
+
version: string;
|
|
40
|
+
name: string;
|
|
41
|
+
path: string;
|
|
42
|
+
hash: string;
|
|
43
|
+
active: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface DuplicateVersionHook {
|
|
46
|
+
agent: AgentId;
|
|
47
|
+
name: string;
|
|
48
|
+
kind: 'duplicate' | 'drift';
|
|
49
|
+
authoritative: VersionHookCopy;
|
|
50
|
+
copies: VersionHookCopy[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Inspect every installed hooks-capable harness without assuming a native
|
|
54
|
+
* settings format. Same-name copies with one content hash are installation
|
|
55
|
+
* noise; multiple hashes are drift. The active version is always selected as
|
|
56
|
+
* the authoritative copy when it participates in the group.
|
|
57
|
+
*/
|
|
58
|
+
export declare function inspectDuplicateVersionHooks(cwd?: string): DuplicateVersionHook[];
|
|
30
59
|
/**
|
|
31
60
|
* List hook entries in a single directory, grouping script + data files by
|
|
32
61
|
* basename. Exported so doctor-diff can reuse the same grouping the sync path
|
package/dist/lib/hooks.js
CHANGED
|
@@ -146,9 +146,113 @@ function isStaleSiblingVersionCommand(command, current) {
|
|
|
146
146
|
const id = versionHomeIdentity(command);
|
|
147
147
|
return id !== null && id.agent === current.agent && id.version !== current.version;
|
|
148
148
|
}
|
|
149
|
-
|
|
149
|
+
function hookResourceName(command) {
|
|
150
|
+
const withoutArgs = command.trim().split(/\s+/)[0];
|
|
151
|
+
return path.basename(withoutArgs).replace(/\.[^.]+$/, '');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Collapse version-scoped registrations by logical resource identity rather
|
|
155
|
+
* than absolute command path. When the same resource is present in several
|
|
156
|
+
* homes, the active home's command wins even if it appears later. Commands
|
|
157
|
+
* outside version homes are user-owned and remain untouched.
|
|
158
|
+
*/
|
|
159
|
+
export function deduplicateVersionHookCommands(commands, activeVersionHome) {
|
|
160
|
+
const active = versionHomeIdentity(activeVersionHome);
|
|
161
|
+
if (!active)
|
|
162
|
+
return [...commands];
|
|
163
|
+
const selected = new Map();
|
|
164
|
+
const passthrough = [];
|
|
165
|
+
for (const command of commands) {
|
|
166
|
+
const identity = versionHomeIdentity(command);
|
|
167
|
+
if (!identity || identity.agent !== active.agent) {
|
|
168
|
+
passthrough.push(command);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const key = `${identity.agent}\0${hookResourceName(command)}`;
|
|
172
|
+
const candidateIsActive = identity.version === active.version;
|
|
173
|
+
const prior = selected.get(key);
|
|
174
|
+
if (!prior || (!prior.active && candidateIsActive)) {
|
|
175
|
+
selected.set(key, { command, active: candidateIsActive });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [...passthrough, ...Array.from(selected.values(), ({ command }) => command)];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Collapse a matcher group's hook entries down to the deduplicated command
|
|
182
|
+
* multiset {@link deduplicateVersionHookCommands} returns, preserving order and
|
|
183
|
+
* the concrete entry objects. The deduped list is honored as a per-command
|
|
184
|
+
* budget, NOT a membership set: two identical active-version entries collapse to
|
|
185
|
+
* one because the budget for that command is one. (A membership `Set.has` test
|
|
186
|
+
* would keep both — the bug this replaces.) Passthrough (user / non-version-home)
|
|
187
|
+
* commands keep their original multiplicity, so genuine user hooks are untouched.
|
|
188
|
+
*/
|
|
189
|
+
function collapseVersionHookEntries(entries, activeVersionHome) {
|
|
190
|
+
const budget = new Map();
|
|
191
|
+
for (const command of deduplicateVersionHookCommands(entries.map((e) => e.command), activeVersionHome)) {
|
|
192
|
+
budget.set(command, (budget.get(command) ?? 0) + 1);
|
|
193
|
+
}
|
|
194
|
+
return entries.filter((e) => {
|
|
195
|
+
const remaining = budget.get(e.command) ?? 0;
|
|
196
|
+
if (remaining <= 0)
|
|
197
|
+
return false;
|
|
198
|
+
budget.set(e.command, remaining - 1);
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
import { getEffectiveHome, getVersionHomePath, listInstalledVersions, resolveVersion } from './versions.js';
|
|
150
203
|
import { generateHookShim, getHookShimPath, isValidHookShimName, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
151
204
|
import { getHookShimsDir } from './state.js';
|
|
205
|
+
function hookContentHash(scriptPath) {
|
|
206
|
+
return crypto.createHash('sha256').update(fs.readFileSync(scriptPath)).digest('hex');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Inspect every installed hooks-capable harness without assuming a native
|
|
210
|
+
* settings format. Same-name copies with one content hash are installation
|
|
211
|
+
* noise; multiple hashes are drift. The active version is always selected as
|
|
212
|
+
* the authoritative copy when it participates in the group.
|
|
213
|
+
*/
|
|
214
|
+
export function inspectDuplicateVersionHooks(cwd = process.cwd()) {
|
|
215
|
+
const byResource = new Map();
|
|
216
|
+
for (const { agent, version } of iterHooksCapableVersions()) {
|
|
217
|
+
const activeVersion = resolveVersion(agent, cwd);
|
|
218
|
+
for (const entry of listHooksInVersionHome(agent, version)) {
|
|
219
|
+
let hash;
|
|
220
|
+
try {
|
|
221
|
+
hash = hookContentHash(entry.scriptPath);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const copy = {
|
|
227
|
+
agent,
|
|
228
|
+
version,
|
|
229
|
+
name: entry.name,
|
|
230
|
+
path: entry.scriptPath,
|
|
231
|
+
hash,
|
|
232
|
+
active: version === activeVersion,
|
|
233
|
+
};
|
|
234
|
+
const key = `${agent}\0${entry.name}`;
|
|
235
|
+
const copies = byResource.get(key) ?? [];
|
|
236
|
+
copies.push(copy);
|
|
237
|
+
byResource.set(key, copies);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const findings = [];
|
|
241
|
+
for (const copies of byResource.values()) {
|
|
242
|
+
if (copies.length < 2)
|
|
243
|
+
continue;
|
|
244
|
+
copies.sort((a, b) => a.version.localeCompare(b.version, undefined, { numeric: true }));
|
|
245
|
+
const authoritative = copies.find((copy) => copy.active) ?? copies[copies.length - 1];
|
|
246
|
+
findings.push({
|
|
247
|
+
agent: authoritative.agent,
|
|
248
|
+
name: authoritative.name,
|
|
249
|
+
kind: new Set(copies.map((copy) => copy.hash)).size === 1 ? 'duplicate' : 'drift',
|
|
250
|
+
authoritative,
|
|
251
|
+
copies,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return findings.sort((a, b) => `${a.agent}/${a.name}`.localeCompare(`${b.agent}/${b.name}`));
|
|
255
|
+
}
|
|
152
256
|
/**
|
|
153
257
|
* Resolve the command path to register for a hook.
|
|
154
258
|
*
|
|
@@ -1351,8 +1455,9 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
1351
1455
|
for (const group of eventEntries) {
|
|
1352
1456
|
if (!group.hooks)
|
|
1353
1457
|
continue;
|
|
1354
|
-
|
|
1458
|
+
const managed = group.hooks.filter((h) => (!isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command)) &&
|
|
1355
1459
|
!isStaleSiblingVersionCommand(h.command, currentVh));
|
|
1460
|
+
group.hooks = collapseVersionHookEntries(managed, versionHome);
|
|
1356
1461
|
}
|
|
1357
1462
|
}
|
|
1358
1463
|
// Remove empty matcher groups left after cleanup
|
|
@@ -1509,8 +1614,9 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
1509
1614
|
for (const group of eventGroups) {
|
|
1510
1615
|
if (!group.hooks)
|
|
1511
1616
|
continue;
|
|
1512
|
-
|
|
1617
|
+
const managed = group.hooks.filter((h) => (!isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command)) &&
|
|
1513
1618
|
!isStaleSiblingVersionCommand(h.command, currentVh));
|
|
1619
|
+
group.hooks = collapseVersionHookEntries(managed, versionHome);
|
|
1514
1620
|
}
|
|
1515
1621
|
}
|
|
1516
1622
|
for (const [event, eventGroups] of Object.entries(hooksFile.hooks)) {
|
|
Binary file
|
|
@@ -124,6 +124,28 @@ export declare function disableMenubarService(): void;
|
|
|
124
124
|
* Best-effort — never throws into startup.
|
|
125
125
|
*/
|
|
126
126
|
export declare function installMenubarLaunchAgentOnUpgrade(): void;
|
|
127
|
+
/** A live MenubarHelper process: its pid and the executable it is running. */
|
|
128
|
+
export interface MenubarProcess {
|
|
129
|
+
pid: number;
|
|
130
|
+
executable: string;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Split the live MenubarHelper processes into the installed bundle's own
|
|
134
|
+
* (`running`) and every other copy (`foreign`).
|
|
135
|
+
*
|
|
136
|
+
* `pgrep -f MenubarHelper` conflated the two, so a stray dev build could hold
|
|
137
|
+
* the global Cmd-Shift-V chord (RegisterEventHotKey is first-come) while status
|
|
138
|
+
* still reported a healthy `running: yes` — the paste was dead and nothing said
|
|
139
|
+
* so. A foreign copy is the thing to look for, so name it.
|
|
140
|
+
*
|
|
141
|
+
* Identity comes from `comm` (the resolved executable), never from a substring
|
|
142
|
+
* of the command line: matching the latter flags any shell that merely mentions
|
|
143
|
+
* MenubarHelper. `command` is consulted only to drop `--notify` one-shots.
|
|
144
|
+
*/
|
|
145
|
+
export declare function classifyMenubarProcesses(commOutput: string, commandOutput: string, installedExec: string): {
|
|
146
|
+
running: boolean;
|
|
147
|
+
foreign: MenubarProcess[];
|
|
148
|
+
};
|
|
127
149
|
export interface MenubarStatus {
|
|
128
150
|
platform: string;
|
|
129
151
|
source: string | null;
|
|
@@ -133,6 +155,8 @@ export interface MenubarStatus {
|
|
|
133
155
|
stale: boolean;
|
|
134
156
|
serviceInstalled: boolean;
|
|
135
157
|
running: boolean;
|
|
158
|
+
/** Live MenubarHelper processes that are NOT the installed bundle. */
|
|
159
|
+
foreignInstances: MenubarProcess[];
|
|
136
160
|
disabledByUser: boolean;
|
|
137
161
|
}
|
|
138
162
|
export declare function getMenubarStatus(): MenubarStatus;
|
|
@@ -441,12 +441,61 @@ export function installMenubarLaunchAgentOnUpgrade() {
|
|
|
441
441
|
/* never block startup on the menu bar */
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
|
+
/** Parse `ps -axo pid=,<field>=` into pid -> field. The field is the rest of the
|
|
445
|
+
* line, so a path containing spaces (App Support does) survives intact. */
|
|
446
|
+
function parsePsLines(psOutput) {
|
|
447
|
+
const out = new Map();
|
|
448
|
+
for (const line of psOutput.split('\n')) {
|
|
449
|
+
const m = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
450
|
+
if (m)
|
|
451
|
+
out.set(Number(m[1]), m[2]);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Split the live MenubarHelper processes into the installed bundle's own
|
|
457
|
+
* (`running`) and every other copy (`foreign`).
|
|
458
|
+
*
|
|
459
|
+
* `pgrep -f MenubarHelper` conflated the two, so a stray dev build could hold
|
|
460
|
+
* the global Cmd-Shift-V chord (RegisterEventHotKey is first-come) while status
|
|
461
|
+
* still reported a healthy `running: yes` — the paste was dead and nothing said
|
|
462
|
+
* so. A foreign copy is the thing to look for, so name it.
|
|
463
|
+
*
|
|
464
|
+
* Identity comes from `comm` (the resolved executable), never from a substring
|
|
465
|
+
* of the command line: matching the latter flags any shell that merely mentions
|
|
466
|
+
* MenubarHelper. `command` is consulted only to drop `--notify` one-shots.
|
|
467
|
+
*/
|
|
468
|
+
export function classifyMenubarProcesses(commOutput, commandOutput, installedExec) {
|
|
469
|
+
const commands = parsePsLines(commandOutput);
|
|
470
|
+
const foreign = [];
|
|
471
|
+
let running = false;
|
|
472
|
+
for (const [pid, executable] of parsePsLines(commOutput)) {
|
|
473
|
+
if (path.basename(executable) !== 'MenubarHelper')
|
|
474
|
+
continue;
|
|
475
|
+
// `--notify` is a one-shot that posts a notification and exits; it runs the
|
|
476
|
+
// installed binary but never claims the status item or the chords.
|
|
477
|
+
if ((commands.get(pid) || '').includes('--notify'))
|
|
478
|
+
continue;
|
|
479
|
+
if (executable === installedExec)
|
|
480
|
+
running = true;
|
|
481
|
+
else
|
|
482
|
+
foreign.push({ pid, executable });
|
|
483
|
+
}
|
|
484
|
+
return { running, foreign };
|
|
485
|
+
}
|
|
444
486
|
export function getMenubarStatus() {
|
|
445
487
|
const dest = installedAppPath();
|
|
446
488
|
let running = false;
|
|
489
|
+
let foreignInstances = [];
|
|
447
490
|
if (onDarwin()) {
|
|
448
|
-
const
|
|
449
|
-
|
|
491
|
+
const ps = (format) => spawnSync('ps', ['-axo', format], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' });
|
|
492
|
+
const comm = ps('pid=,comm=');
|
|
493
|
+
const command = ps('pid=,command=');
|
|
494
|
+
if (comm.status === 0 && command.status === 0) {
|
|
495
|
+
const c = classifyMenubarProcesses(comm.stdout || '', command.stdout || '', installedExecutablePath());
|
|
496
|
+
running = c.running;
|
|
497
|
+
foreignInstances = c.foreign;
|
|
498
|
+
}
|
|
450
499
|
}
|
|
451
500
|
const serviceInstalled = menubarServiceInstalled();
|
|
452
501
|
return {
|
|
@@ -458,6 +507,7 @@ export function getMenubarStatus() {
|
|
|
458
507
|
stale: onDarwin() && serviceInstalled && menubarSetupStale(),
|
|
459
508
|
serviceInstalled,
|
|
460
509
|
running,
|
|
510
|
+
foreignInstances,
|
|
461
511
|
disabledByUser: menubarDisabledByUser(),
|
|
462
512
|
};
|
|
463
513
|
}
|
|
@@ -233,6 +233,27 @@ export declare function convertToKimiFormat(set: PermissionSet): {
|
|
|
233
233
|
* OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
|
|
234
234
|
*/
|
|
235
235
|
export declare function convertToOpenCodeFormat(set: PermissionSet): OpenCodePermissions;
|
|
236
|
+
/**
|
|
237
|
+
* Default extra writable roots for Codex's `workspace-write` sandbox: the
|
|
238
|
+
* regenerable package/toolchain caches that build/test/install write OUTSIDE the
|
|
239
|
+
* workspace (cargo registry, npm/bun/pnpm caches, GOPATH/GOCACHE, the OS cache
|
|
240
|
+
* root, …). Without these, a sandboxed `cargo build` / `go build` / `npm install`
|
|
241
|
+
* fails on its cache write, which is what pushes users to `--mode full` (YOLO).
|
|
242
|
+
*
|
|
243
|
+
* Credential dirs (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config`, `~/.netrc`) are
|
|
244
|
+
* deliberately EXCLUDED so the sandbox stays meaningful — this is far from
|
|
245
|
+
* danger-full-access. Resolved per platform + home; each box regenerates its own
|
|
246
|
+
* Codex config on sync, so the paths always match the box Codex runs on.
|
|
247
|
+
*/
|
|
248
|
+
export declare function codexDefaultWritableRoots(home?: string, platform?: NodeJS.Platform): string[];
|
|
249
|
+
/**
|
|
250
|
+
* Merge Codex `[sandbox_workspace_write]` config, UNIONing `writable_roots` so a
|
|
251
|
+
* baseline cache root never clobbers a root the user configured directly (a
|
|
252
|
+
* plain object spread would overwrite the whole array), while merging scalar
|
|
253
|
+
* keys (`network_access`) normally. Shared by the user- and version-scoped Codex
|
|
254
|
+
* config writers so the two can't drift.
|
|
255
|
+
*/
|
|
256
|
+
export declare function mergeCodexSandboxWrite(existing: Record<string, unknown> | undefined, incoming: NonNullable<CodexPermissions['sandbox_workspace_write']>): Record<string, unknown>;
|
|
236
257
|
/**
|
|
237
258
|
* Convert canonical permission set to Codex format.
|
|
238
259
|
* Codex uses coarse-grained modes, so we infer the best fit.
|
package/dist/lib/permissions.js
CHANGED
|
@@ -1007,6 +1007,48 @@ export function convertToOpenCodeFormat(set) {
|
|
|
1007
1007
|
},
|
|
1008
1008
|
};
|
|
1009
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Default extra writable roots for Codex's `workspace-write` sandbox: the
|
|
1012
|
+
* regenerable package/toolchain caches that build/test/install write OUTSIDE the
|
|
1013
|
+
* workspace (cargo registry, npm/bun/pnpm caches, GOPATH/GOCACHE, the OS cache
|
|
1014
|
+
* root, …). Without these, a sandboxed `cargo build` / `go build` / `npm install`
|
|
1015
|
+
* fails on its cache write, which is what pushes users to `--mode full` (YOLO).
|
|
1016
|
+
*
|
|
1017
|
+
* Credential dirs (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config`, `~/.netrc`) are
|
|
1018
|
+
* deliberately EXCLUDED so the sandbox stays meaningful — this is far from
|
|
1019
|
+
* danger-full-access. Resolved per platform + home; each box regenerates its own
|
|
1020
|
+
* Codex config on sync, so the paths always match the box Codex runs on.
|
|
1021
|
+
*/
|
|
1022
|
+
export function codexDefaultWritableRoots(home = HOME, platform = process.platform) {
|
|
1023
|
+
const shared = ['.cargo', '.rustup', '.npm', '.bun', 'go', '.deno', '.gradle', '.m2', '.gem'];
|
|
1024
|
+
const roots = shared.map((d) => path.join(home, d));
|
|
1025
|
+
if (platform === 'darwin') {
|
|
1026
|
+
roots.push(path.join(home, 'Library', 'Caches'), path.join(home, 'Library', 'pnpm'));
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
// Linux/XDG: ~/.cache covers pip, uv, go-build, ms-playwright, etc.
|
|
1030
|
+
roots.push(path.join(home, '.cache'), path.join(home, '.local', 'share'), path.join(home, '.local', 'state'));
|
|
1031
|
+
}
|
|
1032
|
+
return roots;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Merge Codex `[sandbox_workspace_write]` config, UNIONing `writable_roots` so a
|
|
1036
|
+
* baseline cache root never clobbers a root the user configured directly (a
|
|
1037
|
+
* plain object spread would overwrite the whole array), while merging scalar
|
|
1038
|
+
* keys (`network_access`) normally. Shared by the user- and version-scoped Codex
|
|
1039
|
+
* config writers so the two can't drift.
|
|
1040
|
+
*/
|
|
1041
|
+
export function mergeCodexSandboxWrite(existing, incoming) {
|
|
1042
|
+
const existingRoots = Array.isArray(existing?.writable_roots)
|
|
1043
|
+
? existing.writable_roots
|
|
1044
|
+
: [];
|
|
1045
|
+
const unionRoots = [...new Set([...existingRoots, ...(incoming.writable_roots ?? [])])];
|
|
1046
|
+
return {
|
|
1047
|
+
...existing,
|
|
1048
|
+
...incoming,
|
|
1049
|
+
...(unionRoots.length > 0 ? { writable_roots: unionRoots } : {}),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1010
1052
|
/**
|
|
1011
1053
|
* Convert canonical permission set to Codex format.
|
|
1012
1054
|
* Codex uses coarse-grained modes, so we infer the best fit.
|
|
@@ -1042,6 +1084,15 @@ export function convertToCodexFormat(set, cwd) {
|
|
|
1042
1084
|
network_access: true,
|
|
1043
1085
|
};
|
|
1044
1086
|
}
|
|
1087
|
+
// Baseline (unconditional): always grant the regenerable build/test/install
|
|
1088
|
+
// caches as writable roots so `agents run codex` in workspace-write can build,
|
|
1089
|
+
// test, and install without escalating to danger-full-access. Merged with
|
|
1090
|
+
// network_access above when set; applyCodexPermissions unions these with any
|
|
1091
|
+
// writable_roots the user configured directly.
|
|
1092
|
+
result.sandbox_workspace_write = {
|
|
1093
|
+
...result.sandbox_workspace_write,
|
|
1094
|
+
writable_roots: codexDefaultWritableRoots(),
|
|
1095
|
+
};
|
|
1045
1096
|
return result;
|
|
1046
1097
|
}
|
|
1047
1098
|
// ============================================================================
|
|
@@ -1333,8 +1384,10 @@ function applyCodexPermissions(set, scope = 'user', cwd, merge = true) {
|
|
|
1333
1384
|
}
|
|
1334
1385
|
if (newPermissions.sandbox_workspace_write) {
|
|
1335
1386
|
const existing = config.sandbox_workspace_write;
|
|
1387
|
+
// merge=false is a deliberate full replace (drops any user-custom roots);
|
|
1388
|
+
// production sync always passes merge=true, taking the union path.
|
|
1336
1389
|
config.sandbox_workspace_write = merge
|
|
1337
|
-
?
|
|
1390
|
+
? mergeCodexSandboxWrite(existing, newPermissions.sandbox_workspace_write)
|
|
1338
1391
|
: newPermissions.sandbox_workspace_write;
|
|
1339
1392
|
}
|
|
1340
1393
|
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
@@ -1450,8 +1503,10 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1450
1503
|
}
|
|
1451
1504
|
if (newPermissions.sandbox_workspace_write) {
|
|
1452
1505
|
const existing = config.sandbox_workspace_write;
|
|
1506
|
+
// merge=false is a deliberate full replace (drops any user-custom roots);
|
|
1507
|
+
// production sync always passes merge=true, taking the union path.
|
|
1453
1508
|
config.sandbox_workspace_write = merge
|
|
1454
|
-
?
|
|
1509
|
+
? mergeCodexSandboxWrite(existing, newPermissions.sandbox_workspace_write)
|
|
1455
1510
|
: newPermissions.sandbox_workspace_write;
|
|
1456
1511
|
}
|
|
1457
1512
|
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -347,6 +347,19 @@ export declare function validateJob(config: Partial<JobConfig>): string[];
|
|
|
347
347
|
export declare function validateTrigger(trigger: unknown): string[];
|
|
348
348
|
/** True when a job's endAt has already elapsed. False when endAt is unset or in the future. */
|
|
349
349
|
export declare function isPastEndAt(config: Pick<JobConfig, 'endAt'>, now?: Date): boolean;
|
|
350
|
+
export interface OneShotScheduleParts {
|
|
351
|
+
minute: number;
|
|
352
|
+
hour: number;
|
|
353
|
+
day: number;
|
|
354
|
+
month: number;
|
|
355
|
+
}
|
|
356
|
+
export declare function parseOneShotLikeSchedule(schedule: string | undefined | null): OneShotScheduleParts | null;
|
|
357
|
+
export declare function isOneShotLikeSchedule(schedule: string | undefined | null): boolean;
|
|
358
|
+
export declare function isOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce'>): boolean;
|
|
359
|
+
export declare function oneShotScheduleFireDate(schedule: string | undefined | null, now?: Date, timezone?: string): Date | null;
|
|
360
|
+
export declare function isPastOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
361
|
+
export declare function hasCompletedOneShotRun(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
362
|
+
export declare function shouldPurgeCompletedOneShotRoutine(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
350
363
|
/** Expand built-in and user-defined template variables in a job's prompt string. */
|
|
351
364
|
export declare function resolveJobPrompt(config: JobConfig): string;
|
|
352
365
|
/** Parse a human-readable timeout string (e.g. "10m", "2h", "1h30m", "3d", "1w") into milliseconds.
|