@cabane/companion 0.6.22 → 0.6.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,7 +142,7 @@ A `coding` agent needs a directory to run in — a path that only exists on your
142
142
  "agents": {
143
143
  "my-coding-agent": {
144
144
  "cwd": "/Users/you/code/project", // working directory the agent runs in
145
- "prepareHook": "git -C /Users/you/code/project pull --ff-only", // optional: runs before each turn
145
+ "prepareHook": { "command": "/Users/you/bin/prepare-turn" }, // optional: resolves the cwd before each turn
146
146
  },
147
147
  },
148
148
  }
@@ -150,6 +150,19 @@ A `coding` agent needs a directory to run in — a path that only exists on your
150
150
 
151
151
  A malformed `agents` block fails `cabane-companion start` immediately with a message pointing at the exact field — it won't silently fall back.
152
152
 
153
+ **A hook that is the same for every agent goes at the top level instead.** If your hook works out the directory itself — from the conversation and agent ids the Companion hands it — then listing every agent by name is a roster you have to remember to update, and an agent you forget gets no hook at all and starts its turn wherever the Companion happens to be. So `prepareHook` can sit beside `agents` rather than inside it:
154
+
155
+ ```jsonc
156
+ {
157
+ "prepareHook": { "command": "/Users/you/bin/prepare-turn" }, // every agent on this device
158
+ "agents": {
159
+ "special-case": { "prepareHook": { "command": "/Users/you/bin/other-hook" } }, // wins for this one
160
+ },
161
+ }
162
+ ```
163
+
164
+ The agent's own entry wins when it names a hook; otherwise the device-level one applies. It survives a re-pair along with the rest of your local settings, and `cabane-companion status` prints it. A hook that fails still fails the turn loudly either way.
165
+
153
166
  ### Auto-memory (off by default)
154
167
 
155
168
  Claude Code's **auto-memory** — where the agent writes notes to a local `~/.claude` memory directory and recalls them on later turns — is **off by default** on every Companion. The stance is that memory-shaped things belong in your Cabane workspace (a shared, versioned `ABOUT.md`-style file or doc), not a private local directory that no one else can see. If you run your own Companion and want your normal Claude Code memory workflow back, set `claudeCode.autoMemory: true` on the agent — the Companion then stops overriding it and your own `~/.claude/settings.json` governs auto-memory as usual (this bites in `coding` mode, where your project settings are read):
package/dist/cli.js CHANGED
@@ -278,6 +278,22 @@ var companionConfigSchema = z2.object({
278
278
  // agentId / username / `slug/username`. Hand-added by the operator; the companion
279
279
  // never writes this (it only persists credentials + the device, elsewhere).
280
280
  agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
281
+ // CT1095: the DEVICE-LEVEL prepare hook — one entry that applies to every agent
282
+ // assigned to this device, unless that agent's own `agents` row names a hook of
283
+ // its own.
284
+ //
285
+ // Until now `prepareHook` existed only under `agents[<id>]`, which made the
286
+ // config a second silent roster: a device whose provisioning is genuinely
287
+ // per-device (ours resolves a directory from the conversation and agent ids the
288
+ // companion already hands the hook) still had to list every agent by id, and an
289
+ // agent nobody remembered to add got no hook at all — no error, no log, just a
290
+ // turn that started in the operator's `$HOME`. That is how @dale woke in
291
+ // `/home/joshed` on 2026-08-17 with a perfectly correct dispatch.
292
+ //
293
+ // Absent → nothing changes for anyone, which is why this can land inert: a
294
+ // device with no device-level entry behaves exactly as it did before. A hook
295
+ // that fails still fails the dispatch loudly, as now.
296
+ prepareHook: prepareHookSchema.optional(),
281
297
  // Dashboard settings (all optional). dashboardPort: preferred bind port (next
282
298
  // free one if taken); autoOpen: whether `start` opens the browser (the
283
299
  // `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
@@ -340,7 +356,9 @@ function migrateConnectedHarnesses(cfg, claudeOnPath2) {
340
356
  }
341
357
  function localAgentConfig(cfg, agent) {
342
358
  const map = cfg.agents ?? {};
343
- return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
359
+ const row = map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
360
+ if (!cfg.prepareHook || row.prepareHook) return row;
361
+ return { ...row, prepareHook: cfg.prepareHook };
344
362
  }
345
363
  function loadConfig() {
346
364
  const path = configPath();
@@ -400,10 +418,11 @@ function loadConfigTolerant() {
400
418
  }
401
419
  const strict = companionConfigSchema.safeParse(parsed);
402
420
  if (strict.success) {
403
- const { agents: agents2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
421
+ const { agents: agents2, prepareHook: prepareHook2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
404
422
  return {
405
423
  local: {
406
424
  ...agents2 !== void 0 ? { agents: agents2 } : {},
425
+ ...prepareHook2 !== void 0 ? { prepareHook: prepareHook2 } : {},
407
426
  ...dashboardPort !== void 0 ? { dashboardPort } : {},
408
427
  ...autoOpen !== void 0 ? { autoOpen } : {},
409
428
  ...logLevel !== void 0 ? { logLevel } : {},
@@ -417,6 +436,8 @@ function loadConfigTolerant() {
417
436
  const local = {};
418
437
  const agents = z2.record(z2.string(), localAgentConfigSchema).safeParse(obj.agents);
419
438
  if (agents.success) local.agents = agents.data;
439
+ const prepareHook = prepareHookSchema.safeParse(obj.prepareHook);
440
+ if (prepareHook.success) local.prepareHook = prepareHook.data;
420
441
  if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
421
442
  if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
422
443
  if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
@@ -8214,7 +8235,8 @@ var SseSubscriber = class {
8214
8235
  }
8215
8236
  async connect() {
8216
8237
  const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
8217
- const url = `${base}/api/workspaces/${this.opts.workspaceId}/events`;
8238
+ const query = this.lastEventId ? "" : "?replayPending=1";
8239
+ const url = `${base}/api/workspaces/${this.opts.workspaceId}/events${query}`;
8218
8240
  this.controller = new AbortController();
8219
8241
  const headers = {
8220
8242
  Authorization: `Bearer ${this.opts.token}`,
@@ -9449,6 +9471,10 @@ async function status() {
9449
9471
  names.length > 0 ? `secrets: ${names.length} declared (${names.join(", ")})
9450
9472
  ` : "secrets: none declared \u2014 add `${VAR}` values to ~/.cabane/secrets.json for MCP servers that need them\n"
9451
9473
  );
9474
+ if (cfg.prepareHook) {
9475
+ process.stdout.write(`prepare hook: ${cfg.prepareHook.command} (device-level, all agents)
9476
+ `);
9477
+ }
9452
9478
  const overrides = Object.keys(cfg.agents ?? {});
9453
9479
  if (overrides.length > 0) {
9454
9480
  process.stdout.write(`local overrides for: ${overrides.join(", ")}
@@ -144,6 +144,22 @@ var companionConfigSchema = z2.object({
144
144
  // agentId / username / `slug/username`. Hand-added by the operator; the companion
145
145
  // never writes this (it only persists credentials + the device, elsewhere).
146
146
  agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
147
+ // CT1095: the DEVICE-LEVEL prepare hook — one entry that applies to every agent
148
+ // assigned to this device, unless that agent's own `agents` row names a hook of
149
+ // its own.
150
+ //
151
+ // Until now `prepareHook` existed only under `agents[<id>]`, which made the
152
+ // config a second silent roster: a device whose provisioning is genuinely
153
+ // per-device (ours resolves a directory from the conversation and agent ids the
154
+ // companion already hands the hook) still had to list every agent by id, and an
155
+ // agent nobody remembered to add got no hook at all — no error, no log, just a
156
+ // turn that started in the operator's `$HOME`. That is how @dale woke in
157
+ // `/home/joshed` on 2026-08-17 with a perfectly correct dispatch.
158
+ //
159
+ // Absent → nothing changes for anyone, which is why this can land inert: a
160
+ // device with no device-level entry behaves exactly as it did before. A hook
161
+ // that fails still fails the dispatch loudly, as now.
162
+ prepareHook: prepareHookSchema.optional(),
147
163
  // Dashboard settings (all optional). dashboardPort: preferred bind port (next
148
164
  // free one if taken); autoOpen: whether `start` opens the browser (the
149
165
  // `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
@@ -252,10 +268,11 @@ function loadConfigTolerant() {
252
268
  }
253
269
  const strict = companionConfigSchema.safeParse(parsed);
254
270
  if (strict.success) {
255
- const { agents: agents2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
271
+ const { agents: agents2, prepareHook: prepareHook2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
256
272
  return {
257
273
  local: {
258
274
  ...agents2 !== void 0 ? { agents: agents2 } : {},
275
+ ...prepareHook2 !== void 0 ? { prepareHook: prepareHook2 } : {},
259
276
  ...dashboardPort !== void 0 ? { dashboardPort } : {},
260
277
  ...autoOpen !== void 0 ? { autoOpen } : {},
261
278
  ...logLevel !== void 0 ? { logLevel } : {},
@@ -269,6 +286,8 @@ function loadConfigTolerant() {
269
286
  const local = {};
270
287
  const agents = z2.record(z2.string(), localAgentConfigSchema).safeParse(obj.agents);
271
288
  if (agents.success) local.agents = agents.data;
289
+ const prepareHook = prepareHookSchema.safeParse(obj.prepareHook);
290
+ if (prepareHook.success) local.prepareHook = prepareHook.data;
272
291
  if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
273
292
  if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
274
293
  if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
package/dist/runtime.js CHANGED
@@ -271,6 +271,22 @@ var companionConfigSchema = z2.object({
271
271
  // agentId / username / `slug/username`. Hand-added by the operator; the companion
272
272
  // never writes this (it only persists credentials + the device, elsewhere).
273
273
  agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
274
+ // CT1095: the DEVICE-LEVEL prepare hook — one entry that applies to every agent
275
+ // assigned to this device, unless that agent's own `agents` row names a hook of
276
+ // its own.
277
+ //
278
+ // Until now `prepareHook` existed only under `agents[<id>]`, which made the
279
+ // config a second silent roster: a device whose provisioning is genuinely
280
+ // per-device (ours resolves a directory from the conversation and agent ids the
281
+ // companion already hands the hook) still had to list every agent by id, and an
282
+ // agent nobody remembered to add got no hook at all — no error, no log, just a
283
+ // turn that started in the operator's `$HOME`. That is how @dale woke in
284
+ // `/home/joshed` on 2026-08-17 with a perfectly correct dispatch.
285
+ //
286
+ // Absent → nothing changes for anyone, which is why this can land inert: a
287
+ // device with no device-level entry behaves exactly as it did before. A hook
288
+ // that fails still fails the dispatch loudly, as now.
289
+ prepareHook: prepareHookSchema.optional(),
274
290
  // Dashboard settings (all optional). dashboardPort: preferred bind port (next
275
291
  // free one if taken); autoOpen: whether `start` opens the browser (the
276
292
  // `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
@@ -333,7 +349,9 @@ function migrateConnectedHarnesses(cfg, claudeOnPath2) {
333
349
  }
334
350
  function localAgentConfig(cfg, agent) {
335
351
  const map = cfg.agents ?? {};
336
- return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
352
+ const row = map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
353
+ if (!cfg.prepareHook || row.prepareHook) return row;
354
+ return { ...row, prepareHook: cfg.prepareHook };
337
355
  }
338
356
  function loadConfig() {
339
357
  const path = configPath();
@@ -7836,7 +7854,8 @@ var SseSubscriber = class {
7836
7854
  }
7837
7855
  async connect() {
7838
7856
  const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
7839
- const url = `${base}/api/workspaces/${this.opts.workspaceId}/events`;
7857
+ const query = this.lastEventId ? "" : "?replayPending=1";
7858
+ const url = `${base}/api/workspaces/${this.opts.workspaceId}/events${query}`;
7840
7859
  this.controller = new AbortController();
7841
7860
  const headers = {
7842
7861
  Authorization: `Bearer ${this.opts.token}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.22",
3
+ "version": "0.6.24",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",