@hydra-acp/cli 0.1.5 → 0.1.7
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 +15 -10
- package/dist/cli.js +543 -178
- package/dist/index.d.ts +10 -1
- package/dist/index.js +135 -37
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -94,12 +94,15 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
94
94
|
tui: z.ZodDefault<z.ZodObject<{
|
|
95
95
|
repaintThrottleMs: z.ZodDefault<z.ZodNumber>;
|
|
96
96
|
maxScrollbackLines: z.ZodDefault<z.ZodNumber>;
|
|
97
|
+
mouse: z.ZodDefault<z.ZodBoolean>;
|
|
97
98
|
}, "strip", z.ZodTypeAny, {
|
|
98
99
|
repaintThrottleMs: number;
|
|
99
100
|
maxScrollbackLines: number;
|
|
101
|
+
mouse: boolean;
|
|
100
102
|
}, {
|
|
101
103
|
repaintThrottleMs?: number | undefined;
|
|
102
104
|
maxScrollbackLines?: number | undefined;
|
|
105
|
+
mouse?: boolean | undefined;
|
|
103
106
|
}>>;
|
|
104
107
|
}, "strip", z.ZodTypeAny, {
|
|
105
108
|
daemon: {
|
|
@@ -122,6 +125,7 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
122
125
|
tui: {
|
|
123
126
|
repaintThrottleMs: number;
|
|
124
127
|
maxScrollbackLines: number;
|
|
128
|
+
mouse: boolean;
|
|
125
129
|
};
|
|
126
130
|
registry: {
|
|
127
131
|
url: string;
|
|
@@ -152,6 +156,7 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
152
156
|
tui?: {
|
|
153
157
|
repaintThrottleMs?: number | undefined;
|
|
154
158
|
maxScrollbackLines?: number | undefined;
|
|
159
|
+
mouse?: boolean | undefined;
|
|
155
160
|
} | undefined;
|
|
156
161
|
registry?: {
|
|
157
162
|
url?: string | undefined;
|
|
@@ -1178,7 +1183,7 @@ interface SpawnPlan {
|
|
|
1178
1183
|
args: string[];
|
|
1179
1184
|
env: Record<string, string>;
|
|
1180
1185
|
}
|
|
1181
|
-
declare function planSpawn(agent: RegistryAgent,
|
|
1186
|
+
declare function planSpawn(agent: RegistryAgent, callerArgs?: string[]): Promise<SpawnPlan>;
|
|
1182
1187
|
|
|
1183
1188
|
type JsonRpcId = string | number;
|
|
1184
1189
|
interface JsonRpcRequest {
|
|
@@ -1476,6 +1481,8 @@ declare class JsonRpcConnection {
|
|
|
1476
1481
|
private requestHandlers;
|
|
1477
1482
|
private defaultRequestHandler;
|
|
1478
1483
|
private notificationHandlers;
|
|
1484
|
+
private bufferedNotifications;
|
|
1485
|
+
private static readonly MAX_BUFFERED_PER_METHOD;
|
|
1479
1486
|
private pending;
|
|
1480
1487
|
private closed;
|
|
1481
1488
|
private closeHandlers;
|
|
@@ -1964,6 +1971,7 @@ interface CreateSessionParams {
|
|
|
1964
1971
|
mcpServers?: unknown[];
|
|
1965
1972
|
title?: string;
|
|
1966
1973
|
agentArgs?: string[];
|
|
1974
|
+
model?: string;
|
|
1967
1975
|
}
|
|
1968
1976
|
interface ResurrectParams {
|
|
1969
1977
|
hydraSessionId: string;
|
|
@@ -2097,6 +2105,7 @@ declare function hydraHome(): string;
|
|
|
2097
2105
|
declare const paths: {
|
|
2098
2106
|
home: typeof hydraHome;
|
|
2099
2107
|
config: () => string;
|
|
2108
|
+
authToken: () => string;
|
|
2100
2109
|
pidFile: () => string;
|
|
2101
2110
|
logFile: () => string;
|
|
2102
2111
|
currentLogFile: () => string;
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,9 @@ function hydraHome() {
|
|
|
30
30
|
var paths = {
|
|
31
31
|
home: hydraHome,
|
|
32
32
|
config: () => path.join(hydraHome(), "config.json"),
|
|
33
|
+
// Auth token lives in its own file so config.json can be version-
|
|
34
|
+
// controlled without leaking the secret. Raw string contents, mode 0600.
|
|
35
|
+
authToken: () => path.join(hydraHome(), "auth-token"),
|
|
33
36
|
pidFile: () => path.join(hydraHome(), "daemon.pid"),
|
|
34
37
|
logFile: () => path.join(hydraHome(), "daemon.log"),
|
|
35
38
|
currentLogFile: () => path.join(hydraHome(), "current.log"),
|
|
@@ -83,7 +86,13 @@ var TuiConfig = z.object({
|
|
|
83
86
|
// Cap on logical lines retained in the in-memory scrollback render
|
|
84
87
|
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
85
88
|
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
86
|
-
maxScrollbackLines: z.number().int().positive().default(1e4)
|
|
89
|
+
maxScrollbackLines: z.number().int().positive().default(1e4),
|
|
90
|
+
// When true (default), the TUI captures mouse events so the wheel can
|
|
91
|
+
// drive scrollback. The cost: terminals route clicks to the app, so
|
|
92
|
+
// text selection requires shift+drag to bypass mouse reporting. Set
|
|
93
|
+
// false to disable capture — wheel scrollback stops working, but
|
|
94
|
+
// plain click-drag selects text via the terminal emulator.
|
|
95
|
+
mouse: z.boolean().default(true)
|
|
87
96
|
});
|
|
88
97
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
89
98
|
var ExtensionBody = z.object({
|
|
@@ -116,7 +125,11 @@ var HydraConfig = z.object({
|
|
|
116
125
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
117
126
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
118
127
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
119
|
-
tui: TuiConfig.default({
|
|
128
|
+
tui: TuiConfig.default({
|
|
129
|
+
repaintThrottleMs: 1e3,
|
|
130
|
+
maxScrollbackLines: 1e4,
|
|
131
|
+
mouse: true
|
|
132
|
+
})
|
|
120
133
|
});
|
|
121
134
|
function extensionList(config) {
|
|
122
135
|
return Object.entries(config.extensions).map(([name, body]) => ({
|
|
@@ -124,56 +137,104 @@ function extensionList(config) {
|
|
|
124
137
|
...body
|
|
125
138
|
}));
|
|
126
139
|
}
|
|
127
|
-
async function
|
|
128
|
-
const configPath = paths.config();
|
|
140
|
+
async function readConfigFile() {
|
|
129
141
|
let raw;
|
|
130
142
|
try {
|
|
131
|
-
raw = await fs.readFile(
|
|
143
|
+
raw = await fs.readFile(paths.config(), "utf8");
|
|
132
144
|
} catch (err) {
|
|
133
145
|
const e = err;
|
|
134
146
|
if (e.code === "ENOENT") {
|
|
135
|
-
|
|
136
|
-
`No config found at ${configPath}. Run \`hydra-acp init\` to create one.`
|
|
137
|
-
);
|
|
147
|
+
return {};
|
|
138
148
|
}
|
|
139
149
|
throw err;
|
|
140
150
|
}
|
|
141
|
-
|
|
142
|
-
return HydraConfig.parse(parsed);
|
|
151
|
+
return JSON.parse(raw);
|
|
143
152
|
}
|
|
144
|
-
async function
|
|
153
|
+
async function loadAuthToken() {
|
|
154
|
+
let tokenFile;
|
|
145
155
|
try {
|
|
146
|
-
await fs.
|
|
156
|
+
const text = await fs.readFile(paths.authToken(), "utf8");
|
|
157
|
+
const trimmed = text.trim();
|
|
158
|
+
if (trimmed.length > 0) {
|
|
159
|
+
tokenFile = trimmed;
|
|
160
|
+
}
|
|
147
161
|
} catch (err) {
|
|
148
162
|
const e = err;
|
|
149
163
|
if (e.code !== "ENOENT") {
|
|
150
164
|
throw err;
|
|
151
165
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
}
|
|
167
|
+
const raw = await readConfigFile();
|
|
168
|
+
const daemon = raw.daemon;
|
|
169
|
+
const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
|
|
170
|
+
if (tokenFile && legacy) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Auth token present in both ${paths.authToken()} and ${paths.config()} (daemon.authToken). Remove daemon.authToken from config.json to resolve.`
|
|
156
173
|
);
|
|
157
|
-
return config;
|
|
158
174
|
}
|
|
159
|
-
|
|
175
|
+
if (tokenFile) {
|
|
176
|
+
return tokenFile;
|
|
177
|
+
}
|
|
178
|
+
if (legacy) {
|
|
179
|
+
await migrateLegacyAuthToken(raw, daemon, legacy);
|
|
180
|
+
return legacy;
|
|
181
|
+
}
|
|
182
|
+
return void 0;
|
|
160
183
|
}
|
|
161
|
-
async function
|
|
184
|
+
async function migrateLegacyAuthToken(raw, daemon, token) {
|
|
185
|
+
await writeAuthToken(token);
|
|
186
|
+
delete daemon.authToken;
|
|
187
|
+
if (Object.keys(daemon).length === 0) {
|
|
188
|
+
delete raw.daemon;
|
|
189
|
+
}
|
|
190
|
+
await fs.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
mode: 384
|
|
193
|
+
});
|
|
194
|
+
process.stderr.write(
|
|
195
|
+
`hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
|
|
196
|
+
`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
async function writeAuthToken(token) {
|
|
162
200
|
await fs.mkdir(paths.home(), { recursive: true });
|
|
163
|
-
await fs.writeFile(paths.
|
|
201
|
+
await fs.writeFile(paths.authToken(), token + "\n", {
|
|
164
202
|
encoding: "utf8",
|
|
165
203
|
mode: 384
|
|
166
204
|
});
|
|
167
205
|
}
|
|
168
|
-
async function
|
|
169
|
-
const token =
|
|
170
|
-
|
|
206
|
+
async function loadConfig() {
|
|
207
|
+
const token = await loadAuthToken();
|
|
208
|
+
if (!token) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`No auth token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const raw = await readConfigFile();
|
|
214
|
+
const daemon = raw.daemon ??= {};
|
|
215
|
+
daemon.authToken = token;
|
|
216
|
+
return HydraConfig.parse(raw);
|
|
217
|
+
}
|
|
218
|
+
async function ensureConfig() {
|
|
219
|
+
if (!await loadAuthToken()) {
|
|
220
|
+
const token = generateAuthToken();
|
|
221
|
+
await writeAuthToken(token);
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
`hydra-acp: initialized ${paths.authToken()} with a fresh auth token.
|
|
224
|
+
`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return loadConfig();
|
|
228
|
+
}
|
|
229
|
+
async function writeConfig(config) {
|
|
171
230
|
await fs.mkdir(paths.home(), { recursive: true });
|
|
172
|
-
|
|
231
|
+
const { daemon, ...rest } = config;
|
|
232
|
+
const { authToken: _authToken, ...daemonRest } = daemon;
|
|
233
|
+
const onDisk = { ...rest, daemon: daemonRest };
|
|
234
|
+
await fs.writeFile(paths.config(), JSON.stringify(onDisk, null, 2) + "\n", {
|
|
173
235
|
encoding: "utf8",
|
|
174
236
|
mode: 384
|
|
175
237
|
});
|
|
176
|
-
return HydraConfig.parse(minimal);
|
|
177
238
|
}
|
|
178
239
|
function generateAuthToken() {
|
|
179
240
|
const bytes = new Uint8Array(32);
|
|
@@ -573,13 +634,13 @@ function npxPackageBasename(agent) {
|
|
|
573
634
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
574
635
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
575
636
|
}
|
|
576
|
-
async function planSpawn(agent,
|
|
637
|
+
async function planSpawn(agent, callerArgs = []) {
|
|
577
638
|
if (agent.distribution.npx) {
|
|
578
639
|
const npx = agent.distribution.npx;
|
|
579
|
-
const
|
|
640
|
+
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
580
641
|
return {
|
|
581
642
|
command: "npx",
|
|
582
|
-
args,
|
|
643
|
+
args: ["-y", npx.package, ...tail],
|
|
583
644
|
env: npx.env ?? {}
|
|
584
645
|
};
|
|
585
646
|
}
|
|
@@ -595,18 +656,19 @@ async function planSpawn(agent, extraArgs = []) {
|
|
|
595
656
|
version: agent.version ?? "current",
|
|
596
657
|
target
|
|
597
658
|
});
|
|
659
|
+
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
598
660
|
return {
|
|
599
661
|
command: cmdPath,
|
|
600
|
-
args:
|
|
662
|
+
args: tail,
|
|
601
663
|
env: target.env ?? {}
|
|
602
664
|
};
|
|
603
665
|
}
|
|
604
666
|
if (agent.distribution.uvx) {
|
|
605
667
|
const uvx = agent.distribution.uvx;
|
|
606
|
-
const
|
|
668
|
+
const tail = callerArgs.length > 0 ? callerArgs : uvx.args ?? [];
|
|
607
669
|
return {
|
|
608
670
|
command: "uvx",
|
|
609
|
-
args,
|
|
671
|
+
args: [uvx.package, ...tail],
|
|
610
672
|
env: uvx.env ?? {}
|
|
611
673
|
};
|
|
612
674
|
}
|
|
@@ -696,6 +758,9 @@ function extractHydraMeta(meta) {
|
|
|
696
758
|
out.resume = parsed.data;
|
|
697
759
|
}
|
|
698
760
|
}
|
|
761
|
+
if (typeof obj.model === "string") {
|
|
762
|
+
out.model = obj.model;
|
|
763
|
+
}
|
|
699
764
|
if (typeof obj.currentModel === "string") {
|
|
700
765
|
out.currentModel = obj.currentModel;
|
|
701
766
|
}
|
|
@@ -862,7 +927,7 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
862
927
|
|
|
863
928
|
// src/acp/connection.ts
|
|
864
929
|
import { nanoid } from "nanoid";
|
|
865
|
-
var JsonRpcConnection = class {
|
|
930
|
+
var JsonRpcConnection = class _JsonRpcConnection {
|
|
866
931
|
constructor(stream) {
|
|
867
932
|
this.stream = stream;
|
|
868
933
|
this.stream.onMessage((m) => this.handleIncoming(m));
|
|
@@ -872,6 +937,16 @@ var JsonRpcConnection = class {
|
|
|
872
937
|
requestHandlers = /* @__PURE__ */ new Map();
|
|
873
938
|
defaultRequestHandler;
|
|
874
939
|
notificationHandlers = /* @__PURE__ */ new Map();
|
|
940
|
+
// Notifications received before a handler was registered. Some agents
|
|
941
|
+
// (e.g. claude-acp) advertise their command list in the same chunk as
|
|
942
|
+
// the `session/new` response, which is processed before the consumer
|
|
943
|
+
// can attach its `session/update` handler. Without this buffer those
|
|
944
|
+
// notifications would be silently dropped, so e.g. `/model` would
|
|
945
|
+
// never appear in the TUI's slash-completion palette. Capped per
|
|
946
|
+
// method to keep the buffer from growing unboundedly when nothing
|
|
947
|
+
// ever subscribes.
|
|
948
|
+
bufferedNotifications = /* @__PURE__ */ new Map();
|
|
949
|
+
static MAX_BUFFERED_PER_METHOD = 64;
|
|
875
950
|
pending = /* @__PURE__ */ new Map();
|
|
876
951
|
closed = false;
|
|
877
952
|
closeHandlers = [];
|
|
@@ -883,6 +958,17 @@ var JsonRpcConnection = class {
|
|
|
883
958
|
}
|
|
884
959
|
onNotification(method, handler) {
|
|
885
960
|
this.notificationHandlers.set(method, handler);
|
|
961
|
+
const queued = this.bufferedNotifications.get(method);
|
|
962
|
+
if (!queued) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
this.bufferedNotifications.delete(method);
|
|
966
|
+
for (const note of queued) {
|
|
967
|
+
try {
|
|
968
|
+
handler(note.params, note.method);
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
886
972
|
}
|
|
887
973
|
onClose(handler) {
|
|
888
974
|
this.closeHandlers.push(handler);
|
|
@@ -968,6 +1054,16 @@ var JsonRpcConnection = class {
|
|
|
968
1054
|
const handler = this.notificationHandlers.get(note.method);
|
|
969
1055
|
if (handler) {
|
|
970
1056
|
handler(note.params, note.method);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
let queued = this.bufferedNotifications.get(note.method);
|
|
1060
|
+
if (!queued) {
|
|
1061
|
+
queued = [];
|
|
1062
|
+
this.bufferedNotifications.set(note.method, queued);
|
|
1063
|
+
}
|
|
1064
|
+
queued.push(note);
|
|
1065
|
+
if (queued.length > _JsonRpcConnection.MAX_BUFFERED_PER_METHOD) {
|
|
1066
|
+
queued.shift();
|
|
971
1067
|
}
|
|
972
1068
|
}
|
|
973
1069
|
handleResponse(res) {
|
|
@@ -1071,12 +1167,12 @@ import { customAlphabet } from "nanoid";
|
|
|
1071
1167
|
var HYDRA_COMMANDS = [
|
|
1072
1168
|
{
|
|
1073
1169
|
verb: "title",
|
|
1074
|
-
name: "
|
|
1170
|
+
name: "hydra title",
|
|
1075
1171
|
description: "Regenerate the session title via the agent (or set manually with an arg)"
|
|
1076
1172
|
},
|
|
1077
1173
|
{
|
|
1078
1174
|
verb: "agent",
|
|
1079
|
-
name: "
|
|
1175
|
+
name: "hydra agent",
|
|
1080
1176
|
argsHint: "<agent>",
|
|
1081
1177
|
description: "Swap the agent backing this session, preserving context"
|
|
1082
1178
|
}
|
|
@@ -2598,7 +2694,8 @@ var SessionManager = class {
|
|
|
2598
2694
|
agentId: params.agentId,
|
|
2599
2695
|
cwd: params.cwd,
|
|
2600
2696
|
agentArgs: params.agentArgs,
|
|
2601
|
-
mcpServers: params.mcpServers
|
|
2697
|
+
mcpServers: params.mcpServers,
|
|
2698
|
+
model: params.model
|
|
2602
2699
|
});
|
|
2603
2700
|
const session = new Session({
|
|
2604
2701
|
cwd: params.cwd,
|
|
@@ -2790,7 +2887,7 @@ var SessionManager = class {
|
|
|
2790
2887
|
);
|
|
2791
2888
|
}
|
|
2792
2889
|
let initialModel = extractInitialModel(newResult);
|
|
2793
|
-
const desired = this.defaultModels[params.agentId];
|
|
2890
|
+
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
2794
2891
|
if (desired && desired !== initialModel) {
|
|
2795
2892
|
try {
|
|
2796
2893
|
await agent.connection.request("session/set_model", {
|
|
@@ -4305,7 +4402,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
4305
4402
|
agentId: params.agentId ?? deps.defaultAgent,
|
|
4306
4403
|
mcpServers: params.mcpServers,
|
|
4307
4404
|
title: hydraMeta.name,
|
|
4308
|
-
agentArgs: hydraMeta.agentArgs
|
|
4405
|
+
agentArgs: hydraMeta.agentArgs,
|
|
4406
|
+
model: hydraMeta.model
|
|
4309
4407
|
});
|
|
4310
4408
|
const client = bindClientToSession(connection, session, state);
|
|
4311
4409
|
await session.attach(client, "full");
|