@hydra-acp/cli 0.1.6 → 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;
@@ -2098,6 +2105,7 @@ declare function hydraHome(): string;
2098
2105
  declare const paths: {
2099
2106
  home: typeof hydraHome;
2100
2107
  config: () => string;
2108
+ authToken: () => string;
2101
2109
  pidFile: () => string;
2102
2110
  logFile: () => string;
2103
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
  }
@@ -865,7 +927,7 @@ function ndjsonStreamFromStdio(stdout, stdin) {
865
927
 
866
928
  // src/acp/connection.ts
867
929
  import { nanoid } from "nanoid";
868
- var JsonRpcConnection = class {
930
+ var JsonRpcConnection = class _JsonRpcConnection {
869
931
  constructor(stream) {
870
932
  this.stream = stream;
871
933
  this.stream.onMessage((m) => this.handleIncoming(m));
@@ -875,6 +937,16 @@ var JsonRpcConnection = class {
875
937
  requestHandlers = /* @__PURE__ */ new Map();
876
938
  defaultRequestHandler;
877
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;
878
950
  pending = /* @__PURE__ */ new Map();
879
951
  closed = false;
880
952
  closeHandlers = [];
@@ -886,6 +958,17 @@ var JsonRpcConnection = class {
886
958
  }
887
959
  onNotification(method, handler) {
888
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
+ }
889
972
  }
890
973
  onClose(handler) {
891
974
  this.closeHandlers.push(handler);
@@ -971,6 +1054,16 @@ var JsonRpcConnection = class {
971
1054
  const handler = this.notificationHandlers.get(note.method);
972
1055
  if (handler) {
973
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();
974
1067
  }
975
1068
  }
976
1069
  handleResponse(res) {
@@ -1074,12 +1167,12 @@ import { customAlphabet } from "nanoid";
1074
1167
  var HYDRA_COMMANDS = [
1075
1168
  {
1076
1169
  verb: "title",
1077
- name: "/hydra title",
1170
+ name: "hydra title",
1078
1171
  description: "Regenerate the session title via the agent (or set manually with an arg)"
1079
1172
  },
1080
1173
  {
1081
1174
  verb: "agent",
1082
- name: "/hydra agent",
1175
+ name: "hydra agent",
1083
1176
  argsHint: "<agent>",
1084
1177
  description: "Swap the agent backing this session, preserving context"
1085
1178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/cli",
3
- "version": "0.1.6",
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",