@phnx-labs/agents-cli 1.20.72 → 1.20.74
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 +395 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/doctor.js +21 -5
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/import.js +51 -3
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.js +18 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +9 -2
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/versions.js +36 -5
- package/dist/commands/view.js +75 -27
- package/dist/index.js +80 -10
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.d.ts +35 -0
- package/dist/lib/agents.js +79 -6
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +56 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +9 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/import.d.ts +23 -0
- package/dist/lib/import.js +74 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +306 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +35 -0
- package/dist/lib/shims.js +159 -13
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +17 -4
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +84 -0
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +269 -78
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/lib/exec.d.ts
CHANGED
|
@@ -158,6 +158,17 @@ export interface ExecOptions {
|
|
|
158
158
|
* `--interactive` takes precedence over `--headless`; the CLI layer rejects passing both.
|
|
159
159
|
*/
|
|
160
160
|
export declare function resolveInteractive(options: Pick<ExecOptions, 'interactive' | 'headless' | 'prompt'>): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* True when a run resolved to *inferred* interactive intent — no prompt and no
|
|
163
|
+
* explicit `--interactive` — but there is no terminal to host the REPL. Launching
|
|
164
|
+
* would attach a TUI to a dead stdin and hang forever, so the caller should fail
|
|
165
|
+
* fast with the headless alternatives instead (RUSH-1829).
|
|
166
|
+
*
|
|
167
|
+
* An explicit `--interactive` is the caller's deliberate choice and is never
|
|
168
|
+
* blocked (they may be driving a PTY we can't detect). Pure — the TTY state is a
|
|
169
|
+
* parameter so this is unit-testable without touching `process.std*`.
|
|
170
|
+
*/
|
|
171
|
+
export declare function inferredInteractiveWithoutTty(options: Pick<ExecOptions, 'interactive' | 'headless' | 'prompt'>, isTty: boolean): boolean;
|
|
161
172
|
/**
|
|
162
173
|
* Decide whether spawnAgent must capture (PIPE + tee) the child's stdout so the
|
|
163
174
|
* live budget watcher can parse it (issue #346, FIX 3).
|
package/dist/lib/exec.js
CHANGED
|
@@ -15,7 +15,8 @@ import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion }
|
|
|
15
15
|
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
|
-
import { getShimsDir } from './state.js';
|
|
18
|
+
import { getShimsDir, getHistoryDir } from './state.js';
|
|
19
|
+
import { resolveCodexHome } from './codex-home.js';
|
|
19
20
|
import { readCodexConfiguredModel } from './shims.js';
|
|
20
21
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
21
22
|
import { recordRunName } from './session/run-names.js';
|
|
@@ -153,6 +154,21 @@ export function resolveInteractive(options) {
|
|
|
153
154
|
return false;
|
|
154
155
|
return options.prompt === undefined;
|
|
155
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* True when a run resolved to *inferred* interactive intent — no prompt and no
|
|
159
|
+
* explicit `--interactive` — but there is no terminal to host the REPL. Launching
|
|
160
|
+
* would attach a TUI to a dead stdin and hang forever, so the caller should fail
|
|
161
|
+
* fast with the headless alternatives instead (RUSH-1829).
|
|
162
|
+
*
|
|
163
|
+
* An explicit `--interactive` is the caller's deliberate choice and is never
|
|
164
|
+
* blocked (they may be driving a PTY we can't detect). Pure — the TTY state is a
|
|
165
|
+
* parameter so this is unit-testable without touching `process.std*`.
|
|
166
|
+
*/
|
|
167
|
+
export function inferredInteractiveWithoutTty(options, isTty) {
|
|
168
|
+
if (options.interactive === true)
|
|
169
|
+
return false;
|
|
170
|
+
return resolveInteractive(options) && !isTty;
|
|
171
|
+
}
|
|
156
172
|
/**
|
|
157
173
|
* Decide whether spawnAgent must capture (PIPE + tee) the child's stdout so the
|
|
158
174
|
* live budget watcher can parse it (issue #346, FIX 3).
|
|
@@ -241,7 +257,12 @@ export function buildExecEnv(options) {
|
|
|
241
257
|
? resolvedVersion
|
|
242
258
|
: (resolvedVersion && isVersionInstalled('codex', resolvedVersion) ? resolvedVersion : null);
|
|
243
259
|
if (version) {
|
|
244
|
-
|
|
260
|
+
// On macOS the deep versioned home overflows the Unix-socket SUN_LEN
|
|
261
|
+
// limit for codex's app-server control socket; resolve to a short,
|
|
262
|
+
// SUN_LEN-safe home (migrating once if needed). See codex-home.ts.
|
|
263
|
+
const versionedHome = path.join(getVersionHomePath('codex', version), '.codex');
|
|
264
|
+
const agentsUserDir = path.dirname(getHistoryDir());
|
|
265
|
+
result.CODEX_HOME = resolveCodexHome(versionedHome, agentsUserDir, version);
|
|
245
266
|
}
|
|
246
267
|
delete result.CLAUDE_CONFIG_DIR;
|
|
247
268
|
delete result.COPILOT_HOME;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AgentId } from './types.js';
|
|
2
|
+
export type ExportMode = 'merge' | 'replace' | 'staged';
|
|
3
|
+
/** Filename of the provenance receipt written into the agent's config dir. */
|
|
4
|
+
export declare const RECEIPT_NAME = ".agents-cli-export.json";
|
|
5
|
+
/** Suffix for the incoming copy of a file the user already has. */
|
|
6
|
+
export declare const CONFLICT_SUFFIX = ".from-agents-cli";
|
|
7
|
+
/** Why an export cannot proceed. Each maps to a distinct user-facing remedy. */
|
|
8
|
+
export type ExportBlocker = {
|
|
9
|
+
kind: 'not-installed';
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'not-isolated';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'no-config';
|
|
14
|
+
source: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'dest-adopted';
|
|
17
|
+
realPath: string;
|
|
18
|
+
adoptedVersion: string;
|
|
19
|
+
};
|
|
20
|
+
/** What the destination `~/.<agent>` currently is. */
|
|
21
|
+
export type DestKind = 'absent' | 'real-dir' | 'foreign-symlink';
|
|
22
|
+
/** One file the export would place, relative to the config dir root. */
|
|
23
|
+
export interface ExportEntry {
|
|
24
|
+
/** Path relative to the config dir, e.g. `prompts/review.md`. */
|
|
25
|
+
rel: string;
|
|
26
|
+
/** Absolute source file inside the isolated home. */
|
|
27
|
+
source: string;
|
|
28
|
+
/** Absolute path this file will be written to. */
|
|
29
|
+
target: string;
|
|
30
|
+
/**
|
|
31
|
+
* Set when the user already has `rel`. `target` then points at the
|
|
32
|
+
* `.from-agents-cli` sibling, and `existing` is the untouched original.
|
|
33
|
+
*/
|
|
34
|
+
existing?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ExportPlan {
|
|
37
|
+
agent: AgentId;
|
|
38
|
+
version: string;
|
|
39
|
+
mode: ExportMode;
|
|
40
|
+
/** The isolated config dir, e.g. `<versionDir>/home/.codex`. */
|
|
41
|
+
source: string;
|
|
42
|
+
/** The user's real config dir, e.g. `~/.codex`. */
|
|
43
|
+
dest: string;
|
|
44
|
+
destKind: DestKind;
|
|
45
|
+
/** Files with no counterpart in `dest` — written in place. */
|
|
46
|
+
writes: ExportEntry[];
|
|
47
|
+
/** Files the user already has — written alongside, never over. */
|
|
48
|
+
conflicts: ExportEntry[];
|
|
49
|
+
/** replace mode only: where `dest` gets moved first. */
|
|
50
|
+
backupPath: string | null;
|
|
51
|
+
/** staged mode only: the subdirectory receiving the whole tree. */
|
|
52
|
+
stagedPath: string | null;
|
|
53
|
+
receiptPath: string;
|
|
54
|
+
blocker: ExportBlocker | null;
|
|
55
|
+
}
|
|
56
|
+
export interface ExportResult {
|
|
57
|
+
exported: boolean;
|
|
58
|
+
dest: string;
|
|
59
|
+
written: string[];
|
|
60
|
+
conflicts: Array<{
|
|
61
|
+
path: string;
|
|
62
|
+
theirs: string;
|
|
63
|
+
}>;
|
|
64
|
+
backupPath: string | null;
|
|
65
|
+
stagedPath: string | null;
|
|
66
|
+
receiptPath: string | null;
|
|
67
|
+
errors: string[];
|
|
68
|
+
}
|
|
69
|
+
/** Build a read-only plan. Performs no mutations. */
|
|
70
|
+
export declare function planExport(agent: AgentId, version: string, timestamp: number, mode?: ExportMode): ExportPlan;
|
|
71
|
+
/** Execute a plan from {@link planExport}. */
|
|
72
|
+
export declare function executeExport(plan: ExportPlan, timestamp: number): ExportResult;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents export` — copy an ISOLATED install's config back out to the user's real
|
|
3
|
+
* `~/.<agent>`, so they can keep their settings and delete agents-cli entirely.
|
|
4
|
+
*
|
|
5
|
+
* Isolation is currently a one-way door: `agents add <agent>@<v> --isolated` builds a
|
|
6
|
+
* self-contained home under the version dir, and nothing ever brings that work back.
|
|
7
|
+
* Reversibility is what makes a sandbox safe to try.
|
|
8
|
+
*
|
|
9
|
+
* Three modes, because "export" means different things depending on which config you
|
|
10
|
+
* consider authoritative:
|
|
11
|
+
*
|
|
12
|
+
* merge (default) — additive. Copy only paths the user doesn't already have. A
|
|
13
|
+
* collision is NOT silently skipped: the incoming file is written
|
|
14
|
+
* beside theirs as `<name>.from-agents-cli` so they can diff and
|
|
15
|
+
* take the parts they want. Nothing of theirs is ever modified.
|
|
16
|
+
* replace — the isolated config becomes `~/.<agent>`; theirs moves to
|
|
17
|
+
* `backups/<agent>/<ts>`. For when you've been living in the
|
|
18
|
+
* sandbox and want to promote it wholesale.
|
|
19
|
+
* staged — write the whole tree to `~/.<agent>/.agents-export-<ts>/` and
|
|
20
|
+
* activate nothing. For inspecting first.
|
|
21
|
+
*
|
|
22
|
+
* Every mode leaves a receipt at `~/.<agent>/.agents-cli-export.json` recording what
|
|
23
|
+
* came from the export. That is what makes provenance answerable ("which of these
|
|
24
|
+
* files are mine?") and the whole operation reversible.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately NOT implemented: key-level merging of file *contents*. The TOML
|
|
27
|
+
* library here does not preserve comments across parse+stringify, so auto-merging a
|
|
28
|
+
* `config.toml` would silently delete the user's comments. Handing over both files
|
|
29
|
+
* and a diff is honest; rewriting their config behind a "merge" flag is not.
|
|
30
|
+
*
|
|
31
|
+
* Split into a read-only {@link planExport} and a mutating {@link executeExport} so
|
|
32
|
+
* `--dry-run` and the real run share one code path, mirroring `lib/uninstall.ts`.
|
|
33
|
+
*/
|
|
34
|
+
import * as fs from 'fs';
|
|
35
|
+
import * as path from 'path';
|
|
36
|
+
import { AGENTS } from './agents.js';
|
|
37
|
+
import { getAgentConfigPath, getConfigSymlinkVersion } from './shims.js';
|
|
38
|
+
import { getVersionHomePath, isVersionIsolated, isVersionInstalled } from './versions.js';
|
|
39
|
+
import { getUserAgentsDir, getBackupsDir } from './state.js';
|
|
40
|
+
import { copyDirStrippingAgentsSymlinks, moveDirCrossDevice } from './config-transfer.js';
|
|
41
|
+
/** Filename of the provenance receipt written into the agent's config dir. */
|
|
42
|
+
export const RECEIPT_NAME = '.agents-cli-export.json';
|
|
43
|
+
/** Suffix for the incoming copy of a file the user already has. */
|
|
44
|
+
export const CONFLICT_SUFFIX = '.from-agents-cli';
|
|
45
|
+
/** The config dir's basename (`.codex`), used to locate it inside a version home. */
|
|
46
|
+
function configBasename(agent) {
|
|
47
|
+
return path.basename(AGENTS[agent].configDir);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every regular file under `root`, as paths relative to it. Symlinks pointing back
|
|
51
|
+
* into `~/.agents` are omitted for the same reason `copyDirStrippingAgentsSymlinks`
|
|
52
|
+
* drops them: they dangle the moment `~/.agents` is disposed, and an export that
|
|
53
|
+
* leaves dangling links is not an export.
|
|
54
|
+
*/
|
|
55
|
+
function walkFiles(root, agentsDir, rel = '') {
|
|
56
|
+
const inside = agentsDir + path.sep;
|
|
57
|
+
const out = [];
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = fs.readdirSync(path.join(root, rel), { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const childRel = rel ? path.join(rel, entry.name) : entry.name;
|
|
67
|
+
const abs = path.join(root, childRel);
|
|
68
|
+
if (entry.isSymbolicLink()) {
|
|
69
|
+
try {
|
|
70
|
+
const tgt = path.resolve(path.dirname(abs), fs.readlinkSync(abs));
|
|
71
|
+
if (tgt === agentsDir || tgt.startsWith(inside))
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
out.push(childRel);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
out.push(...walkFiles(root, agentsDir, childRel));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
out.push(childRel);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Build a read-only plan. Performs no mutations. */
|
|
89
|
+
export function planExport(agent, version, timestamp, mode = 'merge') {
|
|
90
|
+
const source = path.join(getVersionHomePath(agent, version), configBasename(agent));
|
|
91
|
+
const dest = getAgentConfigPath(agent);
|
|
92
|
+
const base = {
|
|
93
|
+
agent,
|
|
94
|
+
version,
|
|
95
|
+
mode,
|
|
96
|
+
source,
|
|
97
|
+
dest,
|
|
98
|
+
destKind: 'absent',
|
|
99
|
+
writes: [],
|
|
100
|
+
conflicts: [],
|
|
101
|
+
backupPath: null,
|
|
102
|
+
stagedPath: null,
|
|
103
|
+
receiptPath: path.join(dest, RECEIPT_NAME),
|
|
104
|
+
};
|
|
105
|
+
if (!isVersionInstalled(agent, version))
|
|
106
|
+
return { ...base, blocker: { kind: 'not-installed' } };
|
|
107
|
+
if (!isVersionIsolated(agent, version))
|
|
108
|
+
return { ...base, blocker: { kind: 'not-isolated' } };
|
|
109
|
+
if (!fs.existsSync(source))
|
|
110
|
+
return { ...base, blocker: { kind: 'no-config', source } };
|
|
111
|
+
// Refuse when `~/.<agent>` is a symlink agents-cli owns: the agent has a normal
|
|
112
|
+
// install that adopted it, so writing there would land inside a version home
|
|
113
|
+
// rather than in the user's real config, and silently mutate that other install.
|
|
114
|
+
const adopted = getConfigSymlinkVersion(agent);
|
|
115
|
+
if (adopted !== null) {
|
|
116
|
+
return { ...base, blocker: { kind: 'dest-adopted', realPath: dest, adoptedVersion: adopted } };
|
|
117
|
+
}
|
|
118
|
+
let destKind = 'absent';
|
|
119
|
+
try {
|
|
120
|
+
destKind = fs.lstatSync(dest).isSymbolicLink() ? 'foreign-symlink' : 'real-dir';
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
destKind = 'absent';
|
|
124
|
+
}
|
|
125
|
+
const agentsDir = getUserAgentsDir();
|
|
126
|
+
const rels = walkFiles(source, agentsDir).sort();
|
|
127
|
+
if (mode === 'staged') {
|
|
128
|
+
const stagedPath = path.join(dest, `.agents-export-${timestamp}`);
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
destKind,
|
|
132
|
+
stagedPath,
|
|
133
|
+
writes: rels.map((rel) => ({
|
|
134
|
+
rel,
|
|
135
|
+
source: path.join(source, rel),
|
|
136
|
+
target: path.join(stagedPath, rel),
|
|
137
|
+
})),
|
|
138
|
+
blocker: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (mode === 'replace') {
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
destKind,
|
|
145
|
+
backupPath: destKind === 'real-dir' ? path.join(getBackupsDir(), agent, String(timestamp)) : null,
|
|
146
|
+
writes: rels.map((rel) => ({
|
|
147
|
+
rel,
|
|
148
|
+
source: path.join(source, rel),
|
|
149
|
+
target: path.join(dest, rel),
|
|
150
|
+
})),
|
|
151
|
+
blocker: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// merge: additive. Anything the user already has becomes a conflict written
|
|
155
|
+
// alongside, so their file is never the thing that changes.
|
|
156
|
+
const writes = [];
|
|
157
|
+
const conflicts = [];
|
|
158
|
+
for (const rel of rels) {
|
|
159
|
+
const target = path.join(dest, rel);
|
|
160
|
+
let exists = false;
|
|
161
|
+
try {
|
|
162
|
+
fs.lstatSync(target);
|
|
163
|
+
exists = true;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
exists = false;
|
|
167
|
+
}
|
|
168
|
+
if (exists) {
|
|
169
|
+
conflicts.push({
|
|
170
|
+
rel,
|
|
171
|
+
source: path.join(source, rel),
|
|
172
|
+
target: target + CONFLICT_SUFFIX,
|
|
173
|
+
existing: target,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
writes.push({ rel, source: path.join(source, rel), target });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { ...base, destKind, writes, conflicts, blocker: null };
|
|
181
|
+
}
|
|
182
|
+
/** Copy one file, creating parent dirs. Symlinks are recreated, not followed. */
|
|
183
|
+
function placeFile(entry) {
|
|
184
|
+
fs.mkdirSync(path.dirname(entry.target), { recursive: true });
|
|
185
|
+
const st = fs.lstatSync(entry.source);
|
|
186
|
+
if (st.isSymbolicLink()) {
|
|
187
|
+
const link = fs.readlinkSync(entry.source);
|
|
188
|
+
try {
|
|
189
|
+
fs.unlinkSync(entry.target);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* nothing there */
|
|
193
|
+
}
|
|
194
|
+
fs.symlinkSync(link, entry.target);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
fs.copyFileSync(entry.source, entry.target);
|
|
198
|
+
}
|
|
199
|
+
function writeReceipt(plan, result, timestamp) {
|
|
200
|
+
const receipt = {
|
|
201
|
+
exportedAt: new Date(timestamp).toISOString(),
|
|
202
|
+
from: `${plan.agent}@${plan.version} (isolated)`,
|
|
203
|
+
mode: plan.mode,
|
|
204
|
+
written: result.written,
|
|
205
|
+
conflicts: result.conflicts,
|
|
206
|
+
...(result.backupPath ? { backupPath: result.backupPath } : {}),
|
|
207
|
+
...(result.stagedPath ? { stagedAt: result.stagedPath } : {}),
|
|
208
|
+
note: plan.mode === 'merge'
|
|
209
|
+
? `Files under "written" came from this export. Paths under "conflicts" already existed and were NOT modified — the incoming version sits beside yours with the ${CONFLICT_SUFFIX} suffix.`
|
|
210
|
+
: plan.mode === 'replace'
|
|
211
|
+
? 'This config was replaced wholesale by the export; the previous one is at backupPath.'
|
|
212
|
+
: 'Nothing was activated — the exported tree sits under stagedAt.',
|
|
213
|
+
};
|
|
214
|
+
fs.mkdirSync(path.dirname(plan.receiptPath), { recursive: true });
|
|
215
|
+
fs.writeFileSync(plan.receiptPath, JSON.stringify(receipt, null, 2) + '\n');
|
|
216
|
+
}
|
|
217
|
+
/** Execute a plan from {@link planExport}. */
|
|
218
|
+
export function executeExport(plan, timestamp) {
|
|
219
|
+
const result = {
|
|
220
|
+
exported: false,
|
|
221
|
+
dest: plan.dest,
|
|
222
|
+
written: [],
|
|
223
|
+
conflicts: [],
|
|
224
|
+
backupPath: null,
|
|
225
|
+
stagedPath: null,
|
|
226
|
+
receiptPath: null,
|
|
227
|
+
errors: [],
|
|
228
|
+
};
|
|
229
|
+
if (plan.blocker) {
|
|
230
|
+
result.errors.push(`refusing to export: ${plan.blocker.kind}`);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
if (plan.mode === 'replace') {
|
|
235
|
+
if (plan.backupPath) {
|
|
236
|
+
fs.mkdirSync(path.dirname(plan.backupPath), { recursive: true });
|
|
237
|
+
moveDirCrossDevice(plan.dest, plan.backupPath);
|
|
238
|
+
result.backupPath = plan.backupPath;
|
|
239
|
+
}
|
|
240
|
+
else if (plan.destKind === 'foreign-symlink') {
|
|
241
|
+
// Not ours to back up, but it must go or cpSync would write through it.
|
|
242
|
+
fs.unlinkSync(plan.dest);
|
|
243
|
+
}
|
|
244
|
+
copyDirStrippingAgentsSymlinks(plan.source, plan.dest, getUserAgentsDir());
|
|
245
|
+
result.written = plan.writes.map((w) => w.rel);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// merge and staged both place individual files; neither disturbs anything
|
|
249
|
+
// the user already has.
|
|
250
|
+
for (const entry of plan.writes) {
|
|
251
|
+
placeFile(entry);
|
|
252
|
+
result.written.push(entry.rel);
|
|
253
|
+
}
|
|
254
|
+
for (const entry of plan.conflicts) {
|
|
255
|
+
placeFile(entry);
|
|
256
|
+
result.conflicts.push({ path: entry.rel, theirs: entry.target });
|
|
257
|
+
}
|
|
258
|
+
if (plan.stagedPath)
|
|
259
|
+
result.stagedPath = plan.stagedPath;
|
|
260
|
+
}
|
|
261
|
+
writeReceipt(plan, result, timestamp);
|
|
262
|
+
result.receiptPath = plan.receiptPath;
|
|
263
|
+
result.exported = true;
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
result.errors.push(err instanceof Error ? err.message : String(err));
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
@@ -27,4 +27,4 @@ export interface PolicyResult {
|
|
|
27
27
|
* Apply policy to a single open block. Returns the action taken (none/defaulted/parked).
|
|
28
28
|
* Caller is responsible for persistence/logging side effects not owned by feed.ts.
|
|
29
29
|
*/
|
|
30
|
-
export declare function applyPolicyToBlock(block: OpenBlock, policy: FeedPolicy, now: Date, root?: string): PolicyResult;
|
|
30
|
+
export declare function applyPolicyToBlock(block: OpenBlock, policy: FeedPolicy, now: Date, root?: string, mailboxRoot?: string): PolicyResult;
|
package/dist/lib/feed-policy.js
CHANGED
|
@@ -28,12 +28,12 @@ export const DEFAULT_POLICY = {
|
|
|
28
28
|
export function getPolicyPath(root) {
|
|
29
29
|
return path.join(root ?? getUserAgentsDir(), POLICY_FILE);
|
|
30
30
|
}
|
|
31
|
-
function normalizeClassPolicy(raw) {
|
|
31
|
+
function normalizeClassPolicy(raw, fallback) {
|
|
32
32
|
const p = (raw ?? {});
|
|
33
|
-
const timeout = typeof p.timeoutMinutes === 'number' ? p.timeoutMinutes :
|
|
33
|
+
const timeout = typeof p.timeoutMinutes === 'number' ? p.timeoutMinutes : fallback.timeoutMinutes;
|
|
34
34
|
return {
|
|
35
35
|
timeoutMinutes: Math.max(1, Math.round(timeout)),
|
|
36
|
-
safeDefault: typeof p.safeDefault === 'string' ? p.safeDefault :
|
|
36
|
+
safeDefault: typeof p.safeDefault === 'string' ? p.safeDefault : fallback.safeDefault,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
export function loadPolicy(root) {
|
|
@@ -45,8 +45,8 @@ export function loadPolicy(root) {
|
|
|
45
45
|
const p = parsed;
|
|
46
46
|
const threshold = p.phoneNotifyThreshold;
|
|
47
47
|
return {
|
|
48
|
-
approval: normalizeClassPolicy(p.approval
|
|
49
|
-
decision: normalizeClassPolicy(p.decision
|
|
48
|
+
approval: normalizeClassPolicy(p.approval, DEFAULT_POLICY.approval),
|
|
49
|
+
decision: normalizeClassPolicy(p.decision, DEFAULT_POLICY.decision),
|
|
50
50
|
phoneNotifyThreshold: threshold === 'low' || threshold === 'medium' || threshold === 'high' ? threshold : DEFAULT_POLICY.phoneNotifyThreshold,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
@@ -71,16 +71,21 @@ export function minutesElapsed(block, now) {
|
|
|
71
71
|
return 0;
|
|
72
72
|
return (now.getTime() - ts) / 60_000;
|
|
73
73
|
}
|
|
74
|
+
function timeoutMinutesForBlock(block, policy) {
|
|
75
|
+
if (typeof block.timeoutMinutes === 'number' && Number.isFinite(block.timeoutMinutes) && block.timeoutMinutes > 0) {
|
|
76
|
+
return Math.max(1, Math.round(block.timeoutMinutes));
|
|
77
|
+
}
|
|
78
|
+
return policy[blockClass(block)].timeoutMinutes;
|
|
79
|
+
}
|
|
74
80
|
export function isTimedOut(block, policy, now) {
|
|
75
|
-
const cls = blockClass(block);
|
|
76
81
|
const minutes = minutesElapsed(block, now);
|
|
77
|
-
return minutes >= policy
|
|
82
|
+
return minutes >= timeoutMinutesForBlock(block, policy);
|
|
78
83
|
}
|
|
79
84
|
/**
|
|
80
85
|
* Apply policy to a single open block. Returns the action taken (none/defaulted/parked).
|
|
81
86
|
* Caller is responsible for persistence/logging side effects not owned by feed.ts.
|
|
82
87
|
*/
|
|
83
|
-
export function applyPolicyToBlock(block, policy, now, root) {
|
|
88
|
+
export function applyPolicyToBlock(block, policy, now, root, mailboxRoot) {
|
|
84
89
|
if (block.answer || block.parkedAt || block.defaultedAt) {
|
|
85
90
|
return { blockId: block.blockId, action: 'none' };
|
|
86
91
|
}
|
|
@@ -97,7 +102,7 @@ export function applyPolicyToBlock(block, policy, now, root) {
|
|
|
97
102
|
if (!claim.ok) {
|
|
98
103
|
return { blockId: block.blockId, action: 'none' };
|
|
99
104
|
}
|
|
100
|
-
const msgId = enqueue(mailboxDir(block.mailboxId,
|
|
105
|
+
const msgId = enqueue(mailboxDir(block.mailboxId, mailboxRoot), {
|
|
101
106
|
to: block.mailboxId,
|
|
102
107
|
text: safeDefault,
|
|
103
108
|
from: 'policy',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { OpenBlock, FeedAskStats } from './feed.js';
|
|
2
|
+
import type { Classification } from './ask-classifier.js';
|
|
3
|
+
import type { ActiveSession } from './session/active.js';
|
|
4
|
+
import type { SessionMeta } from './session/types.js';
|
|
5
|
+
export interface FeedSessionSignal {
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
mailboxId?: string;
|
|
8
|
+
kind?: string;
|
|
9
|
+
host?: string;
|
|
10
|
+
context?: ActiveSession['context'];
|
|
11
|
+
runtime?: string;
|
|
12
|
+
pid?: number;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
startedAtMs?: number;
|
|
15
|
+
status?: ActiveSession['status'];
|
|
16
|
+
tokPerSec?: number;
|
|
17
|
+
costUsd?: number;
|
|
18
|
+
durationMs?: number;
|
|
19
|
+
cloudProvider?: string;
|
|
20
|
+
cloudTaskId?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RankedFeedBlock extends OpenBlock {
|
|
23
|
+
delayRank: NonNullable<OpenBlock['delayRank']>;
|
|
24
|
+
}
|
|
25
|
+
export declare function buildSessionSignals(active: ActiveSession[], metas?: SessionMeta[]): FeedSessionSignal[];
|
|
26
|
+
export declare function decisionIrreducibility(c: Classification): number;
|
|
27
|
+
export declare function burnUsdPerHour(signal: FeedSessionSignal | undefined, block: OpenBlock): number;
|
|
28
|
+
export declare function rankFeedBlocks(blocks: OpenBlock[], signals?: FeedSessionSignal[], now?: Date): RankedFeedBlock[];
|
|
29
|
+
export declare function recentAskCount(stats: FeedAskStats, now?: Date): number;
|
|
30
|
+
export declare function needyControlCards(stats: FeedAskStats[], signals?: FeedSessionSignal[], now?: Date, threshold?: number): OpenBlock[];
|
|
31
|
+
export declare function runawayControlCards(signals: FeedSessionSignal[], now?: Date): OpenBlock[];
|
|
32
|
+
export declare function synthesizeControlCards(signals: FeedSessionSignal[], stats: FeedAskStats[], now?: Date): OpenBlock[];
|