@phnx-labs/agents-cli 1.20.33 → 1.20.35
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 +29 -0
- package/README.md +32 -2
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.js +108 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/rules.js +1 -1
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +93 -17
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/teams.js +70 -6
- package/dist/commands/versions.js +2 -1
- package/dist/commands/view.d.ts +42 -0
- package/dist/commands/view.js +213 -83
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +61 -32
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +145 -41
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +27 -0
- package/dist/lib/browser/drivers/ssh.js +94 -19
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +43 -11
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- 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 +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +40 -9
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +33 -1
- package/dist/lib/secrets/index.js +90 -9
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +30 -2
- package/dist/lib/shims.js +107 -50
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +27 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive drift-sync flow — the single "we detected drift, want to fix it?"
|
|
3
|
+
* action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
|
|
4
|
+
* SYNC" row.
|
|
5
|
+
*
|
|
6
|
+
* It composes existing pieces, re-implementing nothing:
|
|
7
|
+
* - computeSyncStatus() — the unified detection engine (sync-status.ts)
|
|
8
|
+
* - pullRepo() — fast-forward the `.system` repo (git.ts)
|
|
9
|
+
* - promptAgentVersionSelection() — the "which agent types / versions?" picker
|
|
10
|
+
* - heal({ mode: 'full' }) — the reconcile engine `doctor --fix` uses
|
|
11
|
+
*
|
|
12
|
+
* Combined flow (one confirmation): if `.system` is behind AND resources drifted,
|
|
13
|
+
* a single "Sync all detected" both pulls `.system` and reconciles the chosen
|
|
14
|
+
* version homes. The security posture is preserved — the `.system` pull only ever
|
|
15
|
+
* happens on an explicit user choice here, never silently (see auto-pull-worker.ts
|
|
16
|
+
* for why system auto-pull is off by default).
|
|
17
|
+
*/
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
20
|
+
import { AGENTS } from './agents.js';
|
|
21
|
+
import { pullRepo } from './git.js';
|
|
22
|
+
import { heal } from './heal.js';
|
|
23
|
+
import { promptAgentVersionSelection } from './versions.js';
|
|
24
|
+
import { isInteractiveTerminal, isPromptCancelled } from '../commands/utils.js';
|
|
25
|
+
import { computeSyncStatus, } from './sync-status.js';
|
|
26
|
+
const agentName = (id) => AGENTS[id]?.name ?? id;
|
|
27
|
+
/** "claude@2.1.170 2 drifted · 1 missing" for one version. */
|
|
28
|
+
function versionLine(v) {
|
|
29
|
+
const bits = [];
|
|
30
|
+
if (v.counts.drifted)
|
|
31
|
+
bits.push(`${v.counts.drifted} drifted`);
|
|
32
|
+
if (v.counts.missing)
|
|
33
|
+
bits.push(`${v.counts.missing} missing`);
|
|
34
|
+
const label = `${agentName(v.agent)}@${v.version}`;
|
|
35
|
+
return ` ${label.padEnd(28)} ${chalk.yellow(bits.join(' · '))}`;
|
|
36
|
+
}
|
|
37
|
+
/** Render the drift summary (system freshness + each version owed a sync). */
|
|
38
|
+
function renderSummary(status, needing) {
|
|
39
|
+
console.log(chalk.bold('\nSync status'));
|
|
40
|
+
if (status.system.behind > 0) {
|
|
41
|
+
console.log(` ${'.system repo'.padEnd(28)} ${chalk.yellow(`${status.system.behind} commit${status.system.behind === 1 ? '' : 's'} behind`)} ${chalk.gray('— pull recommended')}`);
|
|
42
|
+
}
|
|
43
|
+
for (const v of needing)
|
|
44
|
+
console.log(versionLine(v));
|
|
45
|
+
if (status.totals.orphan > 0) {
|
|
46
|
+
console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Fast-forward the `.system` repo. Returns whether it actually moved. */
|
|
50
|
+
async function pullSystem(status) {
|
|
51
|
+
if (status.system.behind <= 0)
|
|
52
|
+
return false;
|
|
53
|
+
const res = await pullRepo(status.system.dir);
|
|
54
|
+
if (res.success) {
|
|
55
|
+
console.log(chalk.green(`Pulled .system (+${status.system.behind}).`));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk.red(`Could not pull .system: ${res.error ?? 'unknown error'}`));
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/** Reconcile a set of versions grouped by agent via the shared heal engine. */
|
|
62
|
+
async function healVersions(versionsByAgent, cwd) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const [agent, versions] of versionsByAgent) {
|
|
65
|
+
if (versions.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const res = await heal({ mode: 'full', cwd, agent, versions });
|
|
68
|
+
out.push(...res.versions);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Report which agents received what after a heal. */
|
|
73
|
+
function reportHealed(healed) {
|
|
74
|
+
const touched = healed.filter((v) => v.healed.length > 0);
|
|
75
|
+
if (touched.length === 0) {
|
|
76
|
+
console.log(chalk.gray('Nothing to reconcile — homes already matched sources.'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const total = touched.reduce((n, v) => n + v.healed.length, 0);
|
|
80
|
+
const agents = [...new Set(touched.map((v) => agentName(v.agent)))].join(', ');
|
|
81
|
+
console.log(chalk.green(`Synced ${total} resource${total === 1 ? '' : 's'} to ${agents}.`));
|
|
82
|
+
}
|
|
83
|
+
function groupNeeding(needing) {
|
|
84
|
+
const m = new Map();
|
|
85
|
+
for (const v of needing) {
|
|
86
|
+
const list = m.get(v.agent) ?? [];
|
|
87
|
+
list.push(v.version);
|
|
88
|
+
m.set(v.agent, list);
|
|
89
|
+
}
|
|
90
|
+
return m;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The unified "drift detected — sync now?" flow. Returns a structured result so
|
|
94
|
+
* callers (menu-bar, doctor) can report without re-scanning.
|
|
95
|
+
*/
|
|
96
|
+
export async function promptDriftSync(opts = {}) {
|
|
97
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
98
|
+
const status = opts.status ?? (await computeSyncStatus({ cwd }));
|
|
99
|
+
const needing = status.agents.filter((a) => a.needsSync);
|
|
100
|
+
const systemBehind = status.system.behind;
|
|
101
|
+
const base = {
|
|
102
|
+
systemBehindBefore: systemBehind,
|
|
103
|
+
systemPulled: false,
|
|
104
|
+
healed: [],
|
|
105
|
+
cancelled: false,
|
|
106
|
+
nothingToDo: false,
|
|
107
|
+
};
|
|
108
|
+
if (systemBehind <= 0 && needing.length === 0) {
|
|
109
|
+
console.log(chalk.green('Everything is in sync.'));
|
|
110
|
+
return { ...base, nothingToDo: true };
|
|
111
|
+
}
|
|
112
|
+
if (!opts.quiet)
|
|
113
|
+
renderSummary(status, needing);
|
|
114
|
+
// Non-interactive OR explicit --yes: reconcile everything detected.
|
|
115
|
+
if (opts.yes || !isInteractiveTerminal()) {
|
|
116
|
+
if (!opts.yes) {
|
|
117
|
+
// Non-TTY without --yes: report, don't act, don't throw.
|
|
118
|
+
console.log(chalk.gray('\nRun `agents status --yes` to sync, or `agents status` in a terminal to choose.'));
|
|
119
|
+
return base;
|
|
120
|
+
}
|
|
121
|
+
const systemPulled = await pullSystem(status);
|
|
122
|
+
const healed = await healVersions(groupNeeding(needing), cwd);
|
|
123
|
+
reportHealed(healed);
|
|
124
|
+
return { ...base, systemPulled, healed };
|
|
125
|
+
}
|
|
126
|
+
// Interactive gate.
|
|
127
|
+
let choice;
|
|
128
|
+
try {
|
|
129
|
+
choice = await select({
|
|
130
|
+
message: 'Sync now?',
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: 'Sync all detected', value: 'all' },
|
|
133
|
+
{ name: 'Choose agents & resources', value: 'choose' },
|
|
134
|
+
{ name: 'No', value: 'no' },
|
|
135
|
+
],
|
|
136
|
+
default: 'all',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (isPromptCancelled(err))
|
|
141
|
+
return { ...base, cancelled: true };
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
if (choice === 'no')
|
|
145
|
+
return { ...base, cancelled: true };
|
|
146
|
+
if (choice === 'all') {
|
|
147
|
+
const systemPulled = await pullSystem(status);
|
|
148
|
+
const healed = await healVersions(groupNeeding(needing), cwd);
|
|
149
|
+
reportHealed(healed);
|
|
150
|
+
return { ...base, systemPulled, healed };
|
|
151
|
+
}
|
|
152
|
+
// choice === 'choose': optional .system pull, then per-agent/version selection.
|
|
153
|
+
let systemPulled = false;
|
|
154
|
+
if (systemBehind > 0) {
|
|
155
|
+
try {
|
|
156
|
+
const pull = await confirm({ message: `Pull .system (${systemBehind} behind) first?`, default: true });
|
|
157
|
+
if (pull)
|
|
158
|
+
systemPulled = await pullSystem(status);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (!isPromptCancelled(err))
|
|
162
|
+
throw err;
|
|
163
|
+
return { ...base, systemPulled, cancelled: true };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const needingAgents = [...new Set(needing.map((a) => a.agent))];
|
|
167
|
+
let selection;
|
|
168
|
+
try {
|
|
169
|
+
selection = await promptAgentVersionSelection(needingAgents);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (isPromptCancelled(err))
|
|
173
|
+
return { ...base, systemPulled, cancelled: true };
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
const healed = await healVersions(selection.versionSelections, cwd);
|
|
177
|
+
reportHealed(healed);
|
|
178
|
+
return { ...base, systemPulled, healed };
|
|
179
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized event logging for agents-cli.
|
|
3
3
|
*
|
|
4
|
-
* Structured JSONL logs at ~/.agents/logs/events-YYYY-MM-DD.jsonl
|
|
4
|
+
* Structured JSONL logs at ~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl
|
|
5
5
|
* with automatic daily rotation and rich metadata for debugging/auditing.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -11,7 +11,7 @@
|
|
|
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
|
-
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' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
|
|
14
|
+
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' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
|
|
15
15
|
export interface EventMeta {
|
|
16
16
|
ts: string;
|
|
17
17
|
tz: string;
|
|
@@ -22,12 +22,18 @@ export interface EventMeta {
|
|
|
22
22
|
pid: number;
|
|
23
23
|
ppid: number;
|
|
24
24
|
event: EventType;
|
|
25
|
+
osUser: string;
|
|
26
|
+
transport: 'local' | 'ssh';
|
|
27
|
+
sshClientIp?: string;
|
|
25
28
|
}
|
|
26
29
|
export interface EventPayload {
|
|
27
30
|
agent?: string;
|
|
28
31
|
version?: string;
|
|
29
32
|
sessionId?: string;
|
|
30
33
|
cwd?: string;
|
|
34
|
+
/** Top-level command group, e.g. 'teams', 'secrets' — the audit filter key. */
|
|
35
|
+
module?: string;
|
|
36
|
+
/** Full command path, e.g. 'teams create', 'secrets get'. */
|
|
31
37
|
command?: string;
|
|
32
38
|
args?: string[];
|
|
33
39
|
input?: string;
|
|
@@ -155,6 +161,7 @@ export declare function query(options: {
|
|
|
155
161
|
eventTypes?: EventType[];
|
|
156
162
|
agent?: string;
|
|
157
163
|
command?: string;
|
|
164
|
+
module?: string;
|
|
158
165
|
limit?: number;
|
|
159
166
|
}): EventRecord[];
|
|
160
167
|
/**
|
package/dist/lib/events.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized event logging for agents-cli.
|
|
3
3
|
*
|
|
4
|
-
* Structured JSONL logs at ~/.agents/logs/events-YYYY-MM-DD.jsonl
|
|
4
|
+
* Structured JSONL logs at ~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl
|
|
5
5
|
* with automatic daily rotation and rich metadata for debugging/auditing.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -15,6 +15,7 @@ import * as fs from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
|
+
import { parseSshConnection } from './session/provenance.js';
|
|
18
19
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
19
20
|
// Logs live under the cache bucket — they're regenerable telemetry.
|
|
20
21
|
const LOGS_DIR = path.join(os.homedir(), '.agents', '.cache', 'logs');
|
|
@@ -130,6 +131,30 @@ function truncatePayload(payload, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
|
130
131
|
}
|
|
131
132
|
return result;
|
|
132
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Who is running this process and from where. Derived once per process from the
|
|
136
|
+
* OS user and $SSH_CONNECTION (via the same parser the sessions layer uses), then
|
|
137
|
+
* cached — provenance can't change mid-process, so every emit() pays for it once.
|
|
138
|
+
*/
|
|
139
|
+
let _origin;
|
|
140
|
+
function auditOrigin() {
|
|
141
|
+
if (_origin)
|
|
142
|
+
return _origin;
|
|
143
|
+
let osUser = 'unknown';
|
|
144
|
+
try {
|
|
145
|
+
osUser = os.userInfo().username;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Container/edge cases where the uid has no passwd entry.
|
|
149
|
+
}
|
|
150
|
+
const ssh = process.env.SSH_CONNECTION ? parseSshConnection(process.env.SSH_CONNECTION) : undefined;
|
|
151
|
+
_origin = {
|
|
152
|
+
osUser,
|
|
153
|
+
transport: ssh ? 'ssh' : 'local',
|
|
154
|
+
...(ssh ? { sshClientIp: ssh.clientIp } : {}),
|
|
155
|
+
};
|
|
156
|
+
return _origin;
|
|
157
|
+
}
|
|
133
158
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
134
159
|
/**
|
|
135
160
|
* Emit a structured event to the daily log file.
|
|
@@ -152,23 +177,33 @@ export function emit(event, payload = {}) {
|
|
|
152
177
|
pid: process.pid,
|
|
153
178
|
ppid: process.ppid,
|
|
154
179
|
event,
|
|
180
|
+
...auditOrigin(),
|
|
155
181
|
...truncatePayload(payload),
|
|
156
182
|
};
|
|
157
183
|
const line = JSON.stringify(record) + '\n';
|
|
158
184
|
const logPath = getLogFilePath();
|
|
185
|
+
const isNew = !fs.existsSync(logPath);
|
|
159
186
|
fs.appendFileSync(logPath, line, { mode: FILE_MODE });
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
// appendFileSync's mode only applies when it CREATES the file, so we chmod
|
|
188
|
+
// to guarantee 0600 — but only on first write to a given path this process.
|
|
189
|
+
// command.start/end fire on every invocation; chmod-per-append would double
|
|
190
|
+
// the syscalls on the hot path for no gain (perms don't drift mid-run).
|
|
191
|
+
if (isNew || logPath !== _chmoddedPath) {
|
|
192
|
+
_chmoddedPath = logPath;
|
|
193
|
+
try {
|
|
194
|
+
fs.chmodSync(logPath, FILE_MODE);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// May fail if not owner
|
|
198
|
+
}
|
|
166
199
|
}
|
|
167
200
|
}
|
|
168
201
|
catch {
|
|
169
202
|
// Silent failure - logging should never break the CLI
|
|
170
203
|
}
|
|
171
204
|
}
|
|
205
|
+
/** Last log path this process chmod'd — avoids a redundant chmod per append. */
|
|
206
|
+
let _chmoddedPath;
|
|
172
207
|
/**
|
|
173
208
|
* Convenience wrapper for timed operations.
|
|
174
209
|
* Returns a function to call when the operation completes.
|
|
@@ -402,7 +437,7 @@ export function maybeRotate() {
|
|
|
402
437
|
* @returns Array of event records
|
|
403
438
|
*/
|
|
404
439
|
export function query(options) {
|
|
405
|
-
const { startDate, endDate = new Date(), eventTypes, agent, command, limit } = options;
|
|
440
|
+
const { startDate, endDate = new Date(), eventTypes, agent, command, module, limit } = options;
|
|
406
441
|
const results = [];
|
|
407
442
|
if (!fs.existsSync(LOGS_DIR))
|
|
408
443
|
return results;
|
|
@@ -410,26 +445,50 @@ export function query(options) {
|
|
|
410
445
|
.filter(f => f.startsWith('events-') && f.endsWith('.jsonl'))
|
|
411
446
|
.sort()
|
|
412
447
|
.reverse();
|
|
448
|
+
// Coarse file skip works on whole days, so floor the bounds to midnight —
|
|
449
|
+
// otherwise a sub-day window (`--since 2h` at 15:00 → startDate 13:00) would
|
|
450
|
+
// drop *today's* file, whose date stamps to 00:00. Precise filtering below is
|
|
451
|
+
// per-record on `ts`.
|
|
452
|
+
const startDay = startDate
|
|
453
|
+
? new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
|
|
454
|
+
: undefined;
|
|
455
|
+
const endDay = endDate
|
|
456
|
+
? new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
|
|
457
|
+
: undefined;
|
|
458
|
+
const startMs = startDate?.getTime();
|
|
459
|
+
const endMs = endDate?.getTime();
|
|
413
460
|
for (const file of files) {
|
|
414
461
|
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
415
462
|
if (!match)
|
|
416
463
|
continue;
|
|
417
464
|
const [, yyyy, mm, dd] = match;
|
|
418
465
|
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
419
|
-
if (
|
|
466
|
+
if (startDay && fileDate < startDay)
|
|
420
467
|
continue;
|
|
421
|
-
if (
|
|
468
|
+
if (endDay && fileDate > endDay)
|
|
422
469
|
continue;
|
|
423
470
|
const content = fs.readFileSync(path.join(LOGS_DIR, file), 'utf-8');
|
|
424
471
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
425
472
|
for (const line of lines.reverse()) {
|
|
426
473
|
try {
|
|
427
474
|
const record = JSON.parse(line);
|
|
475
|
+
// Precise per-record window — the file skip above is day-granular only.
|
|
476
|
+
const recMs = Date.parse(record.ts);
|
|
477
|
+
if (startMs !== undefined && !isNaN(recMs) && recMs < startMs)
|
|
478
|
+
continue;
|
|
479
|
+
if (endMs !== undefined && !isNaN(recMs) && recMs > endMs)
|
|
480
|
+
continue;
|
|
428
481
|
if (eventTypes && !eventTypes.includes(record.event))
|
|
429
482
|
continue;
|
|
430
483
|
if (agent && record.agent !== agent)
|
|
431
484
|
continue;
|
|
432
|
-
|
|
485
|
+
// `--command` matches by path prefix on a word boundary, so a coarse
|
|
486
|
+
// "teams" catches "teams create"/"teams remove" while an exact
|
|
487
|
+
// "teams create" stays exact.
|
|
488
|
+
if (command && record.command !== command &&
|
|
489
|
+
!(typeof record.command === 'string' && record.command.startsWith(command + ' ')))
|
|
490
|
+
continue;
|
|
491
|
+
if (module && record.module !== module)
|
|
433
492
|
continue;
|
|
434
493
|
results.push(record);
|
|
435
494
|
if (limit && results.length >= limit) {
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -189,6 +189,21 @@ export declare function nativeResume(agent: AgentId): boolean;
|
|
|
189
189
|
export declare function buildExecCommand(options: ExecOptions): string[];
|
|
190
190
|
/** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
|
|
191
191
|
export declare function execAgent(options: ExecOptions): Promise<number>;
|
|
192
|
+
/**
|
|
193
|
+
* Resolve how to spawn a shim target for a platform. Pure — testable on any host.
|
|
194
|
+
*
|
|
195
|
+
* POSIX always execs the binary directly (no shell). On Windows a bare
|
|
196
|
+
* (non-absolute) name or a `.cmd` companion goes through the shell so cmd.exe
|
|
197
|
+
* resolves it via PATHEXT — the common, `.cmd`-present path; an absolute `.cmd`
|
|
198
|
+
* or extensionless path is exec'd through the shell / directly. npm always ships
|
|
199
|
+
* a `<cmd>.cmd` companion on Windows, so the runnable target `execShimPassthrough`
|
|
200
|
+
* hands us is the `.cmd` (never a bare `.ps1`).
|
|
201
|
+
*/
|
|
202
|
+
export declare function resolveShimSpawn(platform: NodeJS.Platform, binary: string, extraArgs: string[]): {
|
|
203
|
+
command: string;
|
|
204
|
+
args: string[];
|
|
205
|
+
shell: boolean;
|
|
206
|
+
};
|
|
192
207
|
/**
|
|
193
208
|
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
194
209
|
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
package/dist/lib/exec.js
CHANGED
|
@@ -16,6 +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 { getShimsDir } from './state.js';
|
|
19
|
+
import { writePidSessionEntry } from './session/pid-registry.js';
|
|
19
20
|
/**
|
|
20
21
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
21
22
|
*
|
|
@@ -374,15 +375,6 @@ export const AGENT_COMMANDS = {
|
|
|
374
375
|
edit: [],
|
|
375
376
|
},
|
|
376
377
|
},
|
|
377
|
-
roo: {
|
|
378
|
-
base: ['roo'],
|
|
379
|
-
promptFlag: 'positional',
|
|
380
|
-
modeFlags: {
|
|
381
|
-
plan: ['--mode', 'architect'],
|
|
382
|
-
edit: ['--mode', 'code'],
|
|
383
|
-
},
|
|
384
|
-
modelFlag: '--model',
|
|
385
|
-
},
|
|
386
378
|
// TODO: --output-format json is documented but currently broken upstream
|
|
387
379
|
// ("flags provided but not defined: -output-format"). Track resolution at
|
|
388
380
|
// https://github.com/google-antigravity/antigravity-cli/issues/7 before
|
|
@@ -665,6 +657,25 @@ export async function execAgent(options) {
|
|
|
665
657
|
const { exitCode } = await spawnAgent(options);
|
|
666
658
|
return exitCode;
|
|
667
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* Resolve how to spawn a shim target for a platform. Pure — testable on any host.
|
|
662
|
+
*
|
|
663
|
+
* POSIX always execs the binary directly (no shell). On Windows a bare
|
|
664
|
+
* (non-absolute) name or a `.cmd` companion goes through the shell so cmd.exe
|
|
665
|
+
* resolves it via PATHEXT — the common, `.cmd`-present path; an absolute `.cmd`
|
|
666
|
+
* or extensionless path is exec'd through the shell / directly. npm always ships
|
|
667
|
+
* a `<cmd>.cmd` companion on Windows, so the runnable target `execShimPassthrough`
|
|
668
|
+
* hands us is the `.cmd` (never a bare `.ps1`).
|
|
669
|
+
*/
|
|
670
|
+
export function resolveShimSpawn(platform, binary, extraArgs) {
|
|
671
|
+
if (platform === 'win32') {
|
|
672
|
+
// Use win32 path semantics regardless of the host running this (the platform
|
|
673
|
+
// is the parameter, not process.platform) so `C:\...` reads as absolute.
|
|
674
|
+
const useShell = !path.win32.isAbsolute(binary) || binary.endsWith('.cmd');
|
|
675
|
+
return { command: binary, args: extraArgs, shell: useShell };
|
|
676
|
+
}
|
|
677
|
+
return { command: binary, args: extraArgs, shell: false };
|
|
678
|
+
}
|
|
668
679
|
/**
|
|
669
680
|
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
670
681
|
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
|
@@ -691,9 +702,9 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
691
702
|
// mode/effort are required by ExecOptions but unused by buildExecEnv (which only
|
|
692
703
|
// derives the per-version config-dir env); pass the agent's default to satisfy the type.
|
|
693
704
|
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto' });
|
|
694
|
-
const
|
|
705
|
+
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
695
706
|
return new Promise((resolve) => {
|
|
696
|
-
const child = spawn(
|
|
707
|
+
const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
|
|
697
708
|
child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
698
709
|
child.on('error', (err) => {
|
|
699
710
|
process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
|
|
@@ -712,6 +723,15 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
712
723
|
* unbuffered.
|
|
713
724
|
*/
|
|
714
725
|
async function spawnAgent(options) {
|
|
726
|
+
// Assign a known session id up front for agents that accept one, so the
|
|
727
|
+
// launcher can record an EXACT pid -> session mapping (see pid-registry) —
|
|
728
|
+
// otherwise the headless `ag sessions --active` path can only guess
|
|
729
|
+
// "newest .jsonl in the cwd" and collapses co-located agents onto one row.
|
|
730
|
+
// Claude: `--session-id <uuid>` CREATES the session with that id (wired in
|
|
731
|
+
// buildExecCommand). Skip on resume — the id is the one being resumed.
|
|
732
|
+
if (options.agent === 'claude' && !options.resume && !options.sessionId) {
|
|
733
|
+
options = { ...options, sessionId: randomUUID() };
|
|
734
|
+
}
|
|
715
735
|
const cmd = buildExecCommand(options);
|
|
716
736
|
const [executable, ...args] = cmd;
|
|
717
737
|
const timeoutMs = options.timeout ? parseTimeout(options.timeout) : undefined;
|
|
@@ -760,6 +780,18 @@ async function spawnAgent(options) {
|
|
|
760
780
|
env: buildExecEnv(options),
|
|
761
781
|
shell: useShell,
|
|
762
782
|
});
|
|
783
|
+
// Record this launch so `ag sessions --active` can map the pid to its exact
|
|
784
|
+
// session (sessionId is set for Claude above) instead of guessing the newest
|
|
785
|
+
// .jsonl in the cwd — the collapse that made co-located agents indistinguishable.
|
|
786
|
+
// Best-effort: pruned when the pid dies; a failed write just degrades to the heuristic.
|
|
787
|
+
writePidSessionEntry({
|
|
788
|
+
pid: child.pid ?? 0,
|
|
789
|
+
agent: options.agent,
|
|
790
|
+
sessionId: options.sessionId,
|
|
791
|
+
cwd: options.cwd || process.cwd(),
|
|
792
|
+
tmuxPane: process.env.TMUX_PANE,
|
|
793
|
+
startedAtMs: Date.now(),
|
|
794
|
+
});
|
|
763
795
|
// Mark startup time (time from function call to process spawn)
|
|
764
796
|
timer.mark('startup');
|
|
765
797
|
let budgetKilled = false;
|
package/dist/lib/hooks.js
CHANGED
|
@@ -910,6 +910,12 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
910
910
|
if (agentId === 'claude') {
|
|
911
911
|
return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes);
|
|
912
912
|
}
|
|
913
|
+
if (agentId === 'droid') {
|
|
914
|
+
// Droid's settings.json hooks schema is identical to Claude's (top-level
|
|
915
|
+
// `hooks` object → event → matcher-group array), so reuse the Claude
|
|
916
|
+
// registrar targeting `.factory/settings.json` (agentConfigDirName('droid')).
|
|
917
|
+
return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, agentConfigDirName('droid'));
|
|
918
|
+
}
|
|
913
919
|
if (agentId === 'codex') {
|
|
914
920
|
return registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes);
|
|
915
921
|
}
|
|
@@ -953,10 +959,10 @@ const ANTIGRAVITY_EVENT_MAP = {
|
|
|
953
959
|
const GEMINI_EVENT_MAP = {
|
|
954
960
|
UserPromptSubmit: 'BeforeAgent',
|
|
955
961
|
};
|
|
956
|
-
function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
962
|
+
function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, configDirName = '.claude') {
|
|
957
963
|
const registered = [];
|
|
958
964
|
const errors = [];
|
|
959
|
-
const configDir = path.join(versionHome,
|
|
965
|
+
const configDir = path.join(versionHome, configDirName);
|
|
960
966
|
const settingsPath = path.join(configDir, 'settings.json');
|
|
961
967
|
let config = {};
|
|
962
968
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -12,7 +12,7 @@ import { randomUUID } from 'crypto';
|
|
|
12
12
|
import { sshExec, shellQuote } from '../ssh-exec.js';
|
|
13
13
|
import { sshTargetFor } from './types.js';
|
|
14
14
|
import { ensureHostReady } from './ready.js';
|
|
15
|
-
import { saveTask, updateTask } from './tasks.js';
|
|
15
|
+
import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
16
16
|
import { followHostTask } from './progress.js';
|
|
17
17
|
// Use $HOME (not ~) so the path is correct whether or not it's quoted and
|
|
18
18
|
// regardless of the run's cwd. Task ids are 8 hex chars, so these paths are
|
|
@@ -61,12 +61,11 @@ async function launchDetached(host, target, opts) {
|
|
|
61
61
|
echo: true,
|
|
62
62
|
timeoutMs: opts.timeoutMs,
|
|
63
63
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
return { task: finished ?? task, exitCode };
|
|
64
|
+
// -1 = the follow window closed while the run continues on the host. Leave the
|
|
65
|
+
// record 'running' (do NOT freeze it terminal) so a later `hosts ps`/`logs`
|
|
66
|
+
// reconcile against the remote `.exit` resolves the true final status.
|
|
67
|
+
const finished = exitCode === -1 ? task : (updateTask(id, terminalPatch(exitCode)) ?? task);
|
|
68
|
+
return { task: finished, exitCode };
|
|
70
69
|
}
|
|
71
70
|
/** Dispatch an `agents run <agent> "<prompt>"` onto a host (the `run --host` path). */
|
|
72
71
|
export async function dispatchToHost(host, opts) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared host-task log viewer — the show-or-follow core behind both
|
|
3
|
+
* `agents hosts logs <id>` and the top-level `agents logs <id>`.
|
|
4
|
+
*
|
|
5
|
+
* A running task with follow re-enters the offset-tail (`followHostTask`);
|
|
6
|
+
* otherwise the captured local mirror (`localLogPath`) is printed. Kept in one
|
|
7
|
+
* place so the two commands can never drift.
|
|
8
|
+
*/
|
|
9
|
+
export interface HostLogResult {
|
|
10
|
+
/** False when no host task with this id exists (caller may fall through to sessions). */
|
|
11
|
+
found: boolean;
|
|
12
|
+
/** Process exit code to adopt when the task was shown/followed. */
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Show (or follow, when running) a dispatched host task's combined-stdout log. */
|
|
16
|
+
export declare function showHostTaskLog(id: string, follow: boolean): Promise<HostLogResult>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared host-task log viewer — the show-or-follow core behind both
|
|
3
|
+
* `agents hosts logs <id>` and the top-level `agents logs <id>`.
|
|
4
|
+
*
|
|
5
|
+
* A running task with follow re-enters the offset-tail (`followHostTask`);
|
|
6
|
+
* otherwise the captured local mirror (`localLogPath`) is printed. Kept in one
|
|
7
|
+
* place so the two commands can never drift.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { loadTask, localLogPath, updateTask, terminalPatch } from './tasks.js';
|
|
12
|
+
import { followHostTask } from './progress.js';
|
|
13
|
+
import { reconcileTask } from './reconcile.js';
|
|
14
|
+
/** Show (or follow, when running) a dispatched host task's combined-stdout log. */
|
|
15
|
+
export async function showHostTaskLog(id, follow) {
|
|
16
|
+
const task = loadTask(id);
|
|
17
|
+
if (!task)
|
|
18
|
+
return { found: false };
|
|
19
|
+
if (follow && task.status === 'running') {
|
|
20
|
+
const code = await followHostTask(task.target, {
|
|
21
|
+
remoteLog: task.remoteLog,
|
|
22
|
+
remoteExit: task.remoteExit,
|
|
23
|
+
taskId: id,
|
|
24
|
+
echo: true,
|
|
25
|
+
});
|
|
26
|
+
// -1 = follow window closed; the run continues on the host (not a failure,
|
|
27
|
+
// so exit 0). Any real code is the finished run — persist the terminal
|
|
28
|
+
// status the killed dispatch follower would have written.
|
|
29
|
+
if (code === -1)
|
|
30
|
+
return { found: true, exitCode: 0 };
|
|
31
|
+
updateTask(id, terminalPatch(code));
|
|
32
|
+
return { found: true, exitCode: code };
|
|
33
|
+
}
|
|
34
|
+
// Non-follow view: heal a still-'running' record from the remote `.exit` so a
|
|
35
|
+
// plain `logs <id>` also unsticks a task whose follower was killed. No-op (no
|
|
36
|
+
// ssh) once the record is already terminal.
|
|
37
|
+
reconcileTask(task);
|
|
38
|
+
try {
|
|
39
|
+
process.stdout.write(fs.readFileSync(localLogPath(id), 'utf-8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
console.log(chalk.gray('(no local log captured for this task)'));
|
|
43
|
+
}
|
|
44
|
+
return { found: true, exitCode: 0 };
|
|
45
|
+
}
|