@phnx-labs/agents-cli 1.20.78 → 1.20.83
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 +595 -0
- package/README.md +26 -26
- 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 +66 -0
- package/dist/commands/doctor.js +596 -33
- 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/menubar.js +12 -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.d.ts +8 -1
- package/dist/commands/routines.js +241 -77
- 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 +115 -0
- package/dist/commands/sessions.js +189 -35
- 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 +52 -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.d.ts +7 -0
- package/dist/lib/exec.js +69 -11
- 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 +71 -0
- package/dist/lib/hooks.js +245 -95
- 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/install-menubar.d.ts +24 -0
- package/dist/lib/menubar/install-menubar.js +52 -2
- 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 +33 -0
- package/dist/lib/routines.js +122 -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/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/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 +128 -26
- package/dist/lib/session/active.js +192 -53
- 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/state.js +57 -1
- 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
package/dist/lib/drift.d.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared drift-detection internals for `agents doctor` (overview mode) and
|
|
3
|
-
* `agents check` (the scriptable, CI-friendly gate).
|
|
3
|
+
* `agents doctor --check` (the scriptable, CI-friendly gate).
|
|
4
4
|
*
|
|
5
5
|
* This is the single source of truth for "is the install out of sync?": the
|
|
6
6
|
* per-version sync status (fresh / stale / never-synced) and the orphan census.
|
|
7
|
-
* `agents doctor` renders it as a human report; `agents check` reduces it
|
|
8
|
-
* exit code. Neither reimplements the diagnostic — both call `computeDrift`.
|
|
7
|
+
* `agents doctor` renders it as a human report; `agents doctor --check` reduces it
|
|
8
|
+
* to an exit code. Neither reimplements the diagnostic — both call `computeDrift`.
|
|
9
9
|
*/
|
|
10
10
|
import type { AgentId } from './types.js';
|
|
11
|
-
import { type VersionResourceReport } from './doctor-diff.js';
|
|
11
|
+
import { type VersionResourceReport, type SourceLayerBehind } from './doctor-diff.js';
|
|
12
12
|
export interface SyncStatusRow {
|
|
13
13
|
agent: AgentId;
|
|
14
14
|
version: string;
|
|
15
15
|
status: 'fresh' | 'stale' | 'never-synced';
|
|
16
16
|
isDefault: boolean;
|
|
17
|
-
/** For stale rows: prioritized lines naming exactly what diverged (plugins first).
|
|
17
|
+
/** For stale rows: prioritized lines naming exactly what diverged (plugins first).
|
|
18
|
+
* Also carries hook-wiring divergence for a version whose hooks are present but
|
|
19
|
+
* not wired into settings.json — independent of manifest staleness. */
|
|
18
20
|
divergence?: string[];
|
|
21
|
+
/** Count of hooks present on disk but NOT wired into the version's settings.json
|
|
22
|
+
* (claude/droid). A non-zero value makes the version out-of-sync even when the
|
|
23
|
+
* manifest reads fresh — the yosemite-s1 blind spot the CI gate must catch. */
|
|
24
|
+
unwiredHooks?: number;
|
|
19
25
|
}
|
|
20
26
|
export interface OrphanRow {
|
|
21
27
|
agent: AgentId;
|
|
@@ -27,6 +33,14 @@ export interface OrphanRow {
|
|
|
27
33
|
export declare function divergenceLines(report: VersionResourceReport): string[];
|
|
28
34
|
export declare function checkSyncStatus(cwd: string): SyncStatusRow[];
|
|
29
35
|
export declare function countOrphans(): OrphanRow[];
|
|
36
|
+
/**
|
|
37
|
+
* Probe each source layer (user, system, enabled extras) for how far it trails
|
|
38
|
+
* its upstream, from the LAST-FETCHED remote-tracking ref (no network). A layer
|
|
39
|
+
* behind origin means every version home is reconciled against stale truth — a
|
|
40
|
+
* drift signal `agents doctor --check` must fail on, not a buried preamble. Returns
|
|
41
|
+
* only the behind layers. Canonical home for both `agents doctor` and `agents doctor --check`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeSourceBehind(): SourceLayerBehind[];
|
|
30
44
|
export interface DriftSummary {
|
|
31
45
|
syncRows: SyncStatusRow[];
|
|
32
46
|
orphanRows: OrphanRow[];
|
|
@@ -36,17 +50,23 @@ export interface DriftSummary {
|
|
|
36
50
|
neverSyncedCount: number;
|
|
37
51
|
/** Versions carrying orphan resources (informational — not a drift signal). */
|
|
38
52
|
orphanVersionCount: number;
|
|
53
|
+
/** Versions with hooks present on disk but not wired into settings.json. */
|
|
54
|
+
unwiredHookVersions: number;
|
|
55
|
+
/** Source layers behind their upstream (reconciled against stale truth). */
|
|
56
|
+
sourceBehind: SourceLayerBehind[];
|
|
39
57
|
/**
|
|
40
|
-
* True when
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
58
|
+
* True when the install is out of sync: any installed version is stale,
|
|
59
|
+
* never-synced, or carries unwired hooks, OR a source layer is behind origin.
|
|
60
|
+
* `agents doctor` surfaces it as "run `agents status`"; `agents doctor --check`
|
|
61
|
+
* maps it to a non-zero exit. Orphans are a `prune` concern, not sync drift, so they do
|
|
62
|
+
* NOT set this flag (mirrors the sync-status engine: an orphan alone never
|
|
63
|
+
* flags needsSync).
|
|
44
64
|
*/
|
|
45
65
|
hasDrift: boolean;
|
|
46
66
|
}
|
|
47
67
|
/**
|
|
48
68
|
* Compute the same drift/divergence diagnostic `agents doctor` prints, reduced
|
|
49
|
-
* to a summary with a single `hasDrift` boolean. The gate `agents check`
|
|
50
|
-
* to an exit code; the readout `agents doctor` renders in full.
|
|
69
|
+
* to a summary with a single `hasDrift` boolean. The gate `agents doctor --check`
|
|
70
|
+
* maps to an exit code; the readout `agents doctor` renders in full.
|
|
51
71
|
*/
|
|
52
72
|
export declare function computeDrift(cwd: string): DriftSummary;
|
package/dist/lib/drift.js
CHANGED
|
@@ -4,7 +4,9 @@ import { loadManifest, isStale } from './staleness/index.js';
|
|
|
4
4
|
import { diffVersionResources } from './doctor-diff.js';
|
|
5
5
|
import { diffVersionCommands, iterCommandsCapableVersions } from './commands.js';
|
|
6
6
|
import { diffVersionSkills, iterSkillsCapableVersions } from './skills.js';
|
|
7
|
-
import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome } from './hooks.js';
|
|
7
|
+
import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome, checkVersionHookWiring } from './hooks.js';
|
|
8
|
+
import { commitsBehindUpstream } from './git.js';
|
|
9
|
+
import { getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos } from './state.js';
|
|
8
10
|
// Lines naming exactly what's out of sync for a version, plugins prioritized:
|
|
9
11
|
// each divergent plugin gets its own line with specifics (stale mirror version,
|
|
10
12
|
// invalid manifest, or the bundled skills/commands missing from the mirror —
|
|
@@ -46,14 +48,37 @@ export function checkSyncStatus(cwd) {
|
|
|
46
48
|
? 'never-synced'
|
|
47
49
|
: isStale(manifest, agent, version, cwd) ? 'stale' : 'fresh';
|
|
48
50
|
const row = { agent, version, status, isDefault: version === def };
|
|
51
|
+
const divergence = [];
|
|
49
52
|
if (status === 'stale') {
|
|
50
53
|
// Resolve the specifics against non-project layers (the global home is
|
|
51
54
|
// never reconciled against per-cwd project resources).
|
|
52
55
|
const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
|
|
53
|
-
|
|
54
|
-
if (lines.length)
|
|
55
|
-
row.divergence = lines;
|
|
56
|
+
divergence.push(...divergenceLines(report));
|
|
56
57
|
}
|
|
58
|
+
// Hook WIRING is independent of manifest staleness: a hook file can be
|
|
59
|
+
// byte-identical to source (fresh) yet absent from settings.json, so it
|
|
60
|
+
// never fires. Surface that for every version, fresh or stale, so overview
|
|
61
|
+
// AND `agents doctor --check` flag it (claude/droid; other agents report unsupported).
|
|
62
|
+
const wiring = checkVersionHookWiring(agent, version);
|
|
63
|
+
if (wiring.supported) {
|
|
64
|
+
const expected = wiring.expected ?? 0;
|
|
65
|
+
if (wiring.settingsMissing && expected > 0) {
|
|
66
|
+
row.unwiredHooks = expected;
|
|
67
|
+
divergence.push(`hooks settings.json missing — ${expected} declared hook(s) never fire`);
|
|
68
|
+
}
|
|
69
|
+
else if (wiring.settingsUnparseable) {
|
|
70
|
+
row.unwiredHooks = Math.max(1, expected);
|
|
71
|
+
divergence.push(`hooks settings.json unparseable — wiring cannot be verified`);
|
|
72
|
+
}
|
|
73
|
+
else if (wiring.unwired.length > 0) {
|
|
74
|
+
row.unwiredHooks = wiring.unwired.length;
|
|
75
|
+
const names = wiring.unwired.map((u) => u.name);
|
|
76
|
+
const shown = names.slice(0, 3).join(', ');
|
|
77
|
+
divergence.push(`hooks ${wiring.unwired.length} unwired (${shown}${names.length > 3 ? ', …' : ''})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (divergence.length)
|
|
81
|
+
row.divergence = divergence;
|
|
57
82
|
rows.push(row);
|
|
58
83
|
}
|
|
59
84
|
}
|
|
@@ -91,22 +116,48 @@ export function countOrphans() {
|
|
|
91
116
|
}
|
|
92
117
|
return Array.from(byKey.values()).filter((r) => r.commands + r.skills + r.hooks > 0);
|
|
93
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Probe each source layer (user, system, enabled extras) for how far it trails
|
|
121
|
+
* its upstream, from the LAST-FETCHED remote-tracking ref (no network). A layer
|
|
122
|
+
* behind origin means every version home is reconciled against stale truth — a
|
|
123
|
+
* drift signal `agents doctor --check` must fail on, not a buried preamble. Returns
|
|
124
|
+
* only the behind layers. Canonical home for both `agents doctor` and `agents doctor --check`.
|
|
125
|
+
*/
|
|
126
|
+
export function computeSourceBehind() {
|
|
127
|
+
const out = [];
|
|
128
|
+
const probe = (layer, dir, label, alias) => {
|
|
129
|
+
const r = commitsBehindUpstream(dir);
|
|
130
|
+
if (r && r.behind > 0)
|
|
131
|
+
out.push({ layer, label, alias, behind: r.behind, branch: r.branch });
|
|
132
|
+
};
|
|
133
|
+
probe('user', getUserAgentsDir(), '~/.agents', 'user');
|
|
134
|
+
probe('system', getSystemAgentsDir(), '~/.agents/.system', 'system');
|
|
135
|
+
for (const e of getEnabledExtraRepos())
|
|
136
|
+
probe('extra', e.dir, e.alias, e.alias);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
94
139
|
/**
|
|
95
140
|
* Compute the same drift/divergence diagnostic `agents doctor` prints, reduced
|
|
96
|
-
* to a summary with a single `hasDrift` boolean. The gate `agents check`
|
|
97
|
-
* to an exit code; the readout `agents doctor` renders in full.
|
|
141
|
+
* to a summary with a single `hasDrift` boolean. The gate `agents doctor --check`
|
|
142
|
+
* maps to an exit code; the readout `agents doctor` renders in full.
|
|
98
143
|
*/
|
|
99
144
|
export function computeDrift(cwd) {
|
|
100
145
|
const syncRows = checkSyncStatus(cwd);
|
|
101
146
|
const orphanRows = countOrphans();
|
|
102
147
|
const staleCount = syncRows.filter((r) => r.status === 'stale').length;
|
|
103
148
|
const neverSyncedCount = syncRows.filter((r) => r.status === 'never-synced').length;
|
|
149
|
+
const unwiredHookVersions = syncRows.filter((r) => (r.unwiredHooks ?? 0) > 0).length;
|
|
150
|
+
const sourceBehind = computeSourceBehind();
|
|
104
151
|
return {
|
|
105
152
|
syncRows,
|
|
106
153
|
orphanRows,
|
|
107
154
|
staleCount,
|
|
108
155
|
neverSyncedCount,
|
|
109
156
|
orphanVersionCount: orphanRows.length,
|
|
110
|
-
|
|
157
|
+
unwiredHookVersions,
|
|
158
|
+
sourceBehind,
|
|
159
|
+
hasDrift: syncRows.some((r) => r.status !== 'fresh') ||
|
|
160
|
+
unwiredHookVersions > 0 ||
|
|
161
|
+
sourceBehind.length > 0,
|
|
111
162
|
};
|
|
112
163
|
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
|
+
import { type ActorKind } from './actor.js';
|
|
14
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
15
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
17
|
export declare function levelFor(event: EventType): EventLevel;
|
|
17
18
|
export interface EventMeta {
|
|
18
19
|
ts: string;
|
|
@@ -30,6 +31,10 @@ export interface EventMeta {
|
|
|
30
31
|
osUser: string;
|
|
31
32
|
transport: 'local' | 'ssh';
|
|
32
33
|
sshClientIp?: string;
|
|
34
|
+
/** Resolved actor id — which human/agent is behind this event (RUSH-2020). */
|
|
35
|
+
actor?: string;
|
|
36
|
+
/** Actor kind (`human`/`agent`). */
|
|
37
|
+
kind?: ActorKind;
|
|
33
38
|
}
|
|
34
39
|
export interface EventPayload {
|
|
35
40
|
agent?: string;
|
|
@@ -196,6 +201,8 @@ export interface EventStats {
|
|
|
196
201
|
byEvent: Record<string, number>;
|
|
197
202
|
byModule: Record<string, number>;
|
|
198
203
|
byUser: Record<string, number>;
|
|
204
|
+
/** Event counts grouped by resolved actor id (the human/agent behind them). */
|
|
205
|
+
byActor: Record<string, number>;
|
|
199
206
|
fileCount: number;
|
|
200
207
|
totalBytes: number;
|
|
201
208
|
}
|
package/dist/lib/events.js
CHANGED
|
@@ -19,6 +19,7 @@ import { gzipSync, gunzipSync } from 'node:zlib';
|
|
|
19
19
|
import { parseSshConnection } from './session/provenance.js';
|
|
20
20
|
import { ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
21
21
|
import { getUserAgentsDir } from './state.js';
|
|
22
|
+
import { resolveActor } from './actor.js';
|
|
22
23
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
23
24
|
// Resolved lazily: events.ts is imported transitively by most CLI surfaces, and
|
|
24
25
|
// import itself must stay side-effect free. Tests may override the exact path.
|
|
@@ -51,7 +52,7 @@ const DIR_MODE = 0o700;
|
|
|
51
52
|
const FILE_MODE = 0o600;
|
|
52
53
|
const AUDIT_EVENTS = new Set([
|
|
53
54
|
'command.start', 'command.end',
|
|
54
|
-
'secrets.get', 'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
55
|
+
'secrets.get', 'secrets.unlocked', 'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
55
56
|
'teams.create', 'teams.add', 'teams.start', 'teams.complete', 'teams.disband',
|
|
56
57
|
'cloud.dispatch', 'cloud.complete', 'cloud.cancel', 'cloud.message',
|
|
57
58
|
'version.install', 'version.switch', 'version.remove',
|
|
@@ -119,6 +120,7 @@ const SENSITIVE_PAYLOAD_KEY = /password|secret|token|api[-_]?key|auth/i;
|
|
|
119
120
|
const RESERVED_META_KEYS = new Set([
|
|
120
121
|
'ts', 'tz', 'tzName', 'hostname', 'platform', 'arch', 'pid', 'ppid',
|
|
121
122
|
'event', 'level', 'caller', 'session', 'osUser', 'transport', 'sshClientIp',
|
|
123
|
+
'actor', 'kind',
|
|
122
124
|
]);
|
|
123
125
|
function promptMarker(value) {
|
|
124
126
|
const { prompt_length, prompt_sha256 } = redactPrompt(value);
|
|
@@ -276,10 +278,13 @@ function auditOrigin() {
|
|
|
276
278
|
// Container/edge cases where the uid has no passwd entry.
|
|
277
279
|
}
|
|
278
280
|
const ssh = process.env.SSH_CONNECTION ? parseSshConnection(process.env.SSH_CONNECTION) : undefined;
|
|
281
|
+
const actor = resolveActor();
|
|
279
282
|
_origin = {
|
|
280
283
|
osUser,
|
|
281
284
|
transport: ssh ? 'ssh' : 'local',
|
|
282
285
|
...(ssh ? { sshClientIp: ssh.clientIp } : {}),
|
|
286
|
+
actor: actor.id,
|
|
287
|
+
kind: actor.kind,
|
|
283
288
|
};
|
|
284
289
|
return _origin;
|
|
285
290
|
}
|
|
@@ -693,6 +698,7 @@ export function stats(options = {}) {
|
|
|
693
698
|
const byEvent = {};
|
|
694
699
|
const byModule = {};
|
|
695
700
|
const byUser = {};
|
|
701
|
+
const byActor = {};
|
|
696
702
|
for (const r of records) {
|
|
697
703
|
const lvl = r.level ?? levelFor(r.event);
|
|
698
704
|
byLevel[lvl] = (byLevel[lvl] ?? 0) + 1;
|
|
@@ -701,6 +707,8 @@ export function stats(options = {}) {
|
|
|
701
707
|
byModule[r.module] = (byModule[r.module] ?? 0) + 1;
|
|
702
708
|
const user = `${r.osUser ?? '?'}@${r.hostname}`;
|
|
703
709
|
byUser[user] = (byUser[user] ?? 0) + 1;
|
|
710
|
+
if (r.actor)
|
|
711
|
+
byActor[r.actor] = (byActor[r.actor] ?? 0) + 1;
|
|
704
712
|
}
|
|
705
713
|
let fileCount = 0;
|
|
706
714
|
let totalBytes = 0;
|
|
@@ -723,6 +731,7 @@ export function stats(options = {}) {
|
|
|
723
731
|
byEvent,
|
|
724
732
|
byModule,
|
|
725
733
|
byUser,
|
|
734
|
+
byActor,
|
|
726
735
|
fileCount,
|
|
727
736
|
totalBytes,
|
|
728
737
|
};
|
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,10 +16,11 @@ 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';
|
|
23
|
+
import { writeSessionActorRecord } from './session/actor-sidecar.js';
|
|
23
24
|
import { loadHookSessionIndex, resolveHookSessionId } from './session/hook-sessions.js';
|
|
24
25
|
import { sessionIdMarkerLine } from './hosts/session-marker.js';
|
|
25
26
|
import { recordRunName } from './session/run-names.js';
|
|
@@ -563,13 +564,6 @@ export const AGENT_COMMANDS = {
|
|
|
563
564
|
},
|
|
564
565
|
modelFlag: '--model',
|
|
565
566
|
},
|
|
566
|
-
forge: {
|
|
567
|
-
base: ['forge'],
|
|
568
|
-
promptFlag: 'positional',
|
|
569
|
-
modeFlags: {
|
|
570
|
-
edit: [],
|
|
571
|
-
},
|
|
572
|
-
},
|
|
573
567
|
};
|
|
574
568
|
/**
|
|
575
569
|
* Whether `agent` has a native resume form (Tier 1). Derived solely from the
|
|
@@ -579,6 +573,15 @@ export const AGENT_COMMANDS = {
|
|
|
579
573
|
export function nativeResume(agent) {
|
|
580
574
|
return AGENT_COMMANDS[agent]?.resume !== undefined;
|
|
581
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
|
+
}
|
|
582
585
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
583
586
|
export function buildExecCommand(options) {
|
|
584
587
|
const template = AGENT_COMMANDS[options.agent];
|
|
@@ -664,6 +667,18 @@ export function buildExecCommand(options) {
|
|
|
664
667
|
// AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
|
|
665
668
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
666
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';
|
|
667
682
|
if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
668
683
|
if (resolvedMode === 'skip') {
|
|
669
684
|
// skip = yolo on resume too; both `codex resume` (TUI) and
|
|
@@ -673,6 +688,8 @@ export function buildExecCommand(options) {
|
|
|
673
688
|
else if (interactive) {
|
|
674
689
|
// `codex resume` (TUI) accepts the same -s/--sandbox flags as a fresh run.
|
|
675
690
|
cmd.push(...modeFlags);
|
|
691
|
+
if (codexWorkspaceWrite)
|
|
692
|
+
cmd.push('-c', codexWritableRootsConfig(getUserAgentsDir()));
|
|
676
693
|
}
|
|
677
694
|
else {
|
|
678
695
|
// `codex exec resume` rejects `--sandbox <mode>` (verified against
|
|
@@ -682,6 +699,7 @@ export function buildExecCommand(options) {
|
|
|
682
699
|
cmd.push('-c', `sandbox_mode=${resolvedMode === 'plan' ? 'read-only' : 'workspace-write'}`);
|
|
683
700
|
if (resolvedMode !== 'plan') {
|
|
684
701
|
cmd.push('-c', 'sandbox_workspace_write.network_access=true');
|
|
702
|
+
cmd.push('-c', codexWritableRootsConfig(getUserAgentsDir()));
|
|
685
703
|
}
|
|
686
704
|
}
|
|
687
705
|
}
|
|
@@ -785,9 +803,16 @@ export function buildExecCommand(options) {
|
|
|
785
803
|
// claude-only, silently dropped for codex and masked by edit mode carrying
|
|
786
804
|
// the approval/sandbox bypass. Codex's resume forms reject --add-dir, so
|
|
787
805
|
// skip it there (claude's flag-based resume accepts it).
|
|
788
|
-
|
|
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 &&
|
|
789
814
|
(options.agent === 'claude' || (options.agent === 'codex' && !options.resume))) {
|
|
790
|
-
for (const dir of
|
|
815
|
+
for (const dir of addDirs) {
|
|
791
816
|
cmd.push('--add-dir', dir);
|
|
792
817
|
}
|
|
793
818
|
}
|
|
@@ -904,15 +929,28 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
904
929
|
// wrapper, not the agent binary — the active scan resolves that by
|
|
905
930
|
// walking the candidate's ancestors (readAncestorSessionEntry).
|
|
906
931
|
if (child.pid) {
|
|
932
|
+
const passthroughSessionId = extractSessionIdArg(rawArgs);
|
|
907
933
|
writePidSessionEntry({
|
|
908
934
|
pid: child.pid,
|
|
909
935
|
agent,
|
|
910
|
-
sessionId:
|
|
936
|
+
sessionId: passthroughSessionId,
|
|
911
937
|
cwd,
|
|
938
|
+
actor: resolveActor().id,
|
|
939
|
+
initiatedBy: resolveActor().kind,
|
|
912
940
|
launchId,
|
|
913
941
|
terminalId: process.env.AGENT_TERMINAL_ID,
|
|
914
942
|
startedAtMs: Date.now(),
|
|
915
943
|
});
|
|
944
|
+
// Durable sessionId -> actor record (RUSH-2019) so the scanner can attribute
|
|
945
|
+
// this session to a person after the pid dies. Best-effort; no-ops without id.
|
|
946
|
+
if (passthroughSessionId) {
|
|
947
|
+
writeSessionActorRecord({
|
|
948
|
+
sessionId: passthroughSessionId,
|
|
949
|
+
actor: resolveActor().id,
|
|
950
|
+
initiatedBy: resolveActor().kind,
|
|
951
|
+
startedAtMs: Date.now(),
|
|
952
|
+
});
|
|
953
|
+
}
|
|
916
954
|
}
|
|
917
955
|
child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
918
956
|
child.on('error', (err) => {
|
|
@@ -1059,6 +1097,8 @@ async function runInTmux(options, executable, args) {
|
|
|
1059
1097
|
agent: options.agent,
|
|
1060
1098
|
sessionId: options.sessionId,
|
|
1061
1099
|
cwd,
|
|
1100
|
+
actor: resolveActor().id,
|
|
1101
|
+
initiatedBy: resolveActor().kind,
|
|
1062
1102
|
// spawnAgent injected AGENT_LAUNCH_ID into options.env before delegating
|
|
1063
1103
|
// here; record the same id so the hook (running under the pane-leaf agent
|
|
1064
1104
|
// pid) reconciles by launchId. This pane's pid usually IS the agent pid,
|
|
@@ -1068,6 +1108,14 @@ async function runInTmux(options, executable, args) {
|
|
|
1068
1108
|
tmuxPane: pane,
|
|
1069
1109
|
startedAtMs: Date.now(),
|
|
1070
1110
|
});
|
|
1111
|
+
if (options.sessionId) {
|
|
1112
|
+
writeSessionActorRecord({
|
|
1113
|
+
sessionId: options.sessionId,
|
|
1114
|
+
actor: resolveActor().id,
|
|
1115
|
+
initiatedBy: resolveActor().kind,
|
|
1116
|
+
startedAtMs: Date.now(),
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1071
1119
|
}
|
|
1072
1120
|
// Recap a dead pane's tail into THIS shell's stderr. The pane-died hook
|
|
1073
1121
|
// detaches the client the instant the agent exits, so a fast failure (a
|
|
@@ -1279,11 +1327,21 @@ async function spawnAgent(options) {
|
|
|
1279
1327
|
agent: options.agent,
|
|
1280
1328
|
sessionId: options.sessionId,
|
|
1281
1329
|
cwd: options.cwd || process.cwd(),
|
|
1330
|
+
actor: resolveActor().id,
|
|
1331
|
+
initiatedBy: resolveActor().kind,
|
|
1282
1332
|
launchId,
|
|
1283
1333
|
terminalId: process.env.AGENT_TERMINAL_ID,
|
|
1284
1334
|
tmuxPane: process.env.TMUX_PANE,
|
|
1285
1335
|
startedAtMs: Date.now(),
|
|
1286
1336
|
});
|
|
1337
|
+
if (options.sessionId) {
|
|
1338
|
+
writeSessionActorRecord({
|
|
1339
|
+
sessionId: options.sessionId,
|
|
1340
|
+
actor: resolveActor().id,
|
|
1341
|
+
initiatedBy: resolveActor().kind,
|
|
1342
|
+
startedAtMs: Date.now(),
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1287
1345
|
// Mark startup time (time from function call to process spawn)
|
|
1288
1346
|
timer.mark('startup');
|
|
1289
1347
|
let budgetKilled = false;
|
package/dist/lib/feed-post.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActivityEvent } from './activity.js';
|
|
1
|
+
import { type ActivityEvent, type Attachment } from './activity.js';
|
|
2
2
|
import { type PidSessionEntry } from './session/pid-registry.js';
|
|
3
3
|
/** Soft cap so a runaway agent can't flood the activity lane with essays. */
|
|
4
4
|
export declare const STATUS_POST_MAX_CHARS = 500;
|
|
@@ -7,8 +7,16 @@ export interface FeedPostInput {
|
|
|
7
7
|
text: string;
|
|
8
8
|
/** Override session id (escape hatch for scripts/tests). Prefer auto-resolve. */
|
|
9
9
|
sessionId?: string;
|
|
10
|
+
/** Generic artifacts to attach: local paths (copied for durability) or URLs. */
|
|
11
|
+
attach?: string[];
|
|
10
12
|
/** Override activity root (tests). */
|
|
11
13
|
activityRoot?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Override the attachments store root (tests). Local files are copied under
|
|
16
|
+
* `<attachmentsRoot>/<sessionId>/<updateId>/`. Defaults to
|
|
17
|
+
* `~/.agents/.history/attachments`.
|
|
18
|
+
*/
|
|
19
|
+
attachmentsRoot?: string;
|
|
12
20
|
/** Override env for identity resolution (tests). */
|
|
13
21
|
env?: NodeJS.ProcessEnv;
|
|
14
22
|
/** Override cwd stamp (defaults to process.cwd() / env). */
|
|
@@ -55,6 +63,25 @@ export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionI
|
|
|
55
63
|
export declare function walkPidRegistry(startPid: number, getParent: (pid: number) => number | undefined, readEntry: (pid: number) => PidSessionEntry | undefined): PidSessionEntry | undefined;
|
|
56
64
|
/** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
|
|
57
65
|
export declare function parentPidOf(pid: number): number | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Turn one `--attach` token into an {@link Attachment}. Remote URLs become
|
|
68
|
+
* `link` (or a media kind when the extension is unambiguous). Local files are
|
|
69
|
+
* classified by extension and, when `copyRoot` is set, copied under
|
|
70
|
+
* `<copyRoot>/<sessionId>/<updateId>/<basename>` so the link survives a worktree
|
|
71
|
+
* delete — the stored `href` then points at the durable copy. A missing local
|
|
72
|
+
* file is dropped (fail-open) rather than throwing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildAttachment(token: string, ctx: {
|
|
75
|
+
copyRoot?: string;
|
|
76
|
+
sessionId: string;
|
|
77
|
+
updateId: string;
|
|
78
|
+
}): Attachment | undefined;
|
|
79
|
+
/** Classify + store every `--attach` token; empty/failed tokens are dropped. */
|
|
80
|
+
export declare function buildAttachments(tokens: string[] | undefined, ctx: {
|
|
81
|
+
copyRoot?: string;
|
|
82
|
+
sessionId: string;
|
|
83
|
+
updateId: string;
|
|
84
|
+
}): Attachment[];
|
|
58
85
|
export declare function normalizeStatusText(text: string): string;
|
|
59
86
|
/**
|
|
60
87
|
* Append a `status.posted` milestone for the calling agent.
|
package/dist/lib/feed-post.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import { spawnSync } from 'child_process';
|
|
18
|
-
import { appendActivityEvent, } from './activity.js';
|
|
18
|
+
import { appendActivityEvent, projectFromCwd, } from './activity.js';
|
|
19
|
+
import { getHistoryDir } from './state.js';
|
|
19
20
|
import { machineId } from './machine-id.js';
|
|
20
21
|
import { isValidMailboxId } from './mailbox.js';
|
|
21
22
|
import { listPidSessionEntries, readPidSessionEntry, } from './session/pid-registry.js';
|
|
@@ -141,6 +142,104 @@ export function parentPidOf(pid) {
|
|
|
141
142
|
}
|
|
142
143
|
return undefined;
|
|
143
144
|
}
|
|
145
|
+
/** Extension → (kind, mediaType) for attachment classification. */
|
|
146
|
+
const MEDIA_BY_EXT = {
|
|
147
|
+
'.png': { kind: 'image', mediaType: 'image/png' },
|
|
148
|
+
'.jpg': { kind: 'image', mediaType: 'image/jpeg' },
|
|
149
|
+
'.jpeg': { kind: 'image', mediaType: 'image/jpeg' },
|
|
150
|
+
'.gif': { kind: 'image', mediaType: 'image/gif' },
|
|
151
|
+
'.svg': { kind: 'image', mediaType: 'image/svg+xml' },
|
|
152
|
+
'.webp': { kind: 'image', mediaType: 'image/webp' },
|
|
153
|
+
'.wav': { kind: 'audio', mediaType: 'audio/wav' },
|
|
154
|
+
'.mp3': { kind: 'audio', mediaType: 'audio/mpeg' },
|
|
155
|
+
'.m4a': { kind: 'audio', mediaType: 'audio/mp4' },
|
|
156
|
+
'.aac': { kind: 'audio', mediaType: 'audio/aac' },
|
|
157
|
+
'.ogg': { kind: 'audio', mediaType: 'audio/ogg' },
|
|
158
|
+
'.flac': { kind: 'audio', mediaType: 'audio/flac' },
|
|
159
|
+
'.mp4': { kind: 'video', mediaType: 'video/mp4' },
|
|
160
|
+
'.mov': { kind: 'video', mediaType: 'video/quicktime' },
|
|
161
|
+
'.webm': { kind: 'video', mediaType: 'video/webm' },
|
|
162
|
+
'.mkv': { kind: 'video', mediaType: 'video/x-matroska' },
|
|
163
|
+
};
|
|
164
|
+
/** True when the token is an http(s) URL (a remote attachment / link). */
|
|
165
|
+
function isRemoteUrl(token) {
|
|
166
|
+
return /^https?:\/\//i.test(token.trim());
|
|
167
|
+
}
|
|
168
|
+
function mediaForExt(ext) {
|
|
169
|
+
const hit = MEDIA_BY_EXT[ext.toLowerCase()];
|
|
170
|
+
return hit ? { kind: hit.kind, mediaType: hit.mediaType } : { kind: 'file' };
|
|
171
|
+
}
|
|
172
|
+
/** A short, filesystem-safe id grouping one post's copied attachments. */
|
|
173
|
+
function newUpdateId(ts) {
|
|
174
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
175
|
+
const stamp = ts.replace(/[^0-9]/g, '').slice(0, 14) || 'post';
|
|
176
|
+
return `${stamp}-${rand}`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Turn one `--attach` token into an {@link Attachment}. Remote URLs become
|
|
180
|
+
* `link` (or a media kind when the extension is unambiguous). Local files are
|
|
181
|
+
* classified by extension and, when `copyRoot` is set, copied under
|
|
182
|
+
* `<copyRoot>/<sessionId>/<updateId>/<basename>` so the link survives a worktree
|
|
183
|
+
* delete — the stored `href` then points at the durable copy. A missing local
|
|
184
|
+
* file is dropped (fail-open) rather than throwing.
|
|
185
|
+
*/
|
|
186
|
+
export function buildAttachment(token, ctx) {
|
|
187
|
+
const value = token.trim();
|
|
188
|
+
if (!value)
|
|
189
|
+
return undefined;
|
|
190
|
+
if (isRemoteUrl(value)) {
|
|
191
|
+
const ext = path.extname(new URL(value).pathname);
|
|
192
|
+
const media = mediaForExt(ext);
|
|
193
|
+
const att = { kind: media.kind === 'file' ? 'link' : media.kind, href: value };
|
|
194
|
+
const name = path.basename(new URL(value).pathname);
|
|
195
|
+
if (name)
|
|
196
|
+
att.name = name;
|
|
197
|
+
if (media.mediaType)
|
|
198
|
+
att.mediaType = media.mediaType;
|
|
199
|
+
return att;
|
|
200
|
+
}
|
|
201
|
+
const abs = path.resolve(value);
|
|
202
|
+
let stat;
|
|
203
|
+
try {
|
|
204
|
+
stat = fs.statSync(abs);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined; // dropped: local file does not exist
|
|
208
|
+
}
|
|
209
|
+
if (!stat.isFile())
|
|
210
|
+
return undefined;
|
|
211
|
+
const media = mediaForExt(path.extname(abs));
|
|
212
|
+
const name = path.basename(abs);
|
|
213
|
+
let href = abs;
|
|
214
|
+
if (ctx.copyRoot) {
|
|
215
|
+
try {
|
|
216
|
+
const destDir = path.join(ctx.copyRoot, ctx.sessionId, ctx.updateId);
|
|
217
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
218
|
+
const dest = path.join(destDir, name);
|
|
219
|
+
fs.copyFileSync(abs, dest);
|
|
220
|
+
href = dest;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
href = abs; // copy failed: fall back to the original path
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const att = { kind: media.kind, href, name, bytes: stat.size };
|
|
227
|
+
if (media.mediaType)
|
|
228
|
+
att.mediaType = media.mediaType;
|
|
229
|
+
return att;
|
|
230
|
+
}
|
|
231
|
+
/** Classify + store every `--attach` token; empty/failed tokens are dropped. */
|
|
232
|
+
export function buildAttachments(tokens, ctx) {
|
|
233
|
+
if (!tokens?.length)
|
|
234
|
+
return [];
|
|
235
|
+
const out = [];
|
|
236
|
+
for (const token of tokens) {
|
|
237
|
+
const att = buildAttachment(token, ctx);
|
|
238
|
+
if (att)
|
|
239
|
+
out.push(att);
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
144
243
|
export function normalizeStatusText(text) {
|
|
145
244
|
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
146
245
|
if (!collapsed)
|
|
@@ -163,8 +262,15 @@ export function postFeedStatus(input) {
|
|
|
163
262
|
throw new Error('No session id. Run from an agents-cli session '
|
|
164
263
|
+ '(AGENT_SESSION_ID / AGENTS_MAILBOX_DIR / pid registry), or pass --session <id>.');
|
|
165
264
|
}
|
|
265
|
+
const ts = input.ts ?? new Date().toISOString();
|
|
266
|
+
const project = projectFromCwd(identity.cwd);
|
|
267
|
+
const attachments = buildAttachments(input.attach, {
|
|
268
|
+
copyRoot: input.attachmentsRoot ?? path.join(getHistoryDir(), 'attachments'),
|
|
269
|
+
sessionId: identity.sessionId,
|
|
270
|
+
updateId: newUpdateId(ts),
|
|
271
|
+
});
|
|
166
272
|
const event = {
|
|
167
|
-
ts
|
|
273
|
+
ts,
|
|
168
274
|
event: 'status.posted',
|
|
169
275
|
sessionId: identity.sessionId,
|
|
170
276
|
mailboxId: identity.mailboxId,
|
|
@@ -174,10 +280,12 @@ export function postFeedStatus(input) {
|
|
|
174
280
|
agent: identity.agent,
|
|
175
281
|
tool: 'feed.post',
|
|
176
282
|
detail,
|
|
283
|
+
...(project ? { project } : {}),
|
|
177
284
|
...(identity.pid !== undefined ? { pid: identity.pid } : {}),
|
|
178
285
|
...(identity.launchId ? { launchId: identity.launchId } : {}),
|
|
179
286
|
...(identity.terminalId ? { terminalId: identity.terminalId } : {}),
|
|
180
287
|
...(identity.tmuxPane ? { tmuxPane: identity.tmuxPane } : {}),
|
|
288
|
+
...(attachments.length ? { attachments } : {}),
|
|
181
289
|
};
|
|
182
290
|
appendActivityEvent(event, input.activityRoot);
|
|
183
291
|
return {
|