@phnx-labs/agents-cli 1.20.92 → 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 +385 -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/events.js +91 -1
- 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 +38 -0
- package/dist/commands/projects.js +355 -48
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- 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 +13 -12
- package/dist/commands/view.js +4 -0
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +38 -6
- package/dist/lib/activity.d.ts +15 -0
- package/dist/lib/activity.js +26 -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/channels/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- 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-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +17 -5
- package/dist/lib/events.js +59 -56
- 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 +129 -0
- package/dist/lib/linear-project-counts.js +206 -0
- package/dist/lib/linear-projects.d.ts +62 -0
- package/dist/lib/linear-projects.js +144 -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/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- 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/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-status.d.ts +32 -1
- package/dist/lib/project-status.js +82 -1
- package/dist/lib/projects.d.ts +6 -0
- package/dist/lib/projects.js +12 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- 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/run-notify.js +3 -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/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- 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-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -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/sqlite.js +28 -1
- 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 +19 -2
- package/dist/lib/state.js +99 -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 +22 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- 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/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
|
-
};
|
|
@@ -1,119 +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
|
-
import * as crypto from 'crypto';
|
|
21
|
-
/** Extract the top-level `timestamp` field both Claude and Codex stamp per line. */
|
|
22
|
-
function lineTimestamp(raw) {
|
|
23
|
-
try {
|
|
24
|
-
const obj = JSON.parse(raw);
|
|
25
|
-
const ts = obj?.timestamp;
|
|
26
|
-
return typeof ts === 'string' ? ts : '';
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/** Parse a transcript's raw text into events, skipping blank lines. */
|
|
33
|
-
export function parseTranscript(content) {
|
|
34
|
-
const out = [];
|
|
35
|
-
for (const line of content.split('\n')) {
|
|
36
|
-
if (line.trim() === '')
|
|
37
|
-
continue;
|
|
38
|
-
out.push({
|
|
39
|
-
raw: line,
|
|
40
|
-
hash: crypto.createHash('sha256').update(line).digest('hex'),
|
|
41
|
-
ts: lineTimestamp(line),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
/** Order events deterministically across machines: by timestamp, then hash. */
|
|
47
|
-
function compareEvents(a, b) {
|
|
48
|
-
if (a.ts !== b.ts)
|
|
49
|
-
return a.ts < b.ts ? -1 : 1;
|
|
50
|
-
if (a.hash !== b.hash)
|
|
51
|
-
return a.hash < b.hash ? -1 : 1;
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Merge copies of the same session into one transcript via G-Set union.
|
|
56
|
-
*
|
|
57
|
-
* Returns a source VERBATIM (no reordering, byte-identical) for the common
|
|
58
|
-
* cases — one source, all sources identical, or one source a superset of the
|
|
59
|
-
* rest — so the steady state never rewrites unchanged files. Only a true fork
|
|
60
|
-
* (each side holds events the other lacks) produces a reordered union, sorted
|
|
61
|
-
* by (timestamp, hash) so every machine derives identical bytes.
|
|
62
|
-
*/
|
|
63
|
-
export function mergeTranscripts(contents) {
|
|
64
|
-
const sources = contents.filter(c => c.length > 0);
|
|
65
|
-
if (sources.length === 0)
|
|
66
|
-
return '';
|
|
67
|
-
if (sources.length === 1)
|
|
68
|
-
return sources[0];
|
|
69
|
-
const parsed = sources.map(parseTranscript);
|
|
70
|
-
// Per-source multiset of event hashes.
|
|
71
|
-
const counts = parsed.map(events => {
|
|
72
|
-
const m = new Map();
|
|
73
|
-
for (const e of events)
|
|
74
|
-
m.set(e.hash, (m.get(e.hash) ?? 0) + 1);
|
|
75
|
-
return m;
|
|
76
|
-
});
|
|
77
|
-
// Global max count per hash + a representative event for raw bytes / ts.
|
|
78
|
-
const maxCount = new Map();
|
|
79
|
-
const rep = new Map();
|
|
80
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
81
|
-
for (const e of parsed[i])
|
|
82
|
-
if (!rep.has(e.hash))
|
|
83
|
-
rep.set(e.hash, e);
|
|
84
|
-
for (const [h, c] of counts[i])
|
|
85
|
-
maxCount.set(h, Math.max(maxCount.get(h) ?? 0, c));
|
|
86
|
-
}
|
|
87
|
-
// Superset fast path: a source whose multiset already equals the global max
|
|
88
|
-
// is the union — return it byte-for-byte (covers identical/subset/prefix).
|
|
89
|
-
for (let i = 0; i < sources.length; i++) {
|
|
90
|
-
let isSuperset = true;
|
|
91
|
-
for (const [h, c] of maxCount) {
|
|
92
|
-
if ((counts[i].get(h) ?? 0) !== c) {
|
|
93
|
-
isSuperset = false;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (isSuperset)
|
|
98
|
-
return sources[i];
|
|
99
|
-
}
|
|
100
|
-
// True fork: emit each distinct event maxCount times, deterministically ordered.
|
|
101
|
-
const distinct = [...rep.values()].sort(compareEvents);
|
|
102
|
-
const lines = [];
|
|
103
|
-
for (const e of distinct) {
|
|
104
|
-
const n = maxCount.get(e.hash) ?? 1;
|
|
105
|
-
for (let k = 0; k < n; k++)
|
|
106
|
-
lines.push(e.raw);
|
|
107
|
-
}
|
|
108
|
-
const trailing = sources.some(s => s.endsWith('\n')) ? '\n' : '';
|
|
109
|
-
return lines.join('\n') + trailing;
|
|
110
|
-
}
|
|
111
|
-
/** Count distinct + total events across copies (for logging / manifest stats). */
|
|
112
|
-
export function transcriptStats(content) {
|
|
113
|
-
const parsed = parseTranscript(content);
|
|
114
|
-
let lastTs = '';
|
|
115
|
-
for (const e of parsed)
|
|
116
|
-
if (e.ts > lastTs)
|
|
117
|
-
lastTs = e.ts;
|
|
118
|
-
return { events: parsed.length, lastTs };
|
|
119
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manifests and the local upload ledger.
|
|
3
|
-
*
|
|
4
|
-
* Each machine publishes one manifest object (sessions/<machine>/manifest.json)
|
|
5
|
-
* listing every session it holds, keyed by session id, with content hash + size
|
|
6
|
-
* + last event timestamp. Pulling machines diff manifests to fetch only changed
|
|
7
|
-
* objects — no per-tick LIST of thousands of keys.
|
|
8
|
-
*
|
|
9
|
-
* The local upload ledger (cache, per machine) records (size, mtime, hash) of
|
|
10
|
-
* files already pushed, so a tick re-uploads only files that actually grew.
|
|
11
|
-
*/
|
|
12
|
-
export interface ManifestEntry {
|
|
13
|
-
/** Storage-relative key, preserved into the mirror layout on the puller. */
|
|
14
|
-
relKey: string;
|
|
15
|
-
size: number;
|
|
16
|
-
/** SHA-256 of the full transcript file. */
|
|
17
|
-
hash: string;
|
|
18
|
-
/** Latest event timestamp in the transcript. */
|
|
19
|
-
lastTs: string;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* sessionId -> the session's file entries.
|
|
23
|
-
* - file-shaped agents (claude/codex/droid) store a single `ManifestEntry`,
|
|
24
|
-
* byte-identical to the pre-multi-file format, so a machine on an older CLI
|
|
25
|
-
* reads them unchanged.
|
|
26
|
-
* - dir-shaped agents (kimi) store a `ManifestEntry[]`, one per constituent file.
|
|
27
|
-
* A machine that predates multi-file support skips these (an unknown agent is
|
|
28
|
-
* skipped whole; a kimi-aware-but-file-shaped reader fails to find the flat
|
|
29
|
-
* object key and simply retries — it never crashes or corrupts).
|
|
30
|
-
*/
|
|
31
|
-
export type AgentManifest = Record<string, ManifestEntry | ManifestEntry[]>;
|
|
32
|
-
/** Normalize a manifest value to its file-entry list (single entry -> length-1). */
|
|
33
|
-
export declare function manifestEntries(value: ManifestEntry | ManifestEntry[]): ManifestEntry[];
|
|
34
|
-
export interface Manifest {
|
|
35
|
-
machine: string;
|
|
36
|
-
updatedAt: string;
|
|
37
|
-
/** agentId -> (sessionId -> entry | entry[]) */
|
|
38
|
-
agents: Record<string, AgentManifest>;
|
|
39
|
-
}
|
|
40
|
-
export declare function emptyManifest(machine: string, updatedAt: string): Manifest;
|
|
41
|
-
export declare function parseManifest(text: string): Manifest | null;
|
|
42
|
-
export declare function hashContent(content: string | Uint8Array): string;
|
|
43
|
-
export declare function loadLocalManifest(): Manifest | null;
|
|
44
|
-
export declare function saveLocalManifest(m: Manifest): void;
|
|
45
|
-
/** key `${agent}/${sessionId}` -> signature of applied source hashes */
|
|
46
|
-
export type PullState = Record<string, string>;
|
|
47
|
-
export declare function loadPullState(): PullState;
|
|
48
|
-
export declare function savePullState(s: PullState): void;
|
|
49
|
-
/** Order-independent signature of the source hashes feeding one mirrored session. */
|
|
50
|
-
export declare function sourceSignature(hashes: string[]): string;
|
|
51
|
-
interface LedgerRow {
|
|
52
|
-
size: number;
|
|
53
|
-
mtimeMs: number;
|
|
54
|
-
hash: string;
|
|
55
|
-
}
|
|
56
|
-
type Ledger = Record<string, LedgerRow>;
|
|
57
|
-
export declare function loadLedger(): Ledger;
|
|
58
|
-
export declare function saveLedger(ledger: Ledger): void;
|
|
59
|
-
/** True if the file at absPath is unchanged since we last uploaded it. */
|
|
60
|
-
export declare function ledgerUnchanged(ledger: Ledger, absPath: string, size: number, mtimeMs: number): boolean;
|
|
61
|
-
export declare function ledgerRecord(ledger: Ledger, absPath: string, size: number, mtimeMs: number, hash: string): void;
|
|
62
|
-
export {};
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manifests and the local upload ledger.
|
|
3
|
-
*
|
|
4
|
-
* Each machine publishes one manifest object (sessions/<machine>/manifest.json)
|
|
5
|
-
* listing every session it holds, keyed by session id, with content hash + size
|
|
6
|
-
* + last event timestamp. Pulling machines diff manifests to fetch only changed
|
|
7
|
-
* objects — no per-tick LIST of thousands of keys.
|
|
8
|
-
*
|
|
9
|
-
* The local upload ledger (cache, per machine) records (size, mtime, hash) of
|
|
10
|
-
* files already pushed, so a tick re-uploads only files that actually grew.
|
|
11
|
-
*/
|
|
12
|
-
import * as fs from 'fs';
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import * as crypto from 'crypto';
|
|
15
|
-
import { getCacheDir } from '../../state.js';
|
|
16
|
-
/** Normalize a manifest value to its file-entry list (single entry -> length-1). */
|
|
17
|
-
export function manifestEntries(value) {
|
|
18
|
-
return Array.isArray(value) ? value : [value];
|
|
19
|
-
}
|
|
20
|
-
export function emptyManifest(machine, updatedAt) {
|
|
21
|
-
return { machine, updatedAt, agents: {} };
|
|
22
|
-
}
|
|
23
|
-
export function parseManifest(text) {
|
|
24
|
-
try {
|
|
25
|
-
const m = JSON.parse(text);
|
|
26
|
-
if (m && typeof m.machine === 'string' && m.agents && typeof m.agents === 'object')
|
|
27
|
-
return m;
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
export function hashContent(content) {
|
|
35
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
36
|
-
}
|
|
37
|
-
function stateDir() {
|
|
38
|
-
return path.join(getCacheDir(), 'state', 'sessions-sync');
|
|
39
|
-
}
|
|
40
|
-
// --- local cached copy of our published manifest ---------------------------
|
|
41
|
-
function localManifestPath() {
|
|
42
|
-
return path.join(stateDir(), 'manifest.json');
|
|
43
|
-
}
|
|
44
|
-
export function loadLocalManifest() {
|
|
45
|
-
try {
|
|
46
|
-
return parseManifest(fs.readFileSync(localManifestPath(), 'utf-8'));
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export function saveLocalManifest(m) {
|
|
53
|
-
const p = localManifestPath();
|
|
54
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
55
|
-
fs.writeFileSync(p, JSON.stringify(m), 'utf-8');
|
|
56
|
-
}
|
|
57
|
-
function pullStatePath() {
|
|
58
|
-
return path.join(stateDir(), 'pull-state.json');
|
|
59
|
-
}
|
|
60
|
-
export function loadPullState() {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(fs.readFileSync(pullStatePath(), 'utf-8'));
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return {};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
export function savePullState(s) {
|
|
69
|
-
const p = pullStatePath();
|
|
70
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
71
|
-
fs.writeFileSync(p, JSON.stringify(s), 'utf-8');
|
|
72
|
-
}
|
|
73
|
-
/** Order-independent signature of the source hashes feeding one mirrored session. */
|
|
74
|
-
export function sourceSignature(hashes) {
|
|
75
|
-
return [...hashes].sort().join(',');
|
|
76
|
-
}
|
|
77
|
-
function ledgerPath() {
|
|
78
|
-
return path.join(getCacheDir(), 'state', 'sessions-sync', 'upload-ledger.json');
|
|
79
|
-
}
|
|
80
|
-
export function loadLedger() {
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(fs.readFileSync(ledgerPath(), 'utf-8'));
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return {};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
export function saveLedger(ledger) {
|
|
89
|
-
const p = ledgerPath();
|
|
90
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
91
|
-
fs.writeFileSync(p, JSON.stringify(ledger), 'utf-8');
|
|
92
|
-
}
|
|
93
|
-
/** True if the file at absPath is unchanged since we last uploaded it. */
|
|
94
|
-
export function ledgerUnchanged(ledger, absPath, size, mtimeMs) {
|
|
95
|
-
const row = ledger[absPath];
|
|
96
|
-
return !!row && row.size === size && row.mtimeMs === Math.floor(mtimeMs);
|
|
97
|
-
}
|
|
98
|
-
export function ledgerRecord(ledger, absPath, size, mtimeMs, hash) {
|
|
99
|
-
ledger[absPath] = { size, mtimeMs: Math.floor(mtimeMs), hash };
|
|
100
|
-
}
|