@phnx-labs/agents-cli 1.20.93 → 1.21.0
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 +264 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +7 -0
- package/dist/lib/activity.js +19 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -2
- 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/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +17 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
package/dist/lib/drive-sync.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote drive sync for agent sessions and config.
|
|
3
|
-
*
|
|
4
|
-
* Provides rsync-based push/pull between the local ~/.agents/drive/
|
|
5
|
-
* directory and a remote host, plus attach/detach to swap the active
|
|
6
|
-
* agent's config directory symlink to the drive directory (for working
|
|
7
|
-
* with a remote machine's session data locally).
|
|
8
|
-
*/
|
|
9
|
-
import * as fs from 'fs';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import * as os from 'os';
|
|
12
|
-
import { execFile } from 'child_process';
|
|
13
|
-
import { promisify } from 'util';
|
|
14
|
-
import { getDriveDir } from './state.js';
|
|
15
|
-
import { AGENTS } from './agents.js';
|
|
16
|
-
import { sshExec } from './ssh-exec.js';
|
|
17
|
-
const execFileAsync = promisify(execFile);
|
|
18
|
-
// `remote` flows from disk into rsync/ssh argv. Use a strict regex so a
|
|
19
|
-
// tampered ~/.agents/drive/config.json can't sneak shell metacharacters or
|
|
20
|
-
// argv flags (leading -) past the boundary.
|
|
21
|
-
const REMOTE_RE = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+$/;
|
|
22
|
-
function assertValidRemote(remote) {
|
|
23
|
-
if (!REMOTE_RE.test(remote)) {
|
|
24
|
-
throw new Error(`Invalid drive remote: ${JSON.stringify(remote)}. Expected: user@host`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const AGENT = 'claude';
|
|
28
|
-
function configPath() {
|
|
29
|
-
return path.join(getDriveDir(), 'config.json');
|
|
30
|
-
}
|
|
31
|
-
/** Read drive config from disk, returning defaults if missing. */
|
|
32
|
-
export function readDriveConfig() {
|
|
33
|
-
const p = configPath();
|
|
34
|
-
if (fs.existsSync(p)) {
|
|
35
|
-
try {
|
|
36
|
-
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
37
|
-
// Re-validate `remote` on every read. setRemote validates on write, but
|
|
38
|
-
// the file can be tampered with out-of-band (a malicious skill, a synced
|
|
39
|
-
// config from a hostile repo, etc.) and pull/push shell out with the value.
|
|
40
|
-
if (parsed.remote != null) {
|
|
41
|
-
assertValidRemote(parsed.remote);
|
|
42
|
-
}
|
|
43
|
-
return parsed;
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
// If the config is malformed or has a tainted remote, fail closed rather
|
|
47
|
-
// than silently fall back to defaults — a tainted remote that fell back
|
|
48
|
-
// to null would mask the attack.
|
|
49
|
-
if (err instanceof SyntaxError) {
|
|
50
|
-
// Bad JSON: fall through to defaults.
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
throw err;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return { remote: null, attached: false, previousTargets: null, lastPull: null, lastPush: null };
|
|
58
|
-
}
|
|
59
|
-
/** Write drive config to disk. */
|
|
60
|
-
export function writeDriveConfig(config) {
|
|
61
|
-
const dir = getDriveDir();
|
|
62
|
-
if (!fs.existsSync(dir)) {
|
|
63
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
-
}
|
|
65
|
-
fs.writeFileSync(configPath(), JSON.stringify(config, null, 2));
|
|
66
|
-
}
|
|
67
|
-
/** Set the remote target (user@host) for rsync operations. */
|
|
68
|
-
export function setRemote(target) {
|
|
69
|
-
assertValidRemote(target);
|
|
70
|
-
const config = readDriveConfig();
|
|
71
|
-
config.remote = target;
|
|
72
|
-
writeDriveConfig(config);
|
|
73
|
-
}
|
|
74
|
-
/** Pull drive data from the remote host via rsync. */
|
|
75
|
-
export async function pull() {
|
|
76
|
-
const config = readDriveConfig();
|
|
77
|
-
if (!config.remote)
|
|
78
|
-
throw new Error('No remote configured. Run: agents drive remote <user@host>');
|
|
79
|
-
assertValidRemote(config.remote);
|
|
80
|
-
const localDir = getDriveDir() + '/';
|
|
81
|
-
const remoteSpec = `${config.remote}:~/.agents/drive/`;
|
|
82
|
-
// Argv form: no shell, no interpolation — even a tainted `config.remote` that
|
|
83
|
-
// somehow got past the regex cannot escape into command syntax.
|
|
84
|
-
await execFileAsync('rsync', ['-az', '--exclude=config.json', remoteSpec, localDir]);
|
|
85
|
-
config.lastPull = new Date().toISOString();
|
|
86
|
-
writeDriveConfig(config);
|
|
87
|
-
}
|
|
88
|
-
/** Push local drive data to the remote host via rsync. */
|
|
89
|
-
export async function push() {
|
|
90
|
-
const config = readDriveConfig();
|
|
91
|
-
if (!config.remote)
|
|
92
|
-
throw new Error('No remote configured. Run: agents drive remote <user@host>');
|
|
93
|
-
assertValidRemote(config.remote);
|
|
94
|
-
const localDir = getDriveDir() + '/';
|
|
95
|
-
const remoteSpec = `${config.remote}:~/.agents/drive/`;
|
|
96
|
-
// Ensure remote directory exists via the shared hardened SSH primitive.
|
|
97
|
-
const mkdir = sshExec(config.remote, 'mkdir -p ~/.agents/drive', { timeoutMs: 30_000 });
|
|
98
|
-
if (mkdir.code !== 0) {
|
|
99
|
-
throw new Error(`Failed to prepare remote drive directory on ${config.remote}${mkdir.stderr.trim() ? `: ${mkdir.stderr.trim()}` : ''}`);
|
|
100
|
-
}
|
|
101
|
-
await execFileAsync('rsync', ['-az', '--exclude=config.json', localDir, remoteSpec]);
|
|
102
|
-
config.lastPush = new Date().toISOString();
|
|
103
|
-
writeDriveConfig(config);
|
|
104
|
-
}
|
|
105
|
-
/** Attach drive by swapping the agent's config directory symlink to the drive directory. */
|
|
106
|
-
export function attach() {
|
|
107
|
-
const config = readDriveConfig();
|
|
108
|
-
if (config.attached)
|
|
109
|
-
throw new Error('Drive is already attached');
|
|
110
|
-
const home = os.homedir();
|
|
111
|
-
const agentConfig = AGENTS[AGENT];
|
|
112
|
-
const driveDir = getDriveDir();
|
|
113
|
-
const configDirPath = agentConfig.configDir; // ~/.claude
|
|
114
|
-
const driveConfigDir = path.join(driveDir, `.${AGENT}`); // ~/.agents/drive/.claude
|
|
115
|
-
// Read current symlink target
|
|
116
|
-
let prevConfigDir = null;
|
|
117
|
-
try {
|
|
118
|
-
const stat = fs.lstatSync(configDirPath);
|
|
119
|
-
if (stat.isSymbolicLink()) {
|
|
120
|
-
prevConfigDir = fs.readlinkSync(configDirPath);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
// Path doesn't exist
|
|
125
|
-
}
|
|
126
|
-
if (!prevConfigDir) {
|
|
127
|
-
throw new Error(`${configDirPath} is not a symlink. Run 'agents use claude' first.`);
|
|
128
|
-
}
|
|
129
|
-
// Save previous targets for home files
|
|
130
|
-
const prevHomeFiles = {};
|
|
131
|
-
for (const hf of agentConfig.homeFiles || []) {
|
|
132
|
-
const hfPath = path.join(home, hf);
|
|
133
|
-
try {
|
|
134
|
-
const stat = fs.lstatSync(hfPath);
|
|
135
|
-
if (stat.isSymbolicLink()) {
|
|
136
|
-
prevHomeFiles[hf] = fs.readlinkSync(hfPath);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// File doesn't exist
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
config.previousTargets = {
|
|
144
|
-
configDir: prevConfigDir,
|
|
145
|
-
homeFiles: prevHomeFiles,
|
|
146
|
-
};
|
|
147
|
-
// Ensure drive config dir exists
|
|
148
|
-
if (!fs.existsSync(driveConfigDir)) {
|
|
149
|
-
fs.mkdirSync(driveConfigDir, { recursive: true });
|
|
150
|
-
}
|
|
151
|
-
// Swap config dir symlink
|
|
152
|
-
fs.unlinkSync(configDirPath);
|
|
153
|
-
fs.symlinkSync(driveConfigDir, configDirPath);
|
|
154
|
-
// Swap home files
|
|
155
|
-
for (const hf of agentConfig.homeFiles || []) {
|
|
156
|
-
const hfPath = path.join(home, hf);
|
|
157
|
-
const driveHfPath = path.join(driveDir, hf);
|
|
158
|
-
if (!fs.existsSync(driveHfPath)) {
|
|
159
|
-
fs.writeFileSync(driveHfPath, '{}');
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
fs.unlinkSync(hfPath);
|
|
163
|
-
}
|
|
164
|
-
catch { /* doesn't exist */ }
|
|
165
|
-
fs.symlinkSync(driveHfPath, hfPath);
|
|
166
|
-
}
|
|
167
|
-
config.attached = true;
|
|
168
|
-
writeDriveConfig(config);
|
|
169
|
-
}
|
|
170
|
-
/** Detach drive by restoring the agent's config directory to its previous symlink target. */
|
|
171
|
-
export function detach() {
|
|
172
|
-
const config = readDriveConfig();
|
|
173
|
-
if (!config.attached)
|
|
174
|
-
throw new Error('Drive is not attached');
|
|
175
|
-
if (!config.previousTargets)
|
|
176
|
-
throw new Error('No previous targets saved');
|
|
177
|
-
const home = os.homedir();
|
|
178
|
-
const agentConfig = AGENTS[AGENT];
|
|
179
|
-
// Restore config dir symlink
|
|
180
|
-
const configDirPath = agentConfig.configDir;
|
|
181
|
-
fs.unlinkSync(configDirPath);
|
|
182
|
-
fs.symlinkSync(config.previousTargets.configDir, configDirPath);
|
|
183
|
-
// Restore home files
|
|
184
|
-
for (const hf of agentConfig.homeFiles || []) {
|
|
185
|
-
const hfPath = path.join(home, hf);
|
|
186
|
-
const prevTarget = config.previousTargets.homeFiles[hf];
|
|
187
|
-
if (prevTarget) {
|
|
188
|
-
try {
|
|
189
|
-
fs.unlinkSync(hfPath);
|
|
190
|
-
}
|
|
191
|
-
catch { /* doesn't exist */ }
|
|
192
|
-
fs.symlinkSync(prevTarget, hfPath);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
config.attached = false;
|
|
196
|
-
config.previousTargets = null;
|
|
197
|
-
writeDriveConfig(config);
|
|
198
|
-
}
|
|
199
|
-
/** Gather current drive status by inspecting config, symlinks, and timestamps. */
|
|
200
|
-
export function getDriveStatus() {
|
|
201
|
-
const config = readDriveConfig();
|
|
202
|
-
const home = os.homedir();
|
|
203
|
-
const agentConfig = AGENTS[AGENT];
|
|
204
|
-
let configDirTarget = null;
|
|
205
|
-
try {
|
|
206
|
-
const stat = fs.lstatSync(agentConfig.configDir);
|
|
207
|
-
if (stat.isSymbolicLink()) {
|
|
208
|
-
configDirTarget = fs.readlinkSync(agentConfig.configDir);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
// Not a symlink or doesn't exist
|
|
213
|
-
}
|
|
214
|
-
const homeFileTargets = {};
|
|
215
|
-
for (const hf of agentConfig.homeFiles || []) {
|
|
216
|
-
try {
|
|
217
|
-
const stat = fs.lstatSync(path.join(home, hf));
|
|
218
|
-
if (stat.isSymbolicLink()) {
|
|
219
|
-
homeFileTargets[hf] = fs.readlinkSync(path.join(home, hf));
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
homeFileTargets[hf] = null;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
homeFileTargets[hf] = null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return {
|
|
230
|
-
remote: config.remote,
|
|
231
|
-
attached: config.attached,
|
|
232
|
-
lastPull: config.lastPull,
|
|
233
|
-
lastPush: config.lastPush,
|
|
234
|
-
driveDir: getDriveDir(),
|
|
235
|
-
configDirTarget,
|
|
236
|
-
homeFileTargets,
|
|
237
|
-
};
|
|
238
|
-
}
|
package/dist/lib/hq/floor.d.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { ActiveSession } from '../session/active.js';
|
|
2
|
-
import type { OpenBlock } from '../feed.js';
|
|
3
|
-
import type { AgentStatusDetail, TaskInfo } from '../teams/api.js';
|
|
4
|
-
export type HqRoomKind = 'team' | 'machine' | 'lobby';
|
|
5
|
-
export type HqAgentMood = 'working' | 'waiting' | 'blocked' | 'celebrating' | 'done' | 'idle';
|
|
6
|
-
export type HqAmbientKind = 'needs_input' | 'error' | 'pr' | 'team_active' | 'idle_scene';
|
|
7
|
-
export interface HqAction {
|
|
8
|
-
id: string;
|
|
9
|
-
label: string;
|
|
10
|
-
command: string[];
|
|
11
|
-
target: {
|
|
12
|
-
kind: 'agent' | 'room' | 'floor';
|
|
13
|
-
id: string;
|
|
14
|
-
};
|
|
15
|
-
destructive?: boolean;
|
|
16
|
-
input?: Array<{
|
|
17
|
-
name: string;
|
|
18
|
-
label: string;
|
|
19
|
-
placeholder?: string;
|
|
20
|
-
required?: boolean;
|
|
21
|
-
}>;
|
|
22
|
-
}
|
|
23
|
-
export interface HqRoom {
|
|
24
|
-
id: string;
|
|
25
|
-
kind: HqRoomKind;
|
|
26
|
-
name: string;
|
|
27
|
-
machine?: string;
|
|
28
|
-
team?: string;
|
|
29
|
-
counts: {
|
|
30
|
-
agents: number;
|
|
31
|
-
needsInput: number;
|
|
32
|
-
running: number;
|
|
33
|
-
failed: number;
|
|
34
|
-
};
|
|
35
|
-
actions: HqAction[];
|
|
36
|
-
}
|
|
37
|
-
export interface HqAgent {
|
|
38
|
-
id: string;
|
|
39
|
-
label: string;
|
|
40
|
-
agent: string;
|
|
41
|
-
roomId: string;
|
|
42
|
-
mood: HqAgentMood;
|
|
43
|
-
status: string;
|
|
44
|
-
activity?: string;
|
|
45
|
-
preview?: string;
|
|
46
|
-
machine?: string;
|
|
47
|
-
team?: string;
|
|
48
|
-
teammate?: string;
|
|
49
|
-
sessionId?: string;
|
|
50
|
-
mailboxId?: string;
|
|
51
|
-
pid?: number;
|
|
52
|
-
prUrl?: string;
|
|
53
|
-
ticketId?: string;
|
|
54
|
-
actions: HqAction[];
|
|
55
|
-
}
|
|
56
|
-
export interface HqAmbientEvent {
|
|
57
|
-
id: string;
|
|
58
|
-
kind: HqAmbientKind;
|
|
59
|
-
roomId: string;
|
|
60
|
-
agentId?: string;
|
|
61
|
-
label: string;
|
|
62
|
-
intensity: 'low' | 'medium' | 'high';
|
|
63
|
-
}
|
|
64
|
-
export interface HqFloorSnapshot {
|
|
65
|
-
version: 1;
|
|
66
|
-
generatedAt: string;
|
|
67
|
-
counters: {
|
|
68
|
-
rooms: number;
|
|
69
|
-
agents: number;
|
|
70
|
-
teams: number;
|
|
71
|
-
needsInput: number;
|
|
72
|
-
failed: number;
|
|
73
|
-
prs: number;
|
|
74
|
-
};
|
|
75
|
-
rooms: HqRoom[];
|
|
76
|
-
agents: HqAgent[];
|
|
77
|
-
ambientEvents: HqAmbientEvent[];
|
|
78
|
-
actions: HqAction[];
|
|
79
|
-
}
|
|
80
|
-
export interface BuildHqFloorInput {
|
|
81
|
-
generatedAt?: Date;
|
|
82
|
-
sessions: ActiveSession[];
|
|
83
|
-
teams: TaskInfo[];
|
|
84
|
-
teammatesByTeam: Map<string, AgentStatusDetail[]>;
|
|
85
|
-
blocks: OpenBlock[];
|
|
86
|
-
}
|
|
87
|
-
export declare function buildHqFloor(input: BuildHqFloorInput): HqFloorSnapshot;
|
package/dist/lib/hq/floor.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import { mailboxIdForActiveSession } from '../mailbox-target.js';
|
|
2
|
-
function slug(s) {
|
|
3
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'unknown';
|
|
4
|
-
}
|
|
5
|
-
function roomIdForTeam(team) {
|
|
6
|
-
return `team:${slug(team)}`;
|
|
7
|
-
}
|
|
8
|
-
function roomIdForMachine(machine) {
|
|
9
|
-
return `machine:${slug(machine)}`;
|
|
10
|
-
}
|
|
11
|
-
function agentDisplayName(session) {
|
|
12
|
-
return session.name || session.label || session.agentId || session.sessionId?.slice(0, 8) || session.kind;
|
|
13
|
-
}
|
|
14
|
-
function sessionMachine(session) {
|
|
15
|
-
return session.machine || session.provenance?.host || session.host || 'local';
|
|
16
|
-
}
|
|
17
|
-
function teamForSession(session, teammatesById) {
|
|
18
|
-
if (session.teamName)
|
|
19
|
-
return session.teamName;
|
|
20
|
-
if (session.agentId && teammatesById.has(session.agentId))
|
|
21
|
-
return teammatesById.get(session.agentId).team;
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
function teammateName(session, teammatesById) {
|
|
25
|
-
if (session.agentId && teammatesById.has(session.agentId)) {
|
|
26
|
-
return teammatesById.get(session.agentId).name || session.agentId.slice(0, 8);
|
|
27
|
-
}
|
|
28
|
-
return session.agentId;
|
|
29
|
-
}
|
|
30
|
-
function moodForSession(session, hasOpenBlock) {
|
|
31
|
-
if (session.status === 'abandoned')
|
|
32
|
-
return 'blocked';
|
|
33
|
-
// A crashed session is NOT `done` — it stopped without finishing, and its last
|
|
34
|
-
// parsed turn often still says `working`, which would otherwise reach the Floor
|
|
35
|
-
// as a happily-running agent. Both lost-host states need a human.
|
|
36
|
-
if (session.status === 'crashed')
|
|
37
|
-
return 'blocked';
|
|
38
|
-
// An orphaned session that is genuinely mid-question needs an answer; one that
|
|
39
|
-
// is merely idle-and-unwatched needs someone to reattach or clean it up. Same
|
|
40
|
-
// distinction `isAwaitingUser` draws — giving both the high-intensity "needs
|
|
41
|
-
// input" alert would train the operator to ignore it.
|
|
42
|
-
if (session.status === 'orphaned') {
|
|
43
|
-
return session.activity === 'waiting_input' ? 'waiting' : 'blocked';
|
|
44
|
-
}
|
|
45
|
-
if (session.status === 'closed')
|
|
46
|
-
return 'done';
|
|
47
|
-
if (session.status === 'input_required' || session.activity === 'waiting_input' || hasOpenBlock)
|
|
48
|
-
return 'waiting';
|
|
49
|
-
if (session.rateLimited)
|
|
50
|
-
return 'blocked';
|
|
51
|
-
if (session.pr)
|
|
52
|
-
return 'celebrating';
|
|
53
|
-
if (session.status === 'running' || session.activity === 'working')
|
|
54
|
-
return 'working';
|
|
55
|
-
return 'idle';
|
|
56
|
-
}
|
|
57
|
-
function action(id, label, command, target, extra = {}) {
|
|
58
|
-
return { id, label, command, target, ...extra };
|
|
59
|
-
}
|
|
60
|
-
function agentActions(agentId, session, mailboxId, team, teammate) {
|
|
61
|
-
const actions = [];
|
|
62
|
-
if (mailboxId) {
|
|
63
|
-
actions.push(action(`agent:${agentId}:message`, 'Message', ['message', mailboxId, '{message}', '--surface', 'hq'], { kind: 'agent', id: agentId }, { input: [{ name: 'message', label: 'Message', placeholder: 'What should this agent do next?', required: true }] }));
|
|
64
|
-
actions.push(action(`agent:${agentId}:stop`, 'Stop', ['feed', '--kill', mailboxId], { kind: 'agent', id: agentId }, { destructive: true }));
|
|
65
|
-
}
|
|
66
|
-
if (team && teammate) {
|
|
67
|
-
actions.push(action(`agent:${agentId}:team-stop`, 'Stop teammate', ['teams', 'stop', team, teammate], { kind: 'agent', id: agentId }, { destructive: true }));
|
|
68
|
-
}
|
|
69
|
-
if (session.sessionId) {
|
|
70
|
-
actions.push(action(`agent:${agentId}:transcript`, 'Transcript', ['sessions', session.sessionId, '--markdown'], { kind: 'agent', id: agentId }));
|
|
71
|
-
}
|
|
72
|
-
return actions;
|
|
73
|
-
}
|
|
74
|
-
function spawnActionForRoom(roomId, team) {
|
|
75
|
-
if (team) {
|
|
76
|
-
return action(`room:${roomId}:spawn-teammate`, 'Spawn teammate', ['teams', 'add', team, '{agent}', '{task}', '--name', '{name}', '--mode', 'edit'], { kind: 'room', id: roomId }, {
|
|
77
|
-
input: [
|
|
78
|
-
{ name: 'agent', label: 'Agent', placeholder: 'claude', required: true },
|
|
79
|
-
{ name: 'name', label: 'Name', placeholder: 'frontend', required: true },
|
|
80
|
-
{ name: 'task', label: 'Task', placeholder: 'Implement the next scoped task', required: true },
|
|
81
|
-
],
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
return action(`room:${roomId}:spawn-run`, 'Run agent', ['run', '{agent}', '{task}', '--name', '{name}'], { kind: 'room', id: roomId }, {
|
|
85
|
-
input: [
|
|
86
|
-
{ name: 'agent', label: 'Agent', placeholder: 'claude', required: true },
|
|
87
|
-
{ name: 'name', label: 'Name', placeholder: 'investigate', required: true },
|
|
88
|
-
{ name: 'task', label: 'Task', placeholder: 'Investigate this room', required: true },
|
|
89
|
-
],
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
export function buildHqFloor(input) {
|
|
93
|
-
const generatedAt = (input.generatedAt ?? new Date()).toISOString();
|
|
94
|
-
const teammatesById = new Map();
|
|
95
|
-
for (const [team, teammates] of input.teammatesByTeam) {
|
|
96
|
-
for (const teammate of teammates)
|
|
97
|
-
teammatesById.set(teammate.agent_id, { ...teammate, team });
|
|
98
|
-
}
|
|
99
|
-
const openBlocksByMailbox = new Map();
|
|
100
|
-
for (const block of input.blocks) {
|
|
101
|
-
const blocks = openBlocksByMailbox.get(block.mailboxId) || [];
|
|
102
|
-
blocks.push(block);
|
|
103
|
-
openBlocksByMailbox.set(block.mailboxId, blocks);
|
|
104
|
-
}
|
|
105
|
-
const rooms = new Map();
|
|
106
|
-
const ensureRoom = (room) => {
|
|
107
|
-
const existing = rooms.get(room.id);
|
|
108
|
-
if (existing)
|
|
109
|
-
return existing;
|
|
110
|
-
const created = {
|
|
111
|
-
...room,
|
|
112
|
-
counts: { agents: 0, needsInput: 0, running: 0, failed: 0 },
|
|
113
|
-
actions: [spawnActionForRoom(room.id, room.team)],
|
|
114
|
-
};
|
|
115
|
-
rooms.set(room.id, created);
|
|
116
|
-
return created;
|
|
117
|
-
};
|
|
118
|
-
for (const team of input.teams) {
|
|
119
|
-
ensureRoom({ id: roomIdForTeam(team.task_name), kind: 'team', name: team.task_name, team: team.task_name });
|
|
120
|
-
}
|
|
121
|
-
const agents = [];
|
|
122
|
-
for (const session of input.sessions) {
|
|
123
|
-
const team = teamForSession(session, teammatesById);
|
|
124
|
-
const machine = sessionMachine(session);
|
|
125
|
-
const room = team
|
|
126
|
-
? ensureRoom({ id: roomIdForTeam(team), kind: 'team', name: team, team })
|
|
127
|
-
: ensureRoom({ id: roomIdForMachine(machine), kind: 'machine', name: machine, machine });
|
|
128
|
-
const mailboxId = mailboxIdForActiveSession(session);
|
|
129
|
-
const blocks = mailboxId ? openBlocksByMailbox.get(mailboxId) || [] : [];
|
|
130
|
-
const id = session.agentId || session.sessionId || `${session.kind}:${agents.length + 1}`;
|
|
131
|
-
const mood = moodForSession(session, blocks.length > 0);
|
|
132
|
-
const teammate = teammateName(session, teammatesById);
|
|
133
|
-
room.counts.agents++;
|
|
134
|
-
if (mood === 'waiting')
|
|
135
|
-
room.counts.needsInput++;
|
|
136
|
-
if (session.status === 'running')
|
|
137
|
-
room.counts.running++;
|
|
138
|
-
if (mood === 'blocked')
|
|
139
|
-
room.counts.failed++;
|
|
140
|
-
agents.push({
|
|
141
|
-
id,
|
|
142
|
-
label: agentDisplayName(session),
|
|
143
|
-
agent: session.kind,
|
|
144
|
-
roomId: room.id,
|
|
145
|
-
mood,
|
|
146
|
-
status: session.status,
|
|
147
|
-
activity: session.activity,
|
|
148
|
-
preview: session.preview || session.question?.text,
|
|
149
|
-
machine,
|
|
150
|
-
team,
|
|
151
|
-
teammate,
|
|
152
|
-
sessionId: session.sessionId,
|
|
153
|
-
mailboxId,
|
|
154
|
-
pid: session.pid,
|
|
155
|
-
prUrl: session.pr?.url,
|
|
156
|
-
ticketId: session.ticket?.id,
|
|
157
|
-
actions: agentActions(id, session, mailboxId, team, teammate),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
const ambientEvents = [];
|
|
161
|
-
for (const agent of agents) {
|
|
162
|
-
if (agent.mood === 'waiting') {
|
|
163
|
-
ambientEvents.push({
|
|
164
|
-
id: `needs-input:${agent.id}`,
|
|
165
|
-
kind: 'needs_input',
|
|
166
|
-
roomId: agent.roomId,
|
|
167
|
-
agentId: agent.id,
|
|
168
|
-
label: `${agent.label} needs input`,
|
|
169
|
-
intensity: 'high',
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
else if (agent.mood === 'blocked') {
|
|
173
|
-
ambientEvents.push({
|
|
174
|
-
id: `blocked:${agent.id}`,
|
|
175
|
-
kind: 'error',
|
|
176
|
-
roomId: agent.roomId,
|
|
177
|
-
agentId: agent.id,
|
|
178
|
-
label: `${agent.label} is blocked`,
|
|
179
|
-
intensity: 'high',
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
else if (agent.mood === 'celebrating' && agent.prUrl) {
|
|
183
|
-
ambientEvents.push({
|
|
184
|
-
id: `pr:${agent.id}`,
|
|
185
|
-
kind: 'pr',
|
|
186
|
-
roomId: agent.roomId,
|
|
187
|
-
agentId: agent.id,
|
|
188
|
-
label: `${agent.label} opened a PR`,
|
|
189
|
-
intensity: 'medium',
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
for (const room of rooms.values()) {
|
|
194
|
-
const roomAgents = agents.filter((agent) => agent.roomId === room.id);
|
|
195
|
-
if (room.kind === 'team' && room.counts.running > 0) {
|
|
196
|
-
ambientEvents.push({
|
|
197
|
-
id: `team-active:${room.id}`,
|
|
198
|
-
kind: 'team_active',
|
|
199
|
-
roomId: room.id,
|
|
200
|
-
label: `${room.name} has ${room.counts.running} active teammate${room.counts.running === 1 ? '' : 's'}`,
|
|
201
|
-
intensity: 'low',
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
if (roomAgents.length > 0 && roomAgents.every((agent) => agent.mood === 'idle')) {
|
|
205
|
-
ambientEvents.push({
|
|
206
|
-
id: `idle:${room.id}`,
|
|
207
|
-
kind: 'idle_scene',
|
|
208
|
-
roomId: room.id,
|
|
209
|
-
label: `${room.name} is calm`,
|
|
210
|
-
intensity: 'low',
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const floorActions = [
|
|
215
|
-
action('floor:create-team', 'Create team room', ['teams', 'create', '{team}', '--description', '{description}'], { kind: 'floor', id: 'floor' }, {
|
|
216
|
-
input: [
|
|
217
|
-
{ name: 'team', label: 'Team', placeholder: 'release-squad', required: true },
|
|
218
|
-
{ name: 'description', label: 'Description', placeholder: 'What this room owns' },
|
|
219
|
-
],
|
|
220
|
-
}),
|
|
221
|
-
];
|
|
222
|
-
const roomList = [...rooms.values()].sort((a, b) => {
|
|
223
|
-
if (a.kind !== b.kind)
|
|
224
|
-
return a.kind === 'team' ? -1 : 1;
|
|
225
|
-
return a.name.localeCompare(b.name);
|
|
226
|
-
});
|
|
227
|
-
return {
|
|
228
|
-
version: 1,
|
|
229
|
-
generatedAt,
|
|
230
|
-
counters: {
|
|
231
|
-
rooms: roomList.length,
|
|
232
|
-
agents: agents.length,
|
|
233
|
-
teams: input.teams.length,
|
|
234
|
-
needsInput: agents.filter((agent) => agent.mood === 'waiting').length,
|
|
235
|
-
failed: agents.filter((agent) => agent.mood === 'blocked').length,
|
|
236
|
-
prs: agents.filter((agent) => agent.prUrl).length,
|
|
237
|
-
},
|
|
238
|
-
rooms: roomList,
|
|
239
|
-
agents,
|
|
240
|
-
ambientEvents,
|
|
241
|
-
actions: floorActions,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CRDT merge for agent transcripts.
|
|
3
|
-
*
|
|
4
|
-
* A transcript (Claude JSONL, Codex JSONL, …) is an append-only log of
|
|
5
|
-
* immutable events: each line is written once and never rewritten, and Claude
|
|
6
|
-
* already tolerates branches within one file (parentUuid fan-out). That makes a
|
|
7
|
-
* transcript a grow-only set (G-Set) of events, and merging two copies of the
|
|
8
|
-
* same session is a set union — associative, commutative, idempotent. Two
|
|
9
|
-
* machines that each appended to the same session therefore converge to the
|
|
10
|
-
* exact same merged file regardless of sync order or timing, with zero conflict
|
|
11
|
-
* resolution and zero data loss.
|
|
12
|
-
*
|
|
13
|
-
* Events are identified by the SHA-256 of their raw line bytes. We deliberately
|
|
14
|
-
* do NOT key on a per-event `uuid`: Codex lines carry no id, and because an
|
|
15
|
-
* event is written exactly once and then copied verbatim across machines, the
|
|
16
|
-
* raw bytes are a stable, agent-agnostic identity. Multiplicity is preserved
|
|
17
|
-
* (some transcripts contain legitimately identical lines, e.g. paired
|
|
18
|
-
* `queue-operation` entries) by taking the per-hash max count across sources.
|
|
19
|
-
*/
|
|
20
|
-
export interface ParsedEvent {
|
|
21
|
-
/** Original line bytes, exactly as stored (no trailing newline). */
|
|
22
|
-
raw: string;
|
|
23
|
-
/** SHA-256 of `raw` — the event's identity. */
|
|
24
|
-
hash: string;
|
|
25
|
-
/** Top-level ISO `timestamp`, or '' when absent/unparseable. */
|
|
26
|
-
ts: string;
|
|
27
|
-
}
|
|
28
|
-
/** Parse a transcript's raw text into events, skipping blank lines. */
|
|
29
|
-
export declare function parseTranscript(content: string): ParsedEvent[];
|
|
30
|
-
/**
|
|
31
|
-
* Merge copies of the same session into one transcript via G-Set union.
|
|
32
|
-
*
|
|
33
|
-
* Returns a source VERBATIM (no reordering, byte-identical) for the common
|
|
34
|
-
* cases — one source, all sources identical, or one source a superset of the
|
|
35
|
-
* rest — so the steady state never rewrites unchanged files. Only a true fork
|
|
36
|
-
* (each side holds events the other lacks) produces a reordered union, sorted
|
|
37
|
-
* by (timestamp, hash) so every machine derives identical bytes.
|
|
38
|
-
*/
|
|
39
|
-
export declare function mergeTranscripts(contents: string[]): string;
|
|
40
|
-
/** Count distinct + total events across copies (for logging / manifest stats). */
|
|
41
|
-
export declare function transcriptStats(content: string): {
|
|
42
|
-
events: number;
|
|
43
|
-
lastTs: string;
|
|
44
|
-
};
|