@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 +14 -1
- package/dist/cli.js +29 -3
- package/dist/pairing-config.js +20 -1
- package/dist/runtime.js +21 -2
- package/package.json +1 -1
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": "
|
|
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
|
-
|
|
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
|
|
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(", ")}
|
package/dist/pairing-config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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",
|