@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/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, extraArgs?: string[]): Promise<SpawnPlan>;
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({ repaintThrottleMs: 1e3, maxScrollbackLines: 1e4 })
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 loadConfig() {
128
- const configPath = paths.config();
140
+ async function readConfigFile() {
129
141
  let raw;
130
142
  try {
131
- raw = await fs.readFile(configPath, "utf8");
143
+ raw = await fs.readFile(paths.config(), "utf8");
132
144
  } catch (err) {
133
145
  const e = err;
134
146
  if (e.code === "ENOENT") {
135
- throw new Error(
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
- const parsed = JSON.parse(raw);
142
- return HydraConfig.parse(parsed);
151
+ return JSON.parse(raw);
143
152
  }
144
- async function ensureConfig() {
153
+ async function loadAuthToken() {
154
+ let tokenFile;
145
155
  try {
146
- await fs.access(paths.config());
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
- const config = await writeMinimalInitConfig();
153
- process.stderr.write(
154
- `hydra-acp: initialized ${paths.config()} with a fresh auth token.
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
- return loadConfig();
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 writeConfig(config) {
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.config(), JSON.stringify(config, null, 2) + "\n", {
201
+ await fs.writeFile(paths.authToken(), token + "\n", {
164
202
  encoding: "utf8",
165
203
  mode: 384
166
204
  });
167
205
  }
168
- async function writeMinimalInitConfig(authToken) {
169
- const token = authToken ?? generateAuthToken();
170
- const minimal = { daemon: { authToken: token } };
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
- await fs.writeFile(paths.config(), JSON.stringify(minimal, null, 2) + "\n", {
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, extraArgs = []) {
637
+ async function planSpawn(agent, callerArgs = []) {
577
638
  if (agent.distribution.npx) {
578
639
  const npx = agent.distribution.npx;
579
- const args = ["-y", npx.package, ...npx.args ?? [], ...extraArgs];
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: [...target.args ?? [], ...extraArgs],
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 args = [uvx.package, ...uvx.args ?? [], ...extraArgs];
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: "/hydra title",
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: "/hydra agent",
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Multi-client ACP session daemon: spawn agents, attach over WSS, multiplex sessions across editors.",
5
5
  "license": "MIT",
6
6
  "type": "module",