@hydra-acp/cli 0.1.22 → 0.1.24
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 +49 -47
- package/dist/cli.js +2622 -433
- package/dist/index.d.ts +171 -26
- package/dist/index.js +1605 -206
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -77,6 +77,12 @@ var init_paths = __esm({
|
|
|
77
77
|
sessionDir: (id) => path.join(hydraHome(), "sessions", id),
|
|
78
78
|
sessionFile: (id) => path.join(hydraHome(), "sessions", id, "meta.json"),
|
|
79
79
|
historyFile: (id) => path.join(hydraHome(), "sessions", id, "history.jsonl"),
|
|
80
|
+
// Persisted prompt queue for a session. ndjson, one record per
|
|
81
|
+
// entry. Survives daemon restarts so queued prompts get a chance to
|
|
82
|
+
// run rather than being silently lost. Entries are removed BEFORE
|
|
83
|
+
// the agent invocation (see Session.drainQueue) so a crash mid-
|
|
84
|
+
// generation doesn't double-run on restart.
|
|
85
|
+
queueFile: (id) => path.join(hydraHome(), "sessions", id, "queue.ndjson"),
|
|
80
86
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
81
87
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
82
88
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
@@ -86,8 +92,68 @@ var init_paths = __esm({
|
|
|
86
92
|
}
|
|
87
93
|
});
|
|
88
94
|
|
|
89
|
-
// src/core/
|
|
95
|
+
// src/core/service-token.ts
|
|
90
96
|
import * as fs from "fs/promises";
|
|
97
|
+
function generateServiceToken() {
|
|
98
|
+
const bytes = new Uint8Array(32);
|
|
99
|
+
crypto.getRandomValues(bytes);
|
|
100
|
+
let hex = "";
|
|
101
|
+
for (const b of bytes) {
|
|
102
|
+
hex += b.toString(16).padStart(2, "0");
|
|
103
|
+
}
|
|
104
|
+
return `hydra_token_${hex}`;
|
|
105
|
+
}
|
|
106
|
+
async function readServiceToken() {
|
|
107
|
+
try {
|
|
108
|
+
const text = await fs.readFile(paths.authToken(), "utf8");
|
|
109
|
+
const trimmed = text.trim();
|
|
110
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const e = err;
|
|
113
|
+
if (e.code === "ENOENT") {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function loadServiceToken() {
|
|
120
|
+
const token = await readServiceToken();
|
|
121
|
+
if (!token) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`No service token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return token;
|
|
127
|
+
}
|
|
128
|
+
async function writeServiceToken(token) {
|
|
129
|
+
await fs.mkdir(paths.home(), { recursive: true });
|
|
130
|
+
await fs.writeFile(paths.authToken(), token + "\n", {
|
|
131
|
+
encoding: "utf8",
|
|
132
|
+
mode: 384
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function ensureServiceToken() {
|
|
136
|
+
const existing = await readServiceToken();
|
|
137
|
+
if (existing) {
|
|
138
|
+
return existing;
|
|
139
|
+
}
|
|
140
|
+
const token = generateServiceToken();
|
|
141
|
+
await writeServiceToken(token);
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`hydra-acp: initialized ${paths.authToken()} with a fresh service token.
|
|
144
|
+
`
|
|
145
|
+
);
|
|
146
|
+
return token;
|
|
147
|
+
}
|
|
148
|
+
var init_service_token = __esm({
|
|
149
|
+
"src/core/service-token.ts"() {
|
|
150
|
+
"use strict";
|
|
151
|
+
init_paths();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// src/core/config.ts
|
|
156
|
+
import * as fs2 from "fs/promises";
|
|
91
157
|
import { homedir as homedir2 } from "os";
|
|
92
158
|
import { z } from "zod";
|
|
93
159
|
function extensionList(config) {
|
|
@@ -99,7 +165,7 @@ function extensionList(config) {
|
|
|
99
165
|
async function readConfigFile() {
|
|
100
166
|
let raw;
|
|
101
167
|
try {
|
|
102
|
-
raw = await
|
|
168
|
+
raw = await fs2.readFile(paths.config(), "utf8");
|
|
103
169
|
} catch (err) {
|
|
104
170
|
const e = err;
|
|
105
171
|
if (e.code === "ENOENT") {
|
|
@@ -109,44 +175,34 @@ async function readConfigFile() {
|
|
|
109
175
|
}
|
|
110
176
|
return JSON.parse(raw);
|
|
111
177
|
}
|
|
112
|
-
async function
|
|
113
|
-
|
|
178
|
+
async function migrateLegacyAuthToken() {
|
|
179
|
+
const raw = await readConfigFile();
|
|
180
|
+
const daemon = raw.daemon;
|
|
181
|
+
const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
|
|
182
|
+
if (!legacy) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
let tokenFileExists = false;
|
|
114
186
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (trimmed.length > 0) {
|
|
118
|
-
tokenFile = trimmed;
|
|
119
|
-
}
|
|
187
|
+
await fs2.access(paths.authToken());
|
|
188
|
+
tokenFileExists = true;
|
|
120
189
|
} catch (err) {
|
|
121
190
|
const e = err;
|
|
122
191
|
if (e.code !== "ENOENT") {
|
|
123
192
|
throw err;
|
|
124
193
|
}
|
|
125
194
|
}
|
|
126
|
-
|
|
127
|
-
const daemon = raw.daemon;
|
|
128
|
-
const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
|
|
129
|
-
if (tokenFile && legacy) {
|
|
195
|
+
if (tokenFileExists) {
|
|
130
196
|
throw new Error(
|
|
131
197
|
`Auth token present in both ${paths.authToken()} and ${paths.config()} (daemon.authToken). Remove daemon.authToken from config.json to resolve.`
|
|
132
198
|
);
|
|
133
199
|
}
|
|
134
|
-
|
|
135
|
-
return tokenFile;
|
|
136
|
-
}
|
|
137
|
-
if (legacy) {
|
|
138
|
-
await migrateLegacyAuthToken(raw, daemon, legacy);
|
|
139
|
-
return legacy;
|
|
140
|
-
}
|
|
141
|
-
return void 0;
|
|
142
|
-
}
|
|
143
|
-
async function migrateLegacyAuthToken(raw, daemon, token) {
|
|
144
|
-
await writeAuthToken(token);
|
|
200
|
+
await writeServiceToken(legacy);
|
|
145
201
|
delete daemon.authToken;
|
|
146
202
|
if (Object.keys(daemon).length === 0) {
|
|
147
203
|
delete raw.daemon;
|
|
148
204
|
}
|
|
149
|
-
await
|
|
205
|
+
await fs2.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
|
|
150
206
|
encoding: "utf8",
|
|
151
207
|
mode: 384
|
|
152
208
|
});
|
|
@@ -155,47 +211,9 @@ async function migrateLegacyAuthToken(raw, daemon, token) {
|
|
|
155
211
|
`
|
|
156
212
|
);
|
|
157
213
|
}
|
|
158
|
-
async function writeAuthToken(token) {
|
|
159
|
-
await fs.mkdir(paths.home(), { recursive: true });
|
|
160
|
-
await fs.writeFile(paths.authToken(), token + "\n", {
|
|
161
|
-
encoding: "utf8",
|
|
162
|
-
mode: 384
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
214
|
async function loadConfig() {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
throw new Error(
|
|
169
|
-
`No auth token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
const raw = await readConfigFile();
|
|
173
|
-
const daemon = raw.daemon ??= {};
|
|
174
|
-
daemon.authToken = token;
|
|
175
|
-
return HydraConfig.parse(raw);
|
|
176
|
-
}
|
|
177
|
-
async function loadConfigReadOnly() {
|
|
178
|
-
return HydraConfigReadOnly.parse(await readConfigFile());
|
|
179
|
-
}
|
|
180
|
-
async function ensureConfig() {
|
|
181
|
-
if (!await loadAuthToken()) {
|
|
182
|
-
const token = generateAuthToken();
|
|
183
|
-
await writeAuthToken(token);
|
|
184
|
-
process.stderr.write(
|
|
185
|
-
`hydra-acp: initialized ${paths.authToken()} with a fresh auth token.
|
|
186
|
-
`
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
return loadConfig();
|
|
190
|
-
}
|
|
191
|
-
function generateAuthToken() {
|
|
192
|
-
const bytes = new Uint8Array(32);
|
|
193
|
-
crypto.getRandomValues(bytes);
|
|
194
|
-
let hex = "";
|
|
195
|
-
for (const b of bytes) {
|
|
196
|
-
hex += b.toString(16).padStart(2, "0");
|
|
197
|
-
}
|
|
198
|
-
return `hydra_token_${hex}`;
|
|
215
|
+
await migrateLegacyAuthToken();
|
|
216
|
+
return HydraConfig.parse(await readConfigFile());
|
|
199
217
|
}
|
|
200
218
|
function expandHome(p) {
|
|
201
219
|
if (p === "~" || p === "$HOME") {
|
|
@@ -209,20 +227,21 @@ function expandHome(p) {
|
|
|
209
227
|
}
|
|
210
228
|
return p;
|
|
211
229
|
}
|
|
212
|
-
var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig
|
|
230
|
+
var REGISTRY_URL_DEFAULT, TlsConfig, DEFAULT_DAEMON_PORT, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig;
|
|
213
231
|
var init_config = __esm({
|
|
214
232
|
"src/core/config.ts"() {
|
|
215
233
|
"use strict";
|
|
216
234
|
init_paths();
|
|
235
|
+
init_service_token();
|
|
217
236
|
REGISTRY_URL_DEFAULT = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
218
237
|
TlsConfig = z.object({
|
|
219
238
|
cert: z.string(),
|
|
220
239
|
key: z.string()
|
|
221
240
|
});
|
|
241
|
+
DEFAULT_DAEMON_PORT = 55514;
|
|
222
242
|
DaemonConfig = z.object({
|
|
223
243
|
host: z.string().default("127.0.0.1"),
|
|
224
|
-
port: z.number().int().positive().default(
|
|
225
|
-
authToken: z.string().min(16),
|
|
244
|
+
port: z.number().int().positive().default(DEFAULT_DAEMON_PORT),
|
|
226
245
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
227
246
|
tls: TlsConfig.optional(),
|
|
228
247
|
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
|
|
@@ -283,7 +302,7 @@ var init_config = __esm({
|
|
|
283
302
|
enabled: z.boolean().default(true)
|
|
284
303
|
});
|
|
285
304
|
HydraConfig = z.object({
|
|
286
|
-
daemon: DaemonConfig,
|
|
305
|
+
daemon: DaemonConfig.default({}),
|
|
287
306
|
registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
|
|
288
307
|
defaultAgent: z.string().default("claude-acp"),
|
|
289
308
|
// Optional per-agent default model id. When a brand-new agent process
|
|
@@ -303,6 +322,11 @@ var init_config = __esm({
|
|
|
303
322
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
304
323
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
305
324
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
325
|
+
// npm registry URL used when installing npm-distributed agents into
|
|
326
|
+
// ~/.hydra-acp/agents. Overrides the global ~/.npmrc registry so a
|
|
327
|
+
// corporate .npmrc pointing at an internal registry doesn't break
|
|
328
|
+
// public-package installs. Omit to let npm use its own defaults.
|
|
329
|
+
npmRegistry: z.string().url().optional(),
|
|
306
330
|
tui: TuiConfig.default({
|
|
307
331
|
repaintThrottleMs: 1e3,
|
|
308
332
|
maxScrollbackLines: 1e4,
|
|
@@ -312,9 +336,6 @@ var init_config = __esm({
|
|
|
312
336
|
progressIndicator: true
|
|
313
337
|
})
|
|
314
338
|
});
|
|
315
|
-
HydraConfigReadOnly = HydraConfig.extend({
|
|
316
|
-
daemon: DaemonConfig.omit({ authToken: true }).default({})
|
|
317
|
-
});
|
|
318
339
|
}
|
|
319
340
|
});
|
|
320
341
|
|
|
@@ -389,12 +410,64 @@ function extractHydraMeta(meta) {
|
|
|
389
410
|
out.availableCommands = cmds;
|
|
390
411
|
}
|
|
391
412
|
}
|
|
413
|
+
if (typeof obj.promptQueueing === "boolean") {
|
|
414
|
+
out.promptQueueing = obj.promptQueueing;
|
|
415
|
+
}
|
|
416
|
+
if (Array.isArray(obj.queue)) {
|
|
417
|
+
const entries = [];
|
|
418
|
+
for (const raw of obj.queue) {
|
|
419
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const r = raw;
|
|
423
|
+
const orig = r.originator;
|
|
424
|
+
if (typeof r.messageId !== "string" || !orig || typeof orig.clientId !== "string" || !Array.isArray(r.prompt) || typeof r.position !== "number" || typeof r.enqueuedAt !== "number") {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const originator = { clientId: orig.clientId };
|
|
428
|
+
if (typeof orig.name === "string") originator.name = orig.name;
|
|
429
|
+
if (typeof orig.version === "string") originator.version = orig.version;
|
|
430
|
+
entries.push({
|
|
431
|
+
messageId: r.messageId,
|
|
432
|
+
originator,
|
|
433
|
+
prompt: r.prompt,
|
|
434
|
+
position: r.position,
|
|
435
|
+
enqueuedAt: r.enqueuedAt
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (entries.length > 0) {
|
|
439
|
+
out.queue = entries;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (Array.isArray(obj.availableModes)) {
|
|
443
|
+
const modes = [];
|
|
444
|
+
for (const raw of obj.availableModes) {
|
|
445
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const m = raw;
|
|
449
|
+
if (typeof m.id !== "string") {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const mode = { id: m.id };
|
|
453
|
+
if (typeof m.name === "string") {
|
|
454
|
+
mode.name = m.name;
|
|
455
|
+
}
|
|
456
|
+
if (typeof m.description === "string") {
|
|
457
|
+
mode.description = m.description;
|
|
458
|
+
}
|
|
459
|
+
modes.push(mode);
|
|
460
|
+
}
|
|
461
|
+
if (modes.length > 0) {
|
|
462
|
+
out.availableModes = modes;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
392
465
|
return out;
|
|
393
466
|
}
|
|
394
467
|
function mergeMeta(passthrough, ours) {
|
|
395
468
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
396
469
|
}
|
|
397
|
-
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
470
|
+
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, ProxyInitializeParams;
|
|
398
471
|
var init_types = __esm({
|
|
399
472
|
"src/acp/types.ts"() {
|
|
400
473
|
"use strict";
|
|
@@ -517,6 +590,49 @@ var init_types = __esm({
|
|
|
517
590
|
SessionCancelParams = z3.object({
|
|
518
591
|
sessionId: z3.string()
|
|
519
592
|
});
|
|
593
|
+
PromptOriginatorSchema = z3.object({
|
|
594
|
+
clientId: z3.string(),
|
|
595
|
+
name: z3.string().optional(),
|
|
596
|
+
version: z3.string().optional()
|
|
597
|
+
});
|
|
598
|
+
PromptQueueAddedParams = z3.object({
|
|
599
|
+
sessionId: z3.string(),
|
|
600
|
+
messageId: z3.string(),
|
|
601
|
+
originator: PromptOriginatorSchema,
|
|
602
|
+
prompt: z3.array(z3.unknown()),
|
|
603
|
+
// 0 = head (currently in-flight). At enqueue time the new entry's
|
|
604
|
+
// position equals the count of entries already ahead of it.
|
|
605
|
+
position: z3.number().int().nonnegative(),
|
|
606
|
+
queueDepth: z3.number().int().positive(),
|
|
607
|
+
enqueuedAt: z3.number()
|
|
608
|
+
});
|
|
609
|
+
PromptQueueUpdatedParams = z3.object({
|
|
610
|
+
sessionId: z3.string(),
|
|
611
|
+
messageId: z3.string(),
|
|
612
|
+
prompt: z3.array(z3.unknown())
|
|
613
|
+
});
|
|
614
|
+
PromptQueueRemovedParams = z3.object({
|
|
615
|
+
sessionId: z3.string(),
|
|
616
|
+
messageId: z3.string(),
|
|
617
|
+
reason: z3.enum(["started", "cancelled", "abandoned"])
|
|
618
|
+
});
|
|
619
|
+
CancelPromptParams = z3.object({
|
|
620
|
+
sessionId: z3.string(),
|
|
621
|
+
messageId: z3.string()
|
|
622
|
+
});
|
|
623
|
+
CancelPromptResult = z3.object({
|
|
624
|
+
cancelled: z3.boolean(),
|
|
625
|
+
reason: z3.enum(["ok", "not_found", "already_running"])
|
|
626
|
+
});
|
|
627
|
+
UpdatePromptParams = z3.object({
|
|
628
|
+
sessionId: z3.string(),
|
|
629
|
+
messageId: z3.string(),
|
|
630
|
+
prompt: z3.array(z3.unknown())
|
|
631
|
+
});
|
|
632
|
+
UpdatePromptResult = z3.object({
|
|
633
|
+
updated: z3.boolean(),
|
|
634
|
+
reason: z3.enum(["ok", "not_found", "already_running"])
|
|
635
|
+
});
|
|
520
636
|
ProxyInitializeParams = z3.object({
|
|
521
637
|
protocolVersion: z3.number().optional(),
|
|
522
638
|
proxyInfo: z3.object({
|
|
@@ -766,6 +882,53 @@ var init_hydra_commands = __esm({
|
|
|
766
882
|
}
|
|
767
883
|
});
|
|
768
884
|
|
|
885
|
+
// src/core/queue-store.ts
|
|
886
|
+
import * as fs6 from "fs/promises";
|
|
887
|
+
async function rewriteQueue(sessionId, entries) {
|
|
888
|
+
const file = paths.queueFile(sessionId);
|
|
889
|
+
if (entries.length === 0) {
|
|
890
|
+
await fs6.unlink(file).catch(() => void 0);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
894
|
+
const body = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
895
|
+
await fs6.writeFile(file, body, "utf8");
|
|
896
|
+
}
|
|
897
|
+
async function loadQueue(sessionId) {
|
|
898
|
+
const file = paths.queueFile(sessionId);
|
|
899
|
+
let text;
|
|
900
|
+
try {
|
|
901
|
+
text = await fs6.readFile(file, "utf8");
|
|
902
|
+
} catch (err) {
|
|
903
|
+
if (err.code === "ENOENT") {
|
|
904
|
+
return [];
|
|
905
|
+
}
|
|
906
|
+
throw err;
|
|
907
|
+
}
|
|
908
|
+
const out = [];
|
|
909
|
+
for (const line of text.split("\n")) {
|
|
910
|
+
if (!line.trim()) continue;
|
|
911
|
+
try {
|
|
912
|
+
const parsed = JSON.parse(line);
|
|
913
|
+
if (parsed && typeof parsed.messageId === "string" && Array.isArray(parsed.prompt) && typeof parsed.enqueuedAt === "number") {
|
|
914
|
+
out.push(parsed);
|
|
915
|
+
}
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return out;
|
|
920
|
+
}
|
|
921
|
+
async function deleteQueue(sessionId) {
|
|
922
|
+
const file = paths.queueFile(sessionId);
|
|
923
|
+
await fs6.unlink(file).catch(() => void 0);
|
|
924
|
+
}
|
|
925
|
+
var init_queue_store = __esm({
|
|
926
|
+
"src/core/queue-store.ts"() {
|
|
927
|
+
"use strict";
|
|
928
|
+
init_paths();
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
769
932
|
// src/core/session.ts
|
|
770
933
|
import { customAlphabet } from "nanoid";
|
|
771
934
|
function generateMessageId() {
|
|
@@ -797,6 +960,47 @@ function sameAdvertisedCommands(a, b) {
|
|
|
797
960
|
}
|
|
798
961
|
return true;
|
|
799
962
|
}
|
|
963
|
+
function sameAdvertisedModes(a, b) {
|
|
964
|
+
if (a.length !== b.length) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
for (let i = 0; i < a.length; i++) {
|
|
968
|
+
if (a[i]?.id !== b[i]?.id || a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
function extractAdvertisedModes(params) {
|
|
975
|
+
const obj = params ?? {};
|
|
976
|
+
const update = obj.update ?? {};
|
|
977
|
+
if (update.sessionUpdate !== "available_modes_update") {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
const list = update.availableModes;
|
|
981
|
+
if (!Array.isArray(list)) {
|
|
982
|
+
return [];
|
|
983
|
+
}
|
|
984
|
+
const out = [];
|
|
985
|
+
for (const raw of list) {
|
|
986
|
+
if (!raw || typeof raw !== "object") {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const m = raw;
|
|
990
|
+
if (typeof m.id !== "string" || m.id.length === 0) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const mode = { id: m.id };
|
|
994
|
+
if (typeof m.name === "string") {
|
|
995
|
+
mode.name = m.name;
|
|
996
|
+
}
|
|
997
|
+
if (typeof m.description === "string") {
|
|
998
|
+
mode.description = m.description;
|
|
999
|
+
}
|
|
1000
|
+
out.push(mode);
|
|
1001
|
+
}
|
|
1002
|
+
return out;
|
|
1003
|
+
}
|
|
800
1004
|
function captureInternalChunk(capture, params) {
|
|
801
1005
|
const obj = params ?? {};
|
|
802
1006
|
const update = obj.update ?? {};
|
|
@@ -955,6 +1159,7 @@ var init_session = __esm({
|
|
|
955
1159
|
"src/core/session.ts"() {
|
|
956
1160
|
"use strict";
|
|
957
1161
|
init_hydra_commands();
|
|
1162
|
+
init_queue_store();
|
|
958
1163
|
init_types();
|
|
959
1164
|
HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
960
1165
|
generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
@@ -984,7 +1189,18 @@ var init_session = __esm({
|
|
|
984
1189
|
clients = /* @__PURE__ */ new Map();
|
|
985
1190
|
historyStore;
|
|
986
1191
|
promptQueue = [];
|
|
1192
|
+
// The entry that drainQueue is currently awaiting. Distinct from
|
|
1193
|
+
// promptQueue[0] (which is the *next* one to dequeue): once shifted
|
|
1194
|
+
// off, the entry lives here for the duration of its task() so
|
|
1195
|
+
// cancelQueuedPrompt can distinguish "still in line" from "running"
|
|
1196
|
+
// and return already_running for the latter.
|
|
1197
|
+
currentEntry;
|
|
987
1198
|
promptInFlight = false;
|
|
1199
|
+
// Serialize disk writes to the persisted queue file. Without this
|
|
1200
|
+
// chain, fire-and-forget appends/rewrites can interleave (e.g.
|
|
1201
|
+
// drainQueue's rewrite-to-empty races a sibling's append-on-
|
|
1202
|
+
// enqueue) and leave the file out of sync with in-memory state.
|
|
1203
|
+
queueWriteChain = Promise.resolve();
|
|
988
1204
|
closed = false;
|
|
989
1205
|
closeHandlers = [];
|
|
990
1206
|
titleHandlers = [];
|
|
@@ -1037,10 +1253,14 @@ var init_session = __esm({
|
|
|
1037
1253
|
// can deliver the merged list via _meta without depending on history
|
|
1038
1254
|
// replay.
|
|
1039
1255
|
agentAdvertisedCommands = [];
|
|
1256
|
+
// Last available_modes_update we observed from the agent. Same
|
|
1257
|
+
// pattern as commands: cache, persist, broadcast on change.
|
|
1258
|
+
agentAdvertisedModes = [];
|
|
1040
1259
|
// Persist hooks for snapshot-shaped state. SessionManager hooks these
|
|
1041
1260
|
// to mirror changes into meta.json so cold-resurrect attaches can
|
|
1042
1261
|
// surface the latest snapshot via the attach response _meta.
|
|
1043
1262
|
agentCommandsHandlers = [];
|
|
1263
|
+
agentModesHandlers = [];
|
|
1044
1264
|
modelHandlers = [];
|
|
1045
1265
|
modeHandlers = [];
|
|
1046
1266
|
usageHandlers = [];
|
|
@@ -1059,6 +1279,9 @@ var init_session = __esm({
|
|
|
1059
1279
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
1060
1280
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
1061
1281
|
}
|
|
1282
|
+
if (init.agentModes && init.agentModes.length > 0) {
|
|
1283
|
+
this.agentAdvertisedModes = [...init.agentModes];
|
|
1284
|
+
}
|
|
1062
1285
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
1063
1286
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
1064
1287
|
this.logger = init.logger;
|
|
@@ -1087,6 +1310,15 @@ var init_session = __esm({
|
|
|
1087
1310
|
}
|
|
1088
1311
|
});
|
|
1089
1312
|
}
|
|
1313
|
+
broadcastAvailableModes() {
|
|
1314
|
+
this.recordAndBroadcast("session/update", {
|
|
1315
|
+
sessionId: this.upstreamSessionId,
|
|
1316
|
+
update: {
|
|
1317
|
+
sessionUpdate: "available_modes_update",
|
|
1318
|
+
availableModes: this.agentAdvertisedModes
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1090
1322
|
// Register session/update, session/request_permission, and onExit
|
|
1091
1323
|
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
1092
1324
|
// the new agent is plumbed identically. The exit handler's identity
|
|
@@ -1104,6 +1336,11 @@ var init_session = __esm({
|
|
|
1104
1336
|
this.setAgentAdvertisedCommands(agentCmds);
|
|
1105
1337
|
return;
|
|
1106
1338
|
}
|
|
1339
|
+
const agentModes = extractAdvertisedModes(params);
|
|
1340
|
+
if (agentModes !== null) {
|
|
1341
|
+
this.setAgentAdvertisedModes(agentModes);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1107
1344
|
if (this.maybeApplyAgentModel(params)) {
|
|
1108
1345
|
this.recordAndBroadcast("session/update", params);
|
|
1109
1346
|
return;
|
|
@@ -1280,7 +1517,7 @@ var init_session = __esm({
|
|
|
1280
1517
|
sessionId,
|
|
1281
1518
|
update: {
|
|
1282
1519
|
sessionUpdate: "current_mode_update",
|
|
1283
|
-
|
|
1520
|
+
currentModeId: this.currentMode
|
|
1284
1521
|
}
|
|
1285
1522
|
},
|
|
1286
1523
|
recordedAt
|
|
@@ -1300,6 +1537,19 @@ var init_session = __esm({
|
|
|
1300
1537
|
recordedAt
|
|
1301
1538
|
});
|
|
1302
1539
|
}
|
|
1540
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
1541
|
+
out.push({
|
|
1542
|
+
method: "session/update",
|
|
1543
|
+
params: {
|
|
1544
|
+
sessionId,
|
|
1545
|
+
update: {
|
|
1546
|
+
sessionUpdate: "available_modes_update",
|
|
1547
|
+
availableModes: [...this.agentAdvertisedModes]
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
recordedAt
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1303
1553
|
if (this.currentUsage !== void 0) {
|
|
1304
1554
|
const u = this.currentUsage;
|
|
1305
1555
|
const update = {
|
|
@@ -1390,34 +1640,28 @@ var init_session = __esm({
|
|
|
1390
1640
|
if (promptText.startsWith("/hydra")) {
|
|
1391
1641
|
return this.handleSlashCommand(promptText);
|
|
1392
1642
|
}
|
|
1393
|
-
|
|
1643
|
+
const messageId = generateMessageId();
|
|
1394
1644
|
this.maybeSeedTitleFromPrompt(params);
|
|
1395
|
-
return this.
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
const sentBy = { clientId: client.clientId };
|
|
1416
|
-
if (client.clientInfo?.name) {
|
|
1417
|
-
sentBy.name = client.clientInfo.name;
|
|
1418
|
-
}
|
|
1419
|
-
if (client.clientInfo?.version) {
|
|
1420
|
-
sentBy.version = client.clientInfo.version;
|
|
1645
|
+
return this.enqueueUserPrompt(client, params, messageId);
|
|
1646
|
+
}
|
|
1647
|
+
// DEVIATION FROM RFD #533: this broadcast is deliberately deferred
|
|
1648
|
+
// until the prompt actually becomes the active turn (i.e. drainQueue
|
|
1649
|
+
// is about to forward it to the agent), NOT when hydra first accepts
|
|
1650
|
+
// the request. The literal RFD doesn't pin the timing — it just says
|
|
1651
|
+
// peers should learn about the turn — but it was authored before
|
|
1652
|
+
// prompt queueing existed, so accept-time and start-time were the
|
|
1653
|
+
// same moment. With hydra's per-session FIFO, deferring gives
|
|
1654
|
+
// prompt_received a single, useful meaning ("the agent is now taking
|
|
1655
|
+
// a turn on this prompt"), which is how attached clients (notably
|
|
1656
|
+
// agent-shell) consume it. The accept-time signal that peers can use
|
|
1657
|
+
// for queue chip rendering is hydra-acp/prompt_queue_added instead.
|
|
1658
|
+
broadcastPromptReceived(entry) {
|
|
1659
|
+
const sentBy = { clientId: entry.originator.clientId };
|
|
1660
|
+
if (entry.originator.name) {
|
|
1661
|
+
sentBy.name = entry.originator.name;
|
|
1662
|
+
}
|
|
1663
|
+
if (entry.originator.version) {
|
|
1664
|
+
sentBy.version = entry.originator.version;
|
|
1421
1665
|
}
|
|
1422
1666
|
this.promptStartedAt = Date.now();
|
|
1423
1667
|
this.recordAndBroadcast(
|
|
@@ -1426,14 +1670,14 @@ var init_session = __esm({
|
|
|
1426
1670
|
sessionId: this.sessionId,
|
|
1427
1671
|
update: {
|
|
1428
1672
|
sessionUpdate: "prompt_received",
|
|
1429
|
-
messageId:
|
|
1430
|
-
prompt:
|
|
1673
|
+
messageId: entry.messageId,
|
|
1674
|
+
prompt: entry.prompt,
|
|
1431
1675
|
sentBy
|
|
1432
1676
|
}
|
|
1433
1677
|
},
|
|
1434
|
-
|
|
1678
|
+
entry.clientId
|
|
1435
1679
|
);
|
|
1436
|
-
const text = extractPromptText(
|
|
1680
|
+
const text = extractPromptText(entry.prompt);
|
|
1437
1681
|
if (text.length > 0) {
|
|
1438
1682
|
this.recordAndBroadcast(
|
|
1439
1683
|
"session/update",
|
|
@@ -1445,7 +1689,7 @@ var init_session = __esm({
|
|
|
1445
1689
|
_meta: { "hydra-acp": { compatFor: "prompt_received" } }
|
|
1446
1690
|
}
|
|
1447
1691
|
},
|
|
1448
|
-
|
|
1692
|
+
entry.clientId
|
|
1449
1693
|
);
|
|
1450
1694
|
}
|
|
1451
1695
|
}
|
|
@@ -1468,6 +1712,172 @@ var init_session = __esm({
|
|
|
1468
1712
|
originatorClientId
|
|
1469
1713
|
);
|
|
1470
1714
|
}
|
|
1715
|
+
// Total visible-or-running entries: the in-flight head (if any) plus
|
|
1716
|
+
// the queue's user-visible waiting entries. Internal entries don't
|
|
1717
|
+
// count — they're an implementation detail and the wire never
|
|
1718
|
+
// surfaces them.
|
|
1719
|
+
visibleQueueDepth() {
|
|
1720
|
+
let count = this.currentEntry?.kind === "user" && !this.currentEntry.cancelled ? 1 : 0;
|
|
1721
|
+
for (const entry of this.promptQueue) {
|
|
1722
|
+
if (entry.kind === "user" && !entry.cancelled) count += 1;
|
|
1723
|
+
}
|
|
1724
|
+
return count;
|
|
1725
|
+
}
|
|
1726
|
+
broadcastQueueAdded(entry) {
|
|
1727
|
+
const depth = this.visibleQueueDepth();
|
|
1728
|
+
const position = Math.max(0, depth - 1);
|
|
1729
|
+
const params = {
|
|
1730
|
+
sessionId: this.sessionId,
|
|
1731
|
+
messageId: entry.messageId,
|
|
1732
|
+
originator: entry.originator,
|
|
1733
|
+
prompt: entry.prompt,
|
|
1734
|
+
position,
|
|
1735
|
+
queueDepth: depth,
|
|
1736
|
+
enqueuedAt: entry.enqueuedAt
|
|
1737
|
+
};
|
|
1738
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue_added", params);
|
|
1739
|
+
}
|
|
1740
|
+
broadcastQueueUpdated(messageId, prompt) {
|
|
1741
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue_updated", {
|
|
1742
|
+
sessionId: this.sessionId,
|
|
1743
|
+
messageId,
|
|
1744
|
+
prompt
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
broadcastQueueRemoved(messageId, reason) {
|
|
1748
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue_removed", {
|
|
1749
|
+
sessionId: this.sessionId,
|
|
1750
|
+
messageId,
|
|
1751
|
+
reason
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
// Fan-out for queue lifecycle notifications. Ephemeral by design —
|
|
1755
|
+
// these signals describe transient daemon state, not conversation
|
|
1756
|
+
// content, so we deliberately bypass recordAndBroadcast (no history,
|
|
1757
|
+
// no idle-timer arm, no rewrite-for-client since we already emit the
|
|
1758
|
+
// hydra sessionId).
|
|
1759
|
+
broadcastQueueNotification(method, params) {
|
|
1760
|
+
for (const client of this.clients.values()) {
|
|
1761
|
+
void client.connection.notify(method, params).catch(() => void 0);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
// Snapshot of user-visible queue state at this moment. Surfaced to
|
|
1765
|
+
// late-attaching clients via the session/attach response _meta so
|
|
1766
|
+
// they boot with the same chip list as their peers without waiting
|
|
1767
|
+
// for new prompt_queue_added notifications. Internal entries are
|
|
1768
|
+
// omitted (they're not surfaced on the wire at all).
|
|
1769
|
+
queueSnapshot() {
|
|
1770
|
+
const out = [];
|
|
1771
|
+
let position = 0;
|
|
1772
|
+
if (this.currentEntry?.kind === "user" && !this.currentEntry.cancelled) {
|
|
1773
|
+
out.push({
|
|
1774
|
+
messageId: this.currentEntry.messageId,
|
|
1775
|
+
originator: this.currentEntry.originator,
|
|
1776
|
+
prompt: this.currentEntry.prompt,
|
|
1777
|
+
position: position++,
|
|
1778
|
+
enqueuedAt: this.currentEntry.enqueuedAt
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
for (const entry of this.promptQueue) {
|
|
1782
|
+
if (entry.kind !== "user" || entry.cancelled) continue;
|
|
1783
|
+
out.push({
|
|
1784
|
+
messageId: entry.messageId,
|
|
1785
|
+
originator: entry.originator,
|
|
1786
|
+
prompt: entry.prompt,
|
|
1787
|
+
position: position++,
|
|
1788
|
+
enqueuedAt: entry.enqueuedAt
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
return out;
|
|
1792
|
+
}
|
|
1793
|
+
// Wait for any pending queue-file writes to settle. Test hook so
|
|
1794
|
+
// assertions about on-disk state don't race with fire-and-forget
|
|
1795
|
+
// rewrites. Production code doesn't need this — the chain
|
|
1796
|
+
// self-serializes.
|
|
1797
|
+
async flushPersistWrites() {
|
|
1798
|
+
await this.queueWriteChain.catch(() => void 0);
|
|
1799
|
+
}
|
|
1800
|
+
// Push pre-existing queue entries back through the daemon-side
|
|
1801
|
+
// pipeline on startup. Called by SessionManager after resurrecting
|
|
1802
|
+
// a session that had a non-empty queue.ndjson on disk. Each entry
|
|
1803
|
+
// gets a synthetic UserPromptQueueEntry with no real caller
|
|
1804
|
+
// (resolve/reject are no-ops since the original WS is long gone),
|
|
1805
|
+
// then drainQueue picks it up like any other entry. Late-attaching
|
|
1806
|
+
// clients see the entries via prompt_queue_added broadcasts and the
|
|
1807
|
+
// attach-response snapshot.
|
|
1808
|
+
replayPersistedQueue(entries) {
|
|
1809
|
+
for (const persisted of entries) {
|
|
1810
|
+
const originator = {
|
|
1811
|
+
clientId: `hydra-resurrected_${persisted.messageId}`
|
|
1812
|
+
};
|
|
1813
|
+
if (persisted.originator.clientInfo.name !== void 0) {
|
|
1814
|
+
originator.name = persisted.originator.clientInfo.name;
|
|
1815
|
+
}
|
|
1816
|
+
if (persisted.originator.clientInfo.version !== void 0) {
|
|
1817
|
+
originator.version = persisted.originator.clientInfo.version;
|
|
1818
|
+
}
|
|
1819
|
+
const entry = {
|
|
1820
|
+
kind: "user",
|
|
1821
|
+
messageId: persisted.messageId,
|
|
1822
|
+
originator,
|
|
1823
|
+
// Synthetic clientId. broadcastTurnComplete uses this as
|
|
1824
|
+
// excludeClientId for the peer-only broadcast; with a synthetic
|
|
1825
|
+
// id no real attached client matches the exclude, so everyone
|
|
1826
|
+
// sees turn_complete — which is what we want, since none of
|
|
1827
|
+
// them originated this restart-replayed prompt.
|
|
1828
|
+
clientId: originator.clientId,
|
|
1829
|
+
prompt: persisted.prompt,
|
|
1830
|
+
enqueuedAt: persisted.enqueuedAt,
|
|
1831
|
+
cancelled: false,
|
|
1832
|
+
resolve: () => void 0,
|
|
1833
|
+
reject: () => void 0
|
|
1834
|
+
};
|
|
1835
|
+
this.promptQueue.push(entry);
|
|
1836
|
+
this.broadcastQueueAdded(entry);
|
|
1837
|
+
}
|
|
1838
|
+
void this.drainQueue();
|
|
1839
|
+
}
|
|
1840
|
+
// Drop a queued prompt by messageId. Returns already_running when
|
|
1841
|
+
// the messageId names the in-flight entry — callers should fall back
|
|
1842
|
+
// to session/cancel for that case. Originator-agnostic: any attached
|
|
1843
|
+
// client may cancel any queued prompt (matches the existing slack
|
|
1844
|
+
// :stop_sign: reaction UX and the TUI's queue-edit dispatcher).
|
|
1845
|
+
cancelQueuedPrompt(messageId) {
|
|
1846
|
+
if (this.currentEntry?.messageId === messageId) {
|
|
1847
|
+
return { cancelled: false, reason: "already_running" };
|
|
1848
|
+
}
|
|
1849
|
+
const idx = this.promptQueue.findIndex((e) => e.messageId === messageId);
|
|
1850
|
+
if (idx < 0) {
|
|
1851
|
+
return { cancelled: false, reason: "not_found" };
|
|
1852
|
+
}
|
|
1853
|
+
const entry = this.promptQueue[idx];
|
|
1854
|
+
entry.cancelled = true;
|
|
1855
|
+
this.promptQueue.splice(idx, 1);
|
|
1856
|
+
if (entry.kind === "user") {
|
|
1857
|
+
this.broadcastQueueRemoved(messageId, "cancelled");
|
|
1858
|
+
this.persistRewrite();
|
|
1859
|
+
}
|
|
1860
|
+
entry.resolve({ stopReason: "cancelled" });
|
|
1861
|
+
return { cancelled: true, reason: "ok" };
|
|
1862
|
+
}
|
|
1863
|
+
// Replace the prompt payload of a queued (not-yet-running) entry.
|
|
1864
|
+
// Returns already_running for the in-flight head; not_found for
|
|
1865
|
+
// unknown messageIds or for internal queue entries (internal tasks
|
|
1866
|
+
// don't expose a mutable prompt). Broadcasts prompt_queue_updated on
|
|
1867
|
+
// success so every attached client refreshes its chip.
|
|
1868
|
+
updateQueuedPrompt(messageId, prompt) {
|
|
1869
|
+
if (this.currentEntry?.messageId === messageId) {
|
|
1870
|
+
return { updated: false, reason: "already_running" };
|
|
1871
|
+
}
|
|
1872
|
+
const entry = this.promptQueue.find((e) => e.messageId === messageId);
|
|
1873
|
+
if (!entry || entry.kind !== "user") {
|
|
1874
|
+
return { updated: false, reason: "not_found" };
|
|
1875
|
+
}
|
|
1876
|
+
entry.prompt = prompt;
|
|
1877
|
+
this.broadcastQueueUpdated(messageId, prompt);
|
|
1878
|
+
this.persistRewrite();
|
|
1879
|
+
return { updated: true, reason: "ok" };
|
|
1880
|
+
}
|
|
1471
1881
|
async cancel(clientId) {
|
|
1472
1882
|
const client = this.clients.get(clientId);
|
|
1473
1883
|
if (!client) {
|
|
@@ -1594,7 +2004,7 @@ var init_session = __esm({
|
|
|
1594
2004
|
if (update.sessionUpdate !== "current_mode_update") {
|
|
1595
2005
|
return false;
|
|
1596
2006
|
}
|
|
1597
|
-
const raw = typeof update.currentMode === "string" ? update.currentMode : typeof update.mode === "string" ? update.mode : void 0;
|
|
2007
|
+
const raw = typeof update.currentModeId === "string" ? update.currentModeId : typeof update.currentMode === "string" ? update.currentMode : typeof update.mode === "string" ? update.mode : void 0;
|
|
1598
2008
|
if (raw === void 0) {
|
|
1599
2009
|
return true;
|
|
1600
2010
|
}
|
|
@@ -1672,12 +2082,29 @@ var init_session = __esm({
|
|
|
1672
2082
|
}
|
|
1673
2083
|
this.broadcastMergedCommands();
|
|
1674
2084
|
}
|
|
2085
|
+
setAgentAdvertisedModes(modes) {
|
|
2086
|
+
if (sameAdvertisedModes(this.agentAdvertisedModes, modes)) {
|
|
2087
|
+
this.broadcastAvailableModes();
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
this.agentAdvertisedModes = modes;
|
|
2091
|
+
for (const handler of this.agentModesHandlers) {
|
|
2092
|
+
try {
|
|
2093
|
+
handler(modes);
|
|
2094
|
+
} catch {
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
this.broadcastAvailableModes();
|
|
2098
|
+
}
|
|
1675
2099
|
// Subscribe to snapshot-state updates. SessionManager wires these to
|
|
1676
2100
|
// persist the new value into meta.json so cold resurrect can restore
|
|
1677
2101
|
// them via the attach response _meta.
|
|
1678
2102
|
onAgentCommandsChange(handler) {
|
|
1679
2103
|
this.agentCommandsHandlers.push(handler);
|
|
1680
2104
|
}
|
|
2105
|
+
onAgentModesChange(handler) {
|
|
2106
|
+
this.agentModesHandlers.push(handler);
|
|
2107
|
+
}
|
|
1681
2108
|
onModelChange(handler) {
|
|
1682
2109
|
this.modelHandlers.push(handler);
|
|
1683
2110
|
}
|
|
@@ -1699,6 +2126,10 @@ var init_session = __esm({
|
|
|
1699
2126
|
agentOnlyAdvertisedCommands() {
|
|
1700
2127
|
return [...this.agentAdvertisedCommands];
|
|
1701
2128
|
}
|
|
2129
|
+
// The agent's advertised modes list, for callers that need a snapshot.
|
|
2130
|
+
availableModes() {
|
|
2131
|
+
return [...this.agentAdvertisedModes];
|
|
2132
|
+
}
|
|
1702
2133
|
// Pick up an agent-emitted session_info_update and store its title
|
|
1703
2134
|
// as our canonical record. The notification is also forwarded to
|
|
1704
2135
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -2011,6 +2442,20 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2011
2442
|
}
|
|
2012
2443
|
this.closed = true;
|
|
2013
2444
|
this.cancelIdleTimer();
|
|
2445
|
+
const stranded = this.promptQueue;
|
|
2446
|
+
this.promptQueue = [];
|
|
2447
|
+
for (const entry of stranded) {
|
|
2448
|
+
entry.cancelled = true;
|
|
2449
|
+
if (entry.kind === "user") {
|
|
2450
|
+
this.broadcastQueueRemoved(entry.messageId, "abandoned");
|
|
2451
|
+
}
|
|
2452
|
+
try {
|
|
2453
|
+
entry.resolve({ stopReason: "cancelled" });
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
const sessionId = this.sessionId;
|
|
2458
|
+
this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => deleteQueue(sessionId).catch(() => void 0));
|
|
2014
2459
|
for (const client of this.clients.values()) {
|
|
2015
2460
|
void client.connection.notify("hydra-acp/session_closed", { sessionId: this.sessionId }).catch(() => void 0);
|
|
2016
2461
|
}
|
|
@@ -2056,7 +2501,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2056
2501
|
if (this.closed || this.idleTimeoutMs <= 0) {
|
|
2057
2502
|
return;
|
|
2058
2503
|
}
|
|
2059
|
-
if (this.turnStartedAt !== void 0 || this.inFlightPermissions.size > 0) {
|
|
2504
|
+
if (this.turnStartedAt !== void 0 || this.inFlightPermissions.size > 0 || this.promptQueue.length > 0) {
|
|
2060
2505
|
this.armIdleTimer(this.idleTimeoutMs);
|
|
2061
2506
|
return;
|
|
2062
2507
|
}
|
|
@@ -2185,20 +2630,88 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2185
2630
|
}
|
|
2186
2631
|
});
|
|
2187
2632
|
}
|
|
2633
|
+
// Schedule an internal task (title regen, agent swap transcript
|
|
2634
|
+
// injection, import seed). Serializes behind any user prompts already
|
|
2635
|
+
// in flight, but doesn't emit prompt_queue_* broadcasts — clients
|
|
2636
|
+
// shouldn't see hydra's housekeeping in their chip list.
|
|
2188
2637
|
async enqueuePrompt(task) {
|
|
2189
2638
|
return new Promise((resolve5, reject) => {
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2639
|
+
const entry = {
|
|
2640
|
+
kind: "internal",
|
|
2641
|
+
messageId: generateMessageId(),
|
|
2642
|
+
enqueuedAt: Date.now(),
|
|
2643
|
+
cancelled: false,
|
|
2644
|
+
task,
|
|
2645
|
+
resolve: resolve5,
|
|
2646
|
+
reject
|
|
2647
|
+
};
|
|
2648
|
+
this.promptQueue.push(entry);
|
|
2649
|
+
void this.drainQueue();
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
// Schedule a user-originated session/prompt. Emits prompt_queue_added
|
|
2653
|
+
// immediately on enqueue (so peer clients can render the queued chip)
|
|
2654
|
+
// and prompt_queue_removed when the entry leaves the queue. The
|
|
2655
|
+
// returned promise resolves with the upstream agent's session/prompt
|
|
2656
|
+
// result, or { stopReason: "cancelled" } if the entry is dropped via
|
|
2657
|
+
// cancelQueuedPrompt before reaching the head.
|
|
2658
|
+
async enqueueUserPrompt(client, params, messageId) {
|
|
2659
|
+
const promptArray = (params ?? {}).prompt ?? [];
|
|
2660
|
+
const originator = { clientId: client.clientId };
|
|
2661
|
+
if (client.clientInfo?.name) originator.name = client.clientInfo.name;
|
|
2662
|
+
if (client.clientInfo?.version)
|
|
2663
|
+
originator.version = client.clientInfo.version;
|
|
2664
|
+
return new Promise((resolve5, reject) => {
|
|
2665
|
+
const entry = {
|
|
2666
|
+
kind: "user",
|
|
2667
|
+
messageId,
|
|
2668
|
+
originator,
|
|
2669
|
+
clientId: client.clientId,
|
|
2670
|
+
prompt: promptArray,
|
|
2671
|
+
enqueuedAt: Date.now(),
|
|
2672
|
+
cancelled: false,
|
|
2673
|
+
resolve: resolve5,
|
|
2674
|
+
reject
|
|
2197
2675
|
};
|
|
2198
|
-
this.promptQueue.push(
|
|
2676
|
+
this.promptQueue.push(entry);
|
|
2677
|
+
this.persistRewrite();
|
|
2678
|
+
this.broadcastQueueAdded(entry);
|
|
2199
2679
|
void this.drainQueue();
|
|
2200
2680
|
});
|
|
2201
2681
|
}
|
|
2682
|
+
// Rewrite the on-disk queue to reflect the current set of WAITING
|
|
2683
|
+
// entries (excluding currentEntry, the in-flight head). Excluding
|
|
2684
|
+
// the head is the key idempotency choice: once drainQueue shifts an
|
|
2685
|
+
// entry off and calls persistRewrite, a daemon crash mid-generation
|
|
2686
|
+
// will NOT re-run it on restart. Partial output (if any streamed
|
|
2687
|
+
// before the crash) stays in history; the prompt itself is lost
|
|
2688
|
+
// and the user can re-submit if they care.
|
|
2689
|
+
//
|
|
2690
|
+
// Snapshots in-memory state synchronously (so subsequent mutations
|
|
2691
|
+
// can't perturb what we're about to write) and chains the write
|
|
2692
|
+
// onto queueWriteChain so all persists are serialized.
|
|
2693
|
+
persistRewrite() {
|
|
2694
|
+
const entries = [];
|
|
2695
|
+
for (const entry of this.promptQueue) {
|
|
2696
|
+
if (entry.kind !== "user" || entry.cancelled) continue;
|
|
2697
|
+
entries.push(this.persistedFromEntry(entry));
|
|
2698
|
+
}
|
|
2699
|
+
const sessionId = this.sessionId;
|
|
2700
|
+
this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => rewriteQueue(sessionId, entries).catch(() => void 0));
|
|
2701
|
+
}
|
|
2702
|
+
persistedFromEntry(entry) {
|
|
2703
|
+
return {
|
|
2704
|
+
messageId: entry.messageId,
|
|
2705
|
+
originator: {
|
|
2706
|
+
clientInfo: {
|
|
2707
|
+
...entry.originator.name !== void 0 ? { name: entry.originator.name } : {},
|
|
2708
|
+
...entry.originator.version !== void 0 ? { version: entry.originator.version } : {}
|
|
2709
|
+
}
|
|
2710
|
+
},
|
|
2711
|
+
prompt: entry.prompt,
|
|
2712
|
+
enqueuedAt: entry.enqueuedAt
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2202
2715
|
async drainQueue() {
|
|
2203
2716
|
if (this.promptInFlight) {
|
|
2204
2717
|
return;
|
|
@@ -2207,27 +2720,78 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2207
2720
|
try {
|
|
2208
2721
|
while (this.promptQueue.length > 0) {
|
|
2209
2722
|
const next = this.promptQueue.shift();
|
|
2210
|
-
if (next) {
|
|
2211
|
-
|
|
2723
|
+
if (!next) {
|
|
2724
|
+
break;
|
|
2725
|
+
}
|
|
2726
|
+
if (next.cancelled) {
|
|
2727
|
+
continue;
|
|
2728
|
+
}
|
|
2729
|
+
this.currentEntry = next;
|
|
2730
|
+
if (next.kind === "user") {
|
|
2731
|
+
this.persistRewrite();
|
|
2732
|
+
}
|
|
2733
|
+
if (next.kind === "user") {
|
|
2734
|
+
this.broadcastQueueRemoved(next.messageId, "started");
|
|
2735
|
+
}
|
|
2736
|
+
try {
|
|
2737
|
+
const result = await this.runQueueEntry(next);
|
|
2738
|
+
next.resolve(result);
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
next.reject(err);
|
|
2741
|
+
} finally {
|
|
2742
|
+
this.currentEntry = void 0;
|
|
2212
2743
|
}
|
|
2213
2744
|
}
|
|
2214
2745
|
} finally {
|
|
2215
2746
|
this.promptInFlight = false;
|
|
2216
2747
|
}
|
|
2217
2748
|
}
|
|
2749
|
+
// Execute a queue entry. User-prompt entries forward to the upstream
|
|
2750
|
+
// agent and pair with broadcastTurnComplete; internal entries run
|
|
2751
|
+
// their captured task closure. Reads entry.prompt at dispatch time
|
|
2752
|
+
// so updateQueuedPrompt's mutations are honoured.
|
|
2753
|
+
//
|
|
2754
|
+
// For user entries, broadcastPromptReceived fires HERE — not in
|
|
2755
|
+
// Session.prompt — so peer clients see prompt_received only when the
|
|
2756
|
+
// turn actually starts (a deliberate deviation from a naive reading
|
|
2757
|
+
// of RFD #533; see the comment on broadcastPromptReceived). Order on
|
|
2758
|
+
// the wire: prompt_queue_removed{started} (already emitted by
|
|
2759
|
+
// drainQueue) → prompt_received → upstream session/prompt.
|
|
2760
|
+
async runQueueEntry(entry) {
|
|
2761
|
+
if (entry.kind === "internal") {
|
|
2762
|
+
return entry.task();
|
|
2763
|
+
}
|
|
2764
|
+
this.broadcastPromptReceived(entry);
|
|
2765
|
+
let response;
|
|
2766
|
+
try {
|
|
2767
|
+
response = await this.agent.connection.request(
|
|
2768
|
+
"session/prompt",
|
|
2769
|
+
{
|
|
2770
|
+
sessionId: this.upstreamSessionId,
|
|
2771
|
+
prompt: entry.prompt
|
|
2772
|
+
}
|
|
2773
|
+
);
|
|
2774
|
+
} catch (err) {
|
|
2775
|
+
this.broadcastTurnComplete(entry.clientId, { stopReason: "error" });
|
|
2776
|
+
throw err;
|
|
2777
|
+
}
|
|
2778
|
+
this.broadcastTurnComplete(entry.clientId, response);
|
|
2779
|
+
return response;
|
|
2780
|
+
}
|
|
2218
2781
|
};
|
|
2219
2782
|
STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
|
|
2220
2783
|
"session_info_update",
|
|
2221
2784
|
"current_model_update",
|
|
2222
2785
|
"current_mode_update",
|
|
2223
2786
|
"available_commands_update",
|
|
2787
|
+
"available_modes_update",
|
|
2224
2788
|
"usage_update"
|
|
2225
2789
|
]);
|
|
2226
2790
|
}
|
|
2227
2791
|
});
|
|
2228
2792
|
|
|
2229
2793
|
// src/core/session-store.ts
|
|
2230
|
-
import * as
|
|
2794
|
+
import * as fs7 from "fs/promises";
|
|
2231
2795
|
import * as path4 from "path";
|
|
2232
2796
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
2233
2797
|
import { z as z4 } from "zod";
|
|
@@ -2256,11 +2820,12 @@ function recordFromMemorySession(args) {
|
|
|
2256
2820
|
currentMode: args.currentMode,
|
|
2257
2821
|
currentUsage: args.currentUsage,
|
|
2258
2822
|
agentCommands: args.agentCommands,
|
|
2823
|
+
agentModes: args.agentModes,
|
|
2259
2824
|
createdAt: args.createdAt ?? now,
|
|
2260
2825
|
updatedAt: args.updatedAt ?? now
|
|
2261
2826
|
};
|
|
2262
2827
|
}
|
|
2263
|
-
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
2828
|
+
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
2264
2829
|
var init_session_store = __esm({
|
|
2265
2830
|
"src/core/session-store.ts"() {
|
|
2266
2831
|
"use strict";
|
|
@@ -2272,6 +2837,11 @@ var init_session_store = __esm({
|
|
|
2272
2837
|
name: z4.string(),
|
|
2273
2838
|
description: z4.string().optional()
|
|
2274
2839
|
});
|
|
2840
|
+
PersistedAgentMode = z4.object({
|
|
2841
|
+
id: z4.string(),
|
|
2842
|
+
name: z4.string().optional(),
|
|
2843
|
+
description: z4.string().optional()
|
|
2844
|
+
});
|
|
2275
2845
|
PersistedUsage = z4.object({
|
|
2276
2846
|
used: z4.number().optional(),
|
|
2277
2847
|
size: z4.number().optional(),
|
|
@@ -2317,6 +2887,7 @@ var init_session_store = __esm({
|
|
|
2317
2887
|
currentMode: z4.string().optional(),
|
|
2318
2888
|
currentUsage: PersistedUsage.optional(),
|
|
2319
2889
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
2890
|
+
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
2320
2891
|
createdAt: z4.string(),
|
|
2321
2892
|
updatedAt: z4.string()
|
|
2322
2893
|
});
|
|
@@ -2324,9 +2895,9 @@ var init_session_store = __esm({
|
|
|
2324
2895
|
SessionStore = class {
|
|
2325
2896
|
async write(record) {
|
|
2326
2897
|
assertSafeId(record.sessionId);
|
|
2327
|
-
await
|
|
2898
|
+
await fs7.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
2328
2899
|
const full = { version: 1, ...record };
|
|
2329
|
-
await
|
|
2900
|
+
await fs7.writeFile(
|
|
2330
2901
|
paths.sessionFile(record.sessionId),
|
|
2331
2902
|
JSON.stringify(full, null, 2) + "\n",
|
|
2332
2903
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -2338,7 +2909,7 @@ var init_session_store = __esm({
|
|
|
2338
2909
|
}
|
|
2339
2910
|
let raw;
|
|
2340
2911
|
try {
|
|
2341
|
-
raw = await
|
|
2912
|
+
raw = await fs7.readFile(paths.sessionFile(sessionId), "utf8");
|
|
2342
2913
|
} catch (err) {
|
|
2343
2914
|
const e = err;
|
|
2344
2915
|
if (e.code === "ENOENT") {
|
|
@@ -2357,7 +2928,7 @@ var init_session_store = __esm({
|
|
|
2357
2928
|
return;
|
|
2358
2929
|
}
|
|
2359
2930
|
try {
|
|
2360
|
-
await
|
|
2931
|
+
await fs7.unlink(paths.sessionFile(sessionId));
|
|
2361
2932
|
} catch (err) {
|
|
2362
2933
|
const e = err;
|
|
2363
2934
|
if (e.code !== "ENOENT") {
|
|
@@ -2365,7 +2936,7 @@ var init_session_store = __esm({
|
|
|
2365
2936
|
}
|
|
2366
2937
|
}
|
|
2367
2938
|
try {
|
|
2368
|
-
await
|
|
2939
|
+
await fs7.rmdir(paths.sessionDir(sessionId));
|
|
2369
2940
|
} catch (err) {
|
|
2370
2941
|
const e = err;
|
|
2371
2942
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -2395,7 +2966,7 @@ var init_session_store = __esm({
|
|
|
2395
2966
|
async list() {
|
|
2396
2967
|
let entries;
|
|
2397
2968
|
try {
|
|
2398
|
-
entries = await
|
|
2969
|
+
entries = await fs7.readdir(paths.sessionsDir());
|
|
2399
2970
|
} catch (err) {
|
|
2400
2971
|
const e = err;
|
|
2401
2972
|
if (e.code === "ENOENT") {
|
|
@@ -2417,12 +2988,12 @@ var init_session_store = __esm({
|
|
|
2417
2988
|
});
|
|
2418
2989
|
|
|
2419
2990
|
// src/tui/history.ts
|
|
2420
|
-
import { promises as
|
|
2991
|
+
import { promises as fs9 } from "fs";
|
|
2421
2992
|
import * as path5 from "path";
|
|
2422
2993
|
async function loadHistory(file) {
|
|
2423
2994
|
let text;
|
|
2424
2995
|
try {
|
|
2425
|
-
text = await
|
|
2996
|
+
text = await fs9.readFile(file, "utf8");
|
|
2426
2997
|
} catch (err) {
|
|
2427
2998
|
if (err.code === "ENOENT") {
|
|
2428
2999
|
return [];
|
|
@@ -2462,9 +3033,9 @@ function appendEntry(history, entry) {
|
|
|
2462
3033
|
return out;
|
|
2463
3034
|
}
|
|
2464
3035
|
async function saveHistory(file, history) {
|
|
2465
|
-
await
|
|
3036
|
+
await fs9.mkdir(path5.dirname(file), { recursive: true });
|
|
2466
3037
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2467
|
-
await
|
|
3038
|
+
await fs9.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2468
3039
|
}
|
|
2469
3040
|
var HISTORY_CAP;
|
|
2470
3041
|
var init_history = __esm({
|
|
@@ -2477,14 +3048,14 @@ var init_history = __esm({
|
|
|
2477
3048
|
// src/core/hydra-version.ts
|
|
2478
3049
|
import { fileURLToPath } from "url";
|
|
2479
3050
|
import * as path6 from "path";
|
|
2480
|
-
import * as
|
|
3051
|
+
import * as fs10 from "fs";
|
|
2481
3052
|
function resolveVersion() {
|
|
2482
3053
|
try {
|
|
2483
3054
|
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
2484
3055
|
for (let i = 0; i < 8; i += 1) {
|
|
2485
3056
|
const candidate = path6.join(dir, "package.json");
|
|
2486
|
-
if (
|
|
2487
|
-
const pkg = JSON.parse(
|
|
3057
|
+
if (fs10.existsSync(candidate)) {
|
|
3058
|
+
const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
|
|
2488
3059
|
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
2489
3060
|
return pkg.version;
|
|
2490
3061
|
}
|
|
@@ -2528,6 +3099,7 @@ function encodeBundle(params) {
|
|
|
2528
3099
|
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
2529
3100
|
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
2530
3101
|
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
3102
|
+
...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
|
|
2531
3103
|
createdAt: params.record.createdAt,
|
|
2532
3104
|
updatedAt: params.record.updatedAt
|
|
2533
3105
|
},
|
|
@@ -2571,6 +3143,7 @@ var init_bundle = __esm({
|
|
|
2571
3143
|
currentMode: z5.string().optional(),
|
|
2572
3144
|
currentUsage: PersistedUsage.optional(),
|
|
2573
3145
|
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
3146
|
+
agentModes: z5.array(PersistedAgentMode).optional(),
|
|
2574
3147
|
createdAt: z5.string(),
|
|
2575
3148
|
updatedAt: z5.string()
|
|
2576
3149
|
});
|
|
@@ -2631,6 +3204,8 @@ function mapUpdate(update) {
|
|
|
2631
3204
|
return mapUsage(u);
|
|
2632
3205
|
case "available_commands_update":
|
|
2633
3206
|
return mapAvailableCommands(u);
|
|
3207
|
+
case "available_modes_update":
|
|
3208
|
+
return mapAvailableModes(u);
|
|
2634
3209
|
case "session_info_update":
|
|
2635
3210
|
return mapSessionInfo(u);
|
|
2636
3211
|
default:
|
|
@@ -2692,6 +3267,31 @@ function mapAvailableCommands(u) {
|
|
|
2692
3267
|
}
|
|
2693
3268
|
return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
|
|
2694
3269
|
}
|
|
3270
|
+
function mapAvailableModes(u) {
|
|
3271
|
+
const list = u.availableModes;
|
|
3272
|
+
if (!Array.isArray(list)) {
|
|
3273
|
+
return null;
|
|
3274
|
+
}
|
|
3275
|
+
const modes = [];
|
|
3276
|
+
for (const raw of list) {
|
|
3277
|
+
if (!raw || typeof raw !== "object") {
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
const m = raw;
|
|
3281
|
+
if (typeof m.id !== "string" || m.id.length === 0) {
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
const mode = { id: sanitizeSingleLine(m.id) };
|
|
3285
|
+
if (typeof m.name === "string") {
|
|
3286
|
+
mode.name = sanitizeSingleLine(m.name);
|
|
3287
|
+
}
|
|
3288
|
+
if (typeof m.description === "string") {
|
|
3289
|
+
mode.description = sanitizeSingleLine(m.description);
|
|
3290
|
+
}
|
|
3291
|
+
modes.push(mode);
|
|
3292
|
+
}
|
|
3293
|
+
return { kind: "available-modes", modes };
|
|
3294
|
+
}
|
|
2695
3295
|
function mapUsage(u) {
|
|
2696
3296
|
const event = { kind: "usage-update" };
|
|
2697
3297
|
if (typeof u.used === "number") {
|
|
@@ -2812,7 +3412,7 @@ function mapPlan(u) {
|
|
|
2812
3412
|
return { kind: "plan", entries: normalized };
|
|
2813
3413
|
}
|
|
2814
3414
|
function mapMode(u) {
|
|
2815
|
-
const mode = readString(u, "currentMode") ?? readString(u, "mode");
|
|
3415
|
+
const mode = readString(u, "currentModeId") ?? readString(u, "currentMode") ?? readString(u, "mode");
|
|
2816
3416
|
if (!mode) {
|
|
2817
3417
|
return null;
|
|
2818
3418
|
}
|
|
@@ -3466,14 +4066,15 @@ var init_session_row = __esm({
|
|
|
3466
4066
|
});
|
|
3467
4067
|
|
|
3468
4068
|
// src/cli/commands/sessions.ts
|
|
3469
|
-
import * as
|
|
3470
|
-
import * as
|
|
4069
|
+
import * as fs17 from "fs/promises";
|
|
4070
|
+
import * as path11 from "path";
|
|
3471
4071
|
async function runSessionsList(opts = {}) {
|
|
3472
4072
|
const config = await loadConfig();
|
|
4073
|
+
const serviceToken = await loadServiceToken();
|
|
3473
4074
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3474
4075
|
const url = new URL(`${baseUrl}/v1/sessions`);
|
|
3475
4076
|
const response = await fetch(url.toString(), {
|
|
3476
|
-
headers: { Authorization: `Bearer ${
|
|
4077
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
3477
4078
|
});
|
|
3478
4079
|
if (!response.ok) {
|
|
3479
4080
|
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
@@ -3529,10 +4130,11 @@ async function runSessionsKill(id) {
|
|
|
3529
4130
|
process.exit(2);
|
|
3530
4131
|
}
|
|
3531
4132
|
const config = await loadConfig();
|
|
4133
|
+
const serviceToken = await loadServiceToken();
|
|
3532
4134
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3533
4135
|
const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
|
|
3534
4136
|
method: "POST",
|
|
3535
|
-
headers: { Authorization: `Bearer ${
|
|
4137
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
3536
4138
|
});
|
|
3537
4139
|
if (!response.ok && response.status !== 204) {
|
|
3538
4140
|
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
@@ -3548,10 +4150,11 @@ async function runSessionsRemove(id) {
|
|
|
3548
4150
|
process.exit(2);
|
|
3549
4151
|
}
|
|
3550
4152
|
const config = await loadConfig();
|
|
4153
|
+
const serviceToken = await loadServiceToken();
|
|
3551
4154
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3552
4155
|
const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
|
|
3553
4156
|
method: "DELETE",
|
|
3554
|
-
headers: { Authorization: `Bearer ${
|
|
4157
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
3555
4158
|
});
|
|
3556
4159
|
if (!response.ok && response.status !== 204) {
|
|
3557
4160
|
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
@@ -3569,11 +4172,12 @@ async function runSessionsExport(id, outPath) {
|
|
|
3569
4172
|
process.exit(2);
|
|
3570
4173
|
}
|
|
3571
4174
|
const config = await loadConfig();
|
|
4175
|
+
const serviceToken = await loadServiceToken();
|
|
3572
4176
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3573
4177
|
const response = await fetch(
|
|
3574
4178
|
`${baseUrl}/v1/sessions/${encodeURIComponent(id)}/export`,
|
|
3575
4179
|
{
|
|
3576
|
-
headers: { Authorization: `Bearer ${
|
|
4180
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
3577
4181
|
}
|
|
3578
4182
|
);
|
|
3579
4183
|
if (!response.ok) {
|
|
@@ -3591,8 +4195,8 @@ async function runSessionsExport(id, outPath) {
|
|
|
3591
4195
|
return;
|
|
3592
4196
|
}
|
|
3593
4197
|
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
3594
|
-
await
|
|
3595
|
-
await
|
|
4198
|
+
await fs17.mkdir(path11.dirname(path11.resolve(resolved)), { recursive: true });
|
|
4199
|
+
await fs17.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
3596
4200
|
process.stdout.write(`Wrote ${resolved}
|
|
3597
4201
|
`);
|
|
3598
4202
|
}
|
|
@@ -3610,14 +4214,15 @@ async function runSessionsTranscript(idOrFile, outPath) {
|
|
|
3610
4214
|
const bundle = decodeBundleOrExit(localFile.raw);
|
|
3611
4215
|
body = bundleToMarkdown(bundle);
|
|
3612
4216
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3613
|
-
defaultName = `${
|
|
4217
|
+
defaultName = `${path11.basename(idOrFile, path11.extname(idOrFile))}-${stamp}.md`;
|
|
3614
4218
|
} else {
|
|
3615
4219
|
const config = await loadConfig();
|
|
4220
|
+
const serviceToken = await loadServiceToken();
|
|
3616
4221
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3617
4222
|
const response = await fetch(
|
|
3618
4223
|
`${baseUrl}/v1/sessions/${encodeURIComponent(idOrFile)}/transcript`,
|
|
3619
4224
|
{
|
|
3620
|
-
headers: { Authorization: `Bearer ${
|
|
4225
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
3621
4226
|
}
|
|
3622
4227
|
);
|
|
3623
4228
|
if (!response.ok) {
|
|
@@ -3638,21 +4243,21 @@ async function runSessionsTranscript(idOrFile, outPath) {
|
|
|
3638
4243
|
return;
|
|
3639
4244
|
}
|
|
3640
4245
|
const resolved = outPath === "." ? defaultName : outPath;
|
|
3641
|
-
await
|
|
3642
|
-
await
|
|
4246
|
+
await fs17.mkdir(path11.dirname(path11.resolve(resolved)), { recursive: true });
|
|
4247
|
+
await fs17.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
3643
4248
|
process.stdout.write(`Wrote ${resolved}
|
|
3644
4249
|
`);
|
|
3645
4250
|
}
|
|
3646
4251
|
async function readBundleFileIfExists(arg) {
|
|
3647
4252
|
try {
|
|
3648
|
-
const stat4 = await
|
|
4253
|
+
const stat4 = await fs17.stat(arg);
|
|
3649
4254
|
if (!stat4.isFile()) {
|
|
3650
4255
|
return null;
|
|
3651
4256
|
}
|
|
3652
4257
|
} catch {
|
|
3653
4258
|
return null;
|
|
3654
4259
|
}
|
|
3655
|
-
const text = await
|
|
4260
|
+
const text = await fs17.readFile(arg, "utf8");
|
|
3656
4261
|
try {
|
|
3657
4262
|
return { raw: JSON.parse(text) };
|
|
3658
4263
|
} catch (err) {
|
|
@@ -3679,9 +4284,9 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
3679
4284
|
}
|
|
3680
4285
|
let cwdOverride;
|
|
3681
4286
|
if (opts.cwd !== void 0) {
|
|
3682
|
-
const resolved =
|
|
4287
|
+
const resolved = path11.resolve(opts.cwd);
|
|
3683
4288
|
try {
|
|
3684
|
-
const stat4 = await
|
|
4289
|
+
const stat4 = await fs17.stat(resolved);
|
|
3685
4290
|
if (!stat4.isDirectory()) {
|
|
3686
4291
|
process.stderr.write(`--cwd ${resolved} is not a directory
|
|
3687
4292
|
`);
|
|
@@ -3698,7 +4303,7 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
3698
4303
|
if (file === "-") {
|
|
3699
4304
|
body = await readStdin();
|
|
3700
4305
|
} else {
|
|
3701
|
-
body = await
|
|
4306
|
+
body = await fs17.readFile(file, "utf8");
|
|
3702
4307
|
}
|
|
3703
4308
|
let bundle;
|
|
3704
4309
|
try {
|
|
@@ -3709,17 +4314,18 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
3709
4314
|
process.exit(1);
|
|
3710
4315
|
}
|
|
3711
4316
|
if (opts.info === true) {
|
|
3712
|
-
const inspectConfig = await
|
|
4317
|
+
const inspectConfig = await loadConfig();
|
|
3713
4318
|
printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
|
|
3714
4319
|
return;
|
|
3715
4320
|
}
|
|
3716
4321
|
const config = await loadConfig();
|
|
4322
|
+
const serviceToken = await loadServiceToken();
|
|
3717
4323
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3718
4324
|
const response = await fetch(`${baseUrl}/v1/sessions/import`, {
|
|
3719
4325
|
method: "POST",
|
|
3720
4326
|
headers: {
|
|
3721
4327
|
"Content-Type": "application/json",
|
|
3722
|
-
Authorization: `Bearer ${
|
|
4328
|
+
Authorization: `Bearer ${serviceToken}`
|
|
3723
4329
|
},
|
|
3724
4330
|
body: JSON.stringify({
|
|
3725
4331
|
bundle,
|
|
@@ -3813,6 +4419,7 @@ var init_sessions = __esm({
|
|
|
3813
4419
|
"src/cli/commands/sessions.ts"() {
|
|
3814
4420
|
"use strict";
|
|
3815
4421
|
init_config();
|
|
4422
|
+
init_service_token();
|
|
3816
4423
|
init_bundle();
|
|
3817
4424
|
init_transcript();
|
|
3818
4425
|
init_session_row();
|
|
@@ -4110,7 +4717,7 @@ var init_update_check = __esm({
|
|
|
4110
4717
|
});
|
|
4111
4718
|
|
|
4112
4719
|
// src/tui/discovery.ts
|
|
4113
|
-
async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
4720
|
+
async function listSessions(config, serviceToken, opts = {}, fetchImpl = fetch) {
|
|
4114
4721
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
4115
4722
|
const url = new URL(`${base}/v1/sessions`);
|
|
4116
4723
|
if (opts.cwd) {
|
|
@@ -4120,7 +4727,7 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
4120
4727
|
url.searchParams.set("all", "true");
|
|
4121
4728
|
}
|
|
4122
4729
|
const response = await fetchImpl(url.toString(), {
|
|
4123
|
-
headers: { Authorization: `Bearer ${
|
|
4730
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
4124
4731
|
});
|
|
4125
4732
|
if (!response.ok) {
|
|
4126
4733
|
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
@@ -4144,21 +4751,21 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
4144
4751
|
importedFromUpstreamSessionId: s.importedFromUpstreamSessionId
|
|
4145
4752
|
}));
|
|
4146
4753
|
}
|
|
4147
|
-
async function killSession(config, id, fetchImpl = fetch) {
|
|
4754
|
+
async function killSession(config, serviceToken, id, fetchImpl = fetch) {
|
|
4148
4755
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
4149
4756
|
const response = await fetchImpl(`${base}/v1/sessions/${id}/kill`, {
|
|
4150
4757
|
method: "POST",
|
|
4151
|
-
headers: { Authorization: `Bearer ${
|
|
4758
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
4152
4759
|
});
|
|
4153
4760
|
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
4154
4761
|
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
4155
4762
|
}
|
|
4156
4763
|
}
|
|
4157
|
-
async function deleteSession(config, id, fetchImpl = fetch) {
|
|
4764
|
+
async function deleteSession(config, serviceToken, id, fetchImpl = fetch) {
|
|
4158
4765
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
4159
4766
|
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
4160
4767
|
method: "DELETE",
|
|
4161
|
-
headers: { Authorization: `Bearer ${
|
|
4768
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
4162
4769
|
});
|
|
4163
4770
|
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
4164
4771
|
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
@@ -4348,6 +4955,10 @@ async function pickSession(term, opts) {
|
|
|
4348
4955
|
const indicatorRow = () => startRow + 3 + viewportSize;
|
|
4349
4956
|
const sessionRow = (sessionIdx) => startRow + 3 + (sessionIdx - scrollOffset);
|
|
4350
4957
|
const renderFromScratch = () => {
|
|
4958
|
+
if (mode === "help") {
|
|
4959
|
+
renderHelp();
|
|
4960
|
+
return;
|
|
4961
|
+
}
|
|
4351
4962
|
computeLayout();
|
|
4352
4963
|
adjustScroll();
|
|
4353
4964
|
startRow = 1;
|
|
@@ -4362,6 +4973,21 @@ async function pickSession(term, opts) {
|
|
|
4362
4973
|
paintIndicator();
|
|
4363
4974
|
term("\n");
|
|
4364
4975
|
};
|
|
4976
|
+
const renderHelp = () => {
|
|
4977
|
+
term.moveTo(1, 1).eraseDisplayBelow();
|
|
4978
|
+
term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
|
|
4979
|
+
for (const entry of HELP_ENTRIES) {
|
|
4980
|
+
if (entry === null) {
|
|
4981
|
+
term("\n");
|
|
4982
|
+
continue;
|
|
4983
|
+
}
|
|
4984
|
+
const [keys, desc] = entry;
|
|
4985
|
+
term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
|
|
4986
|
+
term.noFormat(desc)("\n");
|
|
4987
|
+
}
|
|
4988
|
+
term("\n");
|
|
4989
|
+
term.dim.noFormat(" press any key to dismiss")("\n");
|
|
4990
|
+
};
|
|
4365
4991
|
const repaintNewItem = () => {
|
|
4366
4992
|
term.moveTo(1, startRow).eraseLineAfter();
|
|
4367
4993
|
paintNewItem();
|
|
@@ -4408,7 +5034,7 @@ async function pickSession(term, opts) {
|
|
|
4408
5034
|
};
|
|
4409
5035
|
const refresh = async (preferredId) => {
|
|
4410
5036
|
try {
|
|
4411
|
-
const next = await listSessions(opts.config);
|
|
5037
|
+
const next = await listSessions(opts.config, opts.serviceToken);
|
|
4412
5038
|
allSessions = sortSessions(next);
|
|
4413
5039
|
applyFilter();
|
|
4414
5040
|
if (preferredId !== void 0) {
|
|
@@ -4439,9 +5065,9 @@ async function pickSession(term, opts) {
|
|
|
4439
5065
|
paintIndicator();
|
|
4440
5066
|
try {
|
|
4441
5067
|
if (kind === "kill") {
|
|
4442
|
-
await killSession(opts.config, target.sessionId);
|
|
5068
|
+
await killSession(opts.config, opts.serviceToken, target.sessionId);
|
|
4443
5069
|
} else {
|
|
4444
|
-
await deleteSession(opts.config, target.sessionId);
|
|
5070
|
+
await deleteSession(opts.config, opts.serviceToken, target.sessionId);
|
|
4445
5071
|
}
|
|
4446
5072
|
mode = "normal";
|
|
4447
5073
|
pendingAction = null;
|
|
@@ -4492,6 +5118,16 @@ async function pickSession(term, opts) {
|
|
|
4492
5118
|
if (mode === "busy") {
|
|
4493
5119
|
return;
|
|
4494
5120
|
}
|
|
5121
|
+
if (mode === "help") {
|
|
5122
|
+
if (name === "CTRL_C") {
|
|
5123
|
+
cleanup();
|
|
5124
|
+
resolve5({ kind: "abort" });
|
|
5125
|
+
return;
|
|
5126
|
+
}
|
|
5127
|
+
mode = "normal";
|
|
5128
|
+
renderFromScratch();
|
|
5129
|
+
return;
|
|
5130
|
+
}
|
|
4495
5131
|
if (mode === "confirm-kill" || mode === "confirm-delete") {
|
|
4496
5132
|
if (data?.isCharacter && (name === "y" || name === "Y")) {
|
|
4497
5133
|
const kind = mode === "confirm-kill" ? "kill" : "delete";
|
|
@@ -4507,6 +5143,11 @@ async function pickSession(term, opts) {
|
|
|
4507
5143
|
return;
|
|
4508
5144
|
}
|
|
4509
5145
|
clearTransient();
|
|
5146
|
+
if (!searchActive && data?.isCharacter && name === "?") {
|
|
5147
|
+
mode = "help";
|
|
5148
|
+
renderHelp();
|
|
5149
|
+
return;
|
|
5150
|
+
}
|
|
4510
5151
|
if (searchActive) {
|
|
4511
5152
|
if (data?.isCharacter) {
|
|
4512
5153
|
searchTerm += name;
|
|
@@ -4655,6 +5296,7 @@ async function pickSession(term, opts) {
|
|
|
4655
5296
|
}
|
|
4656
5297
|
case "ESCAPE":
|
|
4657
5298
|
case "CTRL_C":
|
|
5299
|
+
case "CTRL_D":
|
|
4658
5300
|
cleanup();
|
|
4659
5301
|
resolve5({ kind: "abort" });
|
|
4660
5302
|
return;
|
|
@@ -4696,7 +5338,7 @@ function matchesSearch(s, term) {
|
|
|
4696
5338
|
}
|
|
4697
5339
|
return false;
|
|
4698
5340
|
}
|
|
4699
|
-
var ROW_PREFIX_WIDTH;
|
|
5341
|
+
var ROW_PREFIX_WIDTH, HELP_KEYS_WIDTH, HELP_ENTRIES;
|
|
4700
5342
|
var init_picker = __esm({
|
|
4701
5343
|
"src/tui/picker.ts"() {
|
|
4702
5344
|
"use strict";
|
|
@@ -4705,13 +5347,31 @@ var init_picker = __esm({
|
|
|
4705
5347
|
init_session();
|
|
4706
5348
|
init_discovery();
|
|
4707
5349
|
ROW_PREFIX_WIDTH = 2;
|
|
5350
|
+
HELP_KEYS_WIDTH = 20;
|
|
5351
|
+
HELP_ENTRIES = [
|
|
5352
|
+
["\u2191 / \u2193 or n / p", "navigate"],
|
|
5353
|
+
["PgUp / PgDn", "page up / page down"],
|
|
5354
|
+
["Home / End", "first / last"],
|
|
5355
|
+
["Enter", "open selected session (or create new)"],
|
|
5356
|
+
null,
|
|
5357
|
+
["/", "search sessions"],
|
|
5358
|
+
["o", "toggle cwd-only filter"],
|
|
5359
|
+
["r", "refresh from daemon"],
|
|
5360
|
+
null,
|
|
5361
|
+
["k", "kill the selected live session"],
|
|
5362
|
+
["d", "delete the selected cold session"],
|
|
5363
|
+
null,
|
|
5364
|
+
["c", "create new session"],
|
|
5365
|
+
["?", "toggle this help"],
|
|
5366
|
+
["q / Esc / ^C / ^D", "quit picker (detach)"]
|
|
5367
|
+
];
|
|
4708
5368
|
}
|
|
4709
5369
|
});
|
|
4710
5370
|
|
|
4711
5371
|
// src/tui/attachments.ts
|
|
4712
|
-
import
|
|
5372
|
+
import path12 from "path";
|
|
4713
5373
|
function mimeFromExtension(p) {
|
|
4714
|
-
return EXTENSION_TO_MIME[
|
|
5374
|
+
return EXTENSION_TO_MIME[path12.extname(p).toLowerCase()] ?? null;
|
|
4715
5375
|
}
|
|
4716
5376
|
function isSupportedImagePath(p) {
|
|
4717
5377
|
return mimeFromExtension(p) !== null;
|
|
@@ -4820,6 +5480,10 @@ var init_attachments = __esm({
|
|
|
4820
5480
|
// src/tui/screen.ts
|
|
4821
5481
|
import stringWidth from "string-width";
|
|
4822
5482
|
import wrapAnsi from "wrap-ansi";
|
|
5483
|
+
function matchBareUrl(text) {
|
|
5484
|
+
const stripped = text.replace(/\r\n?$|\n$/, "");
|
|
5485
|
+
return BARE_URL_RE.test(stripped) ? stripped : null;
|
|
5486
|
+
}
|
|
4823
5487
|
function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null) {
|
|
4824
5488
|
const active = activeCol === null ? "" : `a${activeCol}`;
|
|
4825
5489
|
if (!line) {
|
|
@@ -5283,6 +5947,14 @@ function mapKeyName(name) {
|
|
|
5283
5947
|
case "ALT_ENTER":
|
|
5284
5948
|
case "META_ENTER":
|
|
5285
5949
|
return "alt-enter";
|
|
5950
|
+
case "ALT_B":
|
|
5951
|
+
case "META_B":
|
|
5952
|
+
return "alt-b";
|
|
5953
|
+
case "ALT_F":
|
|
5954
|
+
case "META_F":
|
|
5955
|
+
return "alt-f";
|
|
5956
|
+
case "CTRL_T":
|
|
5957
|
+
return "ctrl-t";
|
|
5286
5958
|
case "SHIFT_TAB":
|
|
5287
5959
|
return "shift-tab";
|
|
5288
5960
|
case "TAB":
|
|
@@ -5315,6 +5987,8 @@ function mapKeyName(name) {
|
|
|
5315
5987
|
return "ctrl-e";
|
|
5316
5988
|
case "CTRL_F":
|
|
5317
5989
|
return "ctrl-f";
|
|
5990
|
+
case "CTRL_G":
|
|
5991
|
+
return "ctrl-g";
|
|
5318
5992
|
case "CTRL_K":
|
|
5319
5993
|
return "ctrl-k";
|
|
5320
5994
|
case "CTRL_L":
|
|
@@ -5343,7 +6017,7 @@ function mapKeyName(name) {
|
|
|
5343
6017
|
return null;
|
|
5344
6018
|
}
|
|
5345
6019
|
}
|
|
5346
|
-
var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
6020
|
+
var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_HELP_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
5347
6021
|
var init_screen = __esm({
|
|
5348
6022
|
"src/tui/screen.ts"() {
|
|
5349
6023
|
"use strict";
|
|
@@ -5357,11 +6031,13 @@ var init_screen = __esm({
|
|
|
5357
6031
|
MAX_PROMPT_ROWS = 8;
|
|
5358
6032
|
MAX_QUEUED_ROWS = 5;
|
|
5359
6033
|
MAX_PERMISSION_ROWS = 12;
|
|
6034
|
+
MAX_HELP_ROWS = 30;
|
|
5360
6035
|
MAX_COMPLETION_ROWS = 6;
|
|
5361
6036
|
MAX_CHIP_ROWS = 4;
|
|
5362
6037
|
CONFIRM_PROMPT_ROWS = 2;
|
|
5363
6038
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
5364
6039
|
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
6040
|
+
BARE_URL_RE = /^(https?|ftp):\/\/\S+$/;
|
|
5365
6041
|
Screen = class {
|
|
5366
6042
|
term;
|
|
5367
6043
|
dispatcher;
|
|
@@ -5418,6 +6094,7 @@ var init_screen = __esm({
|
|
|
5418
6094
|
lastFrameH = 0;
|
|
5419
6095
|
permissionPrompt = null;
|
|
5420
6096
|
confirmPrompt = null;
|
|
6097
|
+
helpPrompt = null;
|
|
5421
6098
|
completions = [];
|
|
5422
6099
|
// Scrollback offset: 0 = pinned to bottom (live), N = N wrapped lines
|
|
5423
6100
|
// above the bottom. Mouse wheel and PgUp/PgDn adjust this; new content
|
|
@@ -5445,8 +6122,8 @@ var init_screen = __esm({
|
|
|
5445
6122
|
bannerSearchIndicator = null;
|
|
5446
6123
|
banner = {
|
|
5447
6124
|
status: "ready",
|
|
5448
|
-
|
|
5449
|
-
hint: "\u21E7\u21E5
|
|
6125
|
+
currentMode: void 0,
|
|
6126
|
+
hint: "\u21E7\u21E5 mode \xB7 \u2303P pick \xB7 \u2303G guide \xB7 \u2303D detach",
|
|
5450
6127
|
queued: 0
|
|
5451
6128
|
};
|
|
5452
6129
|
sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -5591,7 +6268,10 @@ var init_screen = __esm({
|
|
|
5591
6268
|
}
|
|
5592
6269
|
const startIdx = text.indexOf(startMarker);
|
|
5593
6270
|
if (startIdx === -1) {
|
|
5594
|
-
|
|
6271
|
+
const url = matchBareUrl(text);
|
|
6272
|
+
if (url !== null) {
|
|
6273
|
+
this.onKey([{ type: "paste", text: url }]);
|
|
6274
|
+
} else if (this.terminalKitStdinHandler) {
|
|
5595
6275
|
this.terminalKitStdinHandler(Buffer.from(text, "binary"));
|
|
5596
6276
|
}
|
|
5597
6277
|
return;
|
|
@@ -5821,11 +6501,11 @@ var init_screen = __esm({
|
|
|
5821
6501
|
return;
|
|
5822
6502
|
}
|
|
5823
6503
|
this.lastWindowTitle = clean;
|
|
5824
|
-
process.stdout.write(`\x1B]
|
|
6504
|
+
process.stdout.write(`\x1B]0;${clean}\x1B\\`);
|
|
5825
6505
|
}
|
|
5826
6506
|
clearWindowTitle() {
|
|
5827
6507
|
this.lastWindowTitle = null;
|
|
5828
|
-
process.stdout.write("\x1B]
|
|
6508
|
+
process.stdout.write("\x1B]0;\x1B\\");
|
|
5829
6509
|
}
|
|
5830
6510
|
setBanner(banner) {
|
|
5831
6511
|
this.banner = { ...this.banner, ...banner };
|
|
@@ -5833,6 +6513,9 @@ var init_screen = __esm({
|
|
|
5833
6513
|
this.drawBanner();
|
|
5834
6514
|
this.placeCursor();
|
|
5835
6515
|
}
|
|
6516
|
+
currentModeId() {
|
|
6517
|
+
return this.banner.currentMode;
|
|
6518
|
+
}
|
|
5836
6519
|
// OSC 9;4 progress-bar control. State 3 = indeterminate (pulsing
|
|
5837
6520
|
// taskbar / dock badge while a turn is running); state 0 = remove.
|
|
5838
6521
|
// ConEmu-flavor sequence — supported by Windows Terminal, WezTerm,
|
|
@@ -6003,6 +6686,16 @@ var init_screen = __esm({
|
|
|
6003
6686
|
this.confirmPrompt = spec ? { ...spec } : null;
|
|
6004
6687
|
this.repaint();
|
|
6005
6688
|
}
|
|
6689
|
+
// Multi-row help cheatsheet that takes over the prompt area. Used by
|
|
6690
|
+
// the ^G hotkey to surface every binding without dropping the user
|
|
6691
|
+
// out of the session. Pass null to dismiss.
|
|
6692
|
+
setHelpPrompt(spec) {
|
|
6693
|
+
this.helpPrompt = spec ? { ...spec, entries: [...spec.entries] } : null;
|
|
6694
|
+
this.repaint();
|
|
6695
|
+
}
|
|
6696
|
+
isHelpPromptActive() {
|
|
6697
|
+
return this.helpPrompt !== null;
|
|
6698
|
+
}
|
|
6006
6699
|
// Slash-command completion list shown directly above the separator. App
|
|
6007
6700
|
// calls this after each keystroke; pass [] to dismiss. Suppressed when
|
|
6008
6701
|
// the permission modal is active (the modal owns the prompt area).
|
|
@@ -6538,7 +7231,7 @@ var init_screen = __esm({
|
|
|
6538
7231
|
this.repaint();
|
|
6539
7232
|
}
|
|
6540
7233
|
completionRows() {
|
|
6541
|
-
if (this.permissionPrompt) {
|
|
7234
|
+
if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
|
|
6542
7235
|
return 0;
|
|
6543
7236
|
}
|
|
6544
7237
|
return Math.min(MAX_COMPLETION_ROWS, this.completions.length);
|
|
@@ -6682,6 +7375,10 @@ var init_screen = __esm({
|
|
|
6682
7375
|
this.drawConfirmPrompt();
|
|
6683
7376
|
return;
|
|
6684
7377
|
}
|
|
7378
|
+
if (this.helpPrompt) {
|
|
7379
|
+
this.drawHelpPrompt();
|
|
7380
|
+
return;
|
|
7381
|
+
}
|
|
6685
7382
|
const w = this.term.width;
|
|
6686
7383
|
const room = Math.max(1, w - 2);
|
|
6687
7384
|
const state = this.dispatcher.state();
|
|
@@ -6731,6 +7428,58 @@ var init_screen = __esm({
|
|
|
6731
7428
|
this.term.dim(` ${truncate(spec.hint, w - 2)}`);
|
|
6732
7429
|
});
|
|
6733
7430
|
}
|
|
7431
|
+
drawHelpPrompt() {
|
|
7432
|
+
const spec = this.helpPrompt;
|
|
7433
|
+
if (!spec) {
|
|
7434
|
+
return;
|
|
7435
|
+
}
|
|
7436
|
+
const w = this.term.width;
|
|
7437
|
+
const rows = this.helpRows();
|
|
7438
|
+
const top = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
7439
|
+
let row = top;
|
|
7440
|
+
const writeRow = (sig, paint) => {
|
|
7441
|
+
if (row >= top + rows) {
|
|
7442
|
+
return;
|
|
7443
|
+
}
|
|
7444
|
+
this.paintRow(row, sig, paint);
|
|
7445
|
+
row += 1;
|
|
7446
|
+
};
|
|
7447
|
+
writeRow(`help|t|${w}|${spec.title}`, () => {
|
|
7448
|
+
this.term.brightYellow(` \u2753 ${truncate(spec.title, w - 5)}`);
|
|
7449
|
+
});
|
|
7450
|
+
const keysWidth = Math.min(
|
|
7451
|
+
24,
|
|
7452
|
+
Math.max(
|
|
7453
|
+
...spec.entries.map((e) => e === null ? 0 : e[0].length),
|
|
7454
|
+
4
|
|
7455
|
+
)
|
|
7456
|
+
);
|
|
7457
|
+
for (const entry of spec.entries) {
|
|
7458
|
+
if (row >= top + rows - 1) {
|
|
7459
|
+
break;
|
|
7460
|
+
}
|
|
7461
|
+
if (entry === null) {
|
|
7462
|
+
writeRow(`help|sep|${w}|${row}`, () => void 0);
|
|
7463
|
+
continue;
|
|
7464
|
+
}
|
|
7465
|
+
const [keys, desc] = entry;
|
|
7466
|
+
const paddedKeys = keys.padEnd(keysWidth);
|
|
7467
|
+
writeRow(`help|e|${w}|${keys}|${desc}`, () => {
|
|
7468
|
+
this.term(" ");
|
|
7469
|
+
this.term.brightCyan.noFormat(paddedKeys);
|
|
7470
|
+
this.term.noFormat(` ${truncate(desc, w - 2 - keysWidth - 1)}`);
|
|
7471
|
+
});
|
|
7472
|
+
}
|
|
7473
|
+
writeRow(`help|hint|${w}|${spec.hint}`, () => {
|
|
7474
|
+
this.term.dim(` ${truncate(spec.hint, w - 2)}`);
|
|
7475
|
+
});
|
|
7476
|
+
}
|
|
7477
|
+
helpRows() {
|
|
7478
|
+
if (!this.helpPrompt) {
|
|
7479
|
+
return 0;
|
|
7480
|
+
}
|
|
7481
|
+
return Math.min(MAX_HELP_ROWS, 2 + this.helpPrompt.entries.length);
|
|
7482
|
+
}
|
|
6734
7483
|
drawPermissionPrompt() {
|
|
6735
7484
|
const spec = this.permissionPrompt;
|
|
6736
7485
|
if (!spec) {
|
|
@@ -6785,10 +7534,9 @@ var init_screen = __esm({
|
|
|
6785
7534
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
6786
7535
|
const right = this.bannerRightContent();
|
|
6787
7536
|
const rightSig = right ? `${right.kind}|${right.text}` : "";
|
|
6788
|
-
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.
|
|
7537
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
|
|
6789
7538
|
this.paintRow(row, sig, () => {
|
|
6790
7539
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
6791
|
-
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
6792
7540
|
if (this.banner.status === "busy") {
|
|
6793
7541
|
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
6794
7542
|
if (elapsedStr) {
|
|
@@ -6807,13 +7555,11 @@ var init_screen = __esm({
|
|
|
6807
7555
|
if (this.scrollOffset > 0) {
|
|
6808
7556
|
this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
|
|
6809
7557
|
}
|
|
6810
|
-
this.
|
|
6811
|
-
|
|
6812
|
-
this.
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
}
|
|
6816
|
-
this.term(" \xB7 ").dim(this.banner.hint);
|
|
7558
|
+
const hint = this.banner.currentMode ? this.banner.hint.replace(
|
|
7559
|
+
"\u21E7\u21E5 mode",
|
|
7560
|
+
`\u21E7\u21E5 mode(${this.banner.currentMode})`
|
|
7561
|
+
) : this.banner.hint;
|
|
7562
|
+
this.term(" \xB7 ").dim(hint);
|
|
6817
7563
|
if (right) {
|
|
6818
7564
|
const visibleWidth = stringWidth(right.text);
|
|
6819
7565
|
const col = Math.max(1, w - visibleWidth + 1);
|
|
@@ -6843,6 +7589,12 @@ var init_screen = __esm({
|
|
|
6843
7589
|
this.term.moveTo(2, top2);
|
|
6844
7590
|
return;
|
|
6845
7591
|
}
|
|
7592
|
+
if (this.helpPrompt) {
|
|
7593
|
+
const rows = this.helpRows();
|
|
7594
|
+
const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
7595
|
+
this.term.moveTo(2, top2);
|
|
7596
|
+
return;
|
|
7597
|
+
}
|
|
6846
7598
|
if (this.scrollbackSearch) {
|
|
6847
7599
|
this.term.hideCursor(true);
|
|
6848
7600
|
return;
|
|
@@ -6869,6 +7621,9 @@ var init_screen = __esm({
|
|
|
6869
7621
|
if (this.confirmPrompt) {
|
|
6870
7622
|
return CONFIRM_PROMPT_ROWS;
|
|
6871
7623
|
}
|
|
7624
|
+
if (this.helpPrompt) {
|
|
7625
|
+
return this.helpRows();
|
|
7626
|
+
}
|
|
6872
7627
|
const w = this.term.width;
|
|
6873
7628
|
const room = Math.max(1, w - 2);
|
|
6874
7629
|
const state = this.dispatcher.state();
|
|
@@ -7156,7 +7911,7 @@ var init_input = __esm({
|
|
|
7156
7911
|
this.retreatHistorySearch();
|
|
7157
7912
|
return [];
|
|
7158
7913
|
}
|
|
7159
|
-
if (event.name === "escape") {
|
|
7914
|
+
if (event.name === "escape" || event.name === "ctrl-c") {
|
|
7160
7915
|
this.cancelHistorySearch();
|
|
7161
7916
|
return [];
|
|
7162
7917
|
}
|
|
@@ -7227,6 +7982,14 @@ var init_input = __esm({
|
|
|
7227
7982
|
case "ctrl-f":
|
|
7228
7983
|
this.moveRight();
|
|
7229
7984
|
return [];
|
|
7985
|
+
case "ctrl-g":
|
|
7986
|
+
return [{ type: "show-help" }];
|
|
7987
|
+
case "alt-b":
|
|
7988
|
+
this.moveWordBackward();
|
|
7989
|
+
return [];
|
|
7990
|
+
case "alt-f":
|
|
7991
|
+
this.moveWordForward();
|
|
7992
|
+
return [];
|
|
7230
7993
|
case "ctrl-k":
|
|
7231
7994
|
this.killToEnd();
|
|
7232
7995
|
return [];
|
|
@@ -7252,6 +8015,8 @@ var init_input = __esm({
|
|
|
7252
8015
|
return [{ type: "redraw" }];
|
|
7253
8016
|
case "ctrl-p":
|
|
7254
8017
|
return [{ type: "switch-session" }];
|
|
8018
|
+
case "ctrl-t":
|
|
8019
|
+
return [{ type: "next-live-session" }];
|
|
7255
8020
|
case "ctrl-r":
|
|
7256
8021
|
return this.startHistorySearch();
|
|
7257
8022
|
case "ctrl-s":
|
|
@@ -7428,6 +8193,44 @@ var init_input = __esm({
|
|
|
7428
8193
|
this.col = 0;
|
|
7429
8194
|
}
|
|
7430
8195
|
}
|
|
8196
|
+
moveWordBackward() {
|
|
8197
|
+
if (this.col === 0) {
|
|
8198
|
+
if (this.row === 0) {
|
|
8199
|
+
return;
|
|
8200
|
+
}
|
|
8201
|
+
this.row -= 1;
|
|
8202
|
+
this.col = this.currentLine().length;
|
|
8203
|
+
return;
|
|
8204
|
+
}
|
|
8205
|
+
const line = this.currentLine();
|
|
8206
|
+
let i = this.col;
|
|
8207
|
+
while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
|
|
8208
|
+
i -= 1;
|
|
8209
|
+
}
|
|
8210
|
+
while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
|
|
8211
|
+
i -= 1;
|
|
8212
|
+
}
|
|
8213
|
+
this.col = i;
|
|
8214
|
+
}
|
|
8215
|
+
moveWordForward() {
|
|
8216
|
+
const line = this.currentLine();
|
|
8217
|
+
if (this.col >= line.length) {
|
|
8218
|
+
if (this.row >= this.buffer.length - 1) {
|
|
8219
|
+
return;
|
|
8220
|
+
}
|
|
8221
|
+
this.row += 1;
|
|
8222
|
+
this.col = 0;
|
|
8223
|
+
return;
|
|
8224
|
+
}
|
|
8225
|
+
let i = this.col;
|
|
8226
|
+
while (i < line.length && /\s/.test(line[i] ?? "")) {
|
|
8227
|
+
i += 1;
|
|
8228
|
+
}
|
|
8229
|
+
while (i < line.length && !/\s/.test(line[i] ?? "")) {
|
|
8230
|
+
i += 1;
|
|
8231
|
+
}
|
|
8232
|
+
this.col = i;
|
|
8233
|
+
}
|
|
7431
8234
|
// Up walks the navigation stack from newest to oldest: pending queue
|
|
7432
8235
|
// items first (so the user can edit something they just enqueued),
|
|
7433
8236
|
// then prompt history. Cursor movement within a multi-line buffer
|
|
@@ -7741,9 +8544,9 @@ var init_input = __esm({
|
|
|
7741
8544
|
|
|
7742
8545
|
// src/tui/clipboard.ts
|
|
7743
8546
|
import { spawn as nodeSpawn } from "child_process";
|
|
7744
|
-
import
|
|
8547
|
+
import fs18 from "fs/promises";
|
|
7745
8548
|
import os4 from "os";
|
|
7746
|
-
import
|
|
8549
|
+
import path13 from "path";
|
|
7747
8550
|
async function readClipboard(envIn = {}) {
|
|
7748
8551
|
const env = { ...defaultEnv, ...envIn };
|
|
7749
8552
|
if (env.platform === "darwin") {
|
|
@@ -7758,7 +8561,7 @@ async function readClipboard(envIn = {}) {
|
|
|
7758
8561
|
};
|
|
7759
8562
|
}
|
|
7760
8563
|
async function readMacOS(env) {
|
|
7761
|
-
const tmpPath =
|
|
8564
|
+
const tmpPath = path13.join(
|
|
7762
8565
|
env.tmpdir(),
|
|
7763
8566
|
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
7764
8567
|
);
|
|
@@ -7782,7 +8585,7 @@ async function readMacOS(env) {
|
|
|
7782
8585
|
return img;
|
|
7783
8586
|
}
|
|
7784
8587
|
} catch {
|
|
7785
|
-
await
|
|
8588
|
+
await fs18.unlink(tmpPath).catch(() => void 0);
|
|
7786
8589
|
}
|
|
7787
8590
|
try {
|
|
7788
8591
|
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
@@ -7870,9 +8673,9 @@ async function which(env, cmd) {
|
|
|
7870
8673
|
}
|
|
7871
8674
|
async function readFileAsAttachment(p, unlinkAfter) {
|
|
7872
8675
|
try {
|
|
7873
|
-
const buf = await
|
|
8676
|
+
const buf = await fs18.readFile(p);
|
|
7874
8677
|
if (unlinkAfter) {
|
|
7875
|
-
await
|
|
8678
|
+
await fs18.unlink(p).catch(() => void 0);
|
|
7876
8679
|
}
|
|
7877
8680
|
if (buf.length === 0) {
|
|
7878
8681
|
return { ok: false, reason: "no image on clipboard" };
|
|
@@ -8062,6 +8865,7 @@ function formatEvent(event) {
|
|
|
8062
8865
|
case "usage-update":
|
|
8063
8866
|
return [];
|
|
8064
8867
|
case "available-commands":
|
|
8868
|
+
case "available-modes":
|
|
8065
8869
|
return [];
|
|
8066
8870
|
case "session-info":
|
|
8067
8871
|
return [];
|
|
@@ -8360,17 +9164,18 @@ var init_format = __esm({
|
|
|
8360
9164
|
import { appendFileSync, statSync, renameSync } from "fs";
|
|
8361
9165
|
import { nanoid as nanoid3 } from "nanoid";
|
|
8362
9166
|
import termkit from "terminal-kit";
|
|
8363
|
-
import
|
|
8364
|
-
import
|
|
9167
|
+
import fs19 from "fs/promises";
|
|
9168
|
+
import path14 from "path";
|
|
8365
9169
|
async function runTuiApp(opts) {
|
|
8366
|
-
const config = await
|
|
9170
|
+
const config = await loadConfig();
|
|
9171
|
+
const serviceToken = await ensureServiceToken();
|
|
8367
9172
|
logMaxBytes = config.tui.logMaxBytes;
|
|
8368
9173
|
await ensureDaemonReachable(config);
|
|
8369
9174
|
const term = termkit.terminal;
|
|
8370
9175
|
const exitHint = {};
|
|
8371
9176
|
let nextOpts = opts;
|
|
8372
9177
|
while (nextOpts !== null) {
|
|
8373
|
-
nextOpts = await runSession(term, config, nextOpts, exitHint);
|
|
9178
|
+
nextOpts = await runSession(term, config, serviceToken, nextOpts, exitHint);
|
|
8374
9179
|
}
|
|
8375
9180
|
const pendingUpdate = await getPendingUpdate();
|
|
8376
9181
|
if (pendingUpdate) {
|
|
@@ -8383,8 +9188,8 @@ async function runTuiApp(opts) {
|
|
|
8383
9188
|
`);
|
|
8384
9189
|
}
|
|
8385
9190
|
}
|
|
8386
|
-
async function runSession(term, config, opts, exitHint) {
|
|
8387
|
-
const ctx = await resolveSession(term, config, opts);
|
|
9191
|
+
async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
9192
|
+
const ctx = await resolveSession(term, config, serviceToken, opts);
|
|
8388
9193
|
if (!ctx) {
|
|
8389
9194
|
term.grabInput(false);
|
|
8390
9195
|
process.exit(0);
|
|
@@ -8393,7 +9198,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8393
9198
|
term.brightYellow(launchLabel)("\n");
|
|
8394
9199
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
8395
9200
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
8396
|
-
const subprotocols = ["acp.v1", `hydra-acp-token.${
|
|
9201
|
+
const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
|
|
8397
9202
|
let onReconnect = null;
|
|
8398
9203
|
let onDisconnectHook = null;
|
|
8399
9204
|
const stream = new ResilientWsStream({
|
|
@@ -8462,20 +9267,31 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8462
9267
|
screenRef.setBanner({ status: "ready", elapsedMs: void 0 });
|
|
8463
9268
|
}
|
|
8464
9269
|
}
|
|
8465
|
-
|
|
8466
|
-
tickWorker();
|
|
8467
|
-
}
|
|
9270
|
+
void delta;
|
|
8468
9271
|
};
|
|
8469
9272
|
let screenRef = null;
|
|
8470
9273
|
let dispatcherRef = null;
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
9274
|
+
let lastSeenMessageId = void 0;
|
|
9275
|
+
let reconnectReplayBuffer = null;
|
|
9276
|
+
const STATE_UPDATE_KINDS2 = /* @__PURE__ */ new Set([
|
|
9277
|
+
"session_info_update",
|
|
9278
|
+
"current_model_update",
|
|
9279
|
+
"current_mode_update",
|
|
9280
|
+
"available_commands_update",
|
|
9281
|
+
"available_modes_update",
|
|
9282
|
+
"usage_update"
|
|
9283
|
+
]);
|
|
9284
|
+
const handleSessionUpdate = (params) => {
|
|
8475
9285
|
const { update } = params ?? {};
|
|
8476
9286
|
const event = mapUpdate(update);
|
|
8477
9287
|
debugLogUpdate(update, event);
|
|
8478
9288
|
const rawTag = update?.sessionUpdate;
|
|
9289
|
+
if (typeof rawTag === "string" && !STATE_UPDATE_KINDS2.has(rawTag)) {
|
|
9290
|
+
const u = update ?? {};
|
|
9291
|
+
if (typeof u.messageId === "string") {
|
|
9292
|
+
lastSeenMessageId = u.messageId;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
8479
9295
|
if (rawTag === "prompt_received") {
|
|
8480
9296
|
adjustPendingTurns(1);
|
|
8481
9297
|
} else if (event?.kind === "turn-complete") {
|
|
@@ -8487,6 +9303,16 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8487
9303
|
}
|
|
8488
9304
|
appendRender(event);
|
|
8489
9305
|
maybeDismissPermissionByToolUpdate(update);
|
|
9306
|
+
};
|
|
9307
|
+
conn.onNotification("session/update", (params) => {
|
|
9308
|
+
if (teardownStarted) {
|
|
9309
|
+
return;
|
|
9310
|
+
}
|
|
9311
|
+
if (reconnectReplayBuffer !== null) {
|
|
9312
|
+
reconnectReplayBuffer.push(params);
|
|
9313
|
+
return;
|
|
9314
|
+
}
|
|
9315
|
+
handleSessionUpdate(params);
|
|
8490
9316
|
});
|
|
8491
9317
|
conn.onNotification("hydra-acp/session_closed", () => {
|
|
8492
9318
|
if (teardownStarted) {
|
|
@@ -8500,6 +9326,75 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8500
9326
|
screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
|
|
8501
9327
|
}
|
|
8502
9328
|
});
|
|
9329
|
+
conn.onNotification("hydra-acp/prompt_queue_added", (params) => {
|
|
9330
|
+
if (teardownStarted) return;
|
|
9331
|
+
const p = params ?? {};
|
|
9332
|
+
if (typeof p.messageId !== "string") return;
|
|
9333
|
+
queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
|
|
9334
|
+
if (screenRef && dispatcherRef) {
|
|
9335
|
+
refreshQueueDisplay();
|
|
9336
|
+
}
|
|
9337
|
+
if (ownClientId !== void 0 && p.originator?.clientId === ownClientId) {
|
|
9338
|
+
const echo = pendingEchoes.shift();
|
|
9339
|
+
if (echo) {
|
|
9340
|
+
echo.messageId = p.messageId;
|
|
9341
|
+
ownPendingByMid.set(p.messageId, echo);
|
|
9342
|
+
}
|
|
9343
|
+
}
|
|
9344
|
+
});
|
|
9345
|
+
conn.onNotification("hydra-acp/prompt_queue_updated", (params) => {
|
|
9346
|
+
if (teardownStarted) return;
|
|
9347
|
+
const p = params ?? {};
|
|
9348
|
+
if (typeof p.messageId !== "string") return;
|
|
9349
|
+
if (!queueCache.has(p.messageId)) return;
|
|
9350
|
+
queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
|
|
9351
|
+
const pending = ownPendingByMid.get(p.messageId);
|
|
9352
|
+
if (pending) {
|
|
9353
|
+
const blocks = Array.isArray(p.prompt) ? p.prompt : [];
|
|
9354
|
+
let text = "";
|
|
9355
|
+
const attachments = [];
|
|
9356
|
+
for (const raw of blocks) {
|
|
9357
|
+
if (!raw || typeof raw !== "object") continue;
|
|
9358
|
+
const b = raw;
|
|
9359
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
9360
|
+
text += b.text;
|
|
9361
|
+
} else if (b.type === "image" && typeof b.data === "string" && typeof b.mimeType === "string") {
|
|
9362
|
+
attachments.push({
|
|
9363
|
+
data: b.data,
|
|
9364
|
+
mimeType: b.mimeType,
|
|
9365
|
+
sizeBytes: Math.floor(b.data.length * 3 / 4)
|
|
9366
|
+
});
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
9369
|
+
pending.text = text;
|
|
9370
|
+
pending.attachments = attachments;
|
|
9371
|
+
}
|
|
9372
|
+
if (screenRef && dispatcherRef) {
|
|
9373
|
+
refreshQueueDisplay();
|
|
9374
|
+
}
|
|
9375
|
+
});
|
|
9376
|
+
conn.onNotification("hydra-acp/prompt_queue_removed", (params) => {
|
|
9377
|
+
if (teardownStarted) return;
|
|
9378
|
+
const p = params ?? {};
|
|
9379
|
+
if (typeof p.messageId !== "string") return;
|
|
9380
|
+
const hadChip = queueCache.delete(p.messageId);
|
|
9381
|
+
if (hadChip && screenRef && dispatcherRef) {
|
|
9382
|
+
refreshQueueDisplay();
|
|
9383
|
+
}
|
|
9384
|
+
const echo = ownPendingByMid.get(p.messageId);
|
|
9385
|
+
if (echo) {
|
|
9386
|
+
ownPendingByMid.delete(p.messageId);
|
|
9387
|
+
if (p.reason === "started") {
|
|
9388
|
+
echo.flushed = true;
|
|
9389
|
+
appendRender({
|
|
9390
|
+
kind: "user-text",
|
|
9391
|
+
text: echo.text,
|
|
9392
|
+
attachments: echo.attachments
|
|
9393
|
+
});
|
|
9394
|
+
currentTurnEcho = echo;
|
|
9395
|
+
}
|
|
9396
|
+
}
|
|
9397
|
+
});
|
|
8503
9398
|
const handlePermissionResolved = (update) => {
|
|
8504
9399
|
const u = update ?? {};
|
|
8505
9400
|
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
|
|
@@ -8627,9 +9522,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8627
9522
|
let resolvedAgentId = ctx.agentId;
|
|
8628
9523
|
let resolvedCwd = ctx.cwd;
|
|
8629
9524
|
let resolvedTitle;
|
|
9525
|
+
let ownClientId;
|
|
8630
9526
|
let initialModel;
|
|
8631
9527
|
let initialMode;
|
|
8632
9528
|
let initialCommands;
|
|
9529
|
+
let initialModes;
|
|
9530
|
+
let initialQueue;
|
|
8633
9531
|
let initialUsage;
|
|
8634
9532
|
let initialTurnStartedAt;
|
|
8635
9533
|
if (ctx.sessionId === "__new__") {
|
|
@@ -8646,6 +9544,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8646
9544
|
...Object.keys(hydraNewMeta).length > 0 ? { _meta: { [HYDRA_META_KEY]: hydraNewMeta } } : {}
|
|
8647
9545
|
});
|
|
8648
9546
|
resolvedSessionId = created.sessionId;
|
|
9547
|
+
if (created.clientId) {
|
|
9548
|
+
ownClientId = created.clientId;
|
|
9549
|
+
}
|
|
8649
9550
|
exitHint.sessionId = resolvedSessionId;
|
|
8650
9551
|
const hydraMeta = extractHydraMeta(created._meta ?? void 0);
|
|
8651
9552
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
@@ -8665,6 +9566,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8665
9566
|
if (hydraMeta.availableCommands) {
|
|
8666
9567
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
8667
9568
|
}
|
|
9569
|
+
if (hydraMeta.availableModes) {
|
|
9570
|
+
initialModes = hydraMeta.availableModes;
|
|
9571
|
+
}
|
|
9572
|
+
initialQueue = hydraMeta.queue;
|
|
8668
9573
|
} else {
|
|
8669
9574
|
const attached = await conn.request("session/attach", {
|
|
8670
9575
|
sessionId: ctx.sessionId,
|
|
@@ -8672,6 +9577,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8672
9577
|
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
8673
9578
|
});
|
|
8674
9579
|
resolvedSessionId = attached.sessionId;
|
|
9580
|
+
if (attached.clientId) {
|
|
9581
|
+
ownClientId = attached.clientId;
|
|
9582
|
+
}
|
|
8675
9583
|
exitHint.sessionId = resolvedSessionId;
|
|
8676
9584
|
const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
|
|
8677
9585
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
@@ -8691,6 +9599,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8691
9599
|
if (hydraMeta.availableCommands) {
|
|
8692
9600
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
8693
9601
|
}
|
|
9602
|
+
if (hydraMeta.availableModes) {
|
|
9603
|
+
initialModes = hydraMeta.availableModes;
|
|
9604
|
+
}
|
|
9605
|
+
initialQueue = hydraMeta.queue;
|
|
8694
9606
|
}
|
|
8695
9607
|
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
8696
9608
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
@@ -8716,6 +9628,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8716
9628
|
if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
|
|
8717
9629
|
continue;
|
|
8718
9630
|
}
|
|
9631
|
+
if (tryHandleHelpKey(ev)) {
|
|
9632
|
+
continue;
|
|
9633
|
+
}
|
|
8719
9634
|
if (tryHandleScrollbackSearchKey(ev)) {
|
|
8720
9635
|
continue;
|
|
8721
9636
|
}
|
|
@@ -8750,6 +9665,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8750
9665
|
{ name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
|
|
8751
9666
|
];
|
|
8752
9667
|
let agentCommands = initialCommands ?? [];
|
|
9668
|
+
let agentModes = initialModes ?? [];
|
|
8753
9669
|
const allCommands = () => {
|
|
8754
9670
|
const seen = /* @__PURE__ */ new Set();
|
|
8755
9671
|
const out = [];
|
|
@@ -8905,7 +9821,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8905
9821
|
usage: { ...usage }
|
|
8906
9822
|
});
|
|
8907
9823
|
if (initialMode) {
|
|
8908
|
-
screen.
|
|
9824
|
+
screen.setBanner({ currentMode: initialMode });
|
|
8909
9825
|
}
|
|
8910
9826
|
void getPendingUpdate().then((info) => {
|
|
8911
9827
|
if (info) {
|
|
@@ -8942,7 +9858,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8942
9858
|
}
|
|
8943
9859
|
let onlyClient = false;
|
|
8944
9860
|
try {
|
|
8945
|
-
const sessions = await listSessions(config);
|
|
9861
|
+
const sessions = await listSessions(config, serviceToken);
|
|
8946
9862
|
const me = sessions.find((s) => s.sessionId === resolvedSessionId);
|
|
8947
9863
|
onlyClient = !me || me.attachedClients <= 1;
|
|
8948
9864
|
} catch {
|
|
@@ -9000,6 +9916,28 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9000
9916
|
}
|
|
9001
9917
|
return true;
|
|
9002
9918
|
};
|
|
9919
|
+
const toggleHelpModal = () => {
|
|
9920
|
+
if (screen.isHelpPromptActive()) {
|
|
9921
|
+
screen.setHelpPrompt(null);
|
|
9922
|
+
return;
|
|
9923
|
+
}
|
|
9924
|
+
screen.setHelpPrompt({
|
|
9925
|
+
title: "Hotkeys",
|
|
9926
|
+
entries: HELP_ENTRIES2,
|
|
9927
|
+
hint: "any key dismisses \xB7 /help lists commands"
|
|
9928
|
+
});
|
|
9929
|
+
};
|
|
9930
|
+
const tryHandleHelpKey = (ev) => {
|
|
9931
|
+
if (!screen.isHelpPromptActive()) {
|
|
9932
|
+
return false;
|
|
9933
|
+
}
|
|
9934
|
+
if (ev.type === "key" && ev.name === "ctrl-g") {
|
|
9935
|
+
screen.setHelpPrompt(null);
|
|
9936
|
+
return true;
|
|
9937
|
+
}
|
|
9938
|
+
screen.setHelpPrompt(null);
|
|
9939
|
+
return true;
|
|
9940
|
+
};
|
|
9003
9941
|
const teardown = () => {
|
|
9004
9942
|
teardownStarted = true;
|
|
9005
9943
|
process.off("SIGINT", sigintHandler);
|
|
@@ -9034,11 +9972,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9034
9972
|
screen.pauseRepaint();
|
|
9035
9973
|
screen.stop();
|
|
9036
9974
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
9037
|
-
const sessions = await listSessions(config);
|
|
9975
|
+
const sessions = await listSessions(config, serviceToken);
|
|
9038
9976
|
const choice = await pickSession(term, {
|
|
9039
9977
|
cwd: resolvedCwd,
|
|
9040
9978
|
sessions,
|
|
9041
9979
|
config,
|
|
9980
|
+
serviceToken,
|
|
9042
9981
|
currentSessionId: resolvedSessionId
|
|
9043
9982
|
});
|
|
9044
9983
|
if (choice.kind === "abort") {
|
|
@@ -9066,37 +10005,76 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9066
10005
|
}
|
|
9067
10006
|
resume(nextOpts);
|
|
9068
10007
|
};
|
|
9069
|
-
const
|
|
10008
|
+
const cycleLiveSession = async () => {
|
|
10009
|
+
if (!finishSession)
|
|
10010
|
+
return;
|
|
10011
|
+
const sessions = await listSessions(config, serviceToken);
|
|
10012
|
+
const live = sessions.filter((s) => s.status === "live");
|
|
10013
|
+
if (live.length <= 1)
|
|
10014
|
+
return;
|
|
10015
|
+
const idx = live.findIndex((s) => s.sessionId === resolvedSessionId);
|
|
10016
|
+
const next = live[(idx + 1) % live.length];
|
|
10017
|
+
const resume = finishSession;
|
|
10018
|
+
finishSession = null;
|
|
10019
|
+
process.off("SIGINT", sigintHandler);
|
|
10020
|
+
void stream.close().catch(() => void 0);
|
|
10021
|
+
const nextOpts = { ...opts, sessionId: next.sessionId, cwd: resolvedCwd };
|
|
10022
|
+
if (next.agentId !== void 0)
|
|
10023
|
+
nextOpts.agentId = next.agentId;
|
|
10024
|
+
resume(nextOpts);
|
|
10025
|
+
};
|
|
9070
10026
|
const handleEffect = (effect) => {
|
|
9071
10027
|
switch (effect.type) {
|
|
9072
10028
|
case "send":
|
|
9073
|
-
enqueuePrompt(effect.text, effect.
|
|
10029
|
+
enqueuePrompt(effect.text, effect.attachments);
|
|
9074
10030
|
return;
|
|
9075
10031
|
case "queue-edit": {
|
|
9076
|
-
const
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
10032
|
+
const mid = queueMessageIdAt(effect.index);
|
|
10033
|
+
if (!mid) {
|
|
10034
|
+
return;
|
|
10035
|
+
}
|
|
10036
|
+
const blocks = [];
|
|
10037
|
+
if (effect.text.length > 0) {
|
|
10038
|
+
blocks.push({ type: "text", text: effect.text });
|
|
10039
|
+
}
|
|
10040
|
+
for (const a of effect.attachments) {
|
|
10041
|
+
blocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
|
|
9085
10042
|
}
|
|
10043
|
+
conn.request("hydra-acp/update_prompt", {
|
|
10044
|
+
sessionId: resolvedSessionId,
|
|
10045
|
+
messageId: mid,
|
|
10046
|
+
prompt: blocks
|
|
10047
|
+
}).then((raw) => {
|
|
10048
|
+
const res = raw;
|
|
10049
|
+
if (!res.updated && res.reason !== "ok") {
|
|
10050
|
+
screen.notify(`queue edit skipped (${res.reason})`);
|
|
10051
|
+
}
|
|
10052
|
+
}).catch((err) => {
|
|
10053
|
+
screen.notify(`queue edit failed: ${err.message}`);
|
|
10054
|
+
});
|
|
9086
10055
|
return;
|
|
9087
10056
|
}
|
|
9088
10057
|
case "queue-remove": {
|
|
9089
|
-
const
|
|
9090
|
-
if (
|
|
9091
|
-
|
|
9092
|
-
refreshQueueDisplay();
|
|
10058
|
+
const mid = queueMessageIdAt(effect.index);
|
|
10059
|
+
if (!mid) {
|
|
10060
|
+
return;
|
|
9093
10061
|
}
|
|
10062
|
+
conn.request("hydra-acp/cancel_prompt", {
|
|
10063
|
+
sessionId: resolvedSessionId,
|
|
10064
|
+
messageId: mid
|
|
10065
|
+
}).then((raw) => {
|
|
10066
|
+
const res = raw;
|
|
10067
|
+
if (!res.cancelled && res.reason !== "ok") {
|
|
10068
|
+
screen.notify(`queue cancel skipped (${res.reason})`);
|
|
10069
|
+
}
|
|
10070
|
+
}).catch((err) => {
|
|
10071
|
+
screen.notify(`queue cancel failed: ${err.message}`);
|
|
10072
|
+
});
|
|
9094
10073
|
return;
|
|
9095
10074
|
}
|
|
9096
10075
|
case "cancel": {
|
|
9097
10076
|
if (effect.prefill && turnInFlight) {
|
|
9098
|
-
const
|
|
9099
|
-
const waitingEmpty = promptQueue.length <= headOffset;
|
|
10077
|
+
const waitingEmpty = queueCache.size === 0;
|
|
9100
10078
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
9101
10079
|
if (waitingEmpty && bufferEmpty) {
|
|
9102
10080
|
pendingPrefill = {
|
|
@@ -9116,7 +10094,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9116
10094
|
void requestExit();
|
|
9117
10095
|
return;
|
|
9118
10096
|
case "plan-toggle":
|
|
9119
|
-
|
|
10097
|
+
void handleModeToggle(effect.on);
|
|
9120
10098
|
return;
|
|
9121
10099
|
case "redraw-banner":
|
|
9122
10100
|
screen.setBanner({});
|
|
@@ -9133,10 +10111,16 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9133
10111
|
case "switch-session":
|
|
9134
10112
|
void switchSession();
|
|
9135
10113
|
return;
|
|
10114
|
+
case "next-live-session":
|
|
10115
|
+
void cycleLiveSession();
|
|
10116
|
+
return;
|
|
9136
10117
|
case "toggle-tools":
|
|
9137
10118
|
toolsExpanded = !toolsExpanded;
|
|
9138
10119
|
renderToolsBlock();
|
|
9139
10120
|
return;
|
|
10121
|
+
case "show-help":
|
|
10122
|
+
toggleHelpModal();
|
|
10123
|
+
return;
|
|
9140
10124
|
case "escalate-search":
|
|
9141
10125
|
screen.enterScrollbackSearch();
|
|
9142
10126
|
screen.updateScrollbackSearchTerm(effect.query);
|
|
@@ -9176,11 +10160,11 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9176
10160
|
}
|
|
9177
10161
|
const mimeType = mimeFromExtension(token);
|
|
9178
10162
|
if (!mimeType) {
|
|
9179
|
-
screen.notify(`unsupported image type: ${
|
|
10163
|
+
screen.notify(`unsupported image type: ${path14.basename(token)}`);
|
|
9180
10164
|
continue;
|
|
9181
10165
|
}
|
|
9182
10166
|
try {
|
|
9183
|
-
const buf = await
|
|
10167
|
+
const buf = await fs19.readFile(token);
|
|
9184
10168
|
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
9185
10169
|
screen.notify(
|
|
9186
10170
|
`image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
|
|
@@ -9190,13 +10174,13 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9190
10174
|
dispatcher.addAttachment({
|
|
9191
10175
|
mimeType,
|
|
9192
10176
|
data: buf.toString("base64"),
|
|
9193
|
-
name:
|
|
10177
|
+
name: path14.basename(token),
|
|
9194
10178
|
sizeBytes: buf.length
|
|
9195
10179
|
});
|
|
9196
10180
|
added++;
|
|
9197
10181
|
} catch (err) {
|
|
9198
10182
|
screen.notify(
|
|
9199
|
-
`cannot read ${
|
|
10183
|
+
`cannot read ${path14.basename(token)}: ${err.message}`
|
|
9200
10184
|
);
|
|
9201
10185
|
}
|
|
9202
10186
|
}
|
|
@@ -9227,18 +10211,54 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9227
10211
|
}
|
|
9228
10212
|
screen.refreshPrompt();
|
|
9229
10213
|
};
|
|
9230
|
-
const
|
|
9231
|
-
|
|
10214
|
+
const formatQueueChipText = (entry) => entry.attachmentCount > 0 ? `${entry.text} \xB7 \u{1F4CE}\xD7${entry.attachmentCount}` : entry.text;
|
|
10215
|
+
const chipFromPrompt = (messageId, prompt) => {
|
|
10216
|
+
const blocks = Array.isArray(prompt) ? prompt : [];
|
|
10217
|
+
let text = "";
|
|
10218
|
+
let attachmentCount = 0;
|
|
10219
|
+
for (const raw of blocks) {
|
|
10220
|
+
if (!raw || typeof raw !== "object") continue;
|
|
10221
|
+
const b = raw;
|
|
10222
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
10223
|
+
text += b.text;
|
|
10224
|
+
} else if (b.type === "image") {
|
|
10225
|
+
attachmentCount += 1;
|
|
10226
|
+
}
|
|
10227
|
+
}
|
|
10228
|
+
return {
|
|
10229
|
+
messageId,
|
|
10230
|
+
text: sanitizeSingleLine(text),
|
|
10231
|
+
attachmentCount
|
|
10232
|
+
};
|
|
10233
|
+
};
|
|
10234
|
+
const queueCache = /* @__PURE__ */ new Map();
|
|
10235
|
+
const pendingEchoes = [];
|
|
10236
|
+
const ownPendingByMid = /* @__PURE__ */ new Map();
|
|
10237
|
+
let currentTurnEcho = null;
|
|
9232
10238
|
const refreshQueueDisplay = () => {
|
|
9233
|
-
const
|
|
9234
|
-
const displayTexts =
|
|
9235
|
-
(p) => p.attachments.length > 0 ? `${p.text} \xB7 \u{1F4CE}\xD7${p.attachments.length}` : p.text
|
|
9236
|
-
);
|
|
10239
|
+
const entries = [...queueCache.values()];
|
|
10240
|
+
const displayTexts = entries.map(formatQueueChipText);
|
|
9237
10241
|
screen.setQueuedPrompts(displayTexts);
|
|
9238
|
-
screen.setBanner({ queued:
|
|
9239
|
-
dispatcher.setQueue(
|
|
10242
|
+
screen.setBanner({ queued: entries.length });
|
|
10243
|
+
dispatcher.setQueue(entries.map((e) => e.text));
|
|
10244
|
+
};
|
|
10245
|
+
const queueMessageIdAt = (index) => {
|
|
10246
|
+
const entries = [...queueCache.values()];
|
|
10247
|
+
return entries[index]?.messageId;
|
|
9240
10248
|
};
|
|
9241
|
-
|
|
10249
|
+
if (initialQueue && initialQueue.length > 0) {
|
|
10250
|
+
for (const entry of initialQueue) {
|
|
10251
|
+
if (entry.position === 0) continue;
|
|
10252
|
+
queueCache.set(
|
|
10253
|
+
entry.messageId,
|
|
10254
|
+
chipFromPrompt(entry.messageId, entry.prompt)
|
|
10255
|
+
);
|
|
10256
|
+
}
|
|
10257
|
+
if (queueCache.size > 0) {
|
|
10258
|
+
refreshQueueDisplay();
|
|
10259
|
+
}
|
|
10260
|
+
}
|
|
10261
|
+
const enqueuePrompt = (text, attachments) => {
|
|
9242
10262
|
screen.scrollToBottom();
|
|
9243
10263
|
if (handleBuiltinCommand(text)) {
|
|
9244
10264
|
return;
|
|
@@ -9246,15 +10266,29 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9246
10266
|
history = appendEntry(history, text);
|
|
9247
10267
|
dispatcher.setHistory(history);
|
|
9248
10268
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
9249
|
-
|
|
9250
|
-
refreshQueueDisplay();
|
|
9251
|
-
tickWorker();
|
|
10269
|
+
void runPrompt(text, attachments);
|
|
9252
10270
|
};
|
|
9253
|
-
const
|
|
9254
|
-
if (
|
|
10271
|
+
const handleModeToggle = async (_on) => {
|
|
10272
|
+
if (agentModes.length === 0) {
|
|
10273
|
+
screen.notify("no modes advertised by agent");
|
|
10274
|
+
return;
|
|
10275
|
+
}
|
|
10276
|
+
const currentMode = screen.currentModeId();
|
|
10277
|
+
const idx = agentModes.findIndex((m) => m.id === currentMode);
|
|
10278
|
+
const nextIdx = idx === -1 ? 0 : (idx + 1) % agentModes.length;
|
|
10279
|
+
const newModeId = agentModes[nextIdx]?.id;
|
|
10280
|
+
if (!newModeId) {
|
|
9255
10281
|
return;
|
|
9256
10282
|
}
|
|
9257
|
-
|
|
10283
|
+
screen.setBanner({ currentMode: newModeId });
|
|
10284
|
+
try {
|
|
10285
|
+
await conn.request("session/set_mode", {
|
|
10286
|
+
sessionId: resolvedSessionId,
|
|
10287
|
+
modeId: newModeId
|
|
10288
|
+
});
|
|
10289
|
+
} catch (err) {
|
|
10290
|
+
screen.notify(`set_mode failed: ${err.message}`);
|
|
10291
|
+
}
|
|
9258
10292
|
};
|
|
9259
10293
|
const handleBuiltinCommand = (text) => {
|
|
9260
10294
|
const trimmed = text.trim();
|
|
@@ -9404,33 +10438,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9404
10438
|
return false;
|
|
9405
10439
|
}
|
|
9406
10440
|
};
|
|
9407
|
-
const
|
|
9408
|
-
workerActive = true;
|
|
9409
|
-
try {
|
|
9410
|
-
while (promptQueue.length > 0 && pendingTurns === 0) {
|
|
9411
|
-
const next = promptQueue[0];
|
|
9412
|
-
if (!next) {
|
|
9413
|
-
break;
|
|
9414
|
-
}
|
|
9415
|
-
refreshQueueDisplay();
|
|
9416
|
-
await processPrompt(next.text, next.planMode, next.attachments);
|
|
9417
|
-
promptQueue.shift();
|
|
9418
|
-
}
|
|
9419
|
-
} finally {
|
|
9420
|
-
workerActive = false;
|
|
9421
|
-
refreshQueueDisplay();
|
|
9422
|
-
if (pendingPrefill !== null) {
|
|
9423
|
-
const { text, attachments } = pendingPrefill;
|
|
9424
|
-
pendingPrefill = null;
|
|
9425
|
-
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
9426
|
-
if (bufferEmpty) {
|
|
9427
|
-
dispatcher.setBuffer(text, attachments);
|
|
9428
|
-
screen.refreshPrompt();
|
|
9429
|
-
}
|
|
9430
|
-
}
|
|
9431
|
-
}
|
|
9432
|
-
};
|
|
9433
|
-
const processPrompt = async (text, planMode, attachments) => {
|
|
10441
|
+
const runPrompt = async (text, attachments) => {
|
|
9434
10442
|
const userBlocks = [];
|
|
9435
10443
|
if (text.length > 0) {
|
|
9436
10444
|
userBlocks.push({ type: "text", text });
|
|
@@ -9438,9 +10446,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9438
10446
|
for (const a of attachments) {
|
|
9439
10447
|
userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
|
|
9440
10448
|
}
|
|
9441
|
-
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
9442
10449
|
adjustPendingTurns(1);
|
|
9443
|
-
|
|
10450
|
+
const echo = { text, attachments, flushed: false };
|
|
10451
|
+
pendingEchoes.push(echo);
|
|
9444
10452
|
let cancelled = false;
|
|
9445
10453
|
turnInFlight = {
|
|
9446
10454
|
text,
|
|
@@ -9459,23 +10467,45 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9459
10467
|
try {
|
|
9460
10468
|
const response = await conn.request("session/prompt", {
|
|
9461
10469
|
sessionId: resolvedSessionId,
|
|
9462
|
-
prompt:
|
|
10470
|
+
prompt: userBlocks
|
|
9463
10471
|
});
|
|
9464
10472
|
if (response && typeof response.stopReason === "string") {
|
|
9465
10473
|
stopReason = response.stopReason;
|
|
9466
10474
|
}
|
|
9467
10475
|
} catch (err) {
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
10476
|
+
const idx = pendingEchoes.indexOf(echo);
|
|
10477
|
+
if (idx >= 0) {
|
|
10478
|
+
pendingEchoes.splice(idx, 1);
|
|
10479
|
+
}
|
|
10480
|
+
if (echo.messageId !== void 0) {
|
|
10481
|
+
ownPendingByMid.delete(echo.messageId);
|
|
10482
|
+
}
|
|
10483
|
+
screen.appendLines([
|
|
10484
|
+
{
|
|
10485
|
+
prefix: "\u2717 ",
|
|
10486
|
+
prefixStyle: "tool-status-fail",
|
|
10487
|
+
body: err.message,
|
|
10488
|
+
bodyStyle: "tool-status-fail"
|
|
10489
|
+
}
|
|
10490
|
+
]);
|
|
9473
10491
|
} finally {
|
|
9474
10492
|
turnInFlight = null;
|
|
9475
10493
|
adjustPendingTurns(-1);
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
10494
|
+
if (echo.flushed && currentTurnEcho === echo) {
|
|
10495
|
+
appendRender(
|
|
10496
|
+
stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
|
|
10497
|
+
);
|
|
10498
|
+
currentTurnEcho = null;
|
|
10499
|
+
}
|
|
10500
|
+
if (pendingPrefill !== null) {
|
|
10501
|
+
const { text: pt, attachments: pa } = pendingPrefill;
|
|
10502
|
+
pendingPrefill = null;
|
|
10503
|
+
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
10504
|
+
if (bufferEmpty) {
|
|
10505
|
+
dispatcher.setBuffer(pt, pa);
|
|
10506
|
+
screen.refreshPrompt();
|
|
10507
|
+
}
|
|
10508
|
+
}
|
|
9479
10509
|
}
|
|
9480
10510
|
};
|
|
9481
10511
|
const toolStates = /* @__PURE__ */ new Map();
|
|
@@ -9605,6 +10635,14 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9605
10635
|
refreshCompletions();
|
|
9606
10636
|
return;
|
|
9607
10637
|
}
|
|
10638
|
+
if (event.kind === "available-modes") {
|
|
10639
|
+
agentModes = event.modes;
|
|
10640
|
+
return;
|
|
10641
|
+
}
|
|
10642
|
+
if (event.kind === "mode-changed") {
|
|
10643
|
+
screen.setBanner({ currentMode: event.mode || void 0 });
|
|
10644
|
+
return;
|
|
10645
|
+
}
|
|
9608
10646
|
if (event.kind === "session-info") {
|
|
9609
10647
|
if (event.title !== void 0) {
|
|
9610
10648
|
screen.setSessionbar({ title: event.title });
|
|
@@ -9640,6 +10678,11 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9640
10678
|
}
|
|
9641
10679
|
if (event.kind === "user-text") {
|
|
9642
10680
|
closeAgentText();
|
|
10681
|
+
if (toolsBlockStartedAt !== null) {
|
|
10682
|
+
toolsBlockEndedAt = Date.now();
|
|
10683
|
+
renderToolsBlock();
|
|
10684
|
+
}
|
|
10685
|
+
currentTurnEcho = null;
|
|
9643
10686
|
screen.ensureSeparator();
|
|
9644
10687
|
const formatted2 = formatEvent(event);
|
|
9645
10688
|
if (formatted2.length > 0) {
|
|
@@ -9708,6 +10751,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9708
10751
|
toolsBlockStopReason = event.stopReason ?? null;
|
|
9709
10752
|
renderToolsBlock();
|
|
9710
10753
|
screen.clearKey("tools");
|
|
10754
|
+
} else if (event.stopReason !== void 0 && event.stopReason !== "end_turn") {
|
|
10755
|
+
screen.appendLines([
|
|
10756
|
+
{
|
|
10757
|
+
prefix: "\u26A0 ",
|
|
10758
|
+
prefixStyle: "tool-status-fail",
|
|
10759
|
+
body: `turn ended: ${event.stopReason}`,
|
|
10760
|
+
bodyStyle: "tool-status-fail"
|
|
10761
|
+
}
|
|
10762
|
+
]);
|
|
9711
10763
|
}
|
|
9712
10764
|
toolStates.clear();
|
|
9713
10765
|
toolCallOrder.length = 0;
|
|
@@ -9755,23 +10807,21 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9755
10807
|
resolve5({ outcome: { outcome: "cancelled" } });
|
|
9756
10808
|
}
|
|
9757
10809
|
closeAgentText();
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
screen.clearKey("tools");
|
|
9763
|
-
toolStates.clear();
|
|
9764
|
-
toolCallOrder.length = 0;
|
|
9765
|
-
toolsBlockStartedAt = null;
|
|
9766
|
-
toolsBlockEndedAt = null;
|
|
9767
|
-
toolsBlockStopReason = null;
|
|
9768
|
-
toolsExpanded = false;
|
|
9769
|
-
}
|
|
9770
|
-
screen.clearKey("plan");
|
|
9771
|
-
lastPlanEvent = null;
|
|
9772
|
-
if (pendingTurns > 0) {
|
|
9773
|
-
adjustPendingTurns(-pendingTurns);
|
|
10810
|
+
};
|
|
10811
|
+
const markToolsBlockRecoveryFailed = () => {
|
|
10812
|
+
if (toolsBlockStartedAt === null) {
|
|
10813
|
+
return;
|
|
9774
10814
|
}
|
|
10815
|
+
toolsBlockEndedAt = Date.now();
|
|
10816
|
+
toolsBlockStopReason = "reconnect-recovery-failed";
|
|
10817
|
+
renderToolsBlock();
|
|
10818
|
+
screen.clearKey("tools");
|
|
10819
|
+
toolStates.clear();
|
|
10820
|
+
toolCallOrder.length = 0;
|
|
10821
|
+
toolsBlockStartedAt = null;
|
|
10822
|
+
toolsBlockEndedAt = null;
|
|
10823
|
+
toolsBlockStopReason = null;
|
|
10824
|
+
toolsExpanded = false;
|
|
9775
10825
|
};
|
|
9776
10826
|
onDisconnectHook = () => {
|
|
9777
10827
|
screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
|
|
@@ -9795,13 +10845,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9795
10845
|
await stream.request(initReq);
|
|
9796
10846
|
} catch {
|
|
9797
10847
|
}
|
|
10848
|
+
const useAfterMessage = lastSeenMessageId !== void 0;
|
|
9798
10849
|
const attachReq = {
|
|
9799
10850
|
jsonrpc: "2.0",
|
|
9800
10851
|
id: `tui-reattach-${nanoid3()}`,
|
|
9801
10852
|
method: "session/attach",
|
|
9802
10853
|
params: {
|
|
9803
10854
|
sessionId: resolvedSessionId,
|
|
9804
|
-
historyPolicy: "none",
|
|
10855
|
+
historyPolicy: useAfterMessage ? "after_message" : "none",
|
|
10856
|
+
...useAfterMessage ? { afterMessageId: lastSeenMessageId } : {},
|
|
9805
10857
|
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
|
|
9806
10858
|
...upstreamSessionId !== void 0 ? {
|
|
9807
10859
|
_meta: {
|
|
@@ -9816,19 +10868,46 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9816
10868
|
} : {}
|
|
9817
10869
|
}
|
|
9818
10870
|
};
|
|
10871
|
+
reconnectReplayBuffer = [];
|
|
10872
|
+
let appliedPolicy;
|
|
10873
|
+
let attachErr;
|
|
9819
10874
|
try {
|
|
9820
10875
|
const resp = await stream.request(attachReq);
|
|
9821
10876
|
if (resp.error) {
|
|
9822
10877
|
throw new Error(resp.error.message);
|
|
9823
10878
|
}
|
|
10879
|
+
const result = resp.result ?? {};
|
|
10880
|
+
if (typeof result.historyPolicy === "string") {
|
|
10881
|
+
appliedPolicy = result.historyPolicy;
|
|
10882
|
+
}
|
|
9824
10883
|
} catch (err) {
|
|
10884
|
+
attachErr = err;
|
|
10885
|
+
}
|
|
10886
|
+
const buffered2 = reconnectReplayBuffer ?? [];
|
|
10887
|
+
reconnectReplayBuffer = null;
|
|
10888
|
+
if (attachErr) {
|
|
10889
|
+
markToolsBlockRecoveryFailed();
|
|
9825
10890
|
screen.appendLines([
|
|
9826
10891
|
{
|
|
9827
10892
|
prefix: " ",
|
|
9828
|
-
body: `reattach failed: ${
|
|
10893
|
+
body: `reattach failed: ${attachErr.message}`,
|
|
10894
|
+
bodyStyle: "tool-status-fail"
|
|
10895
|
+
}
|
|
10896
|
+
]);
|
|
10897
|
+
} else if (useAfterMessage && appliedPolicy !== "after_message") {
|
|
10898
|
+
markToolsBlockRecoveryFailed();
|
|
10899
|
+
screen.appendLines([
|
|
10900
|
+
{
|
|
10901
|
+
prefix: "\u26A0 ",
|
|
10902
|
+
prefixStyle: "tool-status-fail",
|
|
10903
|
+
body: "reconnect couldn't replay events since last seen \u2014 scrollback may be incomplete",
|
|
9829
10904
|
bodyStyle: "tool-status-fail"
|
|
9830
10905
|
}
|
|
9831
10906
|
]);
|
|
10907
|
+
} else {
|
|
10908
|
+
for (const params of buffered2) {
|
|
10909
|
+
handleSessionUpdate(params);
|
|
10910
|
+
}
|
|
9832
10911
|
}
|
|
9833
10912
|
screen.setBanner({
|
|
9834
10913
|
status: pendingTurns > 0 ? "busy" : "ready",
|
|
@@ -9846,7 +10925,7 @@ connection lost: ${err.message}
|
|
|
9846
10925
|
process.on("SIGINT", sigintHandler);
|
|
9847
10926
|
return await sessionDone;
|
|
9848
10927
|
}
|
|
9849
|
-
async function resolveSession(term, config, opts) {
|
|
10928
|
+
async function resolveSession(term, config, serviceToken, opts) {
|
|
9850
10929
|
const cwd = opts.cwd ?? process.cwd();
|
|
9851
10930
|
if (opts.sessionId) {
|
|
9852
10931
|
return {
|
|
@@ -9859,7 +10938,7 @@ async function resolveSession(term, config, opts) {
|
|
|
9859
10938
|
return newCtx(opts, cwd, config);
|
|
9860
10939
|
}
|
|
9861
10940
|
if (opts.resume) {
|
|
9862
|
-
const sessions2 = await listSessions(config, { cwd, all: true });
|
|
10941
|
+
const sessions2 = await listSessions(config, serviceToken, { cwd, all: true });
|
|
9863
10942
|
const target = pickMostRecent(sessions2, cwd);
|
|
9864
10943
|
if (!target) {
|
|
9865
10944
|
term.yellow(`No sessions found for ${cwd}.
|
|
@@ -9872,14 +10951,15 @@ async function resolveSession(term, config, opts) {
|
|
|
9872
10951
|
cwd
|
|
9873
10952
|
};
|
|
9874
10953
|
}
|
|
9875
|
-
const sessions = await listSessions(config);
|
|
10954
|
+
const sessions = await listSessions(config, serviceToken);
|
|
9876
10955
|
if (sessions.length === 0) {
|
|
9877
10956
|
return newCtx(opts, cwd, config);
|
|
9878
10957
|
}
|
|
9879
10958
|
const choice = await pickSession(term, {
|
|
9880
10959
|
cwd,
|
|
9881
10960
|
sessions,
|
|
9882
|
-
config
|
|
10961
|
+
config,
|
|
10962
|
+
serviceToken
|
|
9883
10963
|
});
|
|
9884
10964
|
if (choice.kind === "abort") {
|
|
9885
10965
|
return null;
|
|
@@ -9934,7 +11014,7 @@ function rotateIfBig(target) {
|
|
|
9934
11014
|
} catch {
|
|
9935
11015
|
}
|
|
9936
11016
|
}
|
|
9937
|
-
var
|
|
11017
|
+
var HELP_ENTRIES2, logMaxBytes;
|
|
9938
11018
|
var init_app = __esm({
|
|
9939
11019
|
"src/tui/app.ts"() {
|
|
9940
11020
|
"use strict";
|
|
@@ -9942,6 +11022,7 @@ var init_app = __esm({
|
|
|
9942
11022
|
init_types();
|
|
9943
11023
|
init_resilient_ws();
|
|
9944
11024
|
init_config();
|
|
11025
|
+
init_service_token();
|
|
9945
11026
|
init_daemon_bootstrap();
|
|
9946
11027
|
init_session();
|
|
9947
11028
|
init_paths();
|
|
@@ -9957,7 +11038,34 @@ var init_app = __esm({
|
|
|
9957
11038
|
init_completion();
|
|
9958
11039
|
init_render_update();
|
|
9959
11040
|
init_format();
|
|
9960
|
-
|
|
11041
|
+
HELP_ENTRIES2 = [
|
|
11042
|
+
["Enter", "send prompt (or queue while a turn is running)"],
|
|
11043
|
+
["Alt+Enter", "newline in prompt"],
|
|
11044
|
+
["Shift+Tab", "cycle agent modes (plan / accept-edits / etc.)"],
|
|
11045
|
+
["Tab", "indent \xB7 slash-command completion"],
|
|
11046
|
+
null,
|
|
11047
|
+
["\u2191 / \u2193", "prompt history \xB7 queue navigation"],
|
|
11048
|
+
["\u2190/\u2192 Home/End", "cursor movement"],
|
|
11049
|
+
["Alt+B / Alt+F", "word back / forward"],
|
|
11050
|
+
["^A / ^E", "line start / end"],
|
|
11051
|
+
["^W / ^U / ^K", "kill word / line / to end"],
|
|
11052
|
+
["^Y", "yank last kill"],
|
|
11053
|
+
null,
|
|
11054
|
+
["^P", "switch session (picker)"],
|
|
11055
|
+
["^T", "next live session"],
|
|
11056
|
+
["^V", "paste image from clipboard"],
|
|
11057
|
+
["^O", "expand / collapse tools block"],
|
|
11058
|
+
null,
|
|
11059
|
+
["^R / ^S", "history reverse / forward search"],
|
|
11060
|
+
["PgUp / PgDn", "scroll scrollback"],
|
|
11061
|
+
["Mouse wheel", "scroll scrollback (when mouse capture is on)"],
|
|
11062
|
+
null,
|
|
11063
|
+
["^C", "cancel turn (twice to exit)"],
|
|
11064
|
+
["Esc", "cancel turn and prefill draft"],
|
|
11065
|
+
["^D", "exit (or delete-forward in prompt)"],
|
|
11066
|
+
["^L", "force full redraw"],
|
|
11067
|
+
["^G", "toggle this help"]
|
|
11068
|
+
];
|
|
9961
11069
|
logMaxBytes = 5 * 1024 * 1024;
|
|
9962
11070
|
}
|
|
9963
11071
|
});
|
|
@@ -10055,23 +11163,25 @@ function resolveOption(flags, key) {
|
|
|
10055
11163
|
// src/cli/commands/init.ts
|
|
10056
11164
|
init_paths();
|
|
10057
11165
|
init_config();
|
|
10058
|
-
|
|
11166
|
+
init_service_token();
|
|
11167
|
+
import * as fs3 from "fs/promises";
|
|
10059
11168
|
async function runInit(flags) {
|
|
10060
|
-
await
|
|
10061
|
-
|
|
11169
|
+
await fs3.mkdir(paths.home(), { recursive: true });
|
|
11170
|
+
await migrateLegacyAuthToken();
|
|
11171
|
+
const existingToken = await readServiceToken();
|
|
10062
11172
|
if (!existingToken) {
|
|
10063
|
-
const token =
|
|
10064
|
-
await
|
|
11173
|
+
const token = generateServiceToken();
|
|
11174
|
+
await writeServiceToken(token);
|
|
10065
11175
|
process.stdout.write(
|
|
10066
11176
|
`Initialized ${paths.authToken()}
|
|
10067
|
-
|
|
11177
|
+
Service token: ${token}
|
|
10068
11178
|
`
|
|
10069
11179
|
);
|
|
10070
11180
|
return;
|
|
10071
11181
|
}
|
|
10072
11182
|
if (flagBool(flags, "rotate-token")) {
|
|
10073
|
-
const newToken =
|
|
10074
|
-
await
|
|
11183
|
+
const newToken = generateServiceToken();
|
|
11184
|
+
await writeServiceToken(newToken);
|
|
10075
11185
|
process.stdout.write(
|
|
10076
11186
|
`Rotated token in ${paths.authToken()}
|
|
10077
11187
|
New token: ${newToken}
|
|
@@ -10079,21 +11189,22 @@ New token: ${newToken}
|
|
|
10079
11189
|
);
|
|
10080
11190
|
return;
|
|
10081
11191
|
}
|
|
10082
|
-
process.stdout.write(`
|
|
11192
|
+
process.stdout.write(`Service token already exists at ${paths.authToken()}.
|
|
10083
11193
|
`);
|
|
10084
|
-
process.stdout.write("Pass --rotate-token to generate a new
|
|
11194
|
+
process.stdout.write("Pass --rotate-token to generate a new service token.\n");
|
|
10085
11195
|
}
|
|
10086
11196
|
|
|
10087
11197
|
// src/cli/commands/daemon.ts
|
|
10088
11198
|
init_paths();
|
|
10089
11199
|
init_config();
|
|
10090
|
-
|
|
11200
|
+
init_service_token();
|
|
11201
|
+
import * as fsp7 from "fs/promises";
|
|
10091
11202
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
10092
11203
|
|
|
10093
11204
|
// src/daemon/server.ts
|
|
10094
11205
|
init_config();
|
|
10095
|
-
import * as
|
|
10096
|
-
import * as
|
|
11206
|
+
import * as fs15 from "fs";
|
|
11207
|
+
import * as fsp5 from "fs/promises";
|
|
10097
11208
|
import Fastify from "fastify";
|
|
10098
11209
|
import websocketPlugin from "@fastify/websocket";
|
|
10099
11210
|
import pino from "pino";
|
|
@@ -10101,12 +11212,12 @@ import createPinoRoll from "pino-roll";
|
|
|
10101
11212
|
|
|
10102
11213
|
// src/core/registry.ts
|
|
10103
11214
|
init_paths();
|
|
10104
|
-
import * as
|
|
11215
|
+
import * as fs5 from "fs/promises";
|
|
10105
11216
|
import { z as z2 } from "zod";
|
|
10106
11217
|
|
|
10107
11218
|
// src/core/binary-install.ts
|
|
10108
11219
|
init_paths();
|
|
10109
|
-
import * as
|
|
11220
|
+
import * as fs4 from "fs";
|
|
10110
11221
|
import * as fsp from "fs/promises";
|
|
10111
11222
|
import * as path2 from "path";
|
|
10112
11223
|
import { spawn } from "child_process";
|
|
@@ -10211,7 +11322,7 @@ async function downloadTo(args) {
|
|
|
10211
11322
|
);
|
|
10212
11323
|
}
|
|
10213
11324
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
10214
|
-
const out =
|
|
11325
|
+
const out = fs4.createWriteStream(dest);
|
|
10215
11326
|
const nodeStream = Readable.fromWeb(response.body);
|
|
10216
11327
|
let received = 0;
|
|
10217
11328
|
let lastEmit = Date.now();
|
|
@@ -10338,7 +11449,8 @@ async function ensureNpmPackage(args) {
|
|
|
10338
11449
|
await installInto({
|
|
10339
11450
|
agentId: args.agentId,
|
|
10340
11451
|
packageSpec: args.packageSpec,
|
|
10341
|
-
installDir
|
|
11452
|
+
installDir,
|
|
11453
|
+
registry: args.registry
|
|
10342
11454
|
});
|
|
10343
11455
|
if (!await fileExists2(binPath)) {
|
|
10344
11456
|
throw new Error(
|
|
@@ -10356,7 +11468,8 @@ async function installInto(args) {
|
|
|
10356
11468
|
);
|
|
10357
11469
|
await runNpmInstall({
|
|
10358
11470
|
packageSpec: args.packageSpec,
|
|
10359
|
-
cwd: tempDir
|
|
11471
|
+
cwd: tempDir,
|
|
11472
|
+
registry: args.registry
|
|
10360
11473
|
});
|
|
10361
11474
|
try {
|
|
10362
11475
|
await fsp2.rename(tempDir, args.installDir);
|
|
@@ -10380,9 +11493,10 @@ async function installInto(args) {
|
|
|
10380
11493
|
}
|
|
10381
11494
|
function runNpmInstall(args) {
|
|
10382
11495
|
return new Promise((resolve5, reject) => {
|
|
11496
|
+
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
10383
11497
|
const child = spawn2(
|
|
10384
11498
|
"npm",
|
|
10385
|
-
["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
|
|
11499
|
+
["install", "--no-audit", "--no-fund", "--silent", ...registryArgs, args.packageSpec],
|
|
10386
11500
|
{
|
|
10387
11501
|
cwd: args.cwd,
|
|
10388
11502
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -10477,10 +11591,12 @@ var RegistryDocument = z2.object({
|
|
|
10477
11591
|
extensions: z2.array(z2.unknown()).optional()
|
|
10478
11592
|
});
|
|
10479
11593
|
var Registry = class {
|
|
10480
|
-
constructor(config) {
|
|
11594
|
+
constructor(config, options = {}) {
|
|
10481
11595
|
this.config = config;
|
|
11596
|
+
this.options = options;
|
|
10482
11597
|
}
|
|
10483
11598
|
config;
|
|
11599
|
+
options;
|
|
10484
11600
|
cache;
|
|
10485
11601
|
async load() {
|
|
10486
11602
|
if (this.cache && this.isFresh(this.cache.fetchedAt)) {
|
|
@@ -10530,12 +11646,17 @@ var Registry = class {
|
|
|
10530
11646
|
}
|
|
10531
11647
|
const raw = await response.json();
|
|
10532
11648
|
const data = RegistryDocument.parse(raw);
|
|
10533
|
-
|
|
11649
|
+
const cached2 = { fetchedAt: Date.now(), raw, data };
|
|
11650
|
+
const hook = this.options.onFetched;
|
|
11651
|
+
if (hook) {
|
|
11652
|
+
void Promise.resolve().then(() => hook(data)).catch(() => void 0);
|
|
11653
|
+
}
|
|
11654
|
+
return cached2;
|
|
10534
11655
|
}
|
|
10535
11656
|
async readDiskCache() {
|
|
10536
11657
|
let text;
|
|
10537
11658
|
try {
|
|
10538
|
-
text = await
|
|
11659
|
+
text = await fs5.readFile(paths.registryCache(), "utf8");
|
|
10539
11660
|
} catch (err) {
|
|
10540
11661
|
const e = err;
|
|
10541
11662
|
if (e.code === "ENOENT") {
|
|
@@ -10561,7 +11682,7 @@ var Registry = class {
|
|
|
10561
11682
|
// without a lock file: the loser of the rename race just gets its
|
|
10562
11683
|
// version replaced by the winner's.
|
|
10563
11684
|
async writeDiskCache(cache) {
|
|
10564
|
-
await
|
|
11685
|
+
await fs5.mkdir(paths.home(), { recursive: true });
|
|
10565
11686
|
const final = paths.registryCache();
|
|
10566
11687
|
const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
|
|
10567
11688
|
const body = JSON.stringify(
|
|
@@ -10570,10 +11691,10 @@ var Registry = class {
|
|
|
10570
11691
|
2
|
|
10571
11692
|
) + "\n";
|
|
10572
11693
|
try {
|
|
10573
|
-
await
|
|
10574
|
-
await
|
|
11694
|
+
await fs5.writeFile(tmp, body, "utf8");
|
|
11695
|
+
await fs5.rename(tmp, final);
|
|
10575
11696
|
} catch (err) {
|
|
10576
|
-
await
|
|
11697
|
+
await fs5.unlink(tmp).catch(() => void 0);
|
|
10577
11698
|
throw err;
|
|
10578
11699
|
}
|
|
10579
11700
|
}
|
|
@@ -10591,7 +11712,8 @@ function npxPackageBasename(agent) {
|
|
|
10591
11712
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
10592
11713
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
10593
11714
|
}
|
|
10594
|
-
async function planSpawn(agent, callerArgs = []) {
|
|
11715
|
+
async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
11716
|
+
const version = agent.version ?? "current";
|
|
10595
11717
|
if (agent.distribution.npx) {
|
|
10596
11718
|
const npx = agent.distribution.npx;
|
|
10597
11719
|
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
@@ -10599,20 +11721,23 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
10599
11721
|
return {
|
|
10600
11722
|
command: "npx",
|
|
10601
11723
|
args: ["-y", npx.package, ...tail],
|
|
10602
|
-
env: npx.env ?? {}
|
|
11724
|
+
env: npx.env ?? {},
|
|
11725
|
+
version
|
|
10603
11726
|
};
|
|
10604
11727
|
}
|
|
10605
11728
|
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
10606
11729
|
const binPath = await ensureNpmPackage({
|
|
10607
11730
|
agentId: agent.id,
|
|
10608
|
-
version
|
|
11731
|
+
version,
|
|
10609
11732
|
packageSpec: npx.package,
|
|
10610
|
-
bin
|
|
11733
|
+
bin,
|
|
11734
|
+
registry: options.npmRegistry
|
|
10611
11735
|
});
|
|
10612
11736
|
return {
|
|
10613
11737
|
command: binPath,
|
|
10614
11738
|
args: tail,
|
|
10615
|
-
env: npx.env ?? {}
|
|
11739
|
+
env: npx.env ?? {},
|
|
11740
|
+
version
|
|
10616
11741
|
};
|
|
10617
11742
|
}
|
|
10618
11743
|
if (agent.distribution.binary) {
|
|
@@ -10624,14 +11749,15 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
10624
11749
|
}
|
|
10625
11750
|
const cmdPath = await ensureBinary({
|
|
10626
11751
|
agentId: agent.id,
|
|
10627
|
-
version
|
|
11752
|
+
version,
|
|
10628
11753
|
target
|
|
10629
11754
|
});
|
|
10630
11755
|
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
10631
11756
|
return {
|
|
10632
11757
|
command: cmdPath,
|
|
10633
11758
|
args: tail,
|
|
10634
|
-
env: target.env ?? {}
|
|
11759
|
+
env: target.env ?? {},
|
|
11760
|
+
version
|
|
10635
11761
|
};
|
|
10636
11762
|
}
|
|
10637
11763
|
if (agent.distribution.uvx) {
|
|
@@ -10640,7 +11766,8 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
10640
11766
|
return {
|
|
10641
11767
|
command: "uvx",
|
|
10642
11768
|
args: [uvx.package, ...tail],
|
|
10643
|
-
env: uvx.env ?? {}
|
|
11769
|
+
env: uvx.env ?? {},
|
|
11770
|
+
version
|
|
10644
11771
|
};
|
|
10645
11772
|
}
|
|
10646
11773
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
@@ -10731,6 +11858,9 @@ init_connection();
|
|
|
10731
11858
|
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
10732
11859
|
var AgentInstance = class _AgentInstance {
|
|
10733
11860
|
agentId;
|
|
11861
|
+
// Version this process was spawned from — used by the registry-fetch
|
|
11862
|
+
// prune sweep to skip install dirs belonging to a live agent.
|
|
11863
|
+
version;
|
|
10734
11864
|
cwd;
|
|
10735
11865
|
connection;
|
|
10736
11866
|
child;
|
|
@@ -10742,6 +11872,7 @@ var AgentInstance = class _AgentInstance {
|
|
|
10742
11872
|
exitHandlers = [];
|
|
10743
11873
|
constructor(opts, child) {
|
|
10744
11874
|
this.agentId = opts.agentId;
|
|
11875
|
+
this.version = opts.plan.version;
|
|
10745
11876
|
this.cwd = opts.cwd;
|
|
10746
11877
|
this.child = child;
|
|
10747
11878
|
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
@@ -10832,7 +11963,7 @@ stderr: ${tail}` : reason;
|
|
|
10832
11963
|
};
|
|
10833
11964
|
|
|
10834
11965
|
// src/core/session-manager.ts
|
|
10835
|
-
import * as
|
|
11966
|
+
import * as fs11 from "fs/promises";
|
|
10836
11967
|
import * as os2 from "os";
|
|
10837
11968
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
10838
11969
|
init_session();
|
|
@@ -10840,7 +11971,7 @@ init_session_store();
|
|
|
10840
11971
|
|
|
10841
11972
|
// src/core/history-store.ts
|
|
10842
11973
|
init_paths();
|
|
10843
|
-
import * as
|
|
11974
|
+
import * as fs8 from "fs/promises";
|
|
10844
11975
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
10845
11976
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
10846
11977
|
var HistoryStore = class {
|
|
@@ -10857,9 +11988,9 @@ var HistoryStore = class {
|
|
|
10857
11988
|
return;
|
|
10858
11989
|
}
|
|
10859
11990
|
return this.enqueue(sessionId, async () => {
|
|
10860
|
-
await
|
|
11991
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
10861
11992
|
const line = JSON.stringify(entry) + "\n";
|
|
10862
|
-
await
|
|
11993
|
+
await fs8.appendFile(paths.historyFile(sessionId), line, {
|
|
10863
11994
|
encoding: "utf8",
|
|
10864
11995
|
mode: 384
|
|
10865
11996
|
});
|
|
@@ -10870,9 +12001,9 @@ var HistoryStore = class {
|
|
|
10870
12001
|
return;
|
|
10871
12002
|
}
|
|
10872
12003
|
return this.enqueue(sessionId, async () => {
|
|
10873
|
-
await
|
|
12004
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
10874
12005
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
10875
|
-
await
|
|
12006
|
+
await fs8.writeFile(paths.historyFile(sessionId), body, {
|
|
10876
12007
|
encoding: "utf8",
|
|
10877
12008
|
mode: 384
|
|
10878
12009
|
});
|
|
@@ -10889,7 +12020,7 @@ var HistoryStore = class {
|
|
|
10889
12020
|
return this.enqueue(sessionId, async () => {
|
|
10890
12021
|
let raw;
|
|
10891
12022
|
try {
|
|
10892
|
-
raw = await
|
|
12023
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
10893
12024
|
} catch (err) {
|
|
10894
12025
|
const e = err;
|
|
10895
12026
|
if (e.code === "ENOENT") {
|
|
@@ -10902,7 +12033,7 @@ var HistoryStore = class {
|
|
|
10902
12033
|
return;
|
|
10903
12034
|
}
|
|
10904
12035
|
const trimmed = lines.slice(-maxEntries);
|
|
10905
|
-
await
|
|
12036
|
+
await fs8.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
10906
12037
|
encoding: "utf8",
|
|
10907
12038
|
mode: 384
|
|
10908
12039
|
});
|
|
@@ -10918,7 +12049,7 @@ var HistoryStore = class {
|
|
|
10918
12049
|
}
|
|
10919
12050
|
let raw;
|
|
10920
12051
|
try {
|
|
10921
|
-
raw = await
|
|
12052
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
10922
12053
|
} catch (err) {
|
|
10923
12054
|
const e = err;
|
|
10924
12055
|
if (e.code === "ENOENT") {
|
|
@@ -10964,7 +12095,7 @@ var HistoryStore = class {
|
|
|
10964
12095
|
}
|
|
10965
12096
|
return this.enqueue(sessionId, async () => {
|
|
10966
12097
|
try {
|
|
10967
|
-
await
|
|
12098
|
+
await fs8.unlink(paths.historyFile(sessionId));
|
|
10968
12099
|
} catch (err) {
|
|
10969
12100
|
const e = err;
|
|
10970
12101
|
if (e.code !== "ENOENT") {
|
|
@@ -10972,7 +12103,7 @@ var HistoryStore = class {
|
|
|
10972
12103
|
}
|
|
10973
12104
|
}
|
|
10974
12105
|
try {
|
|
10975
|
-
await
|
|
12106
|
+
await fs8.rmdir(paths.sessionDir(sessionId));
|
|
10976
12107
|
} catch (err) {
|
|
10977
12108
|
const e = err;
|
|
10978
12109
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -11000,6 +12131,8 @@ init_paths();
|
|
|
11000
12131
|
init_history();
|
|
11001
12132
|
init_types();
|
|
11002
12133
|
init_hydra_version();
|
|
12134
|
+
init_queue_store();
|
|
12135
|
+
var QUEUE_REPLAY_TTL_MS = 15 * 60 * 1e3;
|
|
11003
12136
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
11004
12137
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
11005
12138
|
var SessionManager = class {
|
|
@@ -11012,6 +12145,7 @@ var SessionManager = class {
|
|
|
11012
12145
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
11013
12146
|
this.defaultModels = options.defaultModels ?? {};
|
|
11014
12147
|
this.logger = options.logger;
|
|
12148
|
+
this.npmRegistry = options.npmRegistry;
|
|
11015
12149
|
}
|
|
11016
12150
|
registry;
|
|
11017
12151
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -11027,6 +12161,7 @@ var SessionManager = class {
|
|
|
11027
12161
|
// back-to-back) don't lose writes via interleaved reads.
|
|
11028
12162
|
metaWriteQueues = /* @__PURE__ */ new Map();
|
|
11029
12163
|
logger;
|
|
12164
|
+
npmRegistry;
|
|
11030
12165
|
async create(params) {
|
|
11031
12166
|
const fresh = await this.bootstrapAgent({
|
|
11032
12167
|
agentId: params.agentId,
|
|
@@ -11048,7 +12183,9 @@ var SessionManager = class {
|
|
|
11048
12183
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
11049
12184
|
historyStore: this.histories,
|
|
11050
12185
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
11051
|
-
currentModel: fresh.initialModel
|
|
12186
|
+
currentModel: fresh.initialModel,
|
|
12187
|
+
currentMode: fresh.initialMode,
|
|
12188
|
+
agentModes: fresh.initialModes
|
|
11052
12189
|
});
|
|
11053
12190
|
await this.attachManagerHooks(session);
|
|
11054
12191
|
return session;
|
|
@@ -11093,7 +12230,7 @@ var SessionManager = class {
|
|
|
11093
12230
|
if (params.upstreamSessionId === "") {
|
|
11094
12231
|
return this.doResurrectFromImport(params);
|
|
11095
12232
|
}
|
|
11096
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
12233
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
|
|
11097
12234
|
const agent = this.spawner({
|
|
11098
12235
|
agentId: params.agentId,
|
|
11099
12236
|
cwd: params.cwd,
|
|
@@ -11147,9 +12284,10 @@ var SessionManager = class {
|
|
|
11147
12284
|
// this fix), fall back to the model the agent ships in its
|
|
11148
12285
|
// session/load response body.
|
|
11149
12286
|
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
11150
|
-
currentMode: params.currentMode,
|
|
12287
|
+
currentMode: params.currentMode ?? extractInitialCurrentMode(loadResult ?? {}),
|
|
11151
12288
|
currentUsage: params.currentUsage,
|
|
11152
12289
|
agentCommands: params.agentCommands,
|
|
12290
|
+
agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
|
|
11153
12291
|
// Only gate the first-prompt title heuristic when we actually have
|
|
11154
12292
|
// a title to preserve. A title-less session (lost to a write race
|
|
11155
12293
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -11192,9 +12330,10 @@ var SessionManager = class {
|
|
|
11192
12330
|
// Prefer the stored value (set by a previous current_model_update);
|
|
11193
12331
|
// fall back to whatever the agent ships in its session/new response.
|
|
11194
12332
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
11195
|
-
currentMode: params.currentMode,
|
|
12333
|
+
currentMode: params.currentMode ?? fresh.initialMode,
|
|
11196
12334
|
currentUsage: params.currentUsage,
|
|
11197
12335
|
agentCommands: params.agentCommands,
|
|
12336
|
+
agentModes: params.agentModes ?? fresh.initialModes,
|
|
11198
12337
|
firstPromptSeeded: !!params.title,
|
|
11199
12338
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
11200
12339
|
});
|
|
@@ -11204,7 +12343,7 @@ var SessionManager = class {
|
|
|
11204
12343
|
}
|
|
11205
12344
|
async resolveImportCwd(cwd) {
|
|
11206
12345
|
try {
|
|
11207
|
-
const stat4 = await
|
|
12346
|
+
const stat4 = await fs11.stat(cwd);
|
|
11208
12347
|
if (stat4.isDirectory()) {
|
|
11209
12348
|
return cwd;
|
|
11210
12349
|
}
|
|
@@ -11224,7 +12363,7 @@ var SessionManager = class {
|
|
|
11224
12363
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
11225
12364
|
throw err;
|
|
11226
12365
|
}
|
|
11227
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
12366
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
|
|
11228
12367
|
const agent = this.spawner({
|
|
11229
12368
|
agentId: params.agentId,
|
|
11230
12369
|
cwd: params.cwd,
|
|
@@ -11261,11 +12400,15 @@ var SessionManager = class {
|
|
|
11261
12400
|
} catch {
|
|
11262
12401
|
}
|
|
11263
12402
|
}
|
|
12403
|
+
const initialModes = extractInitialModes(newResult);
|
|
12404
|
+
const initialMode = extractInitialCurrentMode(newResult);
|
|
11264
12405
|
return {
|
|
11265
12406
|
agent,
|
|
11266
12407
|
upstreamSessionId: sessionIdRaw,
|
|
11267
12408
|
agentMeta: newResult._meta,
|
|
11268
|
-
initialModel
|
|
12409
|
+
initialModel,
|
|
12410
|
+
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
12411
|
+
initialMode
|
|
11269
12412
|
};
|
|
11270
12413
|
} catch (err) {
|
|
11271
12414
|
await agent.kill().catch(() => void 0);
|
|
@@ -11317,6 +12460,15 @@ var SessionManager = class {
|
|
|
11317
12460
|
}))
|
|
11318
12461
|
}).catch(() => void 0);
|
|
11319
12462
|
});
|
|
12463
|
+
session.onAgentModesChange((modes) => {
|
|
12464
|
+
void this.persistSnapshot(session.sessionId, {
|
|
12465
|
+
agentModes: modes.map((m) => ({
|
|
12466
|
+
id: m.id,
|
|
12467
|
+
...m.name !== void 0 ? { name: m.name } : {},
|
|
12468
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
12469
|
+
}))
|
|
12470
|
+
}).catch(() => void 0);
|
|
12471
|
+
});
|
|
11320
12472
|
this.sessions.set(session.sessionId, session);
|
|
11321
12473
|
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
11322
12474
|
const existing = await this.store.read(session.sessionId);
|
|
@@ -11359,6 +12511,7 @@ var SessionManager = class {
|
|
|
11359
12511
|
currentMode: record.currentMode,
|
|
11360
12512
|
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
11361
12513
|
agentCommands: record.agentCommands,
|
|
12514
|
+
agentModes: record.agentModes,
|
|
11362
12515
|
createdAt: record.createdAt
|
|
11363
12516
|
};
|
|
11364
12517
|
}
|
|
@@ -11383,6 +12536,23 @@ var SessionManager = class {
|
|
|
11383
12536
|
get(sessionId) {
|
|
11384
12537
|
return this.sessions.get(sessionId);
|
|
11385
12538
|
}
|
|
12539
|
+
// Snapshot of which agent versions are currently in use by live
|
|
12540
|
+
// sessions, keyed by agentId. Read by the registry-fetch prune sweep
|
|
12541
|
+
// so it can skip install dirs that still back a running process.
|
|
12542
|
+
activeAgentVersions() {
|
|
12543
|
+
const out = /* @__PURE__ */ new Map();
|
|
12544
|
+
for (const session of this.sessions.values()) {
|
|
12545
|
+
const id = session.agent.agentId;
|
|
12546
|
+
const version = session.agent.version;
|
|
12547
|
+
let set = out.get(id);
|
|
12548
|
+
if (!set) {
|
|
12549
|
+
set = /* @__PURE__ */ new Set();
|
|
12550
|
+
out.set(id, set);
|
|
12551
|
+
}
|
|
12552
|
+
set.add(version);
|
|
12553
|
+
}
|
|
12554
|
+
return out;
|
|
12555
|
+
}
|
|
11386
12556
|
// Resolve a user-typed session id (which may have the hydra_session_
|
|
11387
12557
|
// prefix stripped — that's what `sessions list` and the picker show) to
|
|
11388
12558
|
// the canonical form that actually exists. Tries the input as-given
|
|
@@ -11636,6 +12806,7 @@ var SessionManager = class {
|
|
|
11636
12806
|
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
11637
12807
|
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
11638
12808
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
12809
|
+
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
11639
12810
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11640
12811
|
});
|
|
11641
12812
|
});
|
|
@@ -11669,6 +12840,53 @@ var SessionManager = class {
|
|
|
11669
12840
|
}
|
|
11670
12841
|
await Promise.allSettled(pending);
|
|
11671
12842
|
}
|
|
12843
|
+
// Startup hook: scan persisted sessions for non-empty queue files,
|
|
12844
|
+
// apply the TTL, resurrect anything with surviving entries, and
|
|
12845
|
+
// replay them through the normal queue path. Called from the daemon
|
|
12846
|
+
// boot sequence; failures per session are logged and don't block
|
|
12847
|
+
// the boot.
|
|
12848
|
+
//
|
|
12849
|
+
// Concurrency is deliberately sequential — resurrect each session
|
|
12850
|
+
// one at a time so a runaway daemon with 100 queued sessions
|
|
12851
|
+
// doesn't burst-spawn 100 agents on startup. Inside a single
|
|
12852
|
+
// session, the queue still drains in parallel-friendly fashion via
|
|
12853
|
+
// drainQueue once resurrect() completes.
|
|
12854
|
+
async resurrectPendingQueues() {
|
|
12855
|
+
const records = await this.store.list().catch(() => []);
|
|
12856
|
+
for (const rec of records) {
|
|
12857
|
+
const queue = await loadQueue(rec.sessionId).catch(() => []);
|
|
12858
|
+
if (queue.length === 0) continue;
|
|
12859
|
+
const now = Date.now();
|
|
12860
|
+
const fresh = queue.filter((e) => now - e.enqueuedAt < QUEUE_REPLAY_TTL_MS);
|
|
12861
|
+
const dropped = queue.length - fresh.length;
|
|
12862
|
+
if (dropped > 0) {
|
|
12863
|
+
this.logger?.info(
|
|
12864
|
+
`queue replay: dropping ${dropped} stale prompt(s) for ${rec.sessionId} (TTL ${QUEUE_REPLAY_TTL_MS / 1e3}s)`
|
|
12865
|
+
);
|
|
12866
|
+
await rewriteQueue(rec.sessionId, fresh).catch(() => void 0);
|
|
12867
|
+
}
|
|
12868
|
+
if (fresh.length === 0) continue;
|
|
12869
|
+
const fromDisk = await this.loadFromDisk(rec.sessionId).catch(() => void 0);
|
|
12870
|
+
if (!fromDisk) {
|
|
12871
|
+
this.logger?.warn(
|
|
12872
|
+
`queue replay: no meta for ${rec.sessionId}; discarding ${fresh.length} entr${fresh.length === 1 ? "y" : "ies"}`
|
|
12873
|
+
);
|
|
12874
|
+
await rewriteQueue(rec.sessionId, []).catch(() => void 0);
|
|
12875
|
+
continue;
|
|
12876
|
+
}
|
|
12877
|
+
try {
|
|
12878
|
+
const session = await this.resurrect(fromDisk);
|
|
12879
|
+
this.logger?.info(
|
|
12880
|
+
`queue replay: resurrected ${rec.sessionId} and replaying ${fresh.length} prompt(s)`
|
|
12881
|
+
);
|
|
12882
|
+
session.replayPersistedQueue(fresh);
|
|
12883
|
+
} catch (err) {
|
|
12884
|
+
this.logger?.warn(
|
|
12885
|
+
`queue replay: failed to resurrect ${rec.sessionId}: ${err.message}`
|
|
12886
|
+
);
|
|
12887
|
+
}
|
|
12888
|
+
}
|
|
12889
|
+
}
|
|
11672
12890
|
};
|
|
11673
12891
|
function mergeForPersistence(session, existing) {
|
|
11674
12892
|
const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
|
|
@@ -11678,6 +12896,18 @@ function mergeForPersistence(session, existing) {
|
|
|
11678
12896
|
return { name: c.name };
|
|
11679
12897
|
}) : void 0;
|
|
11680
12898
|
const agentCommands = persistedCommands ?? existing?.agentCommands;
|
|
12899
|
+
const sessionModes = session.availableModes();
|
|
12900
|
+
const persistedModes = sessionModes.length > 0 ? sessionModes.map((m) => {
|
|
12901
|
+
const out = { id: m.id };
|
|
12902
|
+
if (m.name !== void 0) {
|
|
12903
|
+
out.name = m.name;
|
|
12904
|
+
}
|
|
12905
|
+
if (m.description !== void 0) {
|
|
12906
|
+
out.description = m.description;
|
|
12907
|
+
}
|
|
12908
|
+
return out;
|
|
12909
|
+
}) : void 0;
|
|
12910
|
+
const agentModes = persistedModes ?? existing?.agentModes;
|
|
11681
12911
|
return recordFromMemorySession({
|
|
11682
12912
|
sessionId: session.sessionId,
|
|
11683
12913
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
@@ -11693,6 +12923,7 @@ function mergeForPersistence(session, existing) {
|
|
|
11693
12923
|
currentMode: session.currentMode ?? existing?.currentMode,
|
|
11694
12924
|
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
11695
12925
|
agentCommands,
|
|
12926
|
+
agentModes,
|
|
11696
12927
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
11697
12928
|
});
|
|
11698
12929
|
}
|
|
@@ -11755,9 +12986,103 @@ function asString(value) {
|
|
|
11755
12986
|
const trimmed = value.trim();
|
|
11756
12987
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
11757
12988
|
}
|
|
12989
|
+
function nonEmptyOrUndefined(arr) {
|
|
12990
|
+
return arr.length > 0 ? arr : void 0;
|
|
12991
|
+
}
|
|
12992
|
+
function extractInitialModes(result) {
|
|
12993
|
+
const direct = parseModesList(result.availableModes);
|
|
12994
|
+
if (direct.length > 0) {
|
|
12995
|
+
return direct;
|
|
12996
|
+
}
|
|
12997
|
+
const modes = result.modes;
|
|
12998
|
+
if (modes && typeof modes === "object" && !Array.isArray(modes)) {
|
|
12999
|
+
const fromModesObj = parseModesList(
|
|
13000
|
+
modes.availableModes
|
|
13001
|
+
);
|
|
13002
|
+
if (fromModesObj.length > 0) {
|
|
13003
|
+
return fromModesObj;
|
|
13004
|
+
}
|
|
13005
|
+
}
|
|
13006
|
+
const meta = result._meta;
|
|
13007
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
13008
|
+
for (const [key, value] of Object.entries(
|
|
13009
|
+
meta
|
|
13010
|
+
)) {
|
|
13011
|
+
if (key === "hydra-acp") {
|
|
13012
|
+
continue;
|
|
13013
|
+
}
|
|
13014
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
13015
|
+
const fromMeta = parseModesList(
|
|
13016
|
+
value.availableModes
|
|
13017
|
+
);
|
|
13018
|
+
if (fromMeta.length > 0) {
|
|
13019
|
+
return fromMeta;
|
|
13020
|
+
}
|
|
13021
|
+
}
|
|
13022
|
+
}
|
|
13023
|
+
}
|
|
13024
|
+
return [];
|
|
13025
|
+
}
|
|
13026
|
+
function extractInitialCurrentMode(result) {
|
|
13027
|
+
const direct = asString(result.currentModeId) ?? asString(result.currentMode) ?? asString(result.modeId) ?? asString(result.mode);
|
|
13028
|
+
if (direct) {
|
|
13029
|
+
return direct;
|
|
13030
|
+
}
|
|
13031
|
+
const modes = result.modes;
|
|
13032
|
+
if (modes && typeof modes === "object" && !Array.isArray(modes)) {
|
|
13033
|
+
const m = asString(modes.currentModeId) ?? asString(modes.currentMode);
|
|
13034
|
+
if (m) {
|
|
13035
|
+
return m;
|
|
13036
|
+
}
|
|
13037
|
+
}
|
|
13038
|
+
const meta = result._meta;
|
|
13039
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
13040
|
+
for (const [key, value] of Object.entries(
|
|
13041
|
+
meta
|
|
13042
|
+
)) {
|
|
13043
|
+
if (key === "hydra-acp") {
|
|
13044
|
+
continue;
|
|
13045
|
+
}
|
|
13046
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
13047
|
+
const m = asString(value.currentModeId) ?? asString(value.currentMode) ?? asString(value.modeId);
|
|
13048
|
+
if (m) {
|
|
13049
|
+
return m;
|
|
13050
|
+
}
|
|
13051
|
+
}
|
|
13052
|
+
}
|
|
13053
|
+
}
|
|
13054
|
+
return void 0;
|
|
13055
|
+
}
|
|
13056
|
+
function parseModesList(list) {
|
|
13057
|
+
if (!Array.isArray(list)) {
|
|
13058
|
+
return [];
|
|
13059
|
+
}
|
|
13060
|
+
const out = [];
|
|
13061
|
+
for (const raw of list) {
|
|
13062
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
13063
|
+
continue;
|
|
13064
|
+
}
|
|
13065
|
+
const r = raw;
|
|
13066
|
+
const id = asString(r.id) ?? asString(r.modeId);
|
|
13067
|
+
if (!id) {
|
|
13068
|
+
continue;
|
|
13069
|
+
}
|
|
13070
|
+
const mode = { id };
|
|
13071
|
+
const name = asString(r.name);
|
|
13072
|
+
if (name) {
|
|
13073
|
+
mode.name = name;
|
|
13074
|
+
}
|
|
13075
|
+
const description = asString(r.description);
|
|
13076
|
+
if (description) {
|
|
13077
|
+
mode.description = description;
|
|
13078
|
+
}
|
|
13079
|
+
out.push(mode);
|
|
13080
|
+
}
|
|
13081
|
+
return out;
|
|
13082
|
+
}
|
|
11758
13083
|
async function loadPromptHistorySafely(sessionId) {
|
|
11759
13084
|
try {
|
|
11760
|
-
const raw = await
|
|
13085
|
+
const raw = await fs11.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
11761
13086
|
const out = [];
|
|
11762
13087
|
for (const line of raw.split("\n")) {
|
|
11763
13088
|
if (line.length === 0) {
|
|
@@ -11778,7 +13103,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
11778
13103
|
}
|
|
11779
13104
|
async function historyMtimeIso(sessionId) {
|
|
11780
13105
|
try {
|
|
11781
|
-
const st = await
|
|
13106
|
+
const st = await fs11.stat(paths.historyFile(sessionId));
|
|
11782
13107
|
return new Date(st.mtimeMs).toISOString();
|
|
11783
13108
|
} catch {
|
|
11784
13109
|
return void 0;
|
|
@@ -11788,7 +13113,7 @@ async function historyMtimeIso(sessionId) {
|
|
|
11788
13113
|
// src/core/extensions.ts
|
|
11789
13114
|
init_paths();
|
|
11790
13115
|
import { spawn as spawn4 } from "child_process";
|
|
11791
|
-
import * as
|
|
13116
|
+
import * as fs12 from "fs";
|
|
11792
13117
|
import * as fsp3 from "fs/promises";
|
|
11793
13118
|
import * as path7 from "path";
|
|
11794
13119
|
var RESTART_BASE_MS = 1e3;
|
|
@@ -12071,7 +13396,7 @@ var ExtensionManager = class {
|
|
|
12071
13396
|
}
|
|
12072
13397
|
const ext = entry.config;
|
|
12073
13398
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
12074
|
-
const logStream =
|
|
13399
|
+
const logStream = fs12.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
12075
13400
|
flags: "a"
|
|
12076
13401
|
});
|
|
12077
13402
|
logStream.write(
|
|
@@ -12083,7 +13408,7 @@ var ExtensionManager = class {
|
|
|
12083
13408
|
HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
|
|
12084
13409
|
HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
|
|
12085
13410
|
HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
|
|
12086
|
-
HYDRA_ACP_TOKEN: ctx.
|
|
13411
|
+
HYDRA_ACP_TOKEN: ctx.serviceToken,
|
|
12087
13412
|
HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
|
|
12088
13413
|
HYDRA_ACP_HOME: ctx.hydraHome,
|
|
12089
13414
|
HYDRA_ACP_EXTENSION_NAME: ext.name,
|
|
@@ -12121,7 +13446,7 @@ var ExtensionManager = class {
|
|
|
12121
13446
|
}
|
|
12122
13447
|
if (typeof child.pid === "number") {
|
|
12123
13448
|
try {
|
|
12124
|
-
|
|
13449
|
+
fs12.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
12125
13450
|
`, {
|
|
12126
13451
|
encoding: "utf8",
|
|
12127
13452
|
mode: 384
|
|
@@ -12146,7 +13471,7 @@ var ExtensionManager = class {
|
|
|
12146
13471
|
});
|
|
12147
13472
|
child.on("exit", (code, signal) => {
|
|
12148
13473
|
try {
|
|
12149
|
-
|
|
13474
|
+
fs12.unlinkSync(paths.extensionPidFile(ext.name));
|
|
12150
13475
|
} catch {
|
|
12151
13476
|
}
|
|
12152
13477
|
logStream.write(
|
|
@@ -12189,25 +13514,324 @@ var ExtensionManager = class {
|
|
|
12189
13514
|
}
|
|
12190
13515
|
}
|
|
12191
13516
|
};
|
|
12192
|
-
function isAlive(pid) {
|
|
12193
|
-
try {
|
|
12194
|
-
process.kill(pid, 0);
|
|
12195
|
-
return true;
|
|
12196
|
-
} catch {
|
|
13517
|
+
function isAlive(pid) {
|
|
13518
|
+
try {
|
|
13519
|
+
process.kill(pid, 0);
|
|
13520
|
+
return true;
|
|
13521
|
+
} catch {
|
|
13522
|
+
return false;
|
|
13523
|
+
}
|
|
13524
|
+
}
|
|
13525
|
+
function withCode2(err, code) {
|
|
13526
|
+
err.code = code;
|
|
13527
|
+
return err;
|
|
13528
|
+
}
|
|
13529
|
+
|
|
13530
|
+
// src/daemon/server.ts
|
|
13531
|
+
init_paths();
|
|
13532
|
+
|
|
13533
|
+
// src/core/agent-prune.ts
|
|
13534
|
+
init_paths();
|
|
13535
|
+
import * as fsp4 from "fs/promises";
|
|
13536
|
+
import * as path8 from "path";
|
|
13537
|
+
var logSink3 = (msg) => {
|
|
13538
|
+
process.stderr.write(msg + "\n");
|
|
13539
|
+
};
|
|
13540
|
+
function setAgentPruneLogger(log) {
|
|
13541
|
+
logSink3 = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
13542
|
+
}
|
|
13543
|
+
async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
13544
|
+
const platformKey = currentPlatformKey();
|
|
13545
|
+
if (!platformKey) {
|
|
13546
|
+
return;
|
|
13547
|
+
}
|
|
13548
|
+
const doc = await registry.load();
|
|
13549
|
+
const desiredByAgent = /* @__PURE__ */ new Map();
|
|
13550
|
+
for (const a of doc.agents) {
|
|
13551
|
+
desiredByAgent.set(a.id, a.version ?? "current");
|
|
13552
|
+
}
|
|
13553
|
+
const activeByAgent = sessionManager.activeAgentVersions();
|
|
13554
|
+
const platformDir = path8.join(paths.agentsDir(), platformKey);
|
|
13555
|
+
let agentEntries;
|
|
13556
|
+
try {
|
|
13557
|
+
agentEntries = await fsp4.readdir(platformDir, { withFileTypes: true });
|
|
13558
|
+
} catch (err) {
|
|
13559
|
+
const e = err;
|
|
13560
|
+
if (e.code === "ENOENT") {
|
|
13561
|
+
return;
|
|
13562
|
+
}
|
|
13563
|
+
logSink3(`hydra-acp: prune: failed to read ${platformDir}: ${e.message}`);
|
|
13564
|
+
return;
|
|
13565
|
+
}
|
|
13566
|
+
for (const agentEntry of agentEntries) {
|
|
13567
|
+
if (!agentEntry.isDirectory()) {
|
|
13568
|
+
continue;
|
|
13569
|
+
}
|
|
13570
|
+
const agentId = agentEntry.name;
|
|
13571
|
+
const desired = desiredByAgent.get(agentId);
|
|
13572
|
+
if (desired === void 0) {
|
|
13573
|
+
continue;
|
|
13574
|
+
}
|
|
13575
|
+
const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
|
|
13576
|
+
const agentDir = path8.join(platformDir, agentId);
|
|
13577
|
+
let versionEntries;
|
|
13578
|
+
try {
|
|
13579
|
+
versionEntries = await fsp4.readdir(agentDir, { withFileTypes: true });
|
|
13580
|
+
} catch (err) {
|
|
13581
|
+
logSink3(
|
|
13582
|
+
`hydra-acp: prune: failed to read ${agentDir}: ${err.message}`
|
|
13583
|
+
);
|
|
13584
|
+
continue;
|
|
13585
|
+
}
|
|
13586
|
+
for (const versionEntry of versionEntries) {
|
|
13587
|
+
if (!versionEntry.isDirectory()) {
|
|
13588
|
+
continue;
|
|
13589
|
+
}
|
|
13590
|
+
const version = versionEntry.name;
|
|
13591
|
+
if (version === desired) {
|
|
13592
|
+
continue;
|
|
13593
|
+
}
|
|
13594
|
+
if (activeVersions.has(version)) {
|
|
13595
|
+
continue;
|
|
13596
|
+
}
|
|
13597
|
+
const versionDir = path8.join(agentDir, version);
|
|
13598
|
+
try {
|
|
13599
|
+
await fsp4.rm(versionDir, { recursive: true, force: true });
|
|
13600
|
+
logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
|
|
13601
|
+
} catch (err) {
|
|
13602
|
+
logSink3(
|
|
13603
|
+
`hydra-acp: prune: failed to remove ${versionDir}: ${err.message}`
|
|
13604
|
+
);
|
|
13605
|
+
}
|
|
13606
|
+
}
|
|
13607
|
+
}
|
|
13608
|
+
}
|
|
13609
|
+
|
|
13610
|
+
// src/daemon/server.ts
|
|
13611
|
+
init_hydra_version();
|
|
13612
|
+
|
|
13613
|
+
// src/core/session-tokens.ts
|
|
13614
|
+
init_paths();
|
|
13615
|
+
import * as fs13 from "fs/promises";
|
|
13616
|
+
import * as path9 from "path";
|
|
13617
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
13618
|
+
var TOKEN_PREFIX = "hydra_session_";
|
|
13619
|
+
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
13620
|
+
var ID_LENGTH = 12;
|
|
13621
|
+
var TOKEN_BYTES = 32;
|
|
13622
|
+
var WRITE_DEBOUNCE_MS = 50;
|
|
13623
|
+
function tokensFilePath() {
|
|
13624
|
+
return path9.join(paths.home(), "session-tokens.json");
|
|
13625
|
+
}
|
|
13626
|
+
function sha256Hex(input) {
|
|
13627
|
+
return createHash("sha256").update(input).digest("hex");
|
|
13628
|
+
}
|
|
13629
|
+
function randomHex(bytes) {
|
|
13630
|
+
return randomBytes(bytes).toString("hex");
|
|
13631
|
+
}
|
|
13632
|
+
function generateId() {
|
|
13633
|
+
return randomHex(ID_LENGTH).slice(0, ID_LENGTH * 2);
|
|
13634
|
+
}
|
|
13635
|
+
function generateToken() {
|
|
13636
|
+
return `${TOKEN_PREFIX}${randomHex(TOKEN_BYTES)}`;
|
|
13637
|
+
}
|
|
13638
|
+
var SessionTokenStore = class _SessionTokenStore {
|
|
13639
|
+
records = /* @__PURE__ */ new Map();
|
|
13640
|
+
// keyed by hash
|
|
13641
|
+
writeTimer = null;
|
|
13642
|
+
writeInflight = null;
|
|
13643
|
+
constructor(records) {
|
|
13644
|
+
for (const r of records) {
|
|
13645
|
+
this.records.set(r.hash, r);
|
|
13646
|
+
}
|
|
13647
|
+
}
|
|
13648
|
+
static async load() {
|
|
13649
|
+
let records = [];
|
|
13650
|
+
try {
|
|
13651
|
+
const raw = await fs13.readFile(tokensFilePath(), "utf8");
|
|
13652
|
+
const parsed = JSON.parse(raw);
|
|
13653
|
+
if (parsed && Array.isArray(parsed.records)) {
|
|
13654
|
+
records = parsed.records.filter(isRecord);
|
|
13655
|
+
}
|
|
13656
|
+
} catch (err) {
|
|
13657
|
+
const e = err;
|
|
13658
|
+
if (e.code !== "ENOENT") {
|
|
13659
|
+
throw err;
|
|
13660
|
+
}
|
|
13661
|
+
}
|
|
13662
|
+
const store = new _SessionTokenStore(records);
|
|
13663
|
+
const removed = store.sweepExpired(/* @__PURE__ */ new Date());
|
|
13664
|
+
if (removed > 0) {
|
|
13665
|
+
await store.flush();
|
|
13666
|
+
}
|
|
13667
|
+
return store;
|
|
13668
|
+
}
|
|
13669
|
+
async issue(opts = {}) {
|
|
13670
|
+
const token = generateToken();
|
|
13671
|
+
const hash = sha256Hex(token);
|
|
13672
|
+
const id = generateId();
|
|
13673
|
+
const now = /* @__PURE__ */ new Date();
|
|
13674
|
+
const ttlSec = opts.ttlSec && opts.ttlSec > 0 ? opts.ttlSec : DEFAULT_TTL_SEC;
|
|
13675
|
+
const expiresAt = new Date(now.getTime() + ttlSec * 1e3);
|
|
13676
|
+
const record = {
|
|
13677
|
+
id,
|
|
13678
|
+
hash,
|
|
13679
|
+
label: opts.label,
|
|
13680
|
+
createdAt: now.toISOString(),
|
|
13681
|
+
expiresAt: expiresAt.toISOString(),
|
|
13682
|
+
lastUsedAt: now.toISOString()
|
|
13683
|
+
};
|
|
13684
|
+
this.records.set(hash, record);
|
|
13685
|
+
this.scheduleWrite();
|
|
13686
|
+
return { id, token, expiresAt: record.expiresAt };
|
|
13687
|
+
}
|
|
13688
|
+
// Verifies a presented token. Returns the matching record id (so the
|
|
13689
|
+
// caller can revoke it on logout) and bumps lastUsedAt; returns
|
|
13690
|
+
// undefined when no record matches or when the matched record has
|
|
13691
|
+
// expired.
|
|
13692
|
+
async verify(token) {
|
|
13693
|
+
if (typeof token !== "string" || !token.startsWith(TOKEN_PREFIX)) {
|
|
13694
|
+
return void 0;
|
|
13695
|
+
}
|
|
13696
|
+
const hash = sha256Hex(token);
|
|
13697
|
+
const record = this.records.get(hash);
|
|
13698
|
+
if (!record) {
|
|
13699
|
+
return void 0;
|
|
13700
|
+
}
|
|
13701
|
+
const expected = Buffer.from(record.hash, "hex");
|
|
13702
|
+
const actual = Buffer.from(hash, "hex");
|
|
13703
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
13704
|
+
return void 0;
|
|
13705
|
+
}
|
|
13706
|
+
const now = /* @__PURE__ */ new Date();
|
|
13707
|
+
if (new Date(record.expiresAt).getTime() <= now.getTime()) {
|
|
13708
|
+
this.records.delete(hash);
|
|
13709
|
+
this.scheduleWrite();
|
|
13710
|
+
return void 0;
|
|
13711
|
+
}
|
|
13712
|
+
record.lastUsedAt = now.toISOString();
|
|
13713
|
+
this.scheduleWrite();
|
|
13714
|
+
return record.id;
|
|
13715
|
+
}
|
|
13716
|
+
async revoke(id) {
|
|
13717
|
+
for (const [hash, r] of this.records) {
|
|
13718
|
+
if (r.id === id) {
|
|
13719
|
+
this.records.delete(hash);
|
|
13720
|
+
this.scheduleWrite();
|
|
13721
|
+
return true;
|
|
13722
|
+
}
|
|
13723
|
+
}
|
|
13724
|
+
return false;
|
|
13725
|
+
}
|
|
13726
|
+
async revokeAll() {
|
|
13727
|
+
const n = this.records.size;
|
|
13728
|
+
this.records.clear();
|
|
13729
|
+
this.scheduleWrite();
|
|
13730
|
+
return n;
|
|
13731
|
+
}
|
|
13732
|
+
list() {
|
|
13733
|
+
return Array.from(this.records.values()).map(({ id, label, createdAt, expiresAt, lastUsedAt }) => ({
|
|
13734
|
+
id,
|
|
13735
|
+
label,
|
|
13736
|
+
createdAt,
|
|
13737
|
+
expiresAt,
|
|
13738
|
+
lastUsedAt
|
|
13739
|
+
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
13740
|
+
}
|
|
13741
|
+
sweepExpired(now = /* @__PURE__ */ new Date()) {
|
|
13742
|
+
let removed = 0;
|
|
13743
|
+
for (const [hash, r] of this.records) {
|
|
13744
|
+
if (new Date(r.expiresAt).getTime() <= now.getTime()) {
|
|
13745
|
+
this.records.delete(hash);
|
|
13746
|
+
removed += 1;
|
|
13747
|
+
}
|
|
13748
|
+
}
|
|
13749
|
+
if (removed > 0) {
|
|
13750
|
+
this.scheduleWrite();
|
|
13751
|
+
}
|
|
13752
|
+
return removed;
|
|
13753
|
+
}
|
|
13754
|
+
// Force any pending write to complete. Useful in tests and at shutdown.
|
|
13755
|
+
async flush() {
|
|
13756
|
+
if (this.writeTimer) {
|
|
13757
|
+
clearTimeout(this.writeTimer);
|
|
13758
|
+
this.writeTimer = null;
|
|
13759
|
+
}
|
|
13760
|
+
await this.persist();
|
|
13761
|
+
}
|
|
13762
|
+
scheduleWrite() {
|
|
13763
|
+
if (this.writeTimer) {
|
|
13764
|
+
return;
|
|
13765
|
+
}
|
|
13766
|
+
this.writeTimer = setTimeout(() => {
|
|
13767
|
+
this.writeTimer = null;
|
|
13768
|
+
this.persist().catch(() => {
|
|
13769
|
+
});
|
|
13770
|
+
}, WRITE_DEBOUNCE_MS);
|
|
13771
|
+
}
|
|
13772
|
+
async persist() {
|
|
13773
|
+
if (this.writeInflight) {
|
|
13774
|
+
await this.writeInflight;
|
|
13775
|
+
}
|
|
13776
|
+
const records = Array.from(this.records.values());
|
|
13777
|
+
const payload = JSON.stringify({ records }, null, 2) + "\n";
|
|
13778
|
+
this.writeInflight = (async () => {
|
|
13779
|
+
await fs13.mkdir(paths.home(), { recursive: true });
|
|
13780
|
+
await fs13.writeFile(tokensFilePath(), payload, {
|
|
13781
|
+
encoding: "utf8",
|
|
13782
|
+
mode: 384
|
|
13783
|
+
});
|
|
13784
|
+
})();
|
|
13785
|
+
try {
|
|
13786
|
+
await this.writeInflight;
|
|
13787
|
+
} finally {
|
|
13788
|
+
this.writeInflight = null;
|
|
13789
|
+
}
|
|
13790
|
+
}
|
|
13791
|
+
};
|
|
13792
|
+
function isRecord(value) {
|
|
13793
|
+
if (!value || typeof value !== "object") {
|
|
12197
13794
|
return false;
|
|
12198
13795
|
}
|
|
13796
|
+
const v = value;
|
|
13797
|
+
return typeof v.id === "string" && typeof v.hash === "string" && typeof v.createdAt === "string" && typeof v.expiresAt === "string" && typeof v.lastUsedAt === "string" && (v.label === void 0 || typeof v.label === "string");
|
|
12199
13798
|
}
|
|
12200
|
-
function withCode2(err, code) {
|
|
12201
|
-
err.code = code;
|
|
12202
|
-
return err;
|
|
12203
|
-
}
|
|
12204
|
-
|
|
12205
|
-
// src/daemon/server.ts
|
|
12206
|
-
init_paths();
|
|
12207
|
-
init_hydra_version();
|
|
12208
13799
|
|
|
12209
13800
|
// src/daemon/auth.ts
|
|
12210
13801
|
var BEARER_PREFIX = "Bearer ";
|
|
13802
|
+
var StaticTokenValidator = class {
|
|
13803
|
+
constructor(token) {
|
|
13804
|
+
this.token = token;
|
|
13805
|
+
}
|
|
13806
|
+
token;
|
|
13807
|
+
async validate(token) {
|
|
13808
|
+
return constantTimeEqual(token, this.token) ? "service" : void 0;
|
|
13809
|
+
}
|
|
13810
|
+
};
|
|
13811
|
+
var SessionTokenValidator = class {
|
|
13812
|
+
constructor(store) {
|
|
13813
|
+
this.store = store;
|
|
13814
|
+
}
|
|
13815
|
+
store;
|
|
13816
|
+
async validate(token) {
|
|
13817
|
+
return this.store.verify(token);
|
|
13818
|
+
}
|
|
13819
|
+
};
|
|
13820
|
+
var CompositeTokenValidator = class {
|
|
13821
|
+
constructor(validators) {
|
|
13822
|
+
this.validators = validators;
|
|
13823
|
+
}
|
|
13824
|
+
validators;
|
|
13825
|
+
async validate(token) {
|
|
13826
|
+
for (const v of this.validators) {
|
|
13827
|
+
const id = await v.validate(token);
|
|
13828
|
+
if (id !== void 0) {
|
|
13829
|
+
return id;
|
|
13830
|
+
}
|
|
13831
|
+
}
|
|
13832
|
+
return void 0;
|
|
13833
|
+
}
|
|
13834
|
+
};
|
|
12211
13835
|
function bearerAuth(opts) {
|
|
12212
13836
|
return async function authMiddleware(request, reply) {
|
|
12213
13837
|
const header = request.headers.authorization;
|
|
@@ -12216,10 +13840,12 @@ function bearerAuth(opts) {
|
|
|
12216
13840
|
return;
|
|
12217
13841
|
}
|
|
12218
13842
|
const token = header.slice(BEARER_PREFIX.length).trim();
|
|
12219
|
-
|
|
13843
|
+
const identity = await opts.validator.validate(token);
|
|
13844
|
+
if (!identity) {
|
|
12220
13845
|
reply.code(403).send({ error: "Invalid token" });
|
|
12221
13846
|
return;
|
|
12222
13847
|
}
|
|
13848
|
+
request.authIdentity = identity;
|
|
12223
13849
|
};
|
|
12224
13850
|
}
|
|
12225
13851
|
function tokenFromUpgradeRequest(req) {
|
|
@@ -12258,6 +13884,40 @@ function constantTimeEqual(a, b) {
|
|
|
12258
13884
|
return mismatch === 0;
|
|
12259
13885
|
}
|
|
12260
13886
|
|
|
13887
|
+
// src/daemon/rate-limit.ts
|
|
13888
|
+
var AuthRateLimiter = class {
|
|
13889
|
+
entries = /* @__PURE__ */ new Map();
|
|
13890
|
+
maxFails;
|
|
13891
|
+
windowMs;
|
|
13892
|
+
constructor(maxFails = 10, windowMs = 15 * 60 * 1e3) {
|
|
13893
|
+
this.maxFails = maxFails;
|
|
13894
|
+
this.windowMs = windowMs;
|
|
13895
|
+
}
|
|
13896
|
+
isBlocked(ip) {
|
|
13897
|
+
const e = this.entries.get(ip);
|
|
13898
|
+
if (!e) {
|
|
13899
|
+
return false;
|
|
13900
|
+
}
|
|
13901
|
+
if (Date.now() - e.windowStart > this.windowMs) {
|
|
13902
|
+
this.entries.delete(ip);
|
|
13903
|
+
return false;
|
|
13904
|
+
}
|
|
13905
|
+
return e.fails >= this.maxFails;
|
|
13906
|
+
}
|
|
13907
|
+
recordFailure(ip) {
|
|
13908
|
+
const now = Date.now();
|
|
13909
|
+
const e = this.entries.get(ip);
|
|
13910
|
+
if (!e || now - e.windowStart > this.windowMs) {
|
|
13911
|
+
this.entries.set(ip, { fails: 1, windowStart: now });
|
|
13912
|
+
return;
|
|
13913
|
+
}
|
|
13914
|
+
e.fails += 1;
|
|
13915
|
+
}
|
|
13916
|
+
recordSuccess(ip) {
|
|
13917
|
+
this.entries.delete(ip);
|
|
13918
|
+
}
|
|
13919
|
+
};
|
|
13920
|
+
|
|
12261
13921
|
// src/daemon/routes/sessions.ts
|
|
12262
13922
|
init_config();
|
|
12263
13923
|
init_bundle();
|
|
@@ -12625,6 +14285,181 @@ function registerConfigRoutes(app, defaults) {
|
|
|
12625
14285
|
});
|
|
12626
14286
|
}
|
|
12627
14287
|
|
|
14288
|
+
// src/daemon/routes/auth.ts
|
|
14289
|
+
import { z as z6 } from "zod";
|
|
14290
|
+
|
|
14291
|
+
// src/core/password.ts
|
|
14292
|
+
init_paths();
|
|
14293
|
+
import * as fs14 from "fs/promises";
|
|
14294
|
+
import * as path10 from "path";
|
|
14295
|
+
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
14296
|
+
import { promisify } from "util";
|
|
14297
|
+
var scryptAsync = promisify(scrypt);
|
|
14298
|
+
function passwordHashPath() {
|
|
14299
|
+
return path10.join(paths.home(), "password-hash");
|
|
14300
|
+
}
|
|
14301
|
+
var DEFAULT_N = 1 << 15;
|
|
14302
|
+
var DEFAULT_R = 8;
|
|
14303
|
+
var DEFAULT_P = 1;
|
|
14304
|
+
var KEY_LEN = 64;
|
|
14305
|
+
var SALT_LEN = 16;
|
|
14306
|
+
var MAX_MEM = 128 * 1024 * 1024;
|
|
14307
|
+
async function setPassword(plaintext) {
|
|
14308
|
+
if (typeof plaintext !== "string" || plaintext.length === 0) {
|
|
14309
|
+
throw new Error("password must be a non-empty string");
|
|
14310
|
+
}
|
|
14311
|
+
const salt = randomBytes2(SALT_LEN);
|
|
14312
|
+
const key = await scryptAsync(plaintext, salt, KEY_LEN, {
|
|
14313
|
+
N: DEFAULT_N,
|
|
14314
|
+
r: DEFAULT_R,
|
|
14315
|
+
p: DEFAULT_P,
|
|
14316
|
+
maxmem: MAX_MEM
|
|
14317
|
+
});
|
|
14318
|
+
const encoded = `scrypt$${DEFAULT_N}$${DEFAULT_R}$${DEFAULT_P}$${salt.toString("hex")}$${key.toString("hex")}
|
|
14319
|
+
`;
|
|
14320
|
+
await fs14.mkdir(paths.home(), { recursive: true });
|
|
14321
|
+
await fs14.writeFile(passwordHashPath(), encoded, {
|
|
14322
|
+
encoding: "utf8",
|
|
14323
|
+
mode: 384
|
|
14324
|
+
});
|
|
14325
|
+
}
|
|
14326
|
+
async function hasPassword() {
|
|
14327
|
+
try {
|
|
14328
|
+
const text = await fs14.readFile(passwordHashPath(), "utf8");
|
|
14329
|
+
return text.trim().length > 0;
|
|
14330
|
+
} catch (err) {
|
|
14331
|
+
const e = err;
|
|
14332
|
+
if (e.code === "ENOENT") {
|
|
14333
|
+
return false;
|
|
14334
|
+
}
|
|
14335
|
+
throw err;
|
|
14336
|
+
}
|
|
14337
|
+
}
|
|
14338
|
+
async function verifyPassword(plaintext) {
|
|
14339
|
+
if (typeof plaintext !== "string" || plaintext.length === 0) {
|
|
14340
|
+
return false;
|
|
14341
|
+
}
|
|
14342
|
+
let line;
|
|
14343
|
+
try {
|
|
14344
|
+
line = (await fs14.readFile(passwordHashPath(), "utf8")).trim();
|
|
14345
|
+
} catch (err) {
|
|
14346
|
+
const e = err;
|
|
14347
|
+
if (e.code === "ENOENT") {
|
|
14348
|
+
return false;
|
|
14349
|
+
}
|
|
14350
|
+
throw err;
|
|
14351
|
+
}
|
|
14352
|
+
const parts = line.split("$");
|
|
14353
|
+
if (parts.length !== 6 || parts[0] !== "scrypt") {
|
|
14354
|
+
return false;
|
|
14355
|
+
}
|
|
14356
|
+
const N = parseInt(parts[1], 10);
|
|
14357
|
+
const r = parseInt(parts[2], 10);
|
|
14358
|
+
const p = parseInt(parts[3], 10);
|
|
14359
|
+
if (!Number.isFinite(N) || !Number.isFinite(r) || !Number.isFinite(p)) {
|
|
14360
|
+
return false;
|
|
14361
|
+
}
|
|
14362
|
+
const salt = Buffer.from(parts[4], "hex");
|
|
14363
|
+
const expected = Buffer.from(parts[5], "hex");
|
|
14364
|
+
if (salt.length === 0 || expected.length === 0) {
|
|
14365
|
+
return false;
|
|
14366
|
+
}
|
|
14367
|
+
const actual = await scryptAsync(plaintext, salt, expected.length, {
|
|
14368
|
+
N,
|
|
14369
|
+
r,
|
|
14370
|
+
p,
|
|
14371
|
+
maxmem: MAX_MEM
|
|
14372
|
+
});
|
|
14373
|
+
if (actual.length !== expected.length) {
|
|
14374
|
+
return false;
|
|
14375
|
+
}
|
|
14376
|
+
return timingSafeEqual2(actual, expected);
|
|
14377
|
+
}
|
|
14378
|
+
|
|
14379
|
+
// src/daemon/routes/auth.ts
|
|
14380
|
+
var LoginBody = z6.object({
|
|
14381
|
+
password: z6.string().min(1),
|
|
14382
|
+
label: z6.string().min(1).max(256).optional(),
|
|
14383
|
+
ttlSec: z6.number().int().positive().optional()
|
|
14384
|
+
});
|
|
14385
|
+
var LogoutBody = z6.object({
|
|
14386
|
+
id: z6.string().optional()
|
|
14387
|
+
}).optional();
|
|
14388
|
+
function registerAuthRoutes(app, deps) {
|
|
14389
|
+
app.post(
|
|
14390
|
+
"/v1/auth/login",
|
|
14391
|
+
{ config: { skipAuth: true } },
|
|
14392
|
+
async (request, reply) => {
|
|
14393
|
+
const ip = remoteIp(request);
|
|
14394
|
+
if (deps.rateLimiter.isBlocked(ip)) {
|
|
14395
|
+
return reply.code(429).send({
|
|
14396
|
+
error: "Too many failed attempts; try again later."
|
|
14397
|
+
});
|
|
14398
|
+
}
|
|
14399
|
+
let body;
|
|
14400
|
+
try {
|
|
14401
|
+
body = LoginBody.parse(request.body);
|
|
14402
|
+
} catch {
|
|
14403
|
+
return reply.code(400).send({ error: "Invalid request body" });
|
|
14404
|
+
}
|
|
14405
|
+
if (!await hasPassword()) {
|
|
14406
|
+
return reply.code(403).send({
|
|
14407
|
+
error: "No password configured. Run `hydra-acp auth password` on the daemon host."
|
|
14408
|
+
});
|
|
14409
|
+
}
|
|
14410
|
+
const ok = await verifyPassword(body.password);
|
|
14411
|
+
if (!ok) {
|
|
14412
|
+
deps.rateLimiter.recordFailure(ip);
|
|
14413
|
+
return reply.code(401).send({ error: "Invalid password" });
|
|
14414
|
+
}
|
|
14415
|
+
deps.rateLimiter.recordSuccess(ip);
|
|
14416
|
+
const issued = await deps.store.issue({
|
|
14417
|
+
label: body.label,
|
|
14418
|
+
ttlSec: body.ttlSec
|
|
14419
|
+
});
|
|
14420
|
+
return reply.code(200).send({
|
|
14421
|
+
session_token: issued.token,
|
|
14422
|
+
id: issued.id,
|
|
14423
|
+
expires_at: issued.expiresAt
|
|
14424
|
+
});
|
|
14425
|
+
}
|
|
14426
|
+
);
|
|
14427
|
+
app.post("/v1/auth/logout", async (request, reply) => {
|
|
14428
|
+
let body = void 0;
|
|
14429
|
+
try {
|
|
14430
|
+
body = LogoutBody.parse(request.body ?? void 0);
|
|
14431
|
+
} catch {
|
|
14432
|
+
return reply.code(400).send({ error: "Invalid request body" });
|
|
14433
|
+
}
|
|
14434
|
+
const id = body?.id ?? request.authIdentity;
|
|
14435
|
+
if (!id || id === "service") {
|
|
14436
|
+
return reply.code(200).send({ revoked: false });
|
|
14437
|
+
}
|
|
14438
|
+
const revoked = await deps.store.revoke(id);
|
|
14439
|
+
return reply.code(200).send({ revoked });
|
|
14440
|
+
});
|
|
14441
|
+
app.get("/v1/auth/verify", async (_request, reply) => {
|
|
14442
|
+
return reply.code(200).send({ ok: true });
|
|
14443
|
+
});
|
|
14444
|
+
app.get("/v1/auth/sessions", async (_request, reply) => {
|
|
14445
|
+
return reply.code(200).send({ sessions: deps.store.list() });
|
|
14446
|
+
});
|
|
14447
|
+
app.delete(
|
|
14448
|
+
"/v1/auth/sessions/:id",
|
|
14449
|
+
async (request, reply) => {
|
|
14450
|
+
const id = request.params.id;
|
|
14451
|
+
const revoked = await deps.store.revoke(id);
|
|
14452
|
+
if (!revoked) {
|
|
14453
|
+
return reply.code(404).send({ error: "Not found" });
|
|
14454
|
+
}
|
|
14455
|
+
return reply.code(204).send();
|
|
14456
|
+
}
|
|
14457
|
+
);
|
|
14458
|
+
}
|
|
14459
|
+
function remoteIp(request) {
|
|
14460
|
+
return request.ip || "unknown";
|
|
14461
|
+
}
|
|
14462
|
+
|
|
12628
14463
|
// src/daemon/acp-ws.ts
|
|
12629
14464
|
init_connection();
|
|
12630
14465
|
init_ws_stream();
|
|
@@ -12632,12 +14467,12 @@ init_types();
|
|
|
12632
14467
|
import { nanoid as nanoid2 } from "nanoid";
|
|
12633
14468
|
init_hydra_version();
|
|
12634
14469
|
function registerAcpWsEndpoint(app, deps) {
|
|
12635
|
-
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
14470
|
+
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
12636
14471
|
const token = tokenFromUpgradeRequest({
|
|
12637
14472
|
headers: request.headers,
|
|
12638
14473
|
url: request.url
|
|
12639
14474
|
});
|
|
12640
|
-
if (!token || !
|
|
14475
|
+
if (!token || !await deps.validator.validate(token)) {
|
|
12641
14476
|
socket.close(4401, "Unauthorized");
|
|
12642
14477
|
return;
|
|
12643
14478
|
}
|
|
@@ -12688,8 +14523,15 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12688
14523
|
}
|
|
12689
14524
|
})();
|
|
12690
14525
|
});
|
|
14526
|
+
const modesPayload = buildModesPayload(session);
|
|
12691
14527
|
return {
|
|
12692
14528
|
sessionId: session.sessionId,
|
|
14529
|
+
// session/new is implicitly an attach; mirror session/attach's
|
|
14530
|
+
// shape by including the clientId so deferred-echo clients
|
|
14531
|
+
// (TUI's queue work) can recognize their own prompt_queue_added
|
|
14532
|
+
// events without an extra round-trip.
|
|
14533
|
+
clientId: client.clientId,
|
|
14534
|
+
...modesPayload ? { modes: modesPayload } : {},
|
|
12693
14535
|
_meta: buildResponseMeta(session)
|
|
12694
14536
|
};
|
|
12695
14537
|
});
|
|
@@ -12750,6 +14592,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12750
14592
|
await connection.notify(note.method, note.params);
|
|
12751
14593
|
}
|
|
12752
14594
|
session.replayPendingPermissions(client);
|
|
14595
|
+
const modesPayload = buildModesPayload(session);
|
|
12753
14596
|
return {
|
|
12754
14597
|
sessionId: session.sessionId,
|
|
12755
14598
|
clientId: client.clientId,
|
|
@@ -12760,6 +14603,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12760
14603
|
// ran, not what was asked for.
|
|
12761
14604
|
historyPolicy: appliedPolicy,
|
|
12762
14605
|
replayed: replay.length,
|
|
14606
|
+
...modesPayload ? { modes: modesPayload } : {},
|
|
12763
14607
|
_meta: buildResponseMeta(session)
|
|
12764
14608
|
};
|
|
12765
14609
|
});
|
|
@@ -12793,7 +14637,29 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12793
14637
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12794
14638
|
throw err;
|
|
12795
14639
|
}
|
|
12796
|
-
|
|
14640
|
+
let session = deps.manager.get(params.sessionId);
|
|
14641
|
+
if (!session) {
|
|
14642
|
+
const fromDisk = await deps.manager.loadFromDisk(params.sessionId);
|
|
14643
|
+
if (!fromDisk) {
|
|
14644
|
+
const err = new Error(
|
|
14645
|
+
`session ${params.sessionId} not found`
|
|
14646
|
+
);
|
|
14647
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14648
|
+
throw err;
|
|
14649
|
+
}
|
|
14650
|
+
app.log.info(
|
|
14651
|
+
`session/prompt auto-resurrecting cold sessionId=${params.sessionId}`
|
|
14652
|
+
);
|
|
14653
|
+
session = await deps.manager.resurrect(fromDisk);
|
|
14654
|
+
const client = bindClientToSession(
|
|
14655
|
+
connection,
|
|
14656
|
+
session,
|
|
14657
|
+
state,
|
|
14658
|
+
void 0,
|
|
14659
|
+
att.clientId
|
|
14660
|
+
);
|
|
14661
|
+
await session.attach(client, "none");
|
|
14662
|
+
}
|
|
12797
14663
|
return session.prompt(att.clientId, params);
|
|
12798
14664
|
});
|
|
12799
14665
|
const handleCancelParams = (raw) => {
|
|
@@ -12825,6 +14691,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12825
14691
|
handleCancelParams(raw);
|
|
12826
14692
|
return null;
|
|
12827
14693
|
});
|
|
14694
|
+
connection.onRequest("hydra-acp/cancel_prompt", async (raw) => {
|
|
14695
|
+
const params = CancelPromptParams.parse(raw);
|
|
14696
|
+
const session = deps.manager.get(params.sessionId);
|
|
14697
|
+
if (!session) {
|
|
14698
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
14699
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14700
|
+
throw err;
|
|
14701
|
+
}
|
|
14702
|
+
return session.cancelQueuedPrompt(params.messageId);
|
|
14703
|
+
});
|
|
14704
|
+
connection.onRequest("hydra-acp/update_prompt", async (raw) => {
|
|
14705
|
+
const params = UpdatePromptParams.parse(raw);
|
|
14706
|
+
const session = deps.manager.get(params.sessionId);
|
|
14707
|
+
if (!session) {
|
|
14708
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
14709
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14710
|
+
throw err;
|
|
14711
|
+
}
|
|
14712
|
+
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
14713
|
+
});
|
|
12828
14714
|
connection.onRequest("session/load", async (raw) => {
|
|
12829
14715
|
const rawObj = raw ?? {};
|
|
12830
14716
|
const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
|
|
@@ -12856,8 +14742,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12856
14742
|
await connection.notify(note.method, note.params);
|
|
12857
14743
|
}
|
|
12858
14744
|
session.replayPendingPermissions(client);
|
|
14745
|
+
const modesPayload = buildModesPayload(session);
|
|
12859
14746
|
return {
|
|
12860
14747
|
sessionId: session.sessionId,
|
|
14748
|
+
// Same as session/new: include clientId so the deferred-echo
|
|
14749
|
+
// path in queue-aware clients can recognize own broadcasts.
|
|
14750
|
+
clientId: client.clientId,
|
|
14751
|
+
...modesPayload ? { modes: modesPayload } : {},
|
|
12861
14752
|
_meta: buildResponseMeta(session)
|
|
12862
14753
|
};
|
|
12863
14754
|
});
|
|
@@ -12883,6 +14774,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12883
14774
|
});
|
|
12884
14775
|
});
|
|
12885
14776
|
}
|
|
14777
|
+
function buildModesPayload(session) {
|
|
14778
|
+
const modes = session.availableModes();
|
|
14779
|
+
if (modes.length === 0) {
|
|
14780
|
+
return void 0;
|
|
14781
|
+
}
|
|
14782
|
+
const availableModes = modes.map((m) => {
|
|
14783
|
+
const out = {
|
|
14784
|
+
id: m.id,
|
|
14785
|
+
// ACP spec requires `name` — fall back to id when the agent didn't
|
|
14786
|
+
// supply one so we never emit an invalid SessionMode.
|
|
14787
|
+
name: m.name ?? m.id
|
|
14788
|
+
};
|
|
14789
|
+
if (m.description !== void 0) {
|
|
14790
|
+
out.description = m.description;
|
|
14791
|
+
}
|
|
14792
|
+
return out;
|
|
14793
|
+
});
|
|
14794
|
+
const currentModeId = session.currentMode ?? modes[0].id;
|
|
14795
|
+
return { currentModeId, availableModes };
|
|
14796
|
+
}
|
|
12886
14797
|
function buildResponseMeta(session) {
|
|
12887
14798
|
const ours = {
|
|
12888
14799
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -12908,9 +14819,17 @@ function buildResponseMeta(session) {
|
|
|
12908
14819
|
if (commands.length > 0) {
|
|
12909
14820
|
ours.availableCommands = commands;
|
|
12910
14821
|
}
|
|
14822
|
+
const modes = session.availableModes();
|
|
14823
|
+
if (modes.length > 0) {
|
|
14824
|
+
ours.availableModes = modes;
|
|
14825
|
+
}
|
|
12911
14826
|
if (session.turnStartedAt !== void 0) {
|
|
12912
14827
|
ours.turnStartedAt = session.turnStartedAt;
|
|
12913
14828
|
}
|
|
14829
|
+
const queue = session.queueSnapshot();
|
|
14830
|
+
if (queue.length > 0) {
|
|
14831
|
+
ours.queue = queue;
|
|
14832
|
+
}
|
|
12914
14833
|
return mergeMeta(session.agentMeta, ours);
|
|
12915
14834
|
}
|
|
12916
14835
|
function buildInitializeResult() {
|
|
@@ -12941,7 +14860,13 @@ function buildInitializeResult() {
|
|
|
12941
14860
|
id: "bearer-token",
|
|
12942
14861
|
description: "Bearer token presented at WS upgrade"
|
|
12943
14862
|
}
|
|
12944
|
-
]
|
|
14863
|
+
],
|
|
14864
|
+
// Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
|
|
14865
|
+
// ACP clients ignore the field; capability-aware clients learn here
|
|
14866
|
+
// that hydra accepts concurrent session/prompt requests and emits
|
|
14867
|
+
// prompt_queue_* notifications so they can stop running their own
|
|
14868
|
+
// local queue.
|
|
14869
|
+
_meta: mergeMeta(void 0, { promptQueueing: true })
|
|
12945
14870
|
};
|
|
12946
14871
|
}
|
|
12947
14872
|
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
@@ -12955,13 +14880,13 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
12955
14880
|
}
|
|
12956
14881
|
|
|
12957
14882
|
// src/daemon/server.ts
|
|
12958
|
-
async function startDaemon(config) {
|
|
14883
|
+
async function startDaemon(config, serviceToken) {
|
|
12959
14884
|
ensureLoopbackOrTls(config);
|
|
12960
14885
|
const httpsOptions = config.daemon.tls ? {
|
|
12961
|
-
key: await
|
|
12962
|
-
cert: await
|
|
14886
|
+
key: await fsp5.readFile(config.daemon.tls.key),
|
|
14887
|
+
cert: await fsp5.readFile(config.daemon.tls.cert)
|
|
12963
14888
|
} : void 0;
|
|
12964
|
-
await
|
|
14889
|
+
await fsp5.mkdir(paths.home(), { recursive: true });
|
|
12965
14890
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
12966
14891
|
config.daemon.logLevel
|
|
12967
14892
|
);
|
|
@@ -12982,7 +14907,13 @@ async function startDaemon(config) {
|
|
|
12982
14907
|
setNpmInstallLogger((msg) => {
|
|
12983
14908
|
app.log.info(msg);
|
|
12984
14909
|
});
|
|
12985
|
-
const
|
|
14910
|
+
const sessionTokenStore = await SessionTokenStore.load();
|
|
14911
|
+
const authRateLimiter = new AuthRateLimiter();
|
|
14912
|
+
const validator = new CompositeTokenValidator([
|
|
14913
|
+
new StaticTokenValidator(serviceToken),
|
|
14914
|
+
new SessionTokenValidator(sessionTokenStore)
|
|
14915
|
+
]);
|
|
14916
|
+
const auth = bearerAuth({ validator });
|
|
12986
14917
|
app.addHook("onRequest", async (request, reply) => {
|
|
12987
14918
|
if (request.routeOptions.config?.skipAuth) {
|
|
12988
14919
|
return;
|
|
@@ -12992,7 +14923,19 @@ async function startDaemon(config) {
|
|
|
12992
14923
|
}
|
|
12993
14924
|
await auth(request, reply);
|
|
12994
14925
|
});
|
|
12995
|
-
const
|
|
14926
|
+
const sweepInterval = setInterval(
|
|
14927
|
+
() => {
|
|
14928
|
+
sessionTokenStore.sweepExpired();
|
|
14929
|
+
},
|
|
14930
|
+
5 * 60 * 1e3
|
|
14931
|
+
);
|
|
14932
|
+
sweepInterval.unref();
|
|
14933
|
+
const registry = new Registry(config, {
|
|
14934
|
+
onFetched: () => {
|
|
14935
|
+
void pruneStaleAgentVersions(registry, manager);
|
|
14936
|
+
}
|
|
14937
|
+
});
|
|
14938
|
+
setAgentPruneLogger((msg) => app.log.info(msg));
|
|
12996
14939
|
const agentLogger = {
|
|
12997
14940
|
info: (msg) => app.log.info(msg),
|
|
12998
14941
|
warn: (msg) => app.log.warn(msg)
|
|
@@ -13006,7 +14949,8 @@ async function startDaemon(config) {
|
|
|
13006
14949
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
13007
14950
|
defaultModels: config.defaultModels,
|
|
13008
14951
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
13009
|
-
logger: agentLogger
|
|
14952
|
+
logger: agentLogger,
|
|
14953
|
+
npmRegistry: config.npmRegistry
|
|
13010
14954
|
});
|
|
13011
14955
|
const extensions = new ExtensionManager(extensionList(config));
|
|
13012
14956
|
registerHealthRoutes(app, HYDRA_VERSION);
|
|
@@ -13020,16 +14964,20 @@ async function startDaemon(config) {
|
|
|
13020
14964
|
defaultAgent: config.defaultAgent,
|
|
13021
14965
|
defaultCwd: config.defaultCwd
|
|
13022
14966
|
});
|
|
14967
|
+
registerAuthRoutes(app, {
|
|
14968
|
+
store: sessionTokenStore,
|
|
14969
|
+
rateLimiter: authRateLimiter
|
|
14970
|
+
});
|
|
13023
14971
|
registerAcpWsEndpoint(app, {
|
|
13024
|
-
|
|
14972
|
+
validator,
|
|
13025
14973
|
manager,
|
|
13026
14974
|
defaultAgent: config.defaultAgent
|
|
13027
14975
|
});
|
|
13028
14976
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
13029
14977
|
const address = app.server.address();
|
|
13030
14978
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
13031
|
-
await
|
|
13032
|
-
await
|
|
14979
|
+
await fsp5.mkdir(paths.home(), { recursive: true });
|
|
14980
|
+
await fsp5.writeFile(
|
|
13033
14981
|
paths.pidFile(),
|
|
13034
14982
|
JSON.stringify({
|
|
13035
14983
|
pid: process.pid,
|
|
@@ -13045,20 +14993,28 @@ async function startDaemon(config) {
|
|
|
13045
14993
|
daemonUrl: `${scheme}://${config.daemon.host}:${boundPort}`,
|
|
13046
14994
|
daemonHost: config.daemon.host,
|
|
13047
14995
|
daemonPort: boundPort,
|
|
13048
|
-
|
|
14996
|
+
serviceToken,
|
|
13049
14997
|
daemonWsUrl: `${wsScheme}://${config.daemon.host}:${boundPort}/acp`,
|
|
13050
14998
|
hydraHome: paths.home()
|
|
13051
14999
|
});
|
|
13052
15000
|
await extensions.start();
|
|
15001
|
+
void manager.resurrectPendingQueues().catch((err) => {
|
|
15002
|
+
app.log.warn(
|
|
15003
|
+
`queue replay scan failed: ${err.message}`
|
|
15004
|
+
);
|
|
15005
|
+
});
|
|
13053
15006
|
const shutdown = async () => {
|
|
15007
|
+
clearInterval(sweepInterval);
|
|
15008
|
+
await sessionTokenStore.flush();
|
|
13054
15009
|
await extensions.stop();
|
|
13055
15010
|
await manager.closeAll();
|
|
13056
15011
|
await manager.flushMetaWrites();
|
|
13057
15012
|
setBinaryInstallLogger(null);
|
|
13058
15013
|
setNpmInstallLogger(null);
|
|
15014
|
+
setAgentPruneLogger(null);
|
|
13059
15015
|
await app.close();
|
|
13060
15016
|
try {
|
|
13061
|
-
|
|
15017
|
+
fs15.unlinkSync(paths.pidFile());
|
|
13062
15018
|
} catch {
|
|
13063
15019
|
}
|
|
13064
15020
|
try {
|
|
@@ -13097,13 +15053,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
13097
15053
|
init_daemon_bootstrap();
|
|
13098
15054
|
|
|
13099
15055
|
// src/cli/commands/log-tail.ts
|
|
13100
|
-
import * as
|
|
13101
|
-
import * as
|
|
15056
|
+
import * as fs16 from "fs";
|
|
15057
|
+
import * as fsp6 from "fs/promises";
|
|
13102
15058
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
13103
15059
|
const opts = parseLogTailFlags(argv);
|
|
13104
15060
|
let stat4;
|
|
13105
15061
|
try {
|
|
13106
|
-
stat4 = await
|
|
15062
|
+
stat4 = await fsp6.stat(logPath);
|
|
13107
15063
|
} catch (err) {
|
|
13108
15064
|
const e = err;
|
|
13109
15065
|
if (e.code === "ENOENT") {
|
|
@@ -13121,7 +15077,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
13121
15077
|
process.stdout.write(`-- following ${logPath} --
|
|
13122
15078
|
`);
|
|
13123
15079
|
let pending = false;
|
|
13124
|
-
const watcher =
|
|
15080
|
+
const watcher = fs16.watch(logPath, () => {
|
|
13125
15081
|
if (pending) {
|
|
13126
15082
|
return;
|
|
13127
15083
|
}
|
|
@@ -13129,14 +15085,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
13129
15085
|
setImmediate(async () => {
|
|
13130
15086
|
pending = false;
|
|
13131
15087
|
try {
|
|
13132
|
-
const s = await
|
|
15088
|
+
const s = await fsp6.stat(logPath);
|
|
13133
15089
|
if (s.size <= position) {
|
|
13134
15090
|
if (s.size < position) {
|
|
13135
15091
|
position = s.size;
|
|
13136
15092
|
}
|
|
13137
15093
|
return;
|
|
13138
15094
|
}
|
|
13139
|
-
const fd = await
|
|
15095
|
+
const fd = await fsp6.open(logPath, "r");
|
|
13140
15096
|
try {
|
|
13141
15097
|
const buf = Buffer.alloc(s.size - position);
|
|
13142
15098
|
await fd.read(buf, 0, buf.length, position);
|
|
@@ -13163,7 +15119,7 @@ async function printTail(logPath, fileSize, lines) {
|
|
|
13163
15119
|
return fileSize;
|
|
13164
15120
|
}
|
|
13165
15121
|
const CHUNK = 64 * 1024;
|
|
13166
|
-
const fd = await
|
|
15122
|
+
const fd = await fsp6.open(logPath, "r");
|
|
13167
15123
|
try {
|
|
13168
15124
|
let position = fileSize;
|
|
13169
15125
|
let collected = "";
|
|
@@ -13221,7 +15177,8 @@ function parseLogTailFlags(argv) {
|
|
|
13221
15177
|
|
|
13222
15178
|
// src/cli/commands/daemon.ts
|
|
13223
15179
|
async function runDaemonStart(flags = {}) {
|
|
13224
|
-
const config = await
|
|
15180
|
+
const config = await loadConfig();
|
|
15181
|
+
const serviceToken = await ensureServiceToken();
|
|
13225
15182
|
if (await pingHealth(config)) {
|
|
13226
15183
|
const info2 = await readPidFile();
|
|
13227
15184
|
process.stdout.write(
|
|
@@ -13231,7 +15188,7 @@ async function runDaemonStart(flags = {}) {
|
|
|
13231
15188
|
return;
|
|
13232
15189
|
}
|
|
13233
15190
|
if (flagBool(flags, "foreground")) {
|
|
13234
|
-
const handle = await startDaemon(config);
|
|
15191
|
+
const handle = await startDaemon(config, serviceToken);
|
|
13235
15192
|
process.stdout.write(
|
|
13236
15193
|
`hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
|
|
13237
15194
|
`
|
|
@@ -13269,7 +15226,8 @@ async function runDaemonStop() {
|
|
|
13269
15226
|
}
|
|
13270
15227
|
}
|
|
13271
15228
|
async function runDaemonRestart() {
|
|
13272
|
-
const config = await
|
|
15229
|
+
const config = await loadConfig();
|
|
15230
|
+
await ensureServiceToken();
|
|
13273
15231
|
const info = await readPidFile();
|
|
13274
15232
|
if (info && isProcessAlive(info.pid)) {
|
|
13275
15233
|
process.stdout.write(`Stopping daemon pid ${info.pid}...
|
|
@@ -13333,7 +15291,7 @@ async function runDaemonStatus() {
|
|
|
13333
15291
|
}
|
|
13334
15292
|
async function readPidFile() {
|
|
13335
15293
|
try {
|
|
13336
|
-
const raw = await
|
|
15294
|
+
const raw = await fsp7.readFile(paths.pidFile(), "utf8");
|
|
13337
15295
|
return JSON.parse(raw);
|
|
13338
15296
|
} catch (err) {
|
|
13339
15297
|
const e = err;
|
|
@@ -13357,16 +15315,18 @@ init_sessions();
|
|
|
13357
15315
|
|
|
13358
15316
|
// src/cli/commands/extensions.ts
|
|
13359
15317
|
init_config();
|
|
15318
|
+
init_service_token();
|
|
13360
15319
|
init_paths();
|
|
13361
|
-
import * as
|
|
15320
|
+
import * as fsp8 from "fs/promises";
|
|
13362
15321
|
init_sessions();
|
|
13363
15322
|
async function runExtensionsList() {
|
|
13364
15323
|
const config = await loadConfig();
|
|
15324
|
+
const serviceToken = await loadServiceToken();
|
|
13365
15325
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13366
15326
|
let body;
|
|
13367
15327
|
try {
|
|
13368
15328
|
const r = await fetch(`${baseUrl}/v1/extensions`, {
|
|
13369
|
-
headers: { Authorization: `Bearer ${
|
|
15329
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13370
15330
|
});
|
|
13371
15331
|
if (!r.ok) {
|
|
13372
15332
|
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
@@ -13469,13 +15429,14 @@ async function runExtensionsAdd(name, argv) {
|
|
|
13469
15429
|
process.stdout.write(`Added extension '${name}' to ${paths.config()}
|
|
13470
15430
|
`);
|
|
13471
15431
|
const config = await loadConfig();
|
|
15432
|
+
const serviceToken = await loadServiceToken();
|
|
13472
15433
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13473
15434
|
const registerBody = { name, ...body };
|
|
13474
15435
|
try {
|
|
13475
15436
|
const r = await fetch(`${baseUrl}/v1/extensions`, {
|
|
13476
15437
|
method: "POST",
|
|
13477
15438
|
headers: {
|
|
13478
|
-
Authorization: `Bearer ${
|
|
15439
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
13479
15440
|
"Content-Type": "application/json"
|
|
13480
15441
|
},
|
|
13481
15442
|
body: JSON.stringify(registerBody)
|
|
@@ -13523,11 +15484,12 @@ async function runExtensionsRemove(name) {
|
|
|
13523
15484
|
process.stdout.write(`Removed extension '${name}' from ${paths.config()}
|
|
13524
15485
|
`);
|
|
13525
15486
|
const config = await loadConfig();
|
|
15487
|
+
const serviceToken = await loadServiceToken();
|
|
13526
15488
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13527
15489
|
try {
|
|
13528
15490
|
const r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}`, {
|
|
13529
15491
|
method: "DELETE",
|
|
13530
|
-
headers: { Authorization: `Bearer ${
|
|
15492
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13531
15493
|
});
|
|
13532
15494
|
if (r.status === 204 || r.status === 404) {
|
|
13533
15495
|
process.stdout.write(`${name}: stopped
|
|
@@ -13554,11 +15516,11 @@ async function runExtensionsRemove(name) {
|
|
|
13554
15516
|
}
|
|
13555
15517
|
}
|
|
13556
15518
|
async function readRawConfig() {
|
|
13557
|
-
const raw = await
|
|
15519
|
+
const raw = await fsp8.readFile(paths.config(), "utf8");
|
|
13558
15520
|
return JSON.parse(raw);
|
|
13559
15521
|
}
|
|
13560
15522
|
async function writeRawConfig(raw) {
|
|
13561
|
-
await
|
|
15523
|
+
await fsp8.writeFile(
|
|
13562
15524
|
paths.config(),
|
|
13563
15525
|
JSON.stringify(raw, null, 2) + "\n",
|
|
13564
15526
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -13587,12 +15549,13 @@ async function postLifecycle(name, verb) {
|
|
|
13587
15549
|
return;
|
|
13588
15550
|
}
|
|
13589
15551
|
const config = await loadConfig();
|
|
15552
|
+
const serviceToken = await loadServiceToken();
|
|
13590
15553
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13591
15554
|
let r;
|
|
13592
15555
|
try {
|
|
13593
15556
|
r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}/${verb}`, {
|
|
13594
15557
|
method: "POST",
|
|
13595
|
-
headers: { Authorization: `Bearer ${
|
|
15558
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13596
15559
|
});
|
|
13597
15560
|
} catch (err) {
|
|
13598
15561
|
process.stderr.write(
|
|
@@ -13623,8 +15586,9 @@ async function postLifecycle(name, verb) {
|
|
|
13623
15586
|
}
|
|
13624
15587
|
async function postLifecycleAll(verb) {
|
|
13625
15588
|
const config = await loadConfig();
|
|
15589
|
+
const serviceToken = await loadServiceToken();
|
|
13626
15590
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13627
|
-
const auth = { Authorization: `Bearer ${
|
|
15591
|
+
const auth = { Authorization: `Bearer ${serviceToken}` };
|
|
13628
15592
|
let listBody;
|
|
13629
15593
|
try {
|
|
13630
15594
|
const r = await fetch(`${baseUrl}/v1/extensions`, { headers: auth });
|
|
@@ -13790,14 +15754,16 @@ function maxLen2(headerCell, values) {
|
|
|
13790
15754
|
|
|
13791
15755
|
// src/cli/commands/agents.ts
|
|
13792
15756
|
init_config();
|
|
15757
|
+
init_service_token();
|
|
13793
15758
|
init_sessions();
|
|
13794
15759
|
async function runAgentsList() {
|
|
13795
15760
|
const config = await loadConfig();
|
|
15761
|
+
const serviceToken = await loadServiceToken();
|
|
13796
15762
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13797
15763
|
let body;
|
|
13798
15764
|
try {
|
|
13799
15765
|
const r = await fetch(`${baseUrl}/v1/agents`, {
|
|
13800
|
-
headers: { Authorization: `Bearer ${
|
|
15766
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13801
15767
|
});
|
|
13802
15768
|
if (!r.ok) {
|
|
13803
15769
|
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
@@ -13854,12 +15820,13 @@ Registry version: ${body.version}
|
|
|
13854
15820
|
}
|
|
13855
15821
|
async function runAgentsRefresh() {
|
|
13856
15822
|
const config = await loadConfig();
|
|
15823
|
+
const serviceToken = await loadServiceToken();
|
|
13857
15824
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13858
15825
|
let body;
|
|
13859
15826
|
try {
|
|
13860
15827
|
const r = await fetch(`${baseUrl}/v1/registry/refresh`, {
|
|
13861
15828
|
method: "POST",
|
|
13862
|
-
headers: { Authorization: `Bearer ${
|
|
15829
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13863
15830
|
});
|
|
13864
15831
|
if (!r.ok) {
|
|
13865
15832
|
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
@@ -13890,8 +15857,197 @@ function maxLen3(headerCell, values) {
|
|
|
13890
15857
|
return max;
|
|
13891
15858
|
}
|
|
13892
15859
|
|
|
15860
|
+
// src/cli/commands/auth.ts
|
|
15861
|
+
init_config();
|
|
15862
|
+
init_service_token();
|
|
15863
|
+
init_sessions();
|
|
15864
|
+
async function promptPassword(prompt) {
|
|
15865
|
+
process.stdout.write(prompt);
|
|
15866
|
+
if (!process.stdin.isTTY) {
|
|
15867
|
+
return readLineFromStdin();
|
|
15868
|
+
}
|
|
15869
|
+
return new Promise((resolve5, reject) => {
|
|
15870
|
+
const stdin = process.stdin;
|
|
15871
|
+
const wasRaw = stdin.isRaw === true;
|
|
15872
|
+
let buffer = "";
|
|
15873
|
+
const cleanup = () => {
|
|
15874
|
+
stdin.removeListener("data", onData);
|
|
15875
|
+
stdin.removeListener("error", onError);
|
|
15876
|
+
if (!wasRaw) {
|
|
15877
|
+
stdin.setRawMode(false);
|
|
15878
|
+
}
|
|
15879
|
+
stdin.pause();
|
|
15880
|
+
};
|
|
15881
|
+
const onData = (chunk) => {
|
|
15882
|
+
for (const byte of chunk) {
|
|
15883
|
+
if (byte === 10 || byte === 13) {
|
|
15884
|
+
process.stdout.write("\n");
|
|
15885
|
+
cleanup();
|
|
15886
|
+
resolve5(buffer);
|
|
15887
|
+
return;
|
|
15888
|
+
}
|
|
15889
|
+
if (byte === 3) {
|
|
15890
|
+
cleanup();
|
|
15891
|
+
reject(new Error("password entry cancelled"));
|
|
15892
|
+
return;
|
|
15893
|
+
}
|
|
15894
|
+
if (byte === 127 || byte === 8) {
|
|
15895
|
+
buffer = buffer.slice(0, -1);
|
|
15896
|
+
continue;
|
|
15897
|
+
}
|
|
15898
|
+
buffer += String.fromCharCode(byte);
|
|
15899
|
+
}
|
|
15900
|
+
};
|
|
15901
|
+
const onError = (err) => {
|
|
15902
|
+
cleanup();
|
|
15903
|
+
reject(err);
|
|
15904
|
+
};
|
|
15905
|
+
stdin.setRawMode(true);
|
|
15906
|
+
stdin.resume();
|
|
15907
|
+
stdin.on("data", onData);
|
|
15908
|
+
stdin.on("error", onError);
|
|
15909
|
+
});
|
|
15910
|
+
}
|
|
15911
|
+
function readLineFromStdin() {
|
|
15912
|
+
return new Promise((resolve5, reject) => {
|
|
15913
|
+
let buffer = "";
|
|
15914
|
+
process.stdin.setEncoding("utf8");
|
|
15915
|
+
const onData = (chunk) => {
|
|
15916
|
+
buffer += chunk;
|
|
15917
|
+
const nl = buffer.indexOf("\n");
|
|
15918
|
+
if (nl !== -1) {
|
|
15919
|
+
process.stdin.removeListener("data", onData);
|
|
15920
|
+
process.stdin.removeListener("error", onError);
|
|
15921
|
+
resolve5(buffer.slice(0, nl).replace(/\r$/, ""));
|
|
15922
|
+
}
|
|
15923
|
+
};
|
|
15924
|
+
const onError = (err) => {
|
|
15925
|
+
process.stdin.removeListener("data", onData);
|
|
15926
|
+
process.stdin.removeListener("error", onError);
|
|
15927
|
+
reject(err);
|
|
15928
|
+
};
|
|
15929
|
+
process.stdin.on("data", onData);
|
|
15930
|
+
process.stdin.on("error", onError);
|
|
15931
|
+
});
|
|
15932
|
+
}
|
|
15933
|
+
async function runAuthPasswordSet(flags) {
|
|
15934
|
+
const force = flagBool(flags, "force");
|
|
15935
|
+
if (await hasPassword() && !force) {
|
|
15936
|
+
const current = await promptPassword("Current password: ");
|
|
15937
|
+
if (!await verifyPassword(current)) {
|
|
15938
|
+
process.stderr.write("Wrong password.\n");
|
|
15939
|
+
process.exit(1);
|
|
15940
|
+
}
|
|
15941
|
+
}
|
|
15942
|
+
const next = await promptPassword("New password: ");
|
|
15943
|
+
if (next.length === 0) {
|
|
15944
|
+
process.stderr.write("Password must not be empty.\n");
|
|
15945
|
+
process.exit(2);
|
|
15946
|
+
}
|
|
15947
|
+
const confirm = await promptPassword("Confirm new password: ");
|
|
15948
|
+
if (next !== confirm) {
|
|
15949
|
+
process.stderr.write("Passwords did not match.\n");
|
|
15950
|
+
process.exit(1);
|
|
15951
|
+
}
|
|
15952
|
+
await setPassword(next);
|
|
15953
|
+
process.stdout.write("Password set.\n");
|
|
15954
|
+
}
|
|
15955
|
+
async function runAuthList() {
|
|
15956
|
+
const config = await loadConfig();
|
|
15957
|
+
const token = await loadServiceToken();
|
|
15958
|
+
const baseUrl = httpBase(
|
|
15959
|
+
config.daemon.host,
|
|
15960
|
+
config.daemon.port,
|
|
15961
|
+
!!config.daemon.tls
|
|
15962
|
+
);
|
|
15963
|
+
const r = await fetch(`${baseUrl}/v1/auth/sessions`, {
|
|
15964
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
15965
|
+
});
|
|
15966
|
+
if (!r.ok) {
|
|
15967
|
+
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
15968
|
+
`);
|
|
15969
|
+
process.exit(1);
|
|
15970
|
+
}
|
|
15971
|
+
const body = await r.json();
|
|
15972
|
+
if (body.sessions.length === 0) {
|
|
15973
|
+
process.stdout.write("No active session tokens.\n");
|
|
15974
|
+
return;
|
|
15975
|
+
}
|
|
15976
|
+
const header = {
|
|
15977
|
+
id: "ID",
|
|
15978
|
+
label: "LABEL",
|
|
15979
|
+
createdAt: "CREATED",
|
|
15980
|
+
expiresAt: "EXPIRES",
|
|
15981
|
+
lastUsedAt: "LAST USED"
|
|
15982
|
+
};
|
|
15983
|
+
const rows = body.sessions.map((s) => ({
|
|
15984
|
+
id: s.id,
|
|
15985
|
+
label: s.label ?? "-",
|
|
15986
|
+
createdAt: s.createdAt,
|
|
15987
|
+
expiresAt: s.expiresAt,
|
|
15988
|
+
lastUsedAt: s.lastUsedAt
|
|
15989
|
+
}));
|
|
15990
|
+
const widths = {
|
|
15991
|
+
id: maxLen4(header.id, rows.map((r2) => r2.id)),
|
|
15992
|
+
label: maxLen4(header.label, rows.map((r2) => r2.label)),
|
|
15993
|
+
createdAt: maxLen4(header.createdAt, rows.map((r2) => r2.createdAt)),
|
|
15994
|
+
expiresAt: maxLen4(header.expiresAt, rows.map((r2) => r2.expiresAt))
|
|
15995
|
+
};
|
|
15996
|
+
const fmt = (r2) => [
|
|
15997
|
+
r2.id.padEnd(widths.id),
|
|
15998
|
+
r2.label.padEnd(widths.label),
|
|
15999
|
+
r2.createdAt.padEnd(widths.createdAt),
|
|
16000
|
+
r2.expiresAt.padEnd(widths.expiresAt),
|
|
16001
|
+
r2.lastUsedAt
|
|
16002
|
+
].join(" ");
|
|
16003
|
+
process.stdout.write(fmt(header) + "\n");
|
|
16004
|
+
for (const r2 of rows) {
|
|
16005
|
+
process.stdout.write(fmt(r2) + "\n");
|
|
16006
|
+
}
|
|
16007
|
+
}
|
|
16008
|
+
async function runAuthRevoke(id) {
|
|
16009
|
+
if (!id) {
|
|
16010
|
+
process.stderr.write("Usage: hydra-acp auth revoke <id>\n");
|
|
16011
|
+
process.exit(2);
|
|
16012
|
+
}
|
|
16013
|
+
const config = await loadConfig();
|
|
16014
|
+
const token = await loadServiceToken();
|
|
16015
|
+
const baseUrl = httpBase(
|
|
16016
|
+
config.daemon.host,
|
|
16017
|
+
config.daemon.port,
|
|
16018
|
+
!!config.daemon.tls
|
|
16019
|
+
);
|
|
16020
|
+
const r = await fetch(`${baseUrl}/v1/auth/sessions/${id}`, {
|
|
16021
|
+
method: "DELETE",
|
|
16022
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
16023
|
+
});
|
|
16024
|
+
if (r.status === 204) {
|
|
16025
|
+
process.stdout.write(`Revoked ${id}
|
|
16026
|
+
`);
|
|
16027
|
+
return;
|
|
16028
|
+
}
|
|
16029
|
+
if (r.status === 404) {
|
|
16030
|
+
process.stderr.write(`No session token with id ${id}
|
|
16031
|
+
`);
|
|
16032
|
+
process.exit(1);
|
|
16033
|
+
}
|
|
16034
|
+
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
16035
|
+
`);
|
|
16036
|
+
process.exit(1);
|
|
16037
|
+
}
|
|
16038
|
+
function maxLen4(headerCell, values) {
|
|
16039
|
+
let max = headerCell.length;
|
|
16040
|
+
for (const v of values) {
|
|
16041
|
+
if (v.length > max) {
|
|
16042
|
+
max = v.length;
|
|
16043
|
+
}
|
|
16044
|
+
}
|
|
16045
|
+
return max;
|
|
16046
|
+
}
|
|
16047
|
+
|
|
13893
16048
|
// src/shim/proxy.ts
|
|
13894
16049
|
init_config();
|
|
16050
|
+
init_service_token();
|
|
13895
16051
|
init_daemon_bootstrap();
|
|
13896
16052
|
init_resilient_ws();
|
|
13897
16053
|
|
|
@@ -14066,13 +16222,14 @@ function isResponse2(msg) {
|
|
|
14066
16222
|
|
|
14067
16223
|
// src/shim/proxy.ts
|
|
14068
16224
|
async function runShim(opts) {
|
|
14069
|
-
const config = await
|
|
16225
|
+
const config = await loadConfig();
|
|
16226
|
+
const serviceToken = await ensureServiceToken();
|
|
14070
16227
|
await ensureDaemonReachable(config);
|
|
14071
16228
|
const tracker = new SessionTracker();
|
|
14072
16229
|
const downstream = ndjsonStreamFromStdio(process.stdin, process.stdout);
|
|
14073
16230
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
14074
16231
|
const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
14075
|
-
const subprotocols = ["acp.v1", `hydra-acp-token.${
|
|
16232
|
+
const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
|
|
14076
16233
|
const upstream = new ResilientWsStream({
|
|
14077
16234
|
url,
|
|
14078
16235
|
subprotocols,
|
|
@@ -14413,6 +16570,7 @@ async function main() {
|
|
|
14413
16570
|
process.exit(2);
|
|
14414
16571
|
return;
|
|
14415
16572
|
}
|
|
16573
|
+
case "session":
|
|
14416
16574
|
case "sessions": {
|
|
14417
16575
|
const sub = positional[1];
|
|
14418
16576
|
if (sub === void 0 || sub === "list") {
|
|
@@ -14449,13 +16607,14 @@ async function main() {
|
|
|
14449
16607
|
});
|
|
14450
16608
|
return;
|
|
14451
16609
|
}
|
|
14452
|
-
process.stderr.write(`Unknown
|
|
16610
|
+
process.stderr.write(`Unknown session subcommand: ${sub}
|
|
14453
16611
|
`);
|
|
14454
16612
|
process.exit(2);
|
|
14455
16613
|
return;
|
|
14456
16614
|
}
|
|
16615
|
+
case "extension":
|
|
14457
16616
|
case "extensions": {
|
|
14458
|
-
const extIdx = argv.indexOf(
|
|
16617
|
+
const extIdx = argv.indexOf(subcommand);
|
|
14459
16618
|
const tail = argv.slice(extIdx + 1);
|
|
14460
16619
|
const sub = tail[0];
|
|
14461
16620
|
const name2 = tail[1];
|
|
@@ -14488,11 +16647,12 @@ async function main() {
|
|
|
14488
16647
|
await runExtensionsLogs(name2, rest);
|
|
14489
16648
|
return;
|
|
14490
16649
|
}
|
|
14491
|
-
process.stderr.write(`Unknown
|
|
16650
|
+
process.stderr.write(`Unknown extension subcommand: ${sub}
|
|
14492
16651
|
`);
|
|
14493
16652
|
process.exit(2);
|
|
14494
16653
|
return;
|
|
14495
16654
|
}
|
|
16655
|
+
case "agent":
|
|
14496
16656
|
case "agents": {
|
|
14497
16657
|
const sub = positional[1];
|
|
14498
16658
|
if (sub === void 0 || sub === "list") {
|
|
@@ -14503,7 +16663,33 @@ async function main() {
|
|
|
14503
16663
|
await runAgentsRefresh();
|
|
14504
16664
|
return;
|
|
14505
16665
|
}
|
|
14506
|
-
process.stderr.write(`Unknown
|
|
16666
|
+
process.stderr.write(`Unknown agent subcommand: ${sub}
|
|
16667
|
+
`);
|
|
16668
|
+
process.exit(2);
|
|
16669
|
+
return;
|
|
16670
|
+
}
|
|
16671
|
+
case "auth": {
|
|
16672
|
+
const sub = positional[1];
|
|
16673
|
+
if (sub === "password") {
|
|
16674
|
+
const action = positional[2];
|
|
16675
|
+
if (action === void 0 || action === "set") {
|
|
16676
|
+
await runAuthPasswordSet(flags);
|
|
16677
|
+
return;
|
|
16678
|
+
}
|
|
16679
|
+
process.stderr.write(`Unknown auth password action: ${action}
|
|
16680
|
+
`);
|
|
16681
|
+
process.exit(2);
|
|
16682
|
+
return;
|
|
16683
|
+
}
|
|
16684
|
+
if (sub === void 0 || sub === "list") {
|
|
16685
|
+
await runAuthList();
|
|
16686
|
+
return;
|
|
16687
|
+
}
|
|
16688
|
+
if (sub === "revoke") {
|
|
16689
|
+
await runAuthRevoke(positional[2]);
|
|
16690
|
+
return;
|
|
16691
|
+
}
|
|
16692
|
+
process.stderr.write(`Unknown auth subcommand: ${sub}
|
|
14507
16693
|
`);
|
|
14508
16694
|
process.exit(2);
|
|
14509
16695
|
return;
|
|
@@ -14583,23 +16769,26 @@ function printHelp() {
|
|
|
14583
16769
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
14584
16770
|
" hydra-acp daemon stop|restart|status",
|
|
14585
16771
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
14586
|
-
" hydra-acp
|
|
16772
|
+
" hydra-acp session [list] [--all] [--json]",
|
|
14587
16773
|
" List sessions (live + 20 most-recent cold; --all for everything; --json emits the raw daemon response as JSON for scripts)",
|
|
14588
|
-
" hydra-acp
|
|
14589
|
-
" hydra-acp
|
|
14590
|
-
" hydra-acp
|
|
16774
|
+
" hydra-acp session kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
16775
|
+
" hydra-acp session remove <id> Remove a session entirely (live or cold)",
|
|
16776
|
+
" hydra-acp session export <id> [--out <file>|.]",
|
|
14591
16777
|
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
14592
|
-
" hydra-acp
|
|
16778
|
+
" hydra-acp session transcript <id>|<file> [--out <file>|.]",
|
|
14593
16779
|
" Render a session as a markdown transcript. Accepts a session id (renders via the daemon) or a local .hydra bundle file (rendered in-process). Writes to <file>, to a default-named file when --out=., or to stdout",
|
|
14594
|
-
" hydra-acp
|
|
16780
|
+
" hydra-acp session import <file>|- [--replace] [--cwd <path>] [--info]",
|
|
14595
16781
|
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live); --cwd overrides the bundle's recorded working directory; --info prints the bundle's meta without importing",
|
|
14596
|
-
" hydra-acp
|
|
14597
|
-
" hydra-acp
|
|
14598
|
-
" hydra-acp
|
|
14599
|
-
" hydra-acp
|
|
14600
|
-
" hydra-acp
|
|
14601
|
-
" hydra-acp
|
|
14602
|
-
" hydra-acp
|
|
16782
|
+
" hydra-acp extension list List configured extensions and live state",
|
|
16783
|
+
" hydra-acp extension add <name> [opts] Add an extension to config",
|
|
16784
|
+
" hydra-acp extension remove <name> Remove an extension from config",
|
|
16785
|
+
" hydra-acp extension start|stop|restart <n>|all Lifecycle on one or all",
|
|
16786
|
+
" hydra-acp extension logs <name> [-f] [-n N] Tail or follow an extension's log",
|
|
16787
|
+
" hydra-acp agent [list] List agents in the cached registry",
|
|
16788
|
+
" hydra-acp agent refresh Force a registry re-fetch",
|
|
16789
|
+
" hydra-acp auth password [--force] Set the daemon's master password",
|
|
16790
|
+
" hydra-acp auth [list] List active session tokens",
|
|
16791
|
+
" hydra-acp auth revoke <id> Revoke a session token",
|
|
14603
16792
|
" hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
|
|
14604
16793
|
" --resume <id> attaches to a specific session; --reattach picks the most-recent in cwd.",
|
|
14605
16794
|
" Smart default (no flags): shows a picker when sessions exist, else new.",
|