@hydra-acp/cli 0.1.22 → 0.1.23
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 +2162 -398
- package/dist/index.d.ts +163 -25
- package/dist/index.js +1477 -193
- 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 path10 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(path10.dirname(path10.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 = `${path10.basename(idOrFile, path10.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(path10.dirname(path10.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 = path10.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}`);
|
|
@@ -4408,7 +5015,7 @@ async function pickSession(term, opts) {
|
|
|
4408
5015
|
};
|
|
4409
5016
|
const refresh = async (preferredId) => {
|
|
4410
5017
|
try {
|
|
4411
|
-
const next = await listSessions(opts.config);
|
|
5018
|
+
const next = await listSessions(opts.config, opts.serviceToken);
|
|
4412
5019
|
allSessions = sortSessions(next);
|
|
4413
5020
|
applyFilter();
|
|
4414
5021
|
if (preferredId !== void 0) {
|
|
@@ -4439,9 +5046,9 @@ async function pickSession(term, opts) {
|
|
|
4439
5046
|
paintIndicator();
|
|
4440
5047
|
try {
|
|
4441
5048
|
if (kind === "kill") {
|
|
4442
|
-
await killSession(opts.config, target.sessionId);
|
|
5049
|
+
await killSession(opts.config, opts.serviceToken, target.sessionId);
|
|
4443
5050
|
} else {
|
|
4444
|
-
await deleteSession(opts.config, target.sessionId);
|
|
5051
|
+
await deleteSession(opts.config, opts.serviceToken, target.sessionId);
|
|
4445
5052
|
}
|
|
4446
5053
|
mode = "normal";
|
|
4447
5054
|
pendingAction = null;
|
|
@@ -4709,9 +5316,9 @@ var init_picker = __esm({
|
|
|
4709
5316
|
});
|
|
4710
5317
|
|
|
4711
5318
|
// src/tui/attachments.ts
|
|
4712
|
-
import
|
|
5319
|
+
import path11 from "path";
|
|
4713
5320
|
function mimeFromExtension(p) {
|
|
4714
|
-
return EXTENSION_TO_MIME[
|
|
5321
|
+
return EXTENSION_TO_MIME[path11.extname(p).toLowerCase()] ?? null;
|
|
4715
5322
|
}
|
|
4716
5323
|
function isSupportedImagePath(p) {
|
|
4717
5324
|
return mimeFromExtension(p) !== null;
|
|
@@ -4820,6 +5427,10 @@ var init_attachments = __esm({
|
|
|
4820
5427
|
// src/tui/screen.ts
|
|
4821
5428
|
import stringWidth from "string-width";
|
|
4822
5429
|
import wrapAnsi from "wrap-ansi";
|
|
5430
|
+
function matchBareUrl(text) {
|
|
5431
|
+
const stripped = text.replace(/\r\n?$|\n$/, "");
|
|
5432
|
+
return BARE_URL_RE.test(stripped) ? stripped : null;
|
|
5433
|
+
}
|
|
4823
5434
|
function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null) {
|
|
4824
5435
|
const active = activeCol === null ? "" : `a${activeCol}`;
|
|
4825
5436
|
if (!line) {
|
|
@@ -5283,6 +5894,8 @@ function mapKeyName(name) {
|
|
|
5283
5894
|
case "ALT_ENTER":
|
|
5284
5895
|
case "META_ENTER":
|
|
5285
5896
|
return "alt-enter";
|
|
5897
|
+
case "CTRL_T":
|
|
5898
|
+
return "ctrl-t";
|
|
5286
5899
|
case "SHIFT_TAB":
|
|
5287
5900
|
return "shift-tab";
|
|
5288
5901
|
case "TAB":
|
|
@@ -5343,7 +5956,7 @@ function mapKeyName(name) {
|
|
|
5343
5956
|
return null;
|
|
5344
5957
|
}
|
|
5345
5958
|
}
|
|
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;
|
|
5959
|
+
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, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
5347
5960
|
var init_screen = __esm({
|
|
5348
5961
|
"src/tui/screen.ts"() {
|
|
5349
5962
|
"use strict";
|
|
@@ -5362,6 +5975,7 @@ var init_screen = __esm({
|
|
|
5362
5975
|
CONFIRM_PROMPT_ROWS = 2;
|
|
5363
5976
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
5364
5977
|
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
5978
|
+
BARE_URL_RE = /^(https?|ftp):\/\/\S+$/;
|
|
5365
5979
|
Screen = class {
|
|
5366
5980
|
term;
|
|
5367
5981
|
dispatcher;
|
|
@@ -5445,8 +6059,8 @@ var init_screen = __esm({
|
|
|
5445
6059
|
bannerSearchIndicator = null;
|
|
5446
6060
|
banner = {
|
|
5447
6061
|
status: "ready",
|
|
5448
|
-
|
|
5449
|
-
hint: "\u21E7\u21E5
|
|
6062
|
+
currentMode: void 0,
|
|
6063
|
+
hint: "\u21E7\u21E5 mode \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
5450
6064
|
queued: 0
|
|
5451
6065
|
};
|
|
5452
6066
|
sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -5591,7 +6205,10 @@ var init_screen = __esm({
|
|
|
5591
6205
|
}
|
|
5592
6206
|
const startIdx = text.indexOf(startMarker);
|
|
5593
6207
|
if (startIdx === -1) {
|
|
5594
|
-
|
|
6208
|
+
const url = matchBareUrl(text);
|
|
6209
|
+
if (url !== null) {
|
|
6210
|
+
this.onKey([{ type: "paste", text: url }]);
|
|
6211
|
+
} else if (this.terminalKitStdinHandler) {
|
|
5595
6212
|
this.terminalKitStdinHandler(Buffer.from(text, "binary"));
|
|
5596
6213
|
}
|
|
5597
6214
|
return;
|
|
@@ -5821,11 +6438,11 @@ var init_screen = __esm({
|
|
|
5821
6438
|
return;
|
|
5822
6439
|
}
|
|
5823
6440
|
this.lastWindowTitle = clean;
|
|
5824
|
-
process.stdout.write(`\x1B]
|
|
6441
|
+
process.stdout.write(`\x1B]0;${clean}\x1B\\`);
|
|
5825
6442
|
}
|
|
5826
6443
|
clearWindowTitle() {
|
|
5827
6444
|
this.lastWindowTitle = null;
|
|
5828
|
-
process.stdout.write("\x1B]
|
|
6445
|
+
process.stdout.write("\x1B]0;\x1B\\");
|
|
5829
6446
|
}
|
|
5830
6447
|
setBanner(banner) {
|
|
5831
6448
|
this.banner = { ...this.banner, ...banner };
|
|
@@ -5833,6 +6450,9 @@ var init_screen = __esm({
|
|
|
5833
6450
|
this.drawBanner();
|
|
5834
6451
|
this.placeCursor();
|
|
5835
6452
|
}
|
|
6453
|
+
currentModeId() {
|
|
6454
|
+
return this.banner.currentMode;
|
|
6455
|
+
}
|
|
5836
6456
|
// OSC 9;4 progress-bar control. State 3 = indeterminate (pulsing
|
|
5837
6457
|
// taskbar / dock badge while a turn is running); state 0 = remove.
|
|
5838
6458
|
// ConEmu-flavor sequence — supported by Windows Terminal, WezTerm,
|
|
@@ -6785,10 +7405,9 @@ var init_screen = __esm({
|
|
|
6785
7405
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
6786
7406
|
const right = this.bannerRightContent();
|
|
6787
7407
|
const rightSig = right ? `${right.kind}|${right.text}` : "";
|
|
6788
|
-
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.
|
|
7408
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
|
|
6789
7409
|
this.paintRow(row, sig, () => {
|
|
6790
7410
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
6791
|
-
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
6792
7411
|
if (this.banner.status === "busy") {
|
|
6793
7412
|
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
6794
7413
|
if (elapsedStr) {
|
|
@@ -6807,13 +7426,11 @@ var init_screen = __esm({
|
|
|
6807
7426
|
if (this.scrollOffset > 0) {
|
|
6808
7427
|
this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
|
|
6809
7428
|
}
|
|
6810
|
-
this.
|
|
6811
|
-
|
|
6812
|
-
this.
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
}
|
|
6816
|
-
this.term(" \xB7 ").dim(this.banner.hint);
|
|
7429
|
+
const hint = this.banner.currentMode ? this.banner.hint.replace(
|
|
7430
|
+
"\u21E7\u21E5 mode",
|
|
7431
|
+
`\u21E7\u21E5 mode(${this.banner.currentMode})`
|
|
7432
|
+
) : this.banner.hint;
|
|
7433
|
+
this.term(" \xB7 ").dim(hint);
|
|
6817
7434
|
if (right) {
|
|
6818
7435
|
const visibleWidth = stringWidth(right.text);
|
|
6819
7436
|
const col = Math.max(1, w - visibleWidth + 1);
|
|
@@ -7156,7 +7773,7 @@ var init_input = __esm({
|
|
|
7156
7773
|
this.retreatHistorySearch();
|
|
7157
7774
|
return [];
|
|
7158
7775
|
}
|
|
7159
|
-
if (event.name === "escape") {
|
|
7776
|
+
if (event.name === "escape" || event.name === "ctrl-c") {
|
|
7160
7777
|
this.cancelHistorySearch();
|
|
7161
7778
|
return [];
|
|
7162
7779
|
}
|
|
@@ -7252,6 +7869,8 @@ var init_input = __esm({
|
|
|
7252
7869
|
return [{ type: "redraw" }];
|
|
7253
7870
|
case "ctrl-p":
|
|
7254
7871
|
return [{ type: "switch-session" }];
|
|
7872
|
+
case "ctrl-t":
|
|
7873
|
+
return [{ type: "next-live-session" }];
|
|
7255
7874
|
case "ctrl-r":
|
|
7256
7875
|
return this.startHistorySearch();
|
|
7257
7876
|
case "ctrl-s":
|
|
@@ -7741,9 +8360,9 @@ var init_input = __esm({
|
|
|
7741
8360
|
|
|
7742
8361
|
// src/tui/clipboard.ts
|
|
7743
8362
|
import { spawn as nodeSpawn } from "child_process";
|
|
7744
|
-
import
|
|
8363
|
+
import fs18 from "fs/promises";
|
|
7745
8364
|
import os4 from "os";
|
|
7746
|
-
import
|
|
8365
|
+
import path12 from "path";
|
|
7747
8366
|
async function readClipboard(envIn = {}) {
|
|
7748
8367
|
const env = { ...defaultEnv, ...envIn };
|
|
7749
8368
|
if (env.platform === "darwin") {
|
|
@@ -7758,7 +8377,7 @@ async function readClipboard(envIn = {}) {
|
|
|
7758
8377
|
};
|
|
7759
8378
|
}
|
|
7760
8379
|
async function readMacOS(env) {
|
|
7761
|
-
const tmpPath =
|
|
8380
|
+
const tmpPath = path12.join(
|
|
7762
8381
|
env.tmpdir(),
|
|
7763
8382
|
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
7764
8383
|
);
|
|
@@ -7782,7 +8401,7 @@ async function readMacOS(env) {
|
|
|
7782
8401
|
return img;
|
|
7783
8402
|
}
|
|
7784
8403
|
} catch {
|
|
7785
|
-
await
|
|
8404
|
+
await fs18.unlink(tmpPath).catch(() => void 0);
|
|
7786
8405
|
}
|
|
7787
8406
|
try {
|
|
7788
8407
|
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
@@ -7870,9 +8489,9 @@ async function which(env, cmd) {
|
|
|
7870
8489
|
}
|
|
7871
8490
|
async function readFileAsAttachment(p, unlinkAfter) {
|
|
7872
8491
|
try {
|
|
7873
|
-
const buf = await
|
|
8492
|
+
const buf = await fs18.readFile(p);
|
|
7874
8493
|
if (unlinkAfter) {
|
|
7875
|
-
await
|
|
8494
|
+
await fs18.unlink(p).catch(() => void 0);
|
|
7876
8495
|
}
|
|
7877
8496
|
if (buf.length === 0) {
|
|
7878
8497
|
return { ok: false, reason: "no image on clipboard" };
|
|
@@ -8062,6 +8681,7 @@ function formatEvent(event) {
|
|
|
8062
8681
|
case "usage-update":
|
|
8063
8682
|
return [];
|
|
8064
8683
|
case "available-commands":
|
|
8684
|
+
case "available-modes":
|
|
8065
8685
|
return [];
|
|
8066
8686
|
case "session-info":
|
|
8067
8687
|
return [];
|
|
@@ -8360,17 +8980,18 @@ var init_format = __esm({
|
|
|
8360
8980
|
import { appendFileSync, statSync, renameSync } from "fs";
|
|
8361
8981
|
import { nanoid as nanoid3 } from "nanoid";
|
|
8362
8982
|
import termkit from "terminal-kit";
|
|
8363
|
-
import
|
|
8364
|
-
import
|
|
8983
|
+
import fs19 from "fs/promises";
|
|
8984
|
+
import path13 from "path";
|
|
8365
8985
|
async function runTuiApp(opts) {
|
|
8366
|
-
const config = await
|
|
8986
|
+
const config = await loadConfig();
|
|
8987
|
+
const serviceToken = await ensureServiceToken();
|
|
8367
8988
|
logMaxBytes = config.tui.logMaxBytes;
|
|
8368
8989
|
await ensureDaemonReachable(config);
|
|
8369
8990
|
const term = termkit.terminal;
|
|
8370
8991
|
const exitHint = {};
|
|
8371
8992
|
let nextOpts = opts;
|
|
8372
8993
|
while (nextOpts !== null) {
|
|
8373
|
-
nextOpts = await runSession(term, config, nextOpts, exitHint);
|
|
8994
|
+
nextOpts = await runSession(term, config, serviceToken, nextOpts, exitHint);
|
|
8374
8995
|
}
|
|
8375
8996
|
const pendingUpdate = await getPendingUpdate();
|
|
8376
8997
|
if (pendingUpdate) {
|
|
@@ -8383,8 +9004,8 @@ async function runTuiApp(opts) {
|
|
|
8383
9004
|
`);
|
|
8384
9005
|
}
|
|
8385
9006
|
}
|
|
8386
|
-
async function runSession(term, config, opts, exitHint) {
|
|
8387
|
-
const ctx = await resolveSession(term, config, opts);
|
|
9007
|
+
async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
9008
|
+
const ctx = await resolveSession(term, config, serviceToken, opts);
|
|
8388
9009
|
if (!ctx) {
|
|
8389
9010
|
term.grabInput(false);
|
|
8390
9011
|
process.exit(0);
|
|
@@ -8393,7 +9014,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8393
9014
|
term.brightYellow(launchLabel)("\n");
|
|
8394
9015
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
8395
9016
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
8396
|
-
const subprotocols = ["acp.v1", `hydra-acp-token.${
|
|
9017
|
+
const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
|
|
8397
9018
|
let onReconnect = null;
|
|
8398
9019
|
let onDisconnectHook = null;
|
|
8399
9020
|
const stream = new ResilientWsStream({
|
|
@@ -8462,9 +9083,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8462
9083
|
screenRef.setBanner({ status: "ready", elapsedMs: void 0 });
|
|
8463
9084
|
}
|
|
8464
9085
|
}
|
|
8465
|
-
|
|
8466
|
-
tickWorker();
|
|
8467
|
-
}
|
|
9086
|
+
void delta;
|
|
8468
9087
|
};
|
|
8469
9088
|
let screenRef = null;
|
|
8470
9089
|
let dispatcherRef = null;
|
|
@@ -8481,23 +9100,91 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8481
9100
|
} else if (event?.kind === "turn-complete") {
|
|
8482
9101
|
adjustPendingTurns(-1);
|
|
8483
9102
|
}
|
|
8484
|
-
if (rawTag === "permission_resolved") {
|
|
8485
|
-
handlePermissionResolved(update);
|
|
8486
|
-
return;
|
|
9103
|
+
if (rawTag === "permission_resolved") {
|
|
9104
|
+
handlePermissionResolved(update);
|
|
9105
|
+
return;
|
|
9106
|
+
}
|
|
9107
|
+
appendRender(event);
|
|
9108
|
+
maybeDismissPermissionByToolUpdate(update);
|
|
9109
|
+
});
|
|
9110
|
+
conn.onNotification("hydra-acp/session_closed", () => {
|
|
9111
|
+
if (teardownStarted) {
|
|
9112
|
+
return;
|
|
9113
|
+
}
|
|
9114
|
+
if (pendingTurns > 0) {
|
|
9115
|
+
adjustPendingTurns(-pendingTurns);
|
|
9116
|
+
}
|
|
9117
|
+
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
9118
|
+
if (screenReady) {
|
|
9119
|
+
screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
|
|
9120
|
+
}
|
|
9121
|
+
});
|
|
9122
|
+
conn.onNotification("hydra-acp/prompt_queue_added", (params) => {
|
|
9123
|
+
if (teardownStarted) return;
|
|
9124
|
+
const p = params ?? {};
|
|
9125
|
+
if (typeof p.messageId !== "string") return;
|
|
9126
|
+
queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
|
|
9127
|
+
if (screenRef && dispatcherRef) {
|
|
9128
|
+
refreshQueueDisplay();
|
|
9129
|
+
}
|
|
9130
|
+
if (ownClientId !== void 0 && p.originator?.clientId === ownClientId) {
|
|
9131
|
+
const echo = pendingEchoes.shift();
|
|
9132
|
+
if (echo) {
|
|
9133
|
+
echo.messageId = p.messageId;
|
|
9134
|
+
ownPendingByMid.set(p.messageId, echo);
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
});
|
|
9138
|
+
conn.onNotification("hydra-acp/prompt_queue_updated", (params) => {
|
|
9139
|
+
if (teardownStarted) return;
|
|
9140
|
+
const p = params ?? {};
|
|
9141
|
+
if (typeof p.messageId !== "string") return;
|
|
9142
|
+
if (!queueCache.has(p.messageId)) return;
|
|
9143
|
+
queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
|
|
9144
|
+
const pending = ownPendingByMid.get(p.messageId);
|
|
9145
|
+
if (pending) {
|
|
9146
|
+
const blocks = Array.isArray(p.prompt) ? p.prompt : [];
|
|
9147
|
+
let text = "";
|
|
9148
|
+
const attachments = [];
|
|
9149
|
+
for (const raw of blocks) {
|
|
9150
|
+
if (!raw || typeof raw !== "object") continue;
|
|
9151
|
+
const b = raw;
|
|
9152
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
9153
|
+
text += b.text;
|
|
9154
|
+
} else if (b.type === "image" && typeof b.data === "string" && typeof b.mimeType === "string") {
|
|
9155
|
+
attachments.push({
|
|
9156
|
+
data: b.data,
|
|
9157
|
+
mimeType: b.mimeType,
|
|
9158
|
+
sizeBytes: Math.floor(b.data.length * 3 / 4)
|
|
9159
|
+
});
|
|
9160
|
+
}
|
|
9161
|
+
}
|
|
9162
|
+
pending.text = text;
|
|
9163
|
+
pending.attachments = attachments;
|
|
9164
|
+
}
|
|
9165
|
+
if (screenRef && dispatcherRef) {
|
|
9166
|
+
refreshQueueDisplay();
|
|
8487
9167
|
}
|
|
8488
|
-
appendRender(event);
|
|
8489
|
-
maybeDismissPermissionByToolUpdate(update);
|
|
8490
9168
|
});
|
|
8491
|
-
conn.onNotification("hydra-acp/
|
|
8492
|
-
if (teardownStarted)
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
9169
|
+
conn.onNotification("hydra-acp/prompt_queue_removed", (params) => {
|
|
9170
|
+
if (teardownStarted) return;
|
|
9171
|
+
const p = params ?? {};
|
|
9172
|
+
if (typeof p.messageId !== "string") return;
|
|
9173
|
+
const hadChip = queueCache.delete(p.messageId);
|
|
9174
|
+
if (hadChip && screenRef && dispatcherRef) {
|
|
9175
|
+
refreshQueueDisplay();
|
|
8497
9176
|
}
|
|
8498
|
-
const
|
|
8499
|
-
if (
|
|
8500
|
-
|
|
9177
|
+
const echo = ownPendingByMid.get(p.messageId);
|
|
9178
|
+
if (echo) {
|
|
9179
|
+
ownPendingByMid.delete(p.messageId);
|
|
9180
|
+
if (p.reason === "started") {
|
|
9181
|
+
echo.flushed = true;
|
|
9182
|
+
appendRender({
|
|
9183
|
+
kind: "user-text",
|
|
9184
|
+
text: echo.text,
|
|
9185
|
+
attachments: echo.attachments
|
|
9186
|
+
});
|
|
9187
|
+
}
|
|
8501
9188
|
}
|
|
8502
9189
|
});
|
|
8503
9190
|
const handlePermissionResolved = (update) => {
|
|
@@ -8627,9 +9314,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8627
9314
|
let resolvedAgentId = ctx.agentId;
|
|
8628
9315
|
let resolvedCwd = ctx.cwd;
|
|
8629
9316
|
let resolvedTitle;
|
|
9317
|
+
let ownClientId;
|
|
8630
9318
|
let initialModel;
|
|
8631
9319
|
let initialMode;
|
|
8632
9320
|
let initialCommands;
|
|
9321
|
+
let initialModes;
|
|
9322
|
+
let initialQueue;
|
|
8633
9323
|
let initialUsage;
|
|
8634
9324
|
let initialTurnStartedAt;
|
|
8635
9325
|
if (ctx.sessionId === "__new__") {
|
|
@@ -8646,6 +9336,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8646
9336
|
...Object.keys(hydraNewMeta).length > 0 ? { _meta: { [HYDRA_META_KEY]: hydraNewMeta } } : {}
|
|
8647
9337
|
});
|
|
8648
9338
|
resolvedSessionId = created.sessionId;
|
|
9339
|
+
if (created.clientId) {
|
|
9340
|
+
ownClientId = created.clientId;
|
|
9341
|
+
}
|
|
8649
9342
|
exitHint.sessionId = resolvedSessionId;
|
|
8650
9343
|
const hydraMeta = extractHydraMeta(created._meta ?? void 0);
|
|
8651
9344
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
@@ -8665,6 +9358,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8665
9358
|
if (hydraMeta.availableCommands) {
|
|
8666
9359
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
8667
9360
|
}
|
|
9361
|
+
if (hydraMeta.availableModes) {
|
|
9362
|
+
initialModes = hydraMeta.availableModes;
|
|
9363
|
+
}
|
|
9364
|
+
initialQueue = hydraMeta.queue;
|
|
8668
9365
|
} else {
|
|
8669
9366
|
const attached = await conn.request("session/attach", {
|
|
8670
9367
|
sessionId: ctx.sessionId,
|
|
@@ -8672,6 +9369,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8672
9369
|
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
8673
9370
|
});
|
|
8674
9371
|
resolvedSessionId = attached.sessionId;
|
|
9372
|
+
if (attached.clientId) {
|
|
9373
|
+
ownClientId = attached.clientId;
|
|
9374
|
+
}
|
|
8675
9375
|
exitHint.sessionId = resolvedSessionId;
|
|
8676
9376
|
const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
|
|
8677
9377
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
@@ -8691,6 +9391,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8691
9391
|
if (hydraMeta.availableCommands) {
|
|
8692
9392
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
8693
9393
|
}
|
|
9394
|
+
if (hydraMeta.availableModes) {
|
|
9395
|
+
initialModes = hydraMeta.availableModes;
|
|
9396
|
+
}
|
|
9397
|
+
initialQueue = hydraMeta.queue;
|
|
8694
9398
|
}
|
|
8695
9399
|
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
8696
9400
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
@@ -8750,6 +9454,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8750
9454
|
{ name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
|
|
8751
9455
|
];
|
|
8752
9456
|
let agentCommands = initialCommands ?? [];
|
|
9457
|
+
let agentModes = initialModes ?? [];
|
|
8753
9458
|
const allCommands = () => {
|
|
8754
9459
|
const seen = /* @__PURE__ */ new Set();
|
|
8755
9460
|
const out = [];
|
|
@@ -8905,7 +9610,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8905
9610
|
usage: { ...usage }
|
|
8906
9611
|
});
|
|
8907
9612
|
if (initialMode) {
|
|
8908
|
-
screen.
|
|
9613
|
+
screen.setBanner({ currentMode: initialMode });
|
|
8909
9614
|
}
|
|
8910
9615
|
void getPendingUpdate().then((info) => {
|
|
8911
9616
|
if (info) {
|
|
@@ -8942,7 +9647,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8942
9647
|
}
|
|
8943
9648
|
let onlyClient = false;
|
|
8944
9649
|
try {
|
|
8945
|
-
const sessions = await listSessions(config);
|
|
9650
|
+
const sessions = await listSessions(config, serviceToken);
|
|
8946
9651
|
const me = sessions.find((s) => s.sessionId === resolvedSessionId);
|
|
8947
9652
|
onlyClient = !me || me.attachedClients <= 1;
|
|
8948
9653
|
} catch {
|
|
@@ -9034,11 +9739,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9034
9739
|
screen.pauseRepaint();
|
|
9035
9740
|
screen.stop();
|
|
9036
9741
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
9037
|
-
const sessions = await listSessions(config);
|
|
9742
|
+
const sessions = await listSessions(config, serviceToken);
|
|
9038
9743
|
const choice = await pickSession(term, {
|
|
9039
9744
|
cwd: resolvedCwd,
|
|
9040
9745
|
sessions,
|
|
9041
9746
|
config,
|
|
9747
|
+
serviceToken,
|
|
9042
9748
|
currentSessionId: resolvedSessionId
|
|
9043
9749
|
});
|
|
9044
9750
|
if (choice.kind === "abort") {
|
|
@@ -9066,37 +9772,76 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9066
9772
|
}
|
|
9067
9773
|
resume(nextOpts);
|
|
9068
9774
|
};
|
|
9069
|
-
const
|
|
9775
|
+
const cycleLiveSession = async () => {
|
|
9776
|
+
if (!finishSession)
|
|
9777
|
+
return;
|
|
9778
|
+
const sessions = await listSessions(config, serviceToken);
|
|
9779
|
+
const live = sessions.filter((s) => s.status === "live");
|
|
9780
|
+
if (live.length <= 1)
|
|
9781
|
+
return;
|
|
9782
|
+
const idx = live.findIndex((s) => s.sessionId === resolvedSessionId);
|
|
9783
|
+
const next = live[(idx + 1) % live.length];
|
|
9784
|
+
const resume = finishSession;
|
|
9785
|
+
finishSession = null;
|
|
9786
|
+
process.off("SIGINT", sigintHandler);
|
|
9787
|
+
void stream.close().catch(() => void 0);
|
|
9788
|
+
const nextOpts = { ...opts, sessionId: next.sessionId, cwd: resolvedCwd };
|
|
9789
|
+
if (next.agentId !== void 0)
|
|
9790
|
+
nextOpts.agentId = next.agentId;
|
|
9791
|
+
resume(nextOpts);
|
|
9792
|
+
};
|
|
9070
9793
|
const handleEffect = (effect) => {
|
|
9071
9794
|
switch (effect.type) {
|
|
9072
9795
|
case "send":
|
|
9073
|
-
enqueuePrompt(effect.text, effect.
|
|
9796
|
+
enqueuePrompt(effect.text, effect.attachments);
|
|
9074
9797
|
return;
|
|
9075
9798
|
case "queue-edit": {
|
|
9076
|
-
const
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
};
|
|
9084
|
-
refreshQueueDisplay();
|
|
9799
|
+
const mid = queueMessageIdAt(effect.index);
|
|
9800
|
+
if (!mid) {
|
|
9801
|
+
return;
|
|
9802
|
+
}
|
|
9803
|
+
const blocks = [];
|
|
9804
|
+
if (effect.text.length > 0) {
|
|
9805
|
+
blocks.push({ type: "text", text: effect.text });
|
|
9085
9806
|
}
|
|
9807
|
+
for (const a of effect.attachments) {
|
|
9808
|
+
blocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
|
|
9809
|
+
}
|
|
9810
|
+
conn.request("hydra-acp/update_prompt", {
|
|
9811
|
+
sessionId: resolvedSessionId,
|
|
9812
|
+
messageId: mid,
|
|
9813
|
+
prompt: blocks
|
|
9814
|
+
}).then((raw) => {
|
|
9815
|
+
const res = raw;
|
|
9816
|
+
if (!res.updated && res.reason !== "ok") {
|
|
9817
|
+
screen.notify(`queue edit skipped (${res.reason})`);
|
|
9818
|
+
}
|
|
9819
|
+
}).catch((err) => {
|
|
9820
|
+
screen.notify(`queue edit failed: ${err.message}`);
|
|
9821
|
+
});
|
|
9086
9822
|
return;
|
|
9087
9823
|
}
|
|
9088
9824
|
case "queue-remove": {
|
|
9089
|
-
const
|
|
9090
|
-
if (
|
|
9091
|
-
|
|
9092
|
-
refreshQueueDisplay();
|
|
9825
|
+
const mid = queueMessageIdAt(effect.index);
|
|
9826
|
+
if (!mid) {
|
|
9827
|
+
return;
|
|
9093
9828
|
}
|
|
9829
|
+
conn.request("hydra-acp/cancel_prompt", {
|
|
9830
|
+
sessionId: resolvedSessionId,
|
|
9831
|
+
messageId: mid
|
|
9832
|
+
}).then((raw) => {
|
|
9833
|
+
const res = raw;
|
|
9834
|
+
if (!res.cancelled && res.reason !== "ok") {
|
|
9835
|
+
screen.notify(`queue cancel skipped (${res.reason})`);
|
|
9836
|
+
}
|
|
9837
|
+
}).catch((err) => {
|
|
9838
|
+
screen.notify(`queue cancel failed: ${err.message}`);
|
|
9839
|
+
});
|
|
9094
9840
|
return;
|
|
9095
9841
|
}
|
|
9096
9842
|
case "cancel": {
|
|
9097
9843
|
if (effect.prefill && turnInFlight) {
|
|
9098
|
-
const
|
|
9099
|
-
const waitingEmpty = promptQueue.length <= headOffset;
|
|
9844
|
+
const waitingEmpty = queueCache.size === 0;
|
|
9100
9845
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
9101
9846
|
if (waitingEmpty && bufferEmpty) {
|
|
9102
9847
|
pendingPrefill = {
|
|
@@ -9116,7 +9861,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9116
9861
|
void requestExit();
|
|
9117
9862
|
return;
|
|
9118
9863
|
case "plan-toggle":
|
|
9119
|
-
|
|
9864
|
+
void handleModeToggle(effect.on);
|
|
9120
9865
|
return;
|
|
9121
9866
|
case "redraw-banner":
|
|
9122
9867
|
screen.setBanner({});
|
|
@@ -9133,6 +9878,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9133
9878
|
case "switch-session":
|
|
9134
9879
|
void switchSession();
|
|
9135
9880
|
return;
|
|
9881
|
+
case "next-live-session":
|
|
9882
|
+
void cycleLiveSession();
|
|
9883
|
+
return;
|
|
9136
9884
|
case "toggle-tools":
|
|
9137
9885
|
toolsExpanded = !toolsExpanded;
|
|
9138
9886
|
renderToolsBlock();
|
|
@@ -9176,11 +9924,11 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9176
9924
|
}
|
|
9177
9925
|
const mimeType = mimeFromExtension(token);
|
|
9178
9926
|
if (!mimeType) {
|
|
9179
|
-
screen.notify(`unsupported image type: ${
|
|
9927
|
+
screen.notify(`unsupported image type: ${path13.basename(token)}`);
|
|
9180
9928
|
continue;
|
|
9181
9929
|
}
|
|
9182
9930
|
try {
|
|
9183
|
-
const buf = await
|
|
9931
|
+
const buf = await fs19.readFile(token);
|
|
9184
9932
|
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
9185
9933
|
screen.notify(
|
|
9186
9934
|
`image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
|
|
@@ -9190,13 +9938,13 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9190
9938
|
dispatcher.addAttachment({
|
|
9191
9939
|
mimeType,
|
|
9192
9940
|
data: buf.toString("base64"),
|
|
9193
|
-
name:
|
|
9941
|
+
name: path13.basename(token),
|
|
9194
9942
|
sizeBytes: buf.length
|
|
9195
9943
|
});
|
|
9196
9944
|
added++;
|
|
9197
9945
|
} catch (err) {
|
|
9198
9946
|
screen.notify(
|
|
9199
|
-
`cannot read ${
|
|
9947
|
+
`cannot read ${path13.basename(token)}: ${err.message}`
|
|
9200
9948
|
);
|
|
9201
9949
|
}
|
|
9202
9950
|
}
|
|
@@ -9227,18 +9975,53 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9227
9975
|
}
|
|
9228
9976
|
screen.refreshPrompt();
|
|
9229
9977
|
};
|
|
9230
|
-
const
|
|
9231
|
-
|
|
9978
|
+
const formatQueueChipText = (entry) => entry.attachmentCount > 0 ? `${entry.text} \xB7 \u{1F4CE}\xD7${entry.attachmentCount}` : entry.text;
|
|
9979
|
+
const chipFromPrompt = (messageId, prompt) => {
|
|
9980
|
+
const blocks = Array.isArray(prompt) ? prompt : [];
|
|
9981
|
+
let text = "";
|
|
9982
|
+
let attachmentCount = 0;
|
|
9983
|
+
for (const raw of blocks) {
|
|
9984
|
+
if (!raw || typeof raw !== "object") continue;
|
|
9985
|
+
const b = raw;
|
|
9986
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
9987
|
+
text += b.text;
|
|
9988
|
+
} else if (b.type === "image") {
|
|
9989
|
+
attachmentCount += 1;
|
|
9990
|
+
}
|
|
9991
|
+
}
|
|
9992
|
+
return {
|
|
9993
|
+
messageId,
|
|
9994
|
+
text: sanitizeSingleLine(text),
|
|
9995
|
+
attachmentCount
|
|
9996
|
+
};
|
|
9997
|
+
};
|
|
9998
|
+
const queueCache = /* @__PURE__ */ new Map();
|
|
9999
|
+
const pendingEchoes = [];
|
|
10000
|
+
const ownPendingByMid = /* @__PURE__ */ new Map();
|
|
9232
10001
|
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
|
-
);
|
|
10002
|
+
const entries = [...queueCache.values()];
|
|
10003
|
+
const displayTexts = entries.map(formatQueueChipText);
|
|
9237
10004
|
screen.setQueuedPrompts(displayTexts);
|
|
9238
|
-
screen.setBanner({ queued:
|
|
9239
|
-
dispatcher.setQueue(
|
|
10005
|
+
screen.setBanner({ queued: entries.length });
|
|
10006
|
+
dispatcher.setQueue(entries.map((e) => e.text));
|
|
10007
|
+
};
|
|
10008
|
+
const queueMessageIdAt = (index) => {
|
|
10009
|
+
const entries = [...queueCache.values()];
|
|
10010
|
+
return entries[index]?.messageId;
|
|
9240
10011
|
};
|
|
9241
|
-
|
|
10012
|
+
if (initialQueue && initialQueue.length > 0) {
|
|
10013
|
+
for (const entry of initialQueue) {
|
|
10014
|
+
if (entry.position === 0) continue;
|
|
10015
|
+
queueCache.set(
|
|
10016
|
+
entry.messageId,
|
|
10017
|
+
chipFromPrompt(entry.messageId, entry.prompt)
|
|
10018
|
+
);
|
|
10019
|
+
}
|
|
10020
|
+
if (queueCache.size > 0) {
|
|
10021
|
+
refreshQueueDisplay();
|
|
10022
|
+
}
|
|
10023
|
+
}
|
|
10024
|
+
const enqueuePrompt = (text, attachments) => {
|
|
9242
10025
|
screen.scrollToBottom();
|
|
9243
10026
|
if (handleBuiltinCommand(text)) {
|
|
9244
10027
|
return;
|
|
@@ -9246,15 +10029,29 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9246
10029
|
history = appendEntry(history, text);
|
|
9247
10030
|
dispatcher.setHistory(history);
|
|
9248
10031
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
9249
|
-
|
|
9250
|
-
refreshQueueDisplay();
|
|
9251
|
-
tickWorker();
|
|
10032
|
+
void runPrompt(text, attachments);
|
|
9252
10033
|
};
|
|
9253
|
-
const
|
|
9254
|
-
if (
|
|
10034
|
+
const handleModeToggle = async (_on) => {
|
|
10035
|
+
if (agentModes.length === 0) {
|
|
10036
|
+
screen.notify("no modes advertised by agent");
|
|
10037
|
+
return;
|
|
10038
|
+
}
|
|
10039
|
+
const currentMode = screen.currentModeId();
|
|
10040
|
+
const idx = agentModes.findIndex((m) => m.id === currentMode);
|
|
10041
|
+
const nextIdx = idx === -1 ? 0 : (idx + 1) % agentModes.length;
|
|
10042
|
+
const newModeId = agentModes[nextIdx]?.id;
|
|
10043
|
+
if (!newModeId) {
|
|
9255
10044
|
return;
|
|
9256
10045
|
}
|
|
9257
|
-
|
|
10046
|
+
screen.setBanner({ currentMode: newModeId });
|
|
10047
|
+
try {
|
|
10048
|
+
await conn.request("session/set_mode", {
|
|
10049
|
+
sessionId: resolvedSessionId,
|
|
10050
|
+
modeId: newModeId
|
|
10051
|
+
});
|
|
10052
|
+
} catch (err) {
|
|
10053
|
+
screen.notify(`set_mode failed: ${err.message}`);
|
|
10054
|
+
}
|
|
9258
10055
|
};
|
|
9259
10056
|
const handleBuiltinCommand = (text) => {
|
|
9260
10057
|
const trimmed = text.trim();
|
|
@@ -9404,33 +10201,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9404
10201
|
return false;
|
|
9405
10202
|
}
|
|
9406
10203
|
};
|
|
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) => {
|
|
10204
|
+
const runPrompt = async (text, attachments) => {
|
|
9434
10205
|
const userBlocks = [];
|
|
9435
10206
|
if (text.length > 0) {
|
|
9436
10207
|
userBlocks.push({ type: "text", text });
|
|
@@ -9438,9 +10209,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9438
10209
|
for (const a of attachments) {
|
|
9439
10210
|
userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
|
|
9440
10211
|
}
|
|
9441
|
-
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
9442
10212
|
adjustPendingTurns(1);
|
|
9443
|
-
|
|
10213
|
+
const echo = { text, attachments, flushed: false };
|
|
10214
|
+
pendingEchoes.push(echo);
|
|
9444
10215
|
let cancelled = false;
|
|
9445
10216
|
turnInFlight = {
|
|
9446
10217
|
text,
|
|
@@ -9459,23 +10230,44 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9459
10230
|
try {
|
|
9460
10231
|
const response = await conn.request("session/prompt", {
|
|
9461
10232
|
sessionId: resolvedSessionId,
|
|
9462
|
-
prompt:
|
|
10233
|
+
prompt: userBlocks
|
|
9463
10234
|
});
|
|
9464
10235
|
if (response && typeof response.stopReason === "string") {
|
|
9465
10236
|
stopReason = response.stopReason;
|
|
9466
10237
|
}
|
|
9467
10238
|
} catch (err) {
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
10239
|
+
const idx = pendingEchoes.indexOf(echo);
|
|
10240
|
+
if (idx >= 0) {
|
|
10241
|
+
pendingEchoes.splice(idx, 1);
|
|
10242
|
+
}
|
|
10243
|
+
if (echo.messageId !== void 0) {
|
|
10244
|
+
ownPendingByMid.delete(echo.messageId);
|
|
10245
|
+
}
|
|
10246
|
+
screen.appendLines([
|
|
10247
|
+
{
|
|
10248
|
+
prefix: "\u2717 ",
|
|
10249
|
+
prefixStyle: "tool-status-fail",
|
|
10250
|
+
body: err.message,
|
|
10251
|
+
bodyStyle: "tool-status-fail"
|
|
10252
|
+
}
|
|
10253
|
+
]);
|
|
9473
10254
|
} finally {
|
|
9474
10255
|
turnInFlight = null;
|
|
9475
10256
|
adjustPendingTurns(-1);
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
10257
|
+
if (echo.flushed) {
|
|
10258
|
+
appendRender(
|
|
10259
|
+
stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
|
|
10260
|
+
);
|
|
10261
|
+
}
|
|
10262
|
+
if (pendingPrefill !== null) {
|
|
10263
|
+
const { text: pt, attachments: pa } = pendingPrefill;
|
|
10264
|
+
pendingPrefill = null;
|
|
10265
|
+
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
10266
|
+
if (bufferEmpty) {
|
|
10267
|
+
dispatcher.setBuffer(pt, pa);
|
|
10268
|
+
screen.refreshPrompt();
|
|
10269
|
+
}
|
|
10270
|
+
}
|
|
9479
10271
|
}
|
|
9480
10272
|
};
|
|
9481
10273
|
const toolStates = /* @__PURE__ */ new Map();
|
|
@@ -9605,6 +10397,14 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9605
10397
|
refreshCompletions();
|
|
9606
10398
|
return;
|
|
9607
10399
|
}
|
|
10400
|
+
if (event.kind === "available-modes") {
|
|
10401
|
+
agentModes = event.modes;
|
|
10402
|
+
return;
|
|
10403
|
+
}
|
|
10404
|
+
if (event.kind === "mode-changed") {
|
|
10405
|
+
screen.setBanner({ currentMode: event.mode || void 0 });
|
|
10406
|
+
return;
|
|
10407
|
+
}
|
|
9608
10408
|
if (event.kind === "session-info") {
|
|
9609
10409
|
if (event.title !== void 0) {
|
|
9610
10410
|
screen.setSessionbar({ title: event.title });
|
|
@@ -9846,7 +10646,7 @@ connection lost: ${err.message}
|
|
|
9846
10646
|
process.on("SIGINT", sigintHandler);
|
|
9847
10647
|
return await sessionDone;
|
|
9848
10648
|
}
|
|
9849
|
-
async function resolveSession(term, config, opts) {
|
|
10649
|
+
async function resolveSession(term, config, serviceToken, opts) {
|
|
9850
10650
|
const cwd = opts.cwd ?? process.cwd();
|
|
9851
10651
|
if (opts.sessionId) {
|
|
9852
10652
|
return {
|
|
@@ -9859,7 +10659,7 @@ async function resolveSession(term, config, opts) {
|
|
|
9859
10659
|
return newCtx(opts, cwd, config);
|
|
9860
10660
|
}
|
|
9861
10661
|
if (opts.resume) {
|
|
9862
|
-
const sessions2 = await listSessions(config, { cwd, all: true });
|
|
10662
|
+
const sessions2 = await listSessions(config, serviceToken, { cwd, all: true });
|
|
9863
10663
|
const target = pickMostRecent(sessions2, cwd);
|
|
9864
10664
|
if (!target) {
|
|
9865
10665
|
term.yellow(`No sessions found for ${cwd}.
|
|
@@ -9872,14 +10672,15 @@ async function resolveSession(term, config, opts) {
|
|
|
9872
10672
|
cwd
|
|
9873
10673
|
};
|
|
9874
10674
|
}
|
|
9875
|
-
const sessions = await listSessions(config);
|
|
10675
|
+
const sessions = await listSessions(config, serviceToken);
|
|
9876
10676
|
if (sessions.length === 0) {
|
|
9877
10677
|
return newCtx(opts, cwd, config);
|
|
9878
10678
|
}
|
|
9879
10679
|
const choice = await pickSession(term, {
|
|
9880
10680
|
cwd,
|
|
9881
10681
|
sessions,
|
|
9882
|
-
config
|
|
10682
|
+
config,
|
|
10683
|
+
serviceToken
|
|
9883
10684
|
});
|
|
9884
10685
|
if (choice.kind === "abort") {
|
|
9885
10686
|
return null;
|
|
@@ -9934,7 +10735,7 @@ function rotateIfBig(target) {
|
|
|
9934
10735
|
} catch {
|
|
9935
10736
|
}
|
|
9936
10737
|
}
|
|
9937
|
-
var
|
|
10738
|
+
var logMaxBytes;
|
|
9938
10739
|
var init_app = __esm({
|
|
9939
10740
|
"src/tui/app.ts"() {
|
|
9940
10741
|
"use strict";
|
|
@@ -9942,6 +10743,7 @@ var init_app = __esm({
|
|
|
9942
10743
|
init_types();
|
|
9943
10744
|
init_resilient_ws();
|
|
9944
10745
|
init_config();
|
|
10746
|
+
init_service_token();
|
|
9945
10747
|
init_daemon_bootstrap();
|
|
9946
10748
|
init_session();
|
|
9947
10749
|
init_paths();
|
|
@@ -9957,7 +10759,6 @@ var init_app = __esm({
|
|
|
9957
10759
|
init_completion();
|
|
9958
10760
|
init_render_update();
|
|
9959
10761
|
init_format();
|
|
9960
|
-
PLAN_PREFIX_TEXT = "Plan mode is on. Outline what you would do without making any changes. Do not edit files, run shell commands, or otherwise execute side effects; produce a plan only.";
|
|
9961
10762
|
logMaxBytes = 5 * 1024 * 1024;
|
|
9962
10763
|
}
|
|
9963
10764
|
});
|
|
@@ -10055,23 +10856,25 @@ function resolveOption(flags, key) {
|
|
|
10055
10856
|
// src/cli/commands/init.ts
|
|
10056
10857
|
init_paths();
|
|
10057
10858
|
init_config();
|
|
10058
|
-
|
|
10859
|
+
init_service_token();
|
|
10860
|
+
import * as fs3 from "fs/promises";
|
|
10059
10861
|
async function runInit(flags) {
|
|
10060
|
-
await
|
|
10061
|
-
|
|
10862
|
+
await fs3.mkdir(paths.home(), { recursive: true });
|
|
10863
|
+
await migrateLegacyAuthToken();
|
|
10864
|
+
const existingToken = await readServiceToken();
|
|
10062
10865
|
if (!existingToken) {
|
|
10063
|
-
const token =
|
|
10064
|
-
await
|
|
10866
|
+
const token = generateServiceToken();
|
|
10867
|
+
await writeServiceToken(token);
|
|
10065
10868
|
process.stdout.write(
|
|
10066
10869
|
`Initialized ${paths.authToken()}
|
|
10067
|
-
|
|
10870
|
+
Service token: ${token}
|
|
10068
10871
|
`
|
|
10069
10872
|
);
|
|
10070
10873
|
return;
|
|
10071
10874
|
}
|
|
10072
10875
|
if (flagBool(flags, "rotate-token")) {
|
|
10073
|
-
const newToken =
|
|
10074
|
-
await
|
|
10876
|
+
const newToken = generateServiceToken();
|
|
10877
|
+
await writeServiceToken(newToken);
|
|
10075
10878
|
process.stdout.write(
|
|
10076
10879
|
`Rotated token in ${paths.authToken()}
|
|
10077
10880
|
New token: ${newToken}
|
|
@@ -10079,20 +10882,21 @@ New token: ${newToken}
|
|
|
10079
10882
|
);
|
|
10080
10883
|
return;
|
|
10081
10884
|
}
|
|
10082
|
-
process.stdout.write(`
|
|
10885
|
+
process.stdout.write(`Service token already exists at ${paths.authToken()}.
|
|
10083
10886
|
`);
|
|
10084
|
-
process.stdout.write("Pass --rotate-token to generate a new
|
|
10887
|
+
process.stdout.write("Pass --rotate-token to generate a new service token.\n");
|
|
10085
10888
|
}
|
|
10086
10889
|
|
|
10087
10890
|
// src/cli/commands/daemon.ts
|
|
10088
10891
|
init_paths();
|
|
10089
10892
|
init_config();
|
|
10893
|
+
init_service_token();
|
|
10090
10894
|
import * as fsp6 from "fs/promises";
|
|
10091
10895
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
10092
10896
|
|
|
10093
10897
|
// src/daemon/server.ts
|
|
10094
10898
|
init_config();
|
|
10095
|
-
import * as
|
|
10899
|
+
import * as fs15 from "fs";
|
|
10096
10900
|
import * as fsp4 from "fs/promises";
|
|
10097
10901
|
import Fastify from "fastify";
|
|
10098
10902
|
import websocketPlugin from "@fastify/websocket";
|
|
@@ -10101,12 +10905,12 @@ import createPinoRoll from "pino-roll";
|
|
|
10101
10905
|
|
|
10102
10906
|
// src/core/registry.ts
|
|
10103
10907
|
init_paths();
|
|
10104
|
-
import * as
|
|
10908
|
+
import * as fs5 from "fs/promises";
|
|
10105
10909
|
import { z as z2 } from "zod";
|
|
10106
10910
|
|
|
10107
10911
|
// src/core/binary-install.ts
|
|
10108
10912
|
init_paths();
|
|
10109
|
-
import * as
|
|
10913
|
+
import * as fs4 from "fs";
|
|
10110
10914
|
import * as fsp from "fs/promises";
|
|
10111
10915
|
import * as path2 from "path";
|
|
10112
10916
|
import { spawn } from "child_process";
|
|
@@ -10211,7 +11015,7 @@ async function downloadTo(args) {
|
|
|
10211
11015
|
);
|
|
10212
11016
|
}
|
|
10213
11017
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
10214
|
-
const out =
|
|
11018
|
+
const out = fs4.createWriteStream(dest);
|
|
10215
11019
|
const nodeStream = Readable.fromWeb(response.body);
|
|
10216
11020
|
let received = 0;
|
|
10217
11021
|
let lastEmit = Date.now();
|
|
@@ -10338,7 +11142,8 @@ async function ensureNpmPackage(args) {
|
|
|
10338
11142
|
await installInto({
|
|
10339
11143
|
agentId: args.agentId,
|
|
10340
11144
|
packageSpec: args.packageSpec,
|
|
10341
|
-
installDir
|
|
11145
|
+
installDir,
|
|
11146
|
+
registry: args.registry
|
|
10342
11147
|
});
|
|
10343
11148
|
if (!await fileExists2(binPath)) {
|
|
10344
11149
|
throw new Error(
|
|
@@ -10356,7 +11161,8 @@ async function installInto(args) {
|
|
|
10356
11161
|
);
|
|
10357
11162
|
await runNpmInstall({
|
|
10358
11163
|
packageSpec: args.packageSpec,
|
|
10359
|
-
cwd: tempDir
|
|
11164
|
+
cwd: tempDir,
|
|
11165
|
+
registry: args.registry
|
|
10360
11166
|
});
|
|
10361
11167
|
try {
|
|
10362
11168
|
await fsp2.rename(tempDir, args.installDir);
|
|
@@ -10380,9 +11186,10 @@ async function installInto(args) {
|
|
|
10380
11186
|
}
|
|
10381
11187
|
function runNpmInstall(args) {
|
|
10382
11188
|
return new Promise((resolve5, reject) => {
|
|
11189
|
+
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
10383
11190
|
const child = spawn2(
|
|
10384
11191
|
"npm",
|
|
10385
|
-
["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
|
|
11192
|
+
["install", "--no-audit", "--no-fund", "--silent", ...registryArgs, args.packageSpec],
|
|
10386
11193
|
{
|
|
10387
11194
|
cwd: args.cwd,
|
|
10388
11195
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -10535,7 +11342,7 @@ var Registry = class {
|
|
|
10535
11342
|
async readDiskCache() {
|
|
10536
11343
|
let text;
|
|
10537
11344
|
try {
|
|
10538
|
-
text = await
|
|
11345
|
+
text = await fs5.readFile(paths.registryCache(), "utf8");
|
|
10539
11346
|
} catch (err) {
|
|
10540
11347
|
const e = err;
|
|
10541
11348
|
if (e.code === "ENOENT") {
|
|
@@ -10561,7 +11368,7 @@ var Registry = class {
|
|
|
10561
11368
|
// without a lock file: the loser of the rename race just gets its
|
|
10562
11369
|
// version replaced by the winner's.
|
|
10563
11370
|
async writeDiskCache(cache) {
|
|
10564
|
-
await
|
|
11371
|
+
await fs5.mkdir(paths.home(), { recursive: true });
|
|
10565
11372
|
const final = paths.registryCache();
|
|
10566
11373
|
const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
|
|
10567
11374
|
const body = JSON.stringify(
|
|
@@ -10570,10 +11377,10 @@ var Registry = class {
|
|
|
10570
11377
|
2
|
|
10571
11378
|
) + "\n";
|
|
10572
11379
|
try {
|
|
10573
|
-
await
|
|
10574
|
-
await
|
|
11380
|
+
await fs5.writeFile(tmp, body, "utf8");
|
|
11381
|
+
await fs5.rename(tmp, final);
|
|
10575
11382
|
} catch (err) {
|
|
10576
|
-
await
|
|
11383
|
+
await fs5.unlink(tmp).catch(() => void 0);
|
|
10577
11384
|
throw err;
|
|
10578
11385
|
}
|
|
10579
11386
|
}
|
|
@@ -10591,7 +11398,7 @@ function npxPackageBasename(agent) {
|
|
|
10591
11398
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
10592
11399
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
10593
11400
|
}
|
|
10594
|
-
async function planSpawn(agent, callerArgs = []) {
|
|
11401
|
+
async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
10595
11402
|
if (agent.distribution.npx) {
|
|
10596
11403
|
const npx = agent.distribution.npx;
|
|
10597
11404
|
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
@@ -10607,7 +11414,8 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
10607
11414
|
agentId: agent.id,
|
|
10608
11415
|
version: agent.version ?? "current",
|
|
10609
11416
|
packageSpec: npx.package,
|
|
10610
|
-
bin
|
|
11417
|
+
bin,
|
|
11418
|
+
registry: options.npmRegistry
|
|
10611
11419
|
});
|
|
10612
11420
|
return {
|
|
10613
11421
|
command: binPath,
|
|
@@ -10832,7 +11640,7 @@ stderr: ${tail}` : reason;
|
|
|
10832
11640
|
};
|
|
10833
11641
|
|
|
10834
11642
|
// src/core/session-manager.ts
|
|
10835
|
-
import * as
|
|
11643
|
+
import * as fs11 from "fs/promises";
|
|
10836
11644
|
import * as os2 from "os";
|
|
10837
11645
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
10838
11646
|
init_session();
|
|
@@ -10840,7 +11648,7 @@ init_session_store();
|
|
|
10840
11648
|
|
|
10841
11649
|
// src/core/history-store.ts
|
|
10842
11650
|
init_paths();
|
|
10843
|
-
import * as
|
|
11651
|
+
import * as fs8 from "fs/promises";
|
|
10844
11652
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
10845
11653
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
10846
11654
|
var HistoryStore = class {
|
|
@@ -10857,9 +11665,9 @@ var HistoryStore = class {
|
|
|
10857
11665
|
return;
|
|
10858
11666
|
}
|
|
10859
11667
|
return this.enqueue(sessionId, async () => {
|
|
10860
|
-
await
|
|
11668
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
10861
11669
|
const line = JSON.stringify(entry) + "\n";
|
|
10862
|
-
await
|
|
11670
|
+
await fs8.appendFile(paths.historyFile(sessionId), line, {
|
|
10863
11671
|
encoding: "utf8",
|
|
10864
11672
|
mode: 384
|
|
10865
11673
|
});
|
|
@@ -10870,9 +11678,9 @@ var HistoryStore = class {
|
|
|
10870
11678
|
return;
|
|
10871
11679
|
}
|
|
10872
11680
|
return this.enqueue(sessionId, async () => {
|
|
10873
|
-
await
|
|
11681
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
10874
11682
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
10875
|
-
await
|
|
11683
|
+
await fs8.writeFile(paths.historyFile(sessionId), body, {
|
|
10876
11684
|
encoding: "utf8",
|
|
10877
11685
|
mode: 384
|
|
10878
11686
|
});
|
|
@@ -10889,7 +11697,7 @@ var HistoryStore = class {
|
|
|
10889
11697
|
return this.enqueue(sessionId, async () => {
|
|
10890
11698
|
let raw;
|
|
10891
11699
|
try {
|
|
10892
|
-
raw = await
|
|
11700
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
10893
11701
|
} catch (err) {
|
|
10894
11702
|
const e = err;
|
|
10895
11703
|
if (e.code === "ENOENT") {
|
|
@@ -10902,7 +11710,7 @@ var HistoryStore = class {
|
|
|
10902
11710
|
return;
|
|
10903
11711
|
}
|
|
10904
11712
|
const trimmed = lines.slice(-maxEntries);
|
|
10905
|
-
await
|
|
11713
|
+
await fs8.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
10906
11714
|
encoding: "utf8",
|
|
10907
11715
|
mode: 384
|
|
10908
11716
|
});
|
|
@@ -10918,7 +11726,7 @@ var HistoryStore = class {
|
|
|
10918
11726
|
}
|
|
10919
11727
|
let raw;
|
|
10920
11728
|
try {
|
|
10921
|
-
raw = await
|
|
11729
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
10922
11730
|
} catch (err) {
|
|
10923
11731
|
const e = err;
|
|
10924
11732
|
if (e.code === "ENOENT") {
|
|
@@ -10964,7 +11772,7 @@ var HistoryStore = class {
|
|
|
10964
11772
|
}
|
|
10965
11773
|
return this.enqueue(sessionId, async () => {
|
|
10966
11774
|
try {
|
|
10967
|
-
await
|
|
11775
|
+
await fs8.unlink(paths.historyFile(sessionId));
|
|
10968
11776
|
} catch (err) {
|
|
10969
11777
|
const e = err;
|
|
10970
11778
|
if (e.code !== "ENOENT") {
|
|
@@ -10972,7 +11780,7 @@ var HistoryStore = class {
|
|
|
10972
11780
|
}
|
|
10973
11781
|
}
|
|
10974
11782
|
try {
|
|
10975
|
-
await
|
|
11783
|
+
await fs8.rmdir(paths.sessionDir(sessionId));
|
|
10976
11784
|
} catch (err) {
|
|
10977
11785
|
const e = err;
|
|
10978
11786
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -11000,6 +11808,8 @@ init_paths();
|
|
|
11000
11808
|
init_history();
|
|
11001
11809
|
init_types();
|
|
11002
11810
|
init_hydra_version();
|
|
11811
|
+
init_queue_store();
|
|
11812
|
+
var QUEUE_REPLAY_TTL_MS = 15 * 60 * 1e3;
|
|
11003
11813
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
11004
11814
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
11005
11815
|
var SessionManager = class {
|
|
@@ -11012,6 +11822,7 @@ var SessionManager = class {
|
|
|
11012
11822
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
11013
11823
|
this.defaultModels = options.defaultModels ?? {};
|
|
11014
11824
|
this.logger = options.logger;
|
|
11825
|
+
this.npmRegistry = options.npmRegistry;
|
|
11015
11826
|
}
|
|
11016
11827
|
registry;
|
|
11017
11828
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -11027,6 +11838,7 @@ var SessionManager = class {
|
|
|
11027
11838
|
// back-to-back) don't lose writes via interleaved reads.
|
|
11028
11839
|
metaWriteQueues = /* @__PURE__ */ new Map();
|
|
11029
11840
|
logger;
|
|
11841
|
+
npmRegistry;
|
|
11030
11842
|
async create(params) {
|
|
11031
11843
|
const fresh = await this.bootstrapAgent({
|
|
11032
11844
|
agentId: params.agentId,
|
|
@@ -11048,7 +11860,9 @@ var SessionManager = class {
|
|
|
11048
11860
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
11049
11861
|
historyStore: this.histories,
|
|
11050
11862
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
11051
|
-
currentModel: fresh.initialModel
|
|
11863
|
+
currentModel: fresh.initialModel,
|
|
11864
|
+
currentMode: fresh.initialMode,
|
|
11865
|
+
agentModes: fresh.initialModes
|
|
11052
11866
|
});
|
|
11053
11867
|
await this.attachManagerHooks(session);
|
|
11054
11868
|
return session;
|
|
@@ -11093,7 +11907,7 @@ var SessionManager = class {
|
|
|
11093
11907
|
if (params.upstreamSessionId === "") {
|
|
11094
11908
|
return this.doResurrectFromImport(params);
|
|
11095
11909
|
}
|
|
11096
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
11910
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
|
|
11097
11911
|
const agent = this.spawner({
|
|
11098
11912
|
agentId: params.agentId,
|
|
11099
11913
|
cwd: params.cwd,
|
|
@@ -11147,9 +11961,10 @@ var SessionManager = class {
|
|
|
11147
11961
|
// this fix), fall back to the model the agent ships in its
|
|
11148
11962
|
// session/load response body.
|
|
11149
11963
|
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
11150
|
-
currentMode: params.currentMode,
|
|
11964
|
+
currentMode: params.currentMode ?? extractInitialCurrentMode(loadResult ?? {}),
|
|
11151
11965
|
currentUsage: params.currentUsage,
|
|
11152
11966
|
agentCommands: params.agentCommands,
|
|
11967
|
+
agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
|
|
11153
11968
|
// Only gate the first-prompt title heuristic when we actually have
|
|
11154
11969
|
// a title to preserve. A title-less session (lost to a write race
|
|
11155
11970
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -11192,9 +12007,10 @@ var SessionManager = class {
|
|
|
11192
12007
|
// Prefer the stored value (set by a previous current_model_update);
|
|
11193
12008
|
// fall back to whatever the agent ships in its session/new response.
|
|
11194
12009
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
11195
|
-
currentMode: params.currentMode,
|
|
12010
|
+
currentMode: params.currentMode ?? fresh.initialMode,
|
|
11196
12011
|
currentUsage: params.currentUsage,
|
|
11197
12012
|
agentCommands: params.agentCommands,
|
|
12013
|
+
agentModes: params.agentModes ?? fresh.initialModes,
|
|
11198
12014
|
firstPromptSeeded: !!params.title,
|
|
11199
12015
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
11200
12016
|
});
|
|
@@ -11204,7 +12020,7 @@ var SessionManager = class {
|
|
|
11204
12020
|
}
|
|
11205
12021
|
async resolveImportCwd(cwd) {
|
|
11206
12022
|
try {
|
|
11207
|
-
const stat4 = await
|
|
12023
|
+
const stat4 = await fs11.stat(cwd);
|
|
11208
12024
|
if (stat4.isDirectory()) {
|
|
11209
12025
|
return cwd;
|
|
11210
12026
|
}
|
|
@@ -11224,7 +12040,7 @@ var SessionManager = class {
|
|
|
11224
12040
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
11225
12041
|
throw err;
|
|
11226
12042
|
}
|
|
11227
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
12043
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
|
|
11228
12044
|
const agent = this.spawner({
|
|
11229
12045
|
agentId: params.agentId,
|
|
11230
12046
|
cwd: params.cwd,
|
|
@@ -11261,11 +12077,15 @@ var SessionManager = class {
|
|
|
11261
12077
|
} catch {
|
|
11262
12078
|
}
|
|
11263
12079
|
}
|
|
12080
|
+
const initialModes = extractInitialModes(newResult);
|
|
12081
|
+
const initialMode = extractInitialCurrentMode(newResult);
|
|
11264
12082
|
return {
|
|
11265
12083
|
agent,
|
|
11266
12084
|
upstreamSessionId: sessionIdRaw,
|
|
11267
12085
|
agentMeta: newResult._meta,
|
|
11268
|
-
initialModel
|
|
12086
|
+
initialModel,
|
|
12087
|
+
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
12088
|
+
initialMode
|
|
11269
12089
|
};
|
|
11270
12090
|
} catch (err) {
|
|
11271
12091
|
await agent.kill().catch(() => void 0);
|
|
@@ -11317,6 +12137,15 @@ var SessionManager = class {
|
|
|
11317
12137
|
}))
|
|
11318
12138
|
}).catch(() => void 0);
|
|
11319
12139
|
});
|
|
12140
|
+
session.onAgentModesChange((modes) => {
|
|
12141
|
+
void this.persistSnapshot(session.sessionId, {
|
|
12142
|
+
agentModes: modes.map((m) => ({
|
|
12143
|
+
id: m.id,
|
|
12144
|
+
...m.name !== void 0 ? { name: m.name } : {},
|
|
12145
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
12146
|
+
}))
|
|
12147
|
+
}).catch(() => void 0);
|
|
12148
|
+
});
|
|
11320
12149
|
this.sessions.set(session.sessionId, session);
|
|
11321
12150
|
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
11322
12151
|
const existing = await this.store.read(session.sessionId);
|
|
@@ -11359,6 +12188,7 @@ var SessionManager = class {
|
|
|
11359
12188
|
currentMode: record.currentMode,
|
|
11360
12189
|
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
11361
12190
|
agentCommands: record.agentCommands,
|
|
12191
|
+
agentModes: record.agentModes,
|
|
11362
12192
|
createdAt: record.createdAt
|
|
11363
12193
|
};
|
|
11364
12194
|
}
|
|
@@ -11636,6 +12466,7 @@ var SessionManager = class {
|
|
|
11636
12466
|
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
11637
12467
|
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
11638
12468
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
12469
|
+
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
11639
12470
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11640
12471
|
});
|
|
11641
12472
|
});
|
|
@@ -11669,6 +12500,53 @@ var SessionManager = class {
|
|
|
11669
12500
|
}
|
|
11670
12501
|
await Promise.allSettled(pending);
|
|
11671
12502
|
}
|
|
12503
|
+
// Startup hook: scan persisted sessions for non-empty queue files,
|
|
12504
|
+
// apply the TTL, resurrect anything with surviving entries, and
|
|
12505
|
+
// replay them through the normal queue path. Called from the daemon
|
|
12506
|
+
// boot sequence; failures per session are logged and don't block
|
|
12507
|
+
// the boot.
|
|
12508
|
+
//
|
|
12509
|
+
// Concurrency is deliberately sequential — resurrect each session
|
|
12510
|
+
// one at a time so a runaway daemon with 100 queued sessions
|
|
12511
|
+
// doesn't burst-spawn 100 agents on startup. Inside a single
|
|
12512
|
+
// session, the queue still drains in parallel-friendly fashion via
|
|
12513
|
+
// drainQueue once resurrect() completes.
|
|
12514
|
+
async resurrectPendingQueues() {
|
|
12515
|
+
const records = await this.store.list().catch(() => []);
|
|
12516
|
+
for (const rec of records) {
|
|
12517
|
+
const queue = await loadQueue(rec.sessionId).catch(() => []);
|
|
12518
|
+
if (queue.length === 0) continue;
|
|
12519
|
+
const now = Date.now();
|
|
12520
|
+
const fresh = queue.filter((e) => now - e.enqueuedAt < QUEUE_REPLAY_TTL_MS);
|
|
12521
|
+
const dropped = queue.length - fresh.length;
|
|
12522
|
+
if (dropped > 0) {
|
|
12523
|
+
this.logger?.info(
|
|
12524
|
+
`queue replay: dropping ${dropped} stale prompt(s) for ${rec.sessionId} (TTL ${QUEUE_REPLAY_TTL_MS / 1e3}s)`
|
|
12525
|
+
);
|
|
12526
|
+
await rewriteQueue(rec.sessionId, fresh).catch(() => void 0);
|
|
12527
|
+
}
|
|
12528
|
+
if (fresh.length === 0) continue;
|
|
12529
|
+
const fromDisk = await this.loadFromDisk(rec.sessionId).catch(() => void 0);
|
|
12530
|
+
if (!fromDisk) {
|
|
12531
|
+
this.logger?.warn(
|
|
12532
|
+
`queue replay: no meta for ${rec.sessionId}; discarding ${fresh.length} entr${fresh.length === 1 ? "y" : "ies"}`
|
|
12533
|
+
);
|
|
12534
|
+
await rewriteQueue(rec.sessionId, []).catch(() => void 0);
|
|
12535
|
+
continue;
|
|
12536
|
+
}
|
|
12537
|
+
try {
|
|
12538
|
+
const session = await this.resurrect(fromDisk);
|
|
12539
|
+
this.logger?.info(
|
|
12540
|
+
`queue replay: resurrected ${rec.sessionId} and replaying ${fresh.length} prompt(s)`
|
|
12541
|
+
);
|
|
12542
|
+
session.replayPersistedQueue(fresh);
|
|
12543
|
+
} catch (err) {
|
|
12544
|
+
this.logger?.warn(
|
|
12545
|
+
`queue replay: failed to resurrect ${rec.sessionId}: ${err.message}`
|
|
12546
|
+
);
|
|
12547
|
+
}
|
|
12548
|
+
}
|
|
12549
|
+
}
|
|
11672
12550
|
};
|
|
11673
12551
|
function mergeForPersistence(session, existing) {
|
|
11674
12552
|
const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
|
|
@@ -11678,6 +12556,18 @@ function mergeForPersistence(session, existing) {
|
|
|
11678
12556
|
return { name: c.name };
|
|
11679
12557
|
}) : void 0;
|
|
11680
12558
|
const agentCommands = persistedCommands ?? existing?.agentCommands;
|
|
12559
|
+
const sessionModes = session.availableModes();
|
|
12560
|
+
const persistedModes = sessionModes.length > 0 ? sessionModes.map((m) => {
|
|
12561
|
+
const out = { id: m.id };
|
|
12562
|
+
if (m.name !== void 0) {
|
|
12563
|
+
out.name = m.name;
|
|
12564
|
+
}
|
|
12565
|
+
if (m.description !== void 0) {
|
|
12566
|
+
out.description = m.description;
|
|
12567
|
+
}
|
|
12568
|
+
return out;
|
|
12569
|
+
}) : void 0;
|
|
12570
|
+
const agentModes = persistedModes ?? existing?.agentModes;
|
|
11681
12571
|
return recordFromMemorySession({
|
|
11682
12572
|
sessionId: session.sessionId,
|
|
11683
12573
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
@@ -11693,6 +12583,7 @@ function mergeForPersistence(session, existing) {
|
|
|
11693
12583
|
currentMode: session.currentMode ?? existing?.currentMode,
|
|
11694
12584
|
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
11695
12585
|
agentCommands,
|
|
12586
|
+
agentModes,
|
|
11696
12587
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
11697
12588
|
});
|
|
11698
12589
|
}
|
|
@@ -11755,9 +12646,103 @@ function asString(value) {
|
|
|
11755
12646
|
const trimmed = value.trim();
|
|
11756
12647
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
11757
12648
|
}
|
|
12649
|
+
function nonEmptyOrUndefined(arr) {
|
|
12650
|
+
return arr.length > 0 ? arr : void 0;
|
|
12651
|
+
}
|
|
12652
|
+
function extractInitialModes(result) {
|
|
12653
|
+
const direct = parseModesList(result.availableModes);
|
|
12654
|
+
if (direct.length > 0) {
|
|
12655
|
+
return direct;
|
|
12656
|
+
}
|
|
12657
|
+
const modes = result.modes;
|
|
12658
|
+
if (modes && typeof modes === "object" && !Array.isArray(modes)) {
|
|
12659
|
+
const fromModesObj = parseModesList(
|
|
12660
|
+
modes.availableModes
|
|
12661
|
+
);
|
|
12662
|
+
if (fromModesObj.length > 0) {
|
|
12663
|
+
return fromModesObj;
|
|
12664
|
+
}
|
|
12665
|
+
}
|
|
12666
|
+
const meta = result._meta;
|
|
12667
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
12668
|
+
for (const [key, value] of Object.entries(
|
|
12669
|
+
meta
|
|
12670
|
+
)) {
|
|
12671
|
+
if (key === "hydra-acp") {
|
|
12672
|
+
continue;
|
|
12673
|
+
}
|
|
12674
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
12675
|
+
const fromMeta = parseModesList(
|
|
12676
|
+
value.availableModes
|
|
12677
|
+
);
|
|
12678
|
+
if (fromMeta.length > 0) {
|
|
12679
|
+
return fromMeta;
|
|
12680
|
+
}
|
|
12681
|
+
}
|
|
12682
|
+
}
|
|
12683
|
+
}
|
|
12684
|
+
return [];
|
|
12685
|
+
}
|
|
12686
|
+
function extractInitialCurrentMode(result) {
|
|
12687
|
+
const direct = asString(result.currentModeId) ?? asString(result.currentMode) ?? asString(result.modeId) ?? asString(result.mode);
|
|
12688
|
+
if (direct) {
|
|
12689
|
+
return direct;
|
|
12690
|
+
}
|
|
12691
|
+
const modes = result.modes;
|
|
12692
|
+
if (modes && typeof modes === "object" && !Array.isArray(modes)) {
|
|
12693
|
+
const m = asString(modes.currentModeId) ?? asString(modes.currentMode);
|
|
12694
|
+
if (m) {
|
|
12695
|
+
return m;
|
|
12696
|
+
}
|
|
12697
|
+
}
|
|
12698
|
+
const meta = result._meta;
|
|
12699
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
12700
|
+
for (const [key, value] of Object.entries(
|
|
12701
|
+
meta
|
|
12702
|
+
)) {
|
|
12703
|
+
if (key === "hydra-acp") {
|
|
12704
|
+
continue;
|
|
12705
|
+
}
|
|
12706
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
12707
|
+
const m = asString(value.currentModeId) ?? asString(value.currentMode) ?? asString(value.modeId);
|
|
12708
|
+
if (m) {
|
|
12709
|
+
return m;
|
|
12710
|
+
}
|
|
12711
|
+
}
|
|
12712
|
+
}
|
|
12713
|
+
}
|
|
12714
|
+
return void 0;
|
|
12715
|
+
}
|
|
12716
|
+
function parseModesList(list) {
|
|
12717
|
+
if (!Array.isArray(list)) {
|
|
12718
|
+
return [];
|
|
12719
|
+
}
|
|
12720
|
+
const out = [];
|
|
12721
|
+
for (const raw of list) {
|
|
12722
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12723
|
+
continue;
|
|
12724
|
+
}
|
|
12725
|
+
const r = raw;
|
|
12726
|
+
const id = asString(r.id) ?? asString(r.modeId);
|
|
12727
|
+
if (!id) {
|
|
12728
|
+
continue;
|
|
12729
|
+
}
|
|
12730
|
+
const mode = { id };
|
|
12731
|
+
const name = asString(r.name);
|
|
12732
|
+
if (name) {
|
|
12733
|
+
mode.name = name;
|
|
12734
|
+
}
|
|
12735
|
+
const description = asString(r.description);
|
|
12736
|
+
if (description) {
|
|
12737
|
+
mode.description = description;
|
|
12738
|
+
}
|
|
12739
|
+
out.push(mode);
|
|
12740
|
+
}
|
|
12741
|
+
return out;
|
|
12742
|
+
}
|
|
11758
12743
|
async function loadPromptHistorySafely(sessionId) {
|
|
11759
12744
|
try {
|
|
11760
|
-
const raw = await
|
|
12745
|
+
const raw = await fs11.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
11761
12746
|
const out = [];
|
|
11762
12747
|
for (const line of raw.split("\n")) {
|
|
11763
12748
|
if (line.length === 0) {
|
|
@@ -11778,7 +12763,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
11778
12763
|
}
|
|
11779
12764
|
async function historyMtimeIso(sessionId) {
|
|
11780
12765
|
try {
|
|
11781
|
-
const st = await
|
|
12766
|
+
const st = await fs11.stat(paths.historyFile(sessionId));
|
|
11782
12767
|
return new Date(st.mtimeMs).toISOString();
|
|
11783
12768
|
} catch {
|
|
11784
12769
|
return void 0;
|
|
@@ -11788,7 +12773,7 @@ async function historyMtimeIso(sessionId) {
|
|
|
11788
12773
|
// src/core/extensions.ts
|
|
11789
12774
|
init_paths();
|
|
11790
12775
|
import { spawn as spawn4 } from "child_process";
|
|
11791
|
-
import * as
|
|
12776
|
+
import * as fs12 from "fs";
|
|
11792
12777
|
import * as fsp3 from "fs/promises";
|
|
11793
12778
|
import * as path7 from "path";
|
|
11794
12779
|
var RESTART_BASE_MS = 1e3;
|
|
@@ -12071,7 +13056,7 @@ var ExtensionManager = class {
|
|
|
12071
13056
|
}
|
|
12072
13057
|
const ext = entry.config;
|
|
12073
13058
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
12074
|
-
const logStream =
|
|
13059
|
+
const logStream = fs12.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
12075
13060
|
flags: "a"
|
|
12076
13061
|
});
|
|
12077
13062
|
logStream.write(
|
|
@@ -12083,7 +13068,7 @@ var ExtensionManager = class {
|
|
|
12083
13068
|
HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
|
|
12084
13069
|
HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
|
|
12085
13070
|
HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
|
|
12086
|
-
HYDRA_ACP_TOKEN: ctx.
|
|
13071
|
+
HYDRA_ACP_TOKEN: ctx.serviceToken,
|
|
12087
13072
|
HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
|
|
12088
13073
|
HYDRA_ACP_HOME: ctx.hydraHome,
|
|
12089
13074
|
HYDRA_ACP_EXTENSION_NAME: ext.name,
|
|
@@ -12121,7 +13106,7 @@ var ExtensionManager = class {
|
|
|
12121
13106
|
}
|
|
12122
13107
|
if (typeof child.pid === "number") {
|
|
12123
13108
|
try {
|
|
12124
|
-
|
|
13109
|
+
fs12.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
12125
13110
|
`, {
|
|
12126
13111
|
encoding: "utf8",
|
|
12127
13112
|
mode: 384
|
|
@@ -12146,7 +13131,7 @@ var ExtensionManager = class {
|
|
|
12146
13131
|
});
|
|
12147
13132
|
child.on("exit", (code, signal) => {
|
|
12148
13133
|
try {
|
|
12149
|
-
|
|
13134
|
+
fs12.unlinkSync(paths.extensionPidFile(ext.name));
|
|
12150
13135
|
} catch {
|
|
12151
13136
|
}
|
|
12152
13137
|
logStream.write(
|
|
@@ -12189,25 +13174,245 @@ var ExtensionManager = class {
|
|
|
12189
13174
|
}
|
|
12190
13175
|
}
|
|
12191
13176
|
};
|
|
12192
|
-
function isAlive(pid) {
|
|
12193
|
-
try {
|
|
12194
|
-
process.kill(pid, 0);
|
|
12195
|
-
return true;
|
|
12196
|
-
} catch {
|
|
13177
|
+
function isAlive(pid) {
|
|
13178
|
+
try {
|
|
13179
|
+
process.kill(pid, 0);
|
|
13180
|
+
return true;
|
|
13181
|
+
} catch {
|
|
13182
|
+
return false;
|
|
13183
|
+
}
|
|
13184
|
+
}
|
|
13185
|
+
function withCode2(err, code) {
|
|
13186
|
+
err.code = code;
|
|
13187
|
+
return err;
|
|
13188
|
+
}
|
|
13189
|
+
|
|
13190
|
+
// src/daemon/server.ts
|
|
13191
|
+
init_paths();
|
|
13192
|
+
init_hydra_version();
|
|
13193
|
+
|
|
13194
|
+
// src/core/session-tokens.ts
|
|
13195
|
+
init_paths();
|
|
13196
|
+
import * as fs13 from "fs/promises";
|
|
13197
|
+
import * as path8 from "path";
|
|
13198
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
13199
|
+
var TOKEN_PREFIX = "hydra_session_";
|
|
13200
|
+
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
13201
|
+
var ID_LENGTH = 12;
|
|
13202
|
+
var TOKEN_BYTES = 32;
|
|
13203
|
+
var WRITE_DEBOUNCE_MS = 50;
|
|
13204
|
+
function tokensFilePath() {
|
|
13205
|
+
return path8.join(paths.home(), "session-tokens.json");
|
|
13206
|
+
}
|
|
13207
|
+
function sha256Hex(input) {
|
|
13208
|
+
return createHash("sha256").update(input).digest("hex");
|
|
13209
|
+
}
|
|
13210
|
+
function randomHex(bytes) {
|
|
13211
|
+
return randomBytes(bytes).toString("hex");
|
|
13212
|
+
}
|
|
13213
|
+
function generateId() {
|
|
13214
|
+
return randomHex(ID_LENGTH).slice(0, ID_LENGTH * 2);
|
|
13215
|
+
}
|
|
13216
|
+
function generateToken() {
|
|
13217
|
+
return `${TOKEN_PREFIX}${randomHex(TOKEN_BYTES)}`;
|
|
13218
|
+
}
|
|
13219
|
+
var SessionTokenStore = class _SessionTokenStore {
|
|
13220
|
+
records = /* @__PURE__ */ new Map();
|
|
13221
|
+
// keyed by hash
|
|
13222
|
+
writeTimer = null;
|
|
13223
|
+
writeInflight = null;
|
|
13224
|
+
constructor(records) {
|
|
13225
|
+
for (const r of records) {
|
|
13226
|
+
this.records.set(r.hash, r);
|
|
13227
|
+
}
|
|
13228
|
+
}
|
|
13229
|
+
static async load() {
|
|
13230
|
+
let records = [];
|
|
13231
|
+
try {
|
|
13232
|
+
const raw = await fs13.readFile(tokensFilePath(), "utf8");
|
|
13233
|
+
const parsed = JSON.parse(raw);
|
|
13234
|
+
if (parsed && Array.isArray(parsed.records)) {
|
|
13235
|
+
records = parsed.records.filter(isRecord);
|
|
13236
|
+
}
|
|
13237
|
+
} catch (err) {
|
|
13238
|
+
const e = err;
|
|
13239
|
+
if (e.code !== "ENOENT") {
|
|
13240
|
+
throw err;
|
|
13241
|
+
}
|
|
13242
|
+
}
|
|
13243
|
+
const store = new _SessionTokenStore(records);
|
|
13244
|
+
const removed = store.sweepExpired(/* @__PURE__ */ new Date());
|
|
13245
|
+
if (removed > 0) {
|
|
13246
|
+
await store.flush();
|
|
13247
|
+
}
|
|
13248
|
+
return store;
|
|
13249
|
+
}
|
|
13250
|
+
async issue(opts = {}) {
|
|
13251
|
+
const token = generateToken();
|
|
13252
|
+
const hash = sha256Hex(token);
|
|
13253
|
+
const id = generateId();
|
|
13254
|
+
const now = /* @__PURE__ */ new Date();
|
|
13255
|
+
const ttlSec = opts.ttlSec && opts.ttlSec > 0 ? opts.ttlSec : DEFAULT_TTL_SEC;
|
|
13256
|
+
const expiresAt = new Date(now.getTime() + ttlSec * 1e3);
|
|
13257
|
+
const record = {
|
|
13258
|
+
id,
|
|
13259
|
+
hash,
|
|
13260
|
+
label: opts.label,
|
|
13261
|
+
createdAt: now.toISOString(),
|
|
13262
|
+
expiresAt: expiresAt.toISOString(),
|
|
13263
|
+
lastUsedAt: now.toISOString()
|
|
13264
|
+
};
|
|
13265
|
+
this.records.set(hash, record);
|
|
13266
|
+
this.scheduleWrite();
|
|
13267
|
+
return { id, token, expiresAt: record.expiresAt };
|
|
13268
|
+
}
|
|
13269
|
+
// Verifies a presented token. Returns the matching record id (so the
|
|
13270
|
+
// caller can revoke it on logout) and bumps lastUsedAt; returns
|
|
13271
|
+
// undefined when no record matches or when the matched record has
|
|
13272
|
+
// expired.
|
|
13273
|
+
async verify(token) {
|
|
13274
|
+
if (typeof token !== "string" || !token.startsWith(TOKEN_PREFIX)) {
|
|
13275
|
+
return void 0;
|
|
13276
|
+
}
|
|
13277
|
+
const hash = sha256Hex(token);
|
|
13278
|
+
const record = this.records.get(hash);
|
|
13279
|
+
if (!record) {
|
|
13280
|
+
return void 0;
|
|
13281
|
+
}
|
|
13282
|
+
const expected = Buffer.from(record.hash, "hex");
|
|
13283
|
+
const actual = Buffer.from(hash, "hex");
|
|
13284
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
13285
|
+
return void 0;
|
|
13286
|
+
}
|
|
13287
|
+
const now = /* @__PURE__ */ new Date();
|
|
13288
|
+
if (new Date(record.expiresAt).getTime() <= now.getTime()) {
|
|
13289
|
+
this.records.delete(hash);
|
|
13290
|
+
this.scheduleWrite();
|
|
13291
|
+
return void 0;
|
|
13292
|
+
}
|
|
13293
|
+
record.lastUsedAt = now.toISOString();
|
|
13294
|
+
this.scheduleWrite();
|
|
13295
|
+
return record.id;
|
|
13296
|
+
}
|
|
13297
|
+
async revoke(id) {
|
|
13298
|
+
for (const [hash, r] of this.records) {
|
|
13299
|
+
if (r.id === id) {
|
|
13300
|
+
this.records.delete(hash);
|
|
13301
|
+
this.scheduleWrite();
|
|
13302
|
+
return true;
|
|
13303
|
+
}
|
|
13304
|
+
}
|
|
13305
|
+
return false;
|
|
13306
|
+
}
|
|
13307
|
+
async revokeAll() {
|
|
13308
|
+
const n = this.records.size;
|
|
13309
|
+
this.records.clear();
|
|
13310
|
+
this.scheduleWrite();
|
|
13311
|
+
return n;
|
|
13312
|
+
}
|
|
13313
|
+
list() {
|
|
13314
|
+
return Array.from(this.records.values()).map(({ id, label, createdAt, expiresAt, lastUsedAt }) => ({
|
|
13315
|
+
id,
|
|
13316
|
+
label,
|
|
13317
|
+
createdAt,
|
|
13318
|
+
expiresAt,
|
|
13319
|
+
lastUsedAt
|
|
13320
|
+
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
13321
|
+
}
|
|
13322
|
+
sweepExpired(now = /* @__PURE__ */ new Date()) {
|
|
13323
|
+
let removed = 0;
|
|
13324
|
+
for (const [hash, r] of this.records) {
|
|
13325
|
+
if (new Date(r.expiresAt).getTime() <= now.getTime()) {
|
|
13326
|
+
this.records.delete(hash);
|
|
13327
|
+
removed += 1;
|
|
13328
|
+
}
|
|
13329
|
+
}
|
|
13330
|
+
if (removed > 0) {
|
|
13331
|
+
this.scheduleWrite();
|
|
13332
|
+
}
|
|
13333
|
+
return removed;
|
|
13334
|
+
}
|
|
13335
|
+
// Force any pending write to complete. Useful in tests and at shutdown.
|
|
13336
|
+
async flush() {
|
|
13337
|
+
if (this.writeTimer) {
|
|
13338
|
+
clearTimeout(this.writeTimer);
|
|
13339
|
+
this.writeTimer = null;
|
|
13340
|
+
}
|
|
13341
|
+
await this.persist();
|
|
13342
|
+
}
|
|
13343
|
+
scheduleWrite() {
|
|
13344
|
+
if (this.writeTimer) {
|
|
13345
|
+
return;
|
|
13346
|
+
}
|
|
13347
|
+
this.writeTimer = setTimeout(() => {
|
|
13348
|
+
this.writeTimer = null;
|
|
13349
|
+
this.persist().catch(() => {
|
|
13350
|
+
});
|
|
13351
|
+
}, WRITE_DEBOUNCE_MS);
|
|
13352
|
+
}
|
|
13353
|
+
async persist() {
|
|
13354
|
+
if (this.writeInflight) {
|
|
13355
|
+
await this.writeInflight;
|
|
13356
|
+
}
|
|
13357
|
+
const records = Array.from(this.records.values());
|
|
13358
|
+
const payload = JSON.stringify({ records }, null, 2) + "\n";
|
|
13359
|
+
this.writeInflight = (async () => {
|
|
13360
|
+
await fs13.mkdir(paths.home(), { recursive: true });
|
|
13361
|
+
await fs13.writeFile(tokensFilePath(), payload, {
|
|
13362
|
+
encoding: "utf8",
|
|
13363
|
+
mode: 384
|
|
13364
|
+
});
|
|
13365
|
+
})();
|
|
13366
|
+
try {
|
|
13367
|
+
await this.writeInflight;
|
|
13368
|
+
} finally {
|
|
13369
|
+
this.writeInflight = null;
|
|
13370
|
+
}
|
|
13371
|
+
}
|
|
13372
|
+
};
|
|
13373
|
+
function isRecord(value) {
|
|
13374
|
+
if (!value || typeof value !== "object") {
|
|
12197
13375
|
return false;
|
|
12198
13376
|
}
|
|
13377
|
+
const v = value;
|
|
13378
|
+
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
13379
|
}
|
|
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
13380
|
|
|
12209
13381
|
// src/daemon/auth.ts
|
|
12210
13382
|
var BEARER_PREFIX = "Bearer ";
|
|
13383
|
+
var StaticTokenValidator = class {
|
|
13384
|
+
constructor(token) {
|
|
13385
|
+
this.token = token;
|
|
13386
|
+
}
|
|
13387
|
+
token;
|
|
13388
|
+
async validate(token) {
|
|
13389
|
+
return constantTimeEqual(token, this.token) ? "service" : void 0;
|
|
13390
|
+
}
|
|
13391
|
+
};
|
|
13392
|
+
var SessionTokenValidator = class {
|
|
13393
|
+
constructor(store) {
|
|
13394
|
+
this.store = store;
|
|
13395
|
+
}
|
|
13396
|
+
store;
|
|
13397
|
+
async validate(token) {
|
|
13398
|
+
return this.store.verify(token);
|
|
13399
|
+
}
|
|
13400
|
+
};
|
|
13401
|
+
var CompositeTokenValidator = class {
|
|
13402
|
+
constructor(validators) {
|
|
13403
|
+
this.validators = validators;
|
|
13404
|
+
}
|
|
13405
|
+
validators;
|
|
13406
|
+
async validate(token) {
|
|
13407
|
+
for (const v of this.validators) {
|
|
13408
|
+
const id = await v.validate(token);
|
|
13409
|
+
if (id !== void 0) {
|
|
13410
|
+
return id;
|
|
13411
|
+
}
|
|
13412
|
+
}
|
|
13413
|
+
return void 0;
|
|
13414
|
+
}
|
|
13415
|
+
};
|
|
12211
13416
|
function bearerAuth(opts) {
|
|
12212
13417
|
return async function authMiddleware(request, reply) {
|
|
12213
13418
|
const header = request.headers.authorization;
|
|
@@ -12216,10 +13421,12 @@ function bearerAuth(opts) {
|
|
|
12216
13421
|
return;
|
|
12217
13422
|
}
|
|
12218
13423
|
const token = header.slice(BEARER_PREFIX.length).trim();
|
|
12219
|
-
|
|
13424
|
+
const identity = await opts.validator.validate(token);
|
|
13425
|
+
if (!identity) {
|
|
12220
13426
|
reply.code(403).send({ error: "Invalid token" });
|
|
12221
13427
|
return;
|
|
12222
13428
|
}
|
|
13429
|
+
request.authIdentity = identity;
|
|
12223
13430
|
};
|
|
12224
13431
|
}
|
|
12225
13432
|
function tokenFromUpgradeRequest(req) {
|
|
@@ -12258,6 +13465,40 @@ function constantTimeEqual(a, b) {
|
|
|
12258
13465
|
return mismatch === 0;
|
|
12259
13466
|
}
|
|
12260
13467
|
|
|
13468
|
+
// src/daemon/rate-limit.ts
|
|
13469
|
+
var AuthRateLimiter = class {
|
|
13470
|
+
entries = /* @__PURE__ */ new Map();
|
|
13471
|
+
maxFails;
|
|
13472
|
+
windowMs;
|
|
13473
|
+
constructor(maxFails = 10, windowMs = 15 * 60 * 1e3) {
|
|
13474
|
+
this.maxFails = maxFails;
|
|
13475
|
+
this.windowMs = windowMs;
|
|
13476
|
+
}
|
|
13477
|
+
isBlocked(ip) {
|
|
13478
|
+
const e = this.entries.get(ip);
|
|
13479
|
+
if (!e) {
|
|
13480
|
+
return false;
|
|
13481
|
+
}
|
|
13482
|
+
if (Date.now() - e.windowStart > this.windowMs) {
|
|
13483
|
+
this.entries.delete(ip);
|
|
13484
|
+
return false;
|
|
13485
|
+
}
|
|
13486
|
+
return e.fails >= this.maxFails;
|
|
13487
|
+
}
|
|
13488
|
+
recordFailure(ip) {
|
|
13489
|
+
const now = Date.now();
|
|
13490
|
+
const e = this.entries.get(ip);
|
|
13491
|
+
if (!e || now - e.windowStart > this.windowMs) {
|
|
13492
|
+
this.entries.set(ip, { fails: 1, windowStart: now });
|
|
13493
|
+
return;
|
|
13494
|
+
}
|
|
13495
|
+
e.fails += 1;
|
|
13496
|
+
}
|
|
13497
|
+
recordSuccess(ip) {
|
|
13498
|
+
this.entries.delete(ip);
|
|
13499
|
+
}
|
|
13500
|
+
};
|
|
13501
|
+
|
|
12261
13502
|
// src/daemon/routes/sessions.ts
|
|
12262
13503
|
init_config();
|
|
12263
13504
|
init_bundle();
|
|
@@ -12625,6 +13866,181 @@ function registerConfigRoutes(app, defaults) {
|
|
|
12625
13866
|
});
|
|
12626
13867
|
}
|
|
12627
13868
|
|
|
13869
|
+
// src/daemon/routes/auth.ts
|
|
13870
|
+
import { z as z6 } from "zod";
|
|
13871
|
+
|
|
13872
|
+
// src/core/password.ts
|
|
13873
|
+
init_paths();
|
|
13874
|
+
import * as fs14 from "fs/promises";
|
|
13875
|
+
import * as path9 from "path";
|
|
13876
|
+
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
13877
|
+
import { promisify } from "util";
|
|
13878
|
+
var scryptAsync = promisify(scrypt);
|
|
13879
|
+
function passwordHashPath() {
|
|
13880
|
+
return path9.join(paths.home(), "password-hash");
|
|
13881
|
+
}
|
|
13882
|
+
var DEFAULT_N = 1 << 15;
|
|
13883
|
+
var DEFAULT_R = 8;
|
|
13884
|
+
var DEFAULT_P = 1;
|
|
13885
|
+
var KEY_LEN = 64;
|
|
13886
|
+
var SALT_LEN = 16;
|
|
13887
|
+
var MAX_MEM = 128 * 1024 * 1024;
|
|
13888
|
+
async function setPassword(plaintext) {
|
|
13889
|
+
if (typeof plaintext !== "string" || plaintext.length === 0) {
|
|
13890
|
+
throw new Error("password must be a non-empty string");
|
|
13891
|
+
}
|
|
13892
|
+
const salt = randomBytes2(SALT_LEN);
|
|
13893
|
+
const key = await scryptAsync(plaintext, salt, KEY_LEN, {
|
|
13894
|
+
N: DEFAULT_N,
|
|
13895
|
+
r: DEFAULT_R,
|
|
13896
|
+
p: DEFAULT_P,
|
|
13897
|
+
maxmem: MAX_MEM
|
|
13898
|
+
});
|
|
13899
|
+
const encoded = `scrypt$${DEFAULT_N}$${DEFAULT_R}$${DEFAULT_P}$${salt.toString("hex")}$${key.toString("hex")}
|
|
13900
|
+
`;
|
|
13901
|
+
await fs14.mkdir(paths.home(), { recursive: true });
|
|
13902
|
+
await fs14.writeFile(passwordHashPath(), encoded, {
|
|
13903
|
+
encoding: "utf8",
|
|
13904
|
+
mode: 384
|
|
13905
|
+
});
|
|
13906
|
+
}
|
|
13907
|
+
async function hasPassword() {
|
|
13908
|
+
try {
|
|
13909
|
+
const text = await fs14.readFile(passwordHashPath(), "utf8");
|
|
13910
|
+
return text.trim().length > 0;
|
|
13911
|
+
} catch (err) {
|
|
13912
|
+
const e = err;
|
|
13913
|
+
if (e.code === "ENOENT") {
|
|
13914
|
+
return false;
|
|
13915
|
+
}
|
|
13916
|
+
throw err;
|
|
13917
|
+
}
|
|
13918
|
+
}
|
|
13919
|
+
async function verifyPassword(plaintext) {
|
|
13920
|
+
if (typeof plaintext !== "string" || plaintext.length === 0) {
|
|
13921
|
+
return false;
|
|
13922
|
+
}
|
|
13923
|
+
let line;
|
|
13924
|
+
try {
|
|
13925
|
+
line = (await fs14.readFile(passwordHashPath(), "utf8")).trim();
|
|
13926
|
+
} catch (err) {
|
|
13927
|
+
const e = err;
|
|
13928
|
+
if (e.code === "ENOENT") {
|
|
13929
|
+
return false;
|
|
13930
|
+
}
|
|
13931
|
+
throw err;
|
|
13932
|
+
}
|
|
13933
|
+
const parts = line.split("$");
|
|
13934
|
+
if (parts.length !== 6 || parts[0] !== "scrypt") {
|
|
13935
|
+
return false;
|
|
13936
|
+
}
|
|
13937
|
+
const N = parseInt(parts[1], 10);
|
|
13938
|
+
const r = parseInt(parts[2], 10);
|
|
13939
|
+
const p = parseInt(parts[3], 10);
|
|
13940
|
+
if (!Number.isFinite(N) || !Number.isFinite(r) || !Number.isFinite(p)) {
|
|
13941
|
+
return false;
|
|
13942
|
+
}
|
|
13943
|
+
const salt = Buffer.from(parts[4], "hex");
|
|
13944
|
+
const expected = Buffer.from(parts[5], "hex");
|
|
13945
|
+
if (salt.length === 0 || expected.length === 0) {
|
|
13946
|
+
return false;
|
|
13947
|
+
}
|
|
13948
|
+
const actual = await scryptAsync(plaintext, salt, expected.length, {
|
|
13949
|
+
N,
|
|
13950
|
+
r,
|
|
13951
|
+
p,
|
|
13952
|
+
maxmem: MAX_MEM
|
|
13953
|
+
});
|
|
13954
|
+
if (actual.length !== expected.length) {
|
|
13955
|
+
return false;
|
|
13956
|
+
}
|
|
13957
|
+
return timingSafeEqual2(actual, expected);
|
|
13958
|
+
}
|
|
13959
|
+
|
|
13960
|
+
// src/daemon/routes/auth.ts
|
|
13961
|
+
var LoginBody = z6.object({
|
|
13962
|
+
password: z6.string().min(1),
|
|
13963
|
+
label: z6.string().min(1).max(256).optional(),
|
|
13964
|
+
ttlSec: z6.number().int().positive().optional()
|
|
13965
|
+
});
|
|
13966
|
+
var LogoutBody = z6.object({
|
|
13967
|
+
id: z6.string().optional()
|
|
13968
|
+
}).optional();
|
|
13969
|
+
function registerAuthRoutes(app, deps) {
|
|
13970
|
+
app.post(
|
|
13971
|
+
"/v1/auth/login",
|
|
13972
|
+
{ config: { skipAuth: true } },
|
|
13973
|
+
async (request, reply) => {
|
|
13974
|
+
const ip = remoteIp(request);
|
|
13975
|
+
if (deps.rateLimiter.isBlocked(ip)) {
|
|
13976
|
+
return reply.code(429).send({
|
|
13977
|
+
error: "Too many failed attempts; try again later."
|
|
13978
|
+
});
|
|
13979
|
+
}
|
|
13980
|
+
let body;
|
|
13981
|
+
try {
|
|
13982
|
+
body = LoginBody.parse(request.body);
|
|
13983
|
+
} catch {
|
|
13984
|
+
return reply.code(400).send({ error: "Invalid request body" });
|
|
13985
|
+
}
|
|
13986
|
+
if (!await hasPassword()) {
|
|
13987
|
+
return reply.code(403).send({
|
|
13988
|
+
error: "No password configured. Run `hydra-acp auth password` on the daemon host."
|
|
13989
|
+
});
|
|
13990
|
+
}
|
|
13991
|
+
const ok = await verifyPassword(body.password);
|
|
13992
|
+
if (!ok) {
|
|
13993
|
+
deps.rateLimiter.recordFailure(ip);
|
|
13994
|
+
return reply.code(401).send({ error: "Invalid password" });
|
|
13995
|
+
}
|
|
13996
|
+
deps.rateLimiter.recordSuccess(ip);
|
|
13997
|
+
const issued = await deps.store.issue({
|
|
13998
|
+
label: body.label,
|
|
13999
|
+
ttlSec: body.ttlSec
|
|
14000
|
+
});
|
|
14001
|
+
return reply.code(200).send({
|
|
14002
|
+
session_token: issued.token,
|
|
14003
|
+
id: issued.id,
|
|
14004
|
+
expires_at: issued.expiresAt
|
|
14005
|
+
});
|
|
14006
|
+
}
|
|
14007
|
+
);
|
|
14008
|
+
app.post("/v1/auth/logout", async (request, reply) => {
|
|
14009
|
+
let body = void 0;
|
|
14010
|
+
try {
|
|
14011
|
+
body = LogoutBody.parse(request.body ?? void 0);
|
|
14012
|
+
} catch {
|
|
14013
|
+
return reply.code(400).send({ error: "Invalid request body" });
|
|
14014
|
+
}
|
|
14015
|
+
const id = body?.id ?? request.authIdentity;
|
|
14016
|
+
if (!id || id === "service") {
|
|
14017
|
+
return reply.code(200).send({ revoked: false });
|
|
14018
|
+
}
|
|
14019
|
+
const revoked = await deps.store.revoke(id);
|
|
14020
|
+
return reply.code(200).send({ revoked });
|
|
14021
|
+
});
|
|
14022
|
+
app.get("/v1/auth/verify", async (_request, reply) => {
|
|
14023
|
+
return reply.code(200).send({ ok: true });
|
|
14024
|
+
});
|
|
14025
|
+
app.get("/v1/auth/sessions", async (_request, reply) => {
|
|
14026
|
+
return reply.code(200).send({ sessions: deps.store.list() });
|
|
14027
|
+
});
|
|
14028
|
+
app.delete(
|
|
14029
|
+
"/v1/auth/sessions/:id",
|
|
14030
|
+
async (request, reply) => {
|
|
14031
|
+
const id = request.params.id;
|
|
14032
|
+
const revoked = await deps.store.revoke(id);
|
|
14033
|
+
if (!revoked) {
|
|
14034
|
+
return reply.code(404).send({ error: "Not found" });
|
|
14035
|
+
}
|
|
14036
|
+
return reply.code(204).send();
|
|
14037
|
+
}
|
|
14038
|
+
);
|
|
14039
|
+
}
|
|
14040
|
+
function remoteIp(request) {
|
|
14041
|
+
return request.ip || "unknown";
|
|
14042
|
+
}
|
|
14043
|
+
|
|
12628
14044
|
// src/daemon/acp-ws.ts
|
|
12629
14045
|
init_connection();
|
|
12630
14046
|
init_ws_stream();
|
|
@@ -12632,12 +14048,12 @@ init_types();
|
|
|
12632
14048
|
import { nanoid as nanoid2 } from "nanoid";
|
|
12633
14049
|
init_hydra_version();
|
|
12634
14050
|
function registerAcpWsEndpoint(app, deps) {
|
|
12635
|
-
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
14051
|
+
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
12636
14052
|
const token = tokenFromUpgradeRequest({
|
|
12637
14053
|
headers: request.headers,
|
|
12638
14054
|
url: request.url
|
|
12639
14055
|
});
|
|
12640
|
-
if (!token || !
|
|
14056
|
+
if (!token || !await deps.validator.validate(token)) {
|
|
12641
14057
|
socket.close(4401, "Unauthorized");
|
|
12642
14058
|
return;
|
|
12643
14059
|
}
|
|
@@ -12688,8 +14104,15 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12688
14104
|
}
|
|
12689
14105
|
})();
|
|
12690
14106
|
});
|
|
14107
|
+
const modesPayload = buildModesPayload(session);
|
|
12691
14108
|
return {
|
|
12692
14109
|
sessionId: session.sessionId,
|
|
14110
|
+
// session/new is implicitly an attach; mirror session/attach's
|
|
14111
|
+
// shape by including the clientId so deferred-echo clients
|
|
14112
|
+
// (TUI's queue work) can recognize their own prompt_queue_added
|
|
14113
|
+
// events without an extra round-trip.
|
|
14114
|
+
clientId: client.clientId,
|
|
14115
|
+
...modesPayload ? { modes: modesPayload } : {},
|
|
12693
14116
|
_meta: buildResponseMeta(session)
|
|
12694
14117
|
};
|
|
12695
14118
|
});
|
|
@@ -12750,6 +14173,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12750
14173
|
await connection.notify(note.method, note.params);
|
|
12751
14174
|
}
|
|
12752
14175
|
session.replayPendingPermissions(client);
|
|
14176
|
+
const modesPayload = buildModesPayload(session);
|
|
12753
14177
|
return {
|
|
12754
14178
|
sessionId: session.sessionId,
|
|
12755
14179
|
clientId: client.clientId,
|
|
@@ -12760,6 +14184,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12760
14184
|
// ran, not what was asked for.
|
|
12761
14185
|
historyPolicy: appliedPolicy,
|
|
12762
14186
|
replayed: replay.length,
|
|
14187
|
+
...modesPayload ? { modes: modesPayload } : {},
|
|
12763
14188
|
_meta: buildResponseMeta(session)
|
|
12764
14189
|
};
|
|
12765
14190
|
});
|
|
@@ -12793,7 +14218,29 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12793
14218
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12794
14219
|
throw err;
|
|
12795
14220
|
}
|
|
12796
|
-
|
|
14221
|
+
let session = deps.manager.get(params.sessionId);
|
|
14222
|
+
if (!session) {
|
|
14223
|
+
const fromDisk = await deps.manager.loadFromDisk(params.sessionId);
|
|
14224
|
+
if (!fromDisk) {
|
|
14225
|
+
const err = new Error(
|
|
14226
|
+
`session ${params.sessionId} not found`
|
|
14227
|
+
);
|
|
14228
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14229
|
+
throw err;
|
|
14230
|
+
}
|
|
14231
|
+
app.log.info(
|
|
14232
|
+
`session/prompt auto-resurrecting cold sessionId=${params.sessionId}`
|
|
14233
|
+
);
|
|
14234
|
+
session = await deps.manager.resurrect(fromDisk);
|
|
14235
|
+
const client = bindClientToSession(
|
|
14236
|
+
connection,
|
|
14237
|
+
session,
|
|
14238
|
+
state,
|
|
14239
|
+
void 0,
|
|
14240
|
+
att.clientId
|
|
14241
|
+
);
|
|
14242
|
+
await session.attach(client, "none");
|
|
14243
|
+
}
|
|
12797
14244
|
return session.prompt(att.clientId, params);
|
|
12798
14245
|
});
|
|
12799
14246
|
const handleCancelParams = (raw) => {
|
|
@@ -12825,6 +14272,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12825
14272
|
handleCancelParams(raw);
|
|
12826
14273
|
return null;
|
|
12827
14274
|
});
|
|
14275
|
+
connection.onRequest("hydra-acp/cancel_prompt", async (raw) => {
|
|
14276
|
+
const params = CancelPromptParams.parse(raw);
|
|
14277
|
+
const session = deps.manager.get(params.sessionId);
|
|
14278
|
+
if (!session) {
|
|
14279
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
14280
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14281
|
+
throw err;
|
|
14282
|
+
}
|
|
14283
|
+
return session.cancelQueuedPrompt(params.messageId);
|
|
14284
|
+
});
|
|
14285
|
+
connection.onRequest("hydra-acp/update_prompt", async (raw) => {
|
|
14286
|
+
const params = UpdatePromptParams.parse(raw);
|
|
14287
|
+
const session = deps.manager.get(params.sessionId);
|
|
14288
|
+
if (!session) {
|
|
14289
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
14290
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14291
|
+
throw err;
|
|
14292
|
+
}
|
|
14293
|
+
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
14294
|
+
});
|
|
12828
14295
|
connection.onRequest("session/load", async (raw) => {
|
|
12829
14296
|
const rawObj = raw ?? {};
|
|
12830
14297
|
const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
|
|
@@ -12856,8 +14323,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12856
14323
|
await connection.notify(note.method, note.params);
|
|
12857
14324
|
}
|
|
12858
14325
|
session.replayPendingPermissions(client);
|
|
14326
|
+
const modesPayload = buildModesPayload(session);
|
|
12859
14327
|
return {
|
|
12860
14328
|
sessionId: session.sessionId,
|
|
14329
|
+
// Same as session/new: include clientId so the deferred-echo
|
|
14330
|
+
// path in queue-aware clients can recognize own broadcasts.
|
|
14331
|
+
clientId: client.clientId,
|
|
14332
|
+
...modesPayload ? { modes: modesPayload } : {},
|
|
12861
14333
|
_meta: buildResponseMeta(session)
|
|
12862
14334
|
};
|
|
12863
14335
|
});
|
|
@@ -12883,6 +14355,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12883
14355
|
});
|
|
12884
14356
|
});
|
|
12885
14357
|
}
|
|
14358
|
+
function buildModesPayload(session) {
|
|
14359
|
+
const modes = session.availableModes();
|
|
14360
|
+
if (modes.length === 0) {
|
|
14361
|
+
return void 0;
|
|
14362
|
+
}
|
|
14363
|
+
const availableModes = modes.map((m) => {
|
|
14364
|
+
const out = {
|
|
14365
|
+
id: m.id,
|
|
14366
|
+
// ACP spec requires `name` — fall back to id when the agent didn't
|
|
14367
|
+
// supply one so we never emit an invalid SessionMode.
|
|
14368
|
+
name: m.name ?? m.id
|
|
14369
|
+
};
|
|
14370
|
+
if (m.description !== void 0) {
|
|
14371
|
+
out.description = m.description;
|
|
14372
|
+
}
|
|
14373
|
+
return out;
|
|
14374
|
+
});
|
|
14375
|
+
const currentModeId = session.currentMode ?? modes[0].id;
|
|
14376
|
+
return { currentModeId, availableModes };
|
|
14377
|
+
}
|
|
12886
14378
|
function buildResponseMeta(session) {
|
|
12887
14379
|
const ours = {
|
|
12888
14380
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -12908,9 +14400,17 @@ function buildResponseMeta(session) {
|
|
|
12908
14400
|
if (commands.length > 0) {
|
|
12909
14401
|
ours.availableCommands = commands;
|
|
12910
14402
|
}
|
|
14403
|
+
const modes = session.availableModes();
|
|
14404
|
+
if (modes.length > 0) {
|
|
14405
|
+
ours.availableModes = modes;
|
|
14406
|
+
}
|
|
12911
14407
|
if (session.turnStartedAt !== void 0) {
|
|
12912
14408
|
ours.turnStartedAt = session.turnStartedAt;
|
|
12913
14409
|
}
|
|
14410
|
+
const queue = session.queueSnapshot();
|
|
14411
|
+
if (queue.length > 0) {
|
|
14412
|
+
ours.queue = queue;
|
|
14413
|
+
}
|
|
12914
14414
|
return mergeMeta(session.agentMeta, ours);
|
|
12915
14415
|
}
|
|
12916
14416
|
function buildInitializeResult() {
|
|
@@ -12941,7 +14441,13 @@ function buildInitializeResult() {
|
|
|
12941
14441
|
id: "bearer-token",
|
|
12942
14442
|
description: "Bearer token presented at WS upgrade"
|
|
12943
14443
|
}
|
|
12944
|
-
]
|
|
14444
|
+
],
|
|
14445
|
+
// Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
|
|
14446
|
+
// ACP clients ignore the field; capability-aware clients learn here
|
|
14447
|
+
// that hydra accepts concurrent session/prompt requests and emits
|
|
14448
|
+
// prompt_queue_* notifications so they can stop running their own
|
|
14449
|
+
// local queue.
|
|
14450
|
+
_meta: mergeMeta(void 0, { promptQueueing: true })
|
|
12945
14451
|
};
|
|
12946
14452
|
}
|
|
12947
14453
|
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
@@ -12955,7 +14461,7 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
12955
14461
|
}
|
|
12956
14462
|
|
|
12957
14463
|
// src/daemon/server.ts
|
|
12958
|
-
async function startDaemon(config) {
|
|
14464
|
+
async function startDaemon(config, serviceToken) {
|
|
12959
14465
|
ensureLoopbackOrTls(config);
|
|
12960
14466
|
const httpsOptions = config.daemon.tls ? {
|
|
12961
14467
|
key: await fsp4.readFile(config.daemon.tls.key),
|
|
@@ -12982,7 +14488,13 @@ async function startDaemon(config) {
|
|
|
12982
14488
|
setNpmInstallLogger((msg) => {
|
|
12983
14489
|
app.log.info(msg);
|
|
12984
14490
|
});
|
|
12985
|
-
const
|
|
14491
|
+
const sessionTokenStore = await SessionTokenStore.load();
|
|
14492
|
+
const authRateLimiter = new AuthRateLimiter();
|
|
14493
|
+
const validator = new CompositeTokenValidator([
|
|
14494
|
+
new StaticTokenValidator(serviceToken),
|
|
14495
|
+
new SessionTokenValidator(sessionTokenStore)
|
|
14496
|
+
]);
|
|
14497
|
+
const auth = bearerAuth({ validator });
|
|
12986
14498
|
app.addHook("onRequest", async (request, reply) => {
|
|
12987
14499
|
if (request.routeOptions.config?.skipAuth) {
|
|
12988
14500
|
return;
|
|
@@ -12992,6 +14504,13 @@ async function startDaemon(config) {
|
|
|
12992
14504
|
}
|
|
12993
14505
|
await auth(request, reply);
|
|
12994
14506
|
});
|
|
14507
|
+
const sweepInterval = setInterval(
|
|
14508
|
+
() => {
|
|
14509
|
+
sessionTokenStore.sweepExpired();
|
|
14510
|
+
},
|
|
14511
|
+
5 * 60 * 1e3
|
|
14512
|
+
);
|
|
14513
|
+
sweepInterval.unref();
|
|
12995
14514
|
const registry = new Registry(config);
|
|
12996
14515
|
const agentLogger = {
|
|
12997
14516
|
info: (msg) => app.log.info(msg),
|
|
@@ -13006,7 +14525,8 @@ async function startDaemon(config) {
|
|
|
13006
14525
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
13007
14526
|
defaultModels: config.defaultModels,
|
|
13008
14527
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
13009
|
-
logger: agentLogger
|
|
14528
|
+
logger: agentLogger,
|
|
14529
|
+
npmRegistry: config.npmRegistry
|
|
13010
14530
|
});
|
|
13011
14531
|
const extensions = new ExtensionManager(extensionList(config));
|
|
13012
14532
|
registerHealthRoutes(app, HYDRA_VERSION);
|
|
@@ -13020,8 +14540,12 @@ async function startDaemon(config) {
|
|
|
13020
14540
|
defaultAgent: config.defaultAgent,
|
|
13021
14541
|
defaultCwd: config.defaultCwd
|
|
13022
14542
|
});
|
|
14543
|
+
registerAuthRoutes(app, {
|
|
14544
|
+
store: sessionTokenStore,
|
|
14545
|
+
rateLimiter: authRateLimiter
|
|
14546
|
+
});
|
|
13023
14547
|
registerAcpWsEndpoint(app, {
|
|
13024
|
-
|
|
14548
|
+
validator,
|
|
13025
14549
|
manager,
|
|
13026
14550
|
defaultAgent: config.defaultAgent
|
|
13027
14551
|
});
|
|
@@ -13045,12 +14569,19 @@ async function startDaemon(config) {
|
|
|
13045
14569
|
daemonUrl: `${scheme}://${config.daemon.host}:${boundPort}`,
|
|
13046
14570
|
daemonHost: config.daemon.host,
|
|
13047
14571
|
daemonPort: boundPort,
|
|
13048
|
-
|
|
14572
|
+
serviceToken,
|
|
13049
14573
|
daemonWsUrl: `${wsScheme}://${config.daemon.host}:${boundPort}/acp`,
|
|
13050
14574
|
hydraHome: paths.home()
|
|
13051
14575
|
});
|
|
13052
14576
|
await extensions.start();
|
|
14577
|
+
void manager.resurrectPendingQueues().catch((err) => {
|
|
14578
|
+
app.log.warn(
|
|
14579
|
+
`queue replay scan failed: ${err.message}`
|
|
14580
|
+
);
|
|
14581
|
+
});
|
|
13053
14582
|
const shutdown = async () => {
|
|
14583
|
+
clearInterval(sweepInterval);
|
|
14584
|
+
await sessionTokenStore.flush();
|
|
13054
14585
|
await extensions.stop();
|
|
13055
14586
|
await manager.closeAll();
|
|
13056
14587
|
await manager.flushMetaWrites();
|
|
@@ -13058,7 +14589,7 @@ async function startDaemon(config) {
|
|
|
13058
14589
|
setNpmInstallLogger(null);
|
|
13059
14590
|
await app.close();
|
|
13060
14591
|
try {
|
|
13061
|
-
|
|
14592
|
+
fs15.unlinkSync(paths.pidFile());
|
|
13062
14593
|
} catch {
|
|
13063
14594
|
}
|
|
13064
14595
|
try {
|
|
@@ -13097,7 +14628,7 @@ function ensureLoopbackOrTls(config) {
|
|
|
13097
14628
|
init_daemon_bootstrap();
|
|
13098
14629
|
|
|
13099
14630
|
// src/cli/commands/log-tail.ts
|
|
13100
|
-
import * as
|
|
14631
|
+
import * as fs16 from "fs";
|
|
13101
14632
|
import * as fsp5 from "fs/promises";
|
|
13102
14633
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
13103
14634
|
const opts = parseLogTailFlags(argv);
|
|
@@ -13121,7 +14652,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
13121
14652
|
process.stdout.write(`-- following ${logPath} --
|
|
13122
14653
|
`);
|
|
13123
14654
|
let pending = false;
|
|
13124
|
-
const watcher =
|
|
14655
|
+
const watcher = fs16.watch(logPath, () => {
|
|
13125
14656
|
if (pending) {
|
|
13126
14657
|
return;
|
|
13127
14658
|
}
|
|
@@ -13221,7 +14752,8 @@ function parseLogTailFlags(argv) {
|
|
|
13221
14752
|
|
|
13222
14753
|
// src/cli/commands/daemon.ts
|
|
13223
14754
|
async function runDaemonStart(flags = {}) {
|
|
13224
|
-
const config = await
|
|
14755
|
+
const config = await loadConfig();
|
|
14756
|
+
const serviceToken = await ensureServiceToken();
|
|
13225
14757
|
if (await pingHealth(config)) {
|
|
13226
14758
|
const info2 = await readPidFile();
|
|
13227
14759
|
process.stdout.write(
|
|
@@ -13231,7 +14763,7 @@ async function runDaemonStart(flags = {}) {
|
|
|
13231
14763
|
return;
|
|
13232
14764
|
}
|
|
13233
14765
|
if (flagBool(flags, "foreground")) {
|
|
13234
|
-
const handle = await startDaemon(config);
|
|
14766
|
+
const handle = await startDaemon(config, serviceToken);
|
|
13235
14767
|
process.stdout.write(
|
|
13236
14768
|
`hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
|
|
13237
14769
|
`
|
|
@@ -13269,7 +14801,8 @@ async function runDaemonStop() {
|
|
|
13269
14801
|
}
|
|
13270
14802
|
}
|
|
13271
14803
|
async function runDaemonRestart() {
|
|
13272
|
-
const config = await
|
|
14804
|
+
const config = await loadConfig();
|
|
14805
|
+
await ensureServiceToken();
|
|
13273
14806
|
const info = await readPidFile();
|
|
13274
14807
|
if (info && isProcessAlive(info.pid)) {
|
|
13275
14808
|
process.stdout.write(`Stopping daemon pid ${info.pid}...
|
|
@@ -13357,16 +14890,18 @@ init_sessions();
|
|
|
13357
14890
|
|
|
13358
14891
|
// src/cli/commands/extensions.ts
|
|
13359
14892
|
init_config();
|
|
14893
|
+
init_service_token();
|
|
13360
14894
|
init_paths();
|
|
13361
14895
|
import * as fsp7 from "fs/promises";
|
|
13362
14896
|
init_sessions();
|
|
13363
14897
|
async function runExtensionsList() {
|
|
13364
14898
|
const config = await loadConfig();
|
|
14899
|
+
const serviceToken = await loadServiceToken();
|
|
13365
14900
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13366
14901
|
let body;
|
|
13367
14902
|
try {
|
|
13368
14903
|
const r = await fetch(`${baseUrl}/v1/extensions`, {
|
|
13369
|
-
headers: { Authorization: `Bearer ${
|
|
14904
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13370
14905
|
});
|
|
13371
14906
|
if (!r.ok) {
|
|
13372
14907
|
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
@@ -13469,13 +15004,14 @@ async function runExtensionsAdd(name, argv) {
|
|
|
13469
15004
|
process.stdout.write(`Added extension '${name}' to ${paths.config()}
|
|
13470
15005
|
`);
|
|
13471
15006
|
const config = await loadConfig();
|
|
15007
|
+
const serviceToken = await loadServiceToken();
|
|
13472
15008
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13473
15009
|
const registerBody = { name, ...body };
|
|
13474
15010
|
try {
|
|
13475
15011
|
const r = await fetch(`${baseUrl}/v1/extensions`, {
|
|
13476
15012
|
method: "POST",
|
|
13477
15013
|
headers: {
|
|
13478
|
-
Authorization: `Bearer ${
|
|
15014
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
13479
15015
|
"Content-Type": "application/json"
|
|
13480
15016
|
},
|
|
13481
15017
|
body: JSON.stringify(registerBody)
|
|
@@ -13523,11 +15059,12 @@ async function runExtensionsRemove(name) {
|
|
|
13523
15059
|
process.stdout.write(`Removed extension '${name}' from ${paths.config()}
|
|
13524
15060
|
`);
|
|
13525
15061
|
const config = await loadConfig();
|
|
15062
|
+
const serviceToken = await loadServiceToken();
|
|
13526
15063
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13527
15064
|
try {
|
|
13528
15065
|
const r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}`, {
|
|
13529
15066
|
method: "DELETE",
|
|
13530
|
-
headers: { Authorization: `Bearer ${
|
|
15067
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13531
15068
|
});
|
|
13532
15069
|
if (r.status === 204 || r.status === 404) {
|
|
13533
15070
|
process.stdout.write(`${name}: stopped
|
|
@@ -13587,12 +15124,13 @@ async function postLifecycle(name, verb) {
|
|
|
13587
15124
|
return;
|
|
13588
15125
|
}
|
|
13589
15126
|
const config = await loadConfig();
|
|
15127
|
+
const serviceToken = await loadServiceToken();
|
|
13590
15128
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13591
15129
|
let r;
|
|
13592
15130
|
try {
|
|
13593
15131
|
r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}/${verb}`, {
|
|
13594
15132
|
method: "POST",
|
|
13595
|
-
headers: { Authorization: `Bearer ${
|
|
15133
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13596
15134
|
});
|
|
13597
15135
|
} catch (err) {
|
|
13598
15136
|
process.stderr.write(
|
|
@@ -13623,8 +15161,9 @@ async function postLifecycle(name, verb) {
|
|
|
13623
15161
|
}
|
|
13624
15162
|
async function postLifecycleAll(verb) {
|
|
13625
15163
|
const config = await loadConfig();
|
|
15164
|
+
const serviceToken = await loadServiceToken();
|
|
13626
15165
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13627
|
-
const auth = { Authorization: `Bearer ${
|
|
15166
|
+
const auth = { Authorization: `Bearer ${serviceToken}` };
|
|
13628
15167
|
let listBody;
|
|
13629
15168
|
try {
|
|
13630
15169
|
const r = await fetch(`${baseUrl}/v1/extensions`, { headers: auth });
|
|
@@ -13790,14 +15329,16 @@ function maxLen2(headerCell, values) {
|
|
|
13790
15329
|
|
|
13791
15330
|
// src/cli/commands/agents.ts
|
|
13792
15331
|
init_config();
|
|
15332
|
+
init_service_token();
|
|
13793
15333
|
init_sessions();
|
|
13794
15334
|
async function runAgentsList() {
|
|
13795
15335
|
const config = await loadConfig();
|
|
15336
|
+
const serviceToken = await loadServiceToken();
|
|
13796
15337
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13797
15338
|
let body;
|
|
13798
15339
|
try {
|
|
13799
15340
|
const r = await fetch(`${baseUrl}/v1/agents`, {
|
|
13800
|
-
headers: { Authorization: `Bearer ${
|
|
15341
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13801
15342
|
});
|
|
13802
15343
|
if (!r.ok) {
|
|
13803
15344
|
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
@@ -13854,12 +15395,13 @@ Registry version: ${body.version}
|
|
|
13854
15395
|
}
|
|
13855
15396
|
async function runAgentsRefresh() {
|
|
13856
15397
|
const config = await loadConfig();
|
|
15398
|
+
const serviceToken = await loadServiceToken();
|
|
13857
15399
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
13858
15400
|
let body;
|
|
13859
15401
|
try {
|
|
13860
15402
|
const r = await fetch(`${baseUrl}/v1/registry/refresh`, {
|
|
13861
15403
|
method: "POST",
|
|
13862
|
-
headers: { Authorization: `Bearer ${
|
|
15404
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
13863
15405
|
});
|
|
13864
15406
|
if (!r.ok) {
|
|
13865
15407
|
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
@@ -13890,8 +15432,197 @@ function maxLen3(headerCell, values) {
|
|
|
13890
15432
|
return max;
|
|
13891
15433
|
}
|
|
13892
15434
|
|
|
15435
|
+
// src/cli/commands/auth.ts
|
|
15436
|
+
init_config();
|
|
15437
|
+
init_service_token();
|
|
15438
|
+
init_sessions();
|
|
15439
|
+
async function promptPassword(prompt) {
|
|
15440
|
+
process.stdout.write(prompt);
|
|
15441
|
+
if (!process.stdin.isTTY) {
|
|
15442
|
+
return readLineFromStdin();
|
|
15443
|
+
}
|
|
15444
|
+
return new Promise((resolve5, reject) => {
|
|
15445
|
+
const stdin = process.stdin;
|
|
15446
|
+
const wasRaw = stdin.isRaw === true;
|
|
15447
|
+
let buffer = "";
|
|
15448
|
+
const cleanup = () => {
|
|
15449
|
+
stdin.removeListener("data", onData);
|
|
15450
|
+
stdin.removeListener("error", onError);
|
|
15451
|
+
if (!wasRaw) {
|
|
15452
|
+
stdin.setRawMode(false);
|
|
15453
|
+
}
|
|
15454
|
+
stdin.pause();
|
|
15455
|
+
};
|
|
15456
|
+
const onData = (chunk) => {
|
|
15457
|
+
for (const byte of chunk) {
|
|
15458
|
+
if (byte === 10 || byte === 13) {
|
|
15459
|
+
process.stdout.write("\n");
|
|
15460
|
+
cleanup();
|
|
15461
|
+
resolve5(buffer);
|
|
15462
|
+
return;
|
|
15463
|
+
}
|
|
15464
|
+
if (byte === 3) {
|
|
15465
|
+
cleanup();
|
|
15466
|
+
reject(new Error("password entry cancelled"));
|
|
15467
|
+
return;
|
|
15468
|
+
}
|
|
15469
|
+
if (byte === 127 || byte === 8) {
|
|
15470
|
+
buffer = buffer.slice(0, -1);
|
|
15471
|
+
continue;
|
|
15472
|
+
}
|
|
15473
|
+
buffer += String.fromCharCode(byte);
|
|
15474
|
+
}
|
|
15475
|
+
};
|
|
15476
|
+
const onError = (err) => {
|
|
15477
|
+
cleanup();
|
|
15478
|
+
reject(err);
|
|
15479
|
+
};
|
|
15480
|
+
stdin.setRawMode(true);
|
|
15481
|
+
stdin.resume();
|
|
15482
|
+
stdin.on("data", onData);
|
|
15483
|
+
stdin.on("error", onError);
|
|
15484
|
+
});
|
|
15485
|
+
}
|
|
15486
|
+
function readLineFromStdin() {
|
|
15487
|
+
return new Promise((resolve5, reject) => {
|
|
15488
|
+
let buffer = "";
|
|
15489
|
+
process.stdin.setEncoding("utf8");
|
|
15490
|
+
const onData = (chunk) => {
|
|
15491
|
+
buffer += chunk;
|
|
15492
|
+
const nl = buffer.indexOf("\n");
|
|
15493
|
+
if (nl !== -1) {
|
|
15494
|
+
process.stdin.removeListener("data", onData);
|
|
15495
|
+
process.stdin.removeListener("error", onError);
|
|
15496
|
+
resolve5(buffer.slice(0, nl).replace(/\r$/, ""));
|
|
15497
|
+
}
|
|
15498
|
+
};
|
|
15499
|
+
const onError = (err) => {
|
|
15500
|
+
process.stdin.removeListener("data", onData);
|
|
15501
|
+
process.stdin.removeListener("error", onError);
|
|
15502
|
+
reject(err);
|
|
15503
|
+
};
|
|
15504
|
+
process.stdin.on("data", onData);
|
|
15505
|
+
process.stdin.on("error", onError);
|
|
15506
|
+
});
|
|
15507
|
+
}
|
|
15508
|
+
async function runAuthPasswordSet(flags) {
|
|
15509
|
+
const force = flagBool(flags, "force");
|
|
15510
|
+
if (await hasPassword() && !force) {
|
|
15511
|
+
const current = await promptPassword("Current password: ");
|
|
15512
|
+
if (!await verifyPassword(current)) {
|
|
15513
|
+
process.stderr.write("Wrong password.\n");
|
|
15514
|
+
process.exit(1);
|
|
15515
|
+
}
|
|
15516
|
+
}
|
|
15517
|
+
const next = await promptPassword("New password: ");
|
|
15518
|
+
if (next.length === 0) {
|
|
15519
|
+
process.stderr.write("Password must not be empty.\n");
|
|
15520
|
+
process.exit(2);
|
|
15521
|
+
}
|
|
15522
|
+
const confirm = await promptPassword("Confirm new password: ");
|
|
15523
|
+
if (next !== confirm) {
|
|
15524
|
+
process.stderr.write("Passwords did not match.\n");
|
|
15525
|
+
process.exit(1);
|
|
15526
|
+
}
|
|
15527
|
+
await setPassword(next);
|
|
15528
|
+
process.stdout.write("Password set.\n");
|
|
15529
|
+
}
|
|
15530
|
+
async function runAuthList() {
|
|
15531
|
+
const config = await loadConfig();
|
|
15532
|
+
const token = await loadServiceToken();
|
|
15533
|
+
const baseUrl = httpBase(
|
|
15534
|
+
config.daemon.host,
|
|
15535
|
+
config.daemon.port,
|
|
15536
|
+
!!config.daemon.tls
|
|
15537
|
+
);
|
|
15538
|
+
const r = await fetch(`${baseUrl}/v1/auth/sessions`, {
|
|
15539
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
15540
|
+
});
|
|
15541
|
+
if (!r.ok) {
|
|
15542
|
+
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
15543
|
+
`);
|
|
15544
|
+
process.exit(1);
|
|
15545
|
+
}
|
|
15546
|
+
const body = await r.json();
|
|
15547
|
+
if (body.sessions.length === 0) {
|
|
15548
|
+
process.stdout.write("No active session tokens.\n");
|
|
15549
|
+
return;
|
|
15550
|
+
}
|
|
15551
|
+
const header = {
|
|
15552
|
+
id: "ID",
|
|
15553
|
+
label: "LABEL",
|
|
15554
|
+
createdAt: "CREATED",
|
|
15555
|
+
expiresAt: "EXPIRES",
|
|
15556
|
+
lastUsedAt: "LAST USED"
|
|
15557
|
+
};
|
|
15558
|
+
const rows = body.sessions.map((s) => ({
|
|
15559
|
+
id: s.id,
|
|
15560
|
+
label: s.label ?? "-",
|
|
15561
|
+
createdAt: s.createdAt,
|
|
15562
|
+
expiresAt: s.expiresAt,
|
|
15563
|
+
lastUsedAt: s.lastUsedAt
|
|
15564
|
+
}));
|
|
15565
|
+
const widths = {
|
|
15566
|
+
id: maxLen4(header.id, rows.map((r2) => r2.id)),
|
|
15567
|
+
label: maxLen4(header.label, rows.map((r2) => r2.label)),
|
|
15568
|
+
createdAt: maxLen4(header.createdAt, rows.map((r2) => r2.createdAt)),
|
|
15569
|
+
expiresAt: maxLen4(header.expiresAt, rows.map((r2) => r2.expiresAt))
|
|
15570
|
+
};
|
|
15571
|
+
const fmt = (r2) => [
|
|
15572
|
+
r2.id.padEnd(widths.id),
|
|
15573
|
+
r2.label.padEnd(widths.label),
|
|
15574
|
+
r2.createdAt.padEnd(widths.createdAt),
|
|
15575
|
+
r2.expiresAt.padEnd(widths.expiresAt),
|
|
15576
|
+
r2.lastUsedAt
|
|
15577
|
+
].join(" ");
|
|
15578
|
+
process.stdout.write(fmt(header) + "\n");
|
|
15579
|
+
for (const r2 of rows) {
|
|
15580
|
+
process.stdout.write(fmt(r2) + "\n");
|
|
15581
|
+
}
|
|
15582
|
+
}
|
|
15583
|
+
async function runAuthRevoke(id) {
|
|
15584
|
+
if (!id) {
|
|
15585
|
+
process.stderr.write("Usage: hydra-acp auth revoke <id>\n");
|
|
15586
|
+
process.exit(2);
|
|
15587
|
+
}
|
|
15588
|
+
const config = await loadConfig();
|
|
15589
|
+
const token = await loadServiceToken();
|
|
15590
|
+
const baseUrl = httpBase(
|
|
15591
|
+
config.daemon.host,
|
|
15592
|
+
config.daemon.port,
|
|
15593
|
+
!!config.daemon.tls
|
|
15594
|
+
);
|
|
15595
|
+
const r = await fetch(`${baseUrl}/v1/auth/sessions/${id}`, {
|
|
15596
|
+
method: "DELETE",
|
|
15597
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
15598
|
+
});
|
|
15599
|
+
if (r.status === 204) {
|
|
15600
|
+
process.stdout.write(`Revoked ${id}
|
|
15601
|
+
`);
|
|
15602
|
+
return;
|
|
15603
|
+
}
|
|
15604
|
+
if (r.status === 404) {
|
|
15605
|
+
process.stderr.write(`No session token with id ${id}
|
|
15606
|
+
`);
|
|
15607
|
+
process.exit(1);
|
|
15608
|
+
}
|
|
15609
|
+
process.stderr.write(`Daemon returned HTTP ${r.status}
|
|
15610
|
+
`);
|
|
15611
|
+
process.exit(1);
|
|
15612
|
+
}
|
|
15613
|
+
function maxLen4(headerCell, values) {
|
|
15614
|
+
let max = headerCell.length;
|
|
15615
|
+
for (const v of values) {
|
|
15616
|
+
if (v.length > max) {
|
|
15617
|
+
max = v.length;
|
|
15618
|
+
}
|
|
15619
|
+
}
|
|
15620
|
+
return max;
|
|
15621
|
+
}
|
|
15622
|
+
|
|
13893
15623
|
// src/shim/proxy.ts
|
|
13894
15624
|
init_config();
|
|
15625
|
+
init_service_token();
|
|
13895
15626
|
init_daemon_bootstrap();
|
|
13896
15627
|
init_resilient_ws();
|
|
13897
15628
|
|
|
@@ -14066,13 +15797,14 @@ function isResponse2(msg) {
|
|
|
14066
15797
|
|
|
14067
15798
|
// src/shim/proxy.ts
|
|
14068
15799
|
async function runShim(opts) {
|
|
14069
|
-
const config = await
|
|
15800
|
+
const config = await loadConfig();
|
|
15801
|
+
const serviceToken = await ensureServiceToken();
|
|
14070
15802
|
await ensureDaemonReachable(config);
|
|
14071
15803
|
const tracker = new SessionTracker();
|
|
14072
15804
|
const downstream = ndjsonStreamFromStdio(process.stdin, process.stdout);
|
|
14073
15805
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
14074
15806
|
const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
14075
|
-
const subprotocols = ["acp.v1", `hydra-acp-token.${
|
|
15807
|
+
const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
|
|
14076
15808
|
const upstream = new ResilientWsStream({
|
|
14077
15809
|
url,
|
|
14078
15810
|
subprotocols,
|
|
@@ -14413,6 +16145,7 @@ async function main() {
|
|
|
14413
16145
|
process.exit(2);
|
|
14414
16146
|
return;
|
|
14415
16147
|
}
|
|
16148
|
+
case "session":
|
|
14416
16149
|
case "sessions": {
|
|
14417
16150
|
const sub = positional[1];
|
|
14418
16151
|
if (sub === void 0 || sub === "list") {
|
|
@@ -14449,13 +16182,14 @@ async function main() {
|
|
|
14449
16182
|
});
|
|
14450
16183
|
return;
|
|
14451
16184
|
}
|
|
14452
|
-
process.stderr.write(`Unknown
|
|
16185
|
+
process.stderr.write(`Unknown session subcommand: ${sub}
|
|
14453
16186
|
`);
|
|
14454
16187
|
process.exit(2);
|
|
14455
16188
|
return;
|
|
14456
16189
|
}
|
|
16190
|
+
case "extension":
|
|
14457
16191
|
case "extensions": {
|
|
14458
|
-
const extIdx = argv.indexOf(
|
|
16192
|
+
const extIdx = argv.indexOf(subcommand);
|
|
14459
16193
|
const tail = argv.slice(extIdx + 1);
|
|
14460
16194
|
const sub = tail[0];
|
|
14461
16195
|
const name2 = tail[1];
|
|
@@ -14488,11 +16222,12 @@ async function main() {
|
|
|
14488
16222
|
await runExtensionsLogs(name2, rest);
|
|
14489
16223
|
return;
|
|
14490
16224
|
}
|
|
14491
|
-
process.stderr.write(`Unknown
|
|
16225
|
+
process.stderr.write(`Unknown extension subcommand: ${sub}
|
|
14492
16226
|
`);
|
|
14493
16227
|
process.exit(2);
|
|
14494
16228
|
return;
|
|
14495
16229
|
}
|
|
16230
|
+
case "agent":
|
|
14496
16231
|
case "agents": {
|
|
14497
16232
|
const sub = positional[1];
|
|
14498
16233
|
if (sub === void 0 || sub === "list") {
|
|
@@ -14503,7 +16238,33 @@ async function main() {
|
|
|
14503
16238
|
await runAgentsRefresh();
|
|
14504
16239
|
return;
|
|
14505
16240
|
}
|
|
14506
|
-
process.stderr.write(`Unknown
|
|
16241
|
+
process.stderr.write(`Unknown agent subcommand: ${sub}
|
|
16242
|
+
`);
|
|
16243
|
+
process.exit(2);
|
|
16244
|
+
return;
|
|
16245
|
+
}
|
|
16246
|
+
case "auth": {
|
|
16247
|
+
const sub = positional[1];
|
|
16248
|
+
if (sub === "password") {
|
|
16249
|
+
const action = positional[2];
|
|
16250
|
+
if (action === void 0 || action === "set") {
|
|
16251
|
+
await runAuthPasswordSet(flags);
|
|
16252
|
+
return;
|
|
16253
|
+
}
|
|
16254
|
+
process.stderr.write(`Unknown auth password action: ${action}
|
|
16255
|
+
`);
|
|
16256
|
+
process.exit(2);
|
|
16257
|
+
return;
|
|
16258
|
+
}
|
|
16259
|
+
if (sub === void 0 || sub === "list") {
|
|
16260
|
+
await runAuthList();
|
|
16261
|
+
return;
|
|
16262
|
+
}
|
|
16263
|
+
if (sub === "revoke") {
|
|
16264
|
+
await runAuthRevoke(positional[2]);
|
|
16265
|
+
return;
|
|
16266
|
+
}
|
|
16267
|
+
process.stderr.write(`Unknown auth subcommand: ${sub}
|
|
14507
16268
|
`);
|
|
14508
16269
|
process.exit(2);
|
|
14509
16270
|
return;
|
|
@@ -14583,23 +16344,26 @@ function printHelp() {
|
|
|
14583
16344
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
14584
16345
|
" hydra-acp daemon stop|restart|status",
|
|
14585
16346
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
14586
|
-
" hydra-acp
|
|
16347
|
+
" hydra-acp session [list] [--all] [--json]",
|
|
14587
16348
|
" 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
|
|
16349
|
+
" hydra-acp session kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
16350
|
+
" hydra-acp session remove <id> Remove a session entirely (live or cold)",
|
|
16351
|
+
" hydra-acp session export <id> [--out <file>|.]",
|
|
14591
16352
|
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
14592
|
-
" hydra-acp
|
|
16353
|
+
" hydra-acp session transcript <id>|<file> [--out <file>|.]",
|
|
14593
16354
|
" 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
|
|
16355
|
+
" hydra-acp session import <file>|- [--replace] [--cwd <path>] [--info]",
|
|
14595
16356
|
" 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
|
|
16357
|
+
" hydra-acp extension list List configured extensions and live state",
|
|
16358
|
+
" hydra-acp extension add <name> [opts] Add an extension to config",
|
|
16359
|
+
" hydra-acp extension remove <name> Remove an extension from config",
|
|
16360
|
+
" hydra-acp extension start|stop|restart <n>|all Lifecycle on one or all",
|
|
16361
|
+
" hydra-acp extension logs <name> [-f] [-n N] Tail or follow an extension's log",
|
|
16362
|
+
" hydra-acp agent [list] List agents in the cached registry",
|
|
16363
|
+
" hydra-acp agent refresh Force a registry re-fetch",
|
|
16364
|
+
" hydra-acp auth password [--force] Set the daemon's master password",
|
|
16365
|
+
" hydra-acp auth [list] List active session tokens",
|
|
16366
|
+
" hydra-acp auth revoke <id> Revoke a session token",
|
|
14603
16367
|
" hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
|
|
14604
16368
|
" --resume <id> attaches to a specific session; --reattach picks the most-recent in cwd.",
|
|
14605
16369
|
" Smart default (no flags): shows a picker when sessions exist, else new.",
|