@zgeoff/atc 0.1.9 → 0.1.10
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/README.md +101 -57
- package/package.json +1 -1
- package/src/agent-adapter.ts +44 -0
- package/src/boot-daemon.ts +7 -1
- package/src/build-cli-command.ts +16 -0
- package/src/claude-adapter.ts +26 -18
- package/src/cli.ts +21 -2
- package/src/collect-clean-env.ts +30 -5
- package/src/config.ts +11 -1
- package/src/daemon-connection.ts +23 -1
- package/src/daemon.ts +58 -57
- package/src/format-overlay-agent-mark.ts +9 -0
- package/src/grok-adapter.ts +274 -0
- package/src/hook-report.ts +8 -4
- package/src/index.ts +79 -15
- package/src/mcp-server.ts +13 -1
- package/src/normalize-hook-event.ts +11 -0
- package/src/pick-tab-target.ts +1 -1
- package/src/print-grok-hook-file.ts +27 -0
- package/src/protocol.ts +1 -1
- package/src/sessions.ts +133 -41
- package/src/state-store.ts +62 -18
- package/src/ui.ts +22 -10
package/src/state-store.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { toAgentKind } from './agent-adapter';
|
|
4
|
+
import type { AgentKind } from './agent-adapter';
|
|
3
5
|
import type { HookEvent } from './hooks';
|
|
4
|
-
import {
|
|
6
|
+
import { parseFleetEntry } from './sessions';
|
|
5
7
|
import type { FleetEntry } from './sessions';
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -21,11 +23,12 @@ export class StateStore {
|
|
|
21
23
|
|
|
22
24
|
this.db.run(`
|
|
23
25
|
CREATE TABLE IF NOT EXISTS fleet (
|
|
24
|
-
|
|
26
|
+
agent_session_id TEXT PRIMARY KEY,
|
|
25
27
|
name TEXT NOT NULL,
|
|
26
28
|
cwd TEXT NOT NULL,
|
|
27
29
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
28
|
-
last_attached INTEGER
|
|
30
|
+
last_attached INTEGER,
|
|
31
|
+
agent TEXT NOT NULL DEFAULT 'claude'
|
|
29
32
|
);
|
|
30
33
|
CREATE TABLE IF NOT EXISTS events (
|
|
31
34
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -39,10 +42,14 @@ export class StateStore {
|
|
|
39
42
|
cwd TEXT PRIMARY KEY,
|
|
40
43
|
last_spawn INTEGER NOT NULL
|
|
41
44
|
);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS prefs (
|
|
46
|
+
key TEXT PRIMARY KEY,
|
|
47
|
+
value TEXT NOT NULL
|
|
48
|
+
);
|
|
42
49
|
`);
|
|
43
50
|
|
|
44
|
-
// Stores created before pinning
|
|
45
|
-
// columns; the create above only applies to fresh databases.
|
|
51
|
+
// Stores created before pinning, attach recency, or agent kind existed
|
|
52
|
+
// lack the columns; the create above only applies to fresh databases.
|
|
46
53
|
const fleetColumns = new Set(
|
|
47
54
|
this.db
|
|
48
55
|
.query<{ name: string }, []>('PRAGMA table_info(fleet)')
|
|
@@ -50,6 +57,12 @@ export class StateStore {
|
|
|
50
57
|
.map((c) => c.name),
|
|
51
58
|
);
|
|
52
59
|
|
|
60
|
+
// Stores created before the id column was agent-neutral carry it under
|
|
61
|
+
// its Claude-era name.
|
|
62
|
+
if (fleetColumns.has('claude_id')) {
|
|
63
|
+
this.db.run('ALTER TABLE fleet RENAME COLUMN claude_id TO agent_session_id');
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
if (!fleetColumns.has('pinned')) {
|
|
54
67
|
this.db.run('ALTER TABLE fleet ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0');
|
|
55
68
|
}
|
|
@@ -58,6 +71,10 @@ export class StateStore {
|
|
|
58
71
|
this.db.run('ALTER TABLE fleet ADD COLUMN last_attached INTEGER');
|
|
59
72
|
}
|
|
60
73
|
|
|
74
|
+
if (!fleetColumns.has('agent')) {
|
|
75
|
+
this.db.run("ALTER TABLE fleet ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude'");
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
if (legacyFleetPath !== undefined) {
|
|
62
79
|
this.adoptLegacyFleet(legacyFleetPath);
|
|
63
80
|
}
|
|
@@ -67,23 +84,25 @@ export class StateStore {
|
|
|
67
84
|
const rows = this.db
|
|
68
85
|
.query<
|
|
69
86
|
{
|
|
70
|
-
|
|
87
|
+
agent_session_id: string;
|
|
71
88
|
name: string;
|
|
72
89
|
cwd: string;
|
|
73
90
|
pinned: number;
|
|
74
91
|
last_attached: number | null;
|
|
92
|
+
agent: string | null;
|
|
75
93
|
},
|
|
76
94
|
[]
|
|
77
|
-
>('SELECT
|
|
95
|
+
>('SELECT agent_session_id, name, cwd, pinned, last_attached, agent FROM fleet')
|
|
78
96
|
.all();
|
|
79
97
|
|
|
80
98
|
const entries: FleetEntry[] = [];
|
|
81
99
|
|
|
82
100
|
for (const row of rows) {
|
|
83
101
|
entries.push({
|
|
84
|
-
|
|
102
|
+
agentSessionID: row.agent_session_id,
|
|
85
103
|
name: row.name,
|
|
86
104
|
cwd: row.cwd,
|
|
105
|
+
agent: toAgentKind(row.agent),
|
|
87
106
|
...(row.pinned === 0 ? {} : { pinned: true }),
|
|
88
107
|
...(row.last_attached === null ? {} : { lastAttachedAt: row.last_attached }),
|
|
89
108
|
});
|
|
@@ -109,13 +128,20 @@ export class StateStore {
|
|
|
109
128
|
this.db.run('DELETE FROM fleet');
|
|
110
129
|
|
|
111
130
|
const insert = this.db.query(
|
|
112
|
-
'INSERT OR REPLACE INTO fleet (
|
|
131
|
+
'INSERT OR REPLACE INTO fleet (agent_session_id, name, cwd, pinned, last_attached, agent) VALUES (?1, ?2, ?3, ?4, ?5, ?6)',
|
|
113
132
|
);
|
|
114
133
|
|
|
115
134
|
for (const entry of all) {
|
|
116
135
|
const pinned = entry.pinned === true ? 1 : 0;
|
|
117
136
|
|
|
118
|
-
insert.run(
|
|
137
|
+
insert.run(
|
|
138
|
+
entry.agentSessionID,
|
|
139
|
+
entry.name,
|
|
140
|
+
entry.cwd,
|
|
141
|
+
pinned,
|
|
142
|
+
entry.lastAttachedAt ?? null,
|
|
143
|
+
entry.agent,
|
|
144
|
+
);
|
|
119
145
|
}
|
|
120
146
|
});
|
|
121
147
|
|
|
@@ -124,7 +150,7 @@ export class StateStore {
|
|
|
124
150
|
|
|
125
151
|
recordEvent(e: HookEvent): void {
|
|
126
152
|
const rawMessage = e.payload['message'];
|
|
127
|
-
const rawSessionID = e.payload['session_id'];
|
|
153
|
+
const rawSessionID = e.payload['session_id'] ?? e.payload['sessionId'];
|
|
128
154
|
const message = typeof rawMessage === 'string' ? rawMessage : null;
|
|
129
155
|
const sessionID = typeof rawSessionID === 'string' ? rawSessionID : null;
|
|
130
156
|
|
|
@@ -151,6 +177,22 @@ export class StateStore {
|
|
|
151
177
|
return rows.map((row) => row.cwd);
|
|
152
178
|
}
|
|
153
179
|
|
|
180
|
+
loadLastUsedAgent(): AgentKind {
|
|
181
|
+
const row = this.db
|
|
182
|
+
.query<{ value: string }, []>("SELECT value FROM prefs WHERE key = 'last_used_agent'")
|
|
183
|
+
.get();
|
|
184
|
+
|
|
185
|
+
return toAgentKind(row?.value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
writeLastUsedAgent(agent: AgentKind): void {
|
|
189
|
+
this.db
|
|
190
|
+
.query(
|
|
191
|
+
'INSERT INTO prefs (key, value) VALUES (?1, ?2) ON CONFLICT(key) DO UPDATE SET value = ?2',
|
|
192
|
+
)
|
|
193
|
+
.run('last_used_agent', agent);
|
|
194
|
+
}
|
|
195
|
+
|
|
154
196
|
stop(): void {
|
|
155
197
|
this.db.close();
|
|
156
198
|
}
|
|
@@ -169,13 +211,15 @@ export class StateStore {
|
|
|
169
211
|
return;
|
|
170
212
|
}
|
|
171
213
|
|
|
172
|
-
const entries =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
214
|
+
const entries: FleetEntry[] = [];
|
|
215
|
+
|
|
216
|
+
for (const entry of parsed) {
|
|
217
|
+
const parsedEntry = parseFleetEntry(entry);
|
|
218
|
+
|
|
219
|
+
if (parsedEntry !== undefined) {
|
|
220
|
+
entries.push(parsedEntry);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
179
223
|
|
|
180
224
|
this.writeFleet(entries);
|
|
181
225
|
} catch {}
|
package/src/ui.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { AgentKind } from './agent-adapter';
|
|
1
2
|
import { formatDir } from './dirs';
|
|
3
|
+
import { formatOverlayAgentMark } from './format-overlay-agent-mark';
|
|
2
4
|
import { RESET_INPUT_MODES } from './reset-input-modes';
|
|
3
5
|
import { PINNED_GROUP_KEY } from './sessions';
|
|
4
6
|
import type { SessionState } from './sessions';
|
|
@@ -159,8 +161,9 @@ function dimRow(width: number, text: string): Row {
|
|
|
159
161
|
// Selection-dependent hints need the liveness facts of each row.
|
|
160
162
|
export interface OverlaySessionView extends SessionView {
|
|
161
163
|
readonly alive: boolean;
|
|
162
|
-
readonly kind: 'pty' | '
|
|
164
|
+
readonly kind: 'pty' | 'headless';
|
|
163
165
|
readonly resumable: boolean;
|
|
166
|
+
readonly agent: AgentKind;
|
|
164
167
|
readonly pinned: boolean;
|
|
165
168
|
readonly repoRoot: string;
|
|
166
169
|
}
|
|
@@ -202,14 +205,18 @@ export function drawOverlay(view: OverlayView) {
|
|
|
202
205
|
const name = truncate(s.name, 16).padEnd(16);
|
|
203
206
|
const state = STATE_LABEL[s.state].padEnd(9);
|
|
204
207
|
const dir = view.grouped ? '' : ` ${truncate(formatDir(s.cwd), 18).padEnd(18)}`;
|
|
205
|
-
const msgWidth = Math.max(4, width - 4 -
|
|
208
|
+
const msgWidth = Math.max(4, width - 4 - 4 - 17 - 10 - (view.grouped ? 0 : 19));
|
|
206
209
|
const msg = truncate(s.lastMsg, msgWidth).padEnd(msgWidth);
|
|
207
210
|
const unread = s.unread ? `${ESC}[1;33m!${ESC}[0m` : ' ';
|
|
208
211
|
const pin = s.pinned ? `${ESC}[93m⋆${ESC}[0m` : ' ';
|
|
212
|
+
const mark = formatOverlayAgentMark(s.agent);
|
|
213
|
+
const styledMark = mark === 'g' ? `${ESC}[90m${mark}${ESC}[0m` : mark;
|
|
209
214
|
const body = `${name} ${state}${dir} ${msg}`;
|
|
210
215
|
const styledBody = sel ? `${ESC}[7m${body}${ESC}[0m` : body;
|
|
211
216
|
|
|
212
|
-
rowsList.push(
|
|
217
|
+
rowsList.push(
|
|
218
|
+
boxRow(width, `${GLYPH[s.state]}${unread}${pin}${styledMark}${styledBody}`, 4 + body.length),
|
|
219
|
+
);
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
rowsList.push(boxDivider(width));
|
|
@@ -245,15 +252,15 @@ export function drawOverlay(view: OverlayView) {
|
|
|
245
252
|
const GLOBAL_HINT = 'g groups · n new · ? keys';
|
|
246
253
|
|
|
247
254
|
// Only the actions valid for the selected row appear; the full reference
|
|
248
|
-
// lives behind ?.
|
|
249
|
-
function buildOverlayHint(s: OverlaySessionView | undefined): string {
|
|
255
|
+
// lives behind ?. Grok has no headless handoff, so H is omitted there.
|
|
256
|
+
export function buildOverlayHint(s: OverlaySessionView | undefined): string {
|
|
250
257
|
if (s === undefined) {
|
|
251
258
|
return GLOBAL_HINT;
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
const actions: string[] = [];
|
|
255
262
|
|
|
256
|
-
if (s.kind === '
|
|
263
|
+
if (s.kind === 'headless' && s.alive) {
|
|
257
264
|
actions.push('P reattach', 'K kill');
|
|
258
265
|
} else if (s.alive) {
|
|
259
266
|
actions.push('⏎ attach');
|
|
@@ -262,7 +269,11 @@ function buildOverlayHint(s: OverlaySessionView | undefined): string {
|
|
|
262
269
|
actions.push('a ack');
|
|
263
270
|
}
|
|
264
271
|
|
|
265
|
-
|
|
272
|
+
if (s.agent !== 'grok') {
|
|
273
|
+
actions.push('H headless');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
actions.push('y yank', 'Y eject', 'K kill');
|
|
266
277
|
} else {
|
|
267
278
|
if (s.resumable) {
|
|
268
279
|
actions.push('P revive', 'y yank');
|
|
@@ -285,13 +296,14 @@ export function drawHelp() {
|
|
|
285
296
|
'⏎ attach the selected session',
|
|
286
297
|
'⇥ attach the most urgent needs-you, else latest done',
|
|
287
298
|
'a ack its notification without attaching',
|
|
288
|
-
'H eject to a headless run',
|
|
299
|
+
'H eject to a headless run (Claude only)',
|
|
289
300
|
'P revive a dead or headless session',
|
|
290
301
|
'y yank the resume command to the clipboard',
|
|
291
302
|
'Y yank the resume command, then kill here',
|
|
292
303
|
'K kill (K again on a dead session forgets it)',
|
|
293
304
|
'p pin or unpin — pinned sessions stay on top',
|
|
294
305
|
'g toggle grouping by repository',
|
|
306
|
+
' Grok rows show a dim g after the pin mark',
|
|
295
307
|
'n new session',
|
|
296
308
|
'r adopt an external session',
|
|
297
309
|
'/ filter · ↑↓/jk move · q quit',
|
|
@@ -359,10 +371,10 @@ export function drawHome(fleetCount = 0, leaderLabel = '^Space') {
|
|
|
359
371
|
out(ansi.clear + ansi.hideCursor);
|
|
360
372
|
|
|
361
373
|
const msgs = [
|
|
362
|
-
'atc — control tower for
|
|
374
|
+
'atc — control tower for Claude and Grok sessions',
|
|
363
375
|
'',
|
|
364
376
|
'n spawn a session',
|
|
365
|
-
'r adopt an existing session
|
|
377
|
+
'r adopt an existing session',
|
|
366
378
|
...(fleetCount > 0 ? [`R restore last fleet (${fleetCount} sessions)`] : []),
|
|
367
379
|
`${leaderLabel.padEnd(7)} session list`,
|
|
368
380
|
'q quit',
|