@cabane/companion 0.6.50 → 0.6.51

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/cli.js CHANGED
@@ -5,7 +5,7 @@ import { Command, Option } from "commander";
5
5
 
6
6
  // src/control-socket.ts
7
7
  import { createHash } from "crypto";
8
- import { existsSync as existsSync2, rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
8
+ import { chmodSync as chmodSync2, existsSync as existsSync2, rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
9
9
  import { createServer, connect } from "net";
10
10
  import { join as join2 } from "path";
11
11
 
@@ -495,13 +495,18 @@ function deleteConfig() {
495
495
 
496
496
  // src/control-socket.ts
497
497
  var CONTROL_TIMEOUT_MS = 1e3;
498
+ var MAX_UNIX_SOCKET_PATH_BYTES = 103;
498
499
  function controlSocketPath() {
499
500
  const dir2 = cabaneDir();
501
+ const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
500
502
  if (process.platform === "win32") {
501
- const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
502
503
  return `\\\\.\\pipe\\cabane-companion-${key}`;
503
504
  }
504
- return join2(dir2, "companion.sock");
505
+ const besideRuntime = join2(dir2, "companion.sock");
506
+ if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
507
+ const filename = `cabane-companion-${key}.sock`;
508
+ const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync2(process.env.XDG_RUNTIME_DIR) ? join2(process.env.XDG_RUNTIME_DIR, filename) : null;
509
+ return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join2("/tmp", filename);
505
510
  }
506
511
  async function startControlServer(handlers) {
507
512
  const path = controlSocketPath();
@@ -522,6 +527,18 @@ async function startControlServer(handlers) {
522
527
  resolve();
523
528
  });
524
529
  });
530
+ if (process.platform !== "win32") {
531
+ try {
532
+ chmodSync2(path, 384);
533
+ } catch (err) {
534
+ await new Promise((resolve) => server.close(() => resolve()));
535
+ try {
536
+ rmSync2(path, { force: true });
537
+ } catch {
538
+ }
539
+ throw err;
540
+ }
541
+ }
525
542
  server.on("error", () => {
526
543
  });
527
544
  return {
@@ -1348,7 +1365,7 @@ import { confirm as confirm2 } from "@inquirer/prompts";
1348
1365
 
1349
1366
  // src/credentials.ts
1350
1367
  import {
1351
- chmodSync as chmodSync2,
1368
+ chmodSync as chmodSync3,
1352
1369
  existsSync as existsSync4,
1353
1370
  mkdirSync as mkdirSync4,
1354
1371
  readFileSync as readFileSync3,
@@ -1383,14 +1400,14 @@ function save(map) {
1383
1400
  const path = credentialsPath();
1384
1401
  mkdirSync4(dirname2(path), { recursive: true });
1385
1402
  try {
1386
- chmodSync2(cabaneDir(), 448);
1403
+ chmodSync3(cabaneDir(), 448);
1387
1404
  } catch {
1388
1405
  }
1389
1406
  const tmp = `${path}.${process.pid}.tmp`;
1390
1407
  try {
1391
1408
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
1392
1409
  try {
1393
- chmodSync2(tmp, 384);
1410
+ chmodSync3(tmp, 384);
1394
1411
  } catch {
1395
1412
  }
1396
1413
  renameSync2(tmp, path);
@@ -7188,7 +7205,7 @@ function resolveMcpSecrets(mcpServers, store) {
7188
7205
  }
7189
7206
 
7190
7207
  // src/transcript-writer.ts
7191
- import { appendFileSync, chmodSync as chmodSync3, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
7208
+ import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
7192
7209
  import { basename, dirname as dirname5, join as join13 } from "path";
7193
7210
  function transcriptsDir() {
7194
7211
  return join13(cabaneDir(), "transcripts");
@@ -7205,7 +7222,7 @@ var TranscriptWriter = class {
7205
7222
  try {
7206
7223
  mkdirSync9(dir2, { recursive: true });
7207
7224
  try {
7208
- chmodSync3(dir2, 448);
7225
+ chmodSync4(dir2, 448);
7209
7226
  } catch {
7210
7227
  }
7211
7228
  pruneOld(dir2, RETAIN);
@@ -7232,10 +7249,10 @@ var TranscriptWriter = class {
7232
7249
  try {
7233
7250
  const dir2 = join13(dirname5(this.path), "anomalies");
7234
7251
  mkdirSync9(dir2, { recursive: true, mode: 448 });
7235
- chmodSync3(dir2, 448);
7252
+ chmodSync4(dir2, 448);
7236
7253
  const target = join13(dir2, basename(this.path));
7237
7254
  copyFileSync(this.path, target);
7238
- chmodSync3(target, 384);
7255
+ chmodSync4(target, 384);
7239
7256
  pruneOld(dir2, ANOMALY_RETAIN);
7240
7257
  } catch (err) {
7241
7258
  this.fail(err);
@@ -10120,7 +10137,8 @@ async function createCompanionRuntime(opts = {}) {
10120
10137
  if (!claim.acquired) {
10121
10138
  return { ok: false, reason: "already-running", existing: claim.existing ?? null };
10122
10139
  }
10123
- process.on("exit", () => clearRuntimeStateIfOurs(instanceId));
10140
+ const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
10141
+ process.on("exit", clearRuntimeOnExit);
10124
10142
  const hub = new CompanionStateHub({
10125
10143
  // CT29: one device, one base URL — the cabane instance this device is paired
10126
10144
  // with. The dashboard's connection line shows it.
@@ -10155,41 +10173,60 @@ async function createCompanionRuntime(opts = {}) {
10155
10173
  if (!result.ok) return { ok: false, error: result.error };
10156
10174
  return { ok: true, message: connectedLine(runtime, verdict) };
10157
10175
  };
10158
- const control = await startControlServer({
10159
- status: () => hub.statusJson(),
10160
- connect: async (runtime, serverUrl) => {
10161
- const result = await connectHarness(runtime, serverUrl);
10162
- return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
10163
- },
10164
- stop: () => void supervisor.requestStop()
10165
- });
10176
+ let control = null;
10166
10177
  let dashboard = null;
10167
- if (opts.dashboard) {
10168
- const preferredPort = opts.port ?? cfg.dashboardPort;
10169
- dashboard = await startDashboard({
10170
- supervisor,
10171
- hub,
10172
- ...preferredPort !== void 0 ? { port: preferredPort } : {}
10178
+ try {
10179
+ control = await startControlServer({
10180
+ status: () => hub.statusJson(),
10181
+ connect: async (runtime, serverUrl) => {
10182
+ const result = await connectHarness(runtime, serverUrl);
10183
+ return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
10184
+ },
10185
+ stop: () => void supervisor.requestStop()
10186
+ });
10187
+ if (opts.dashboard) {
10188
+ const preferredPort = opts.port ?? cfg.dashboardPort;
10189
+ dashboard = await startDashboard({
10190
+ supervisor,
10191
+ hub,
10192
+ ...preferredPort !== void 0 ? { port: preferredPort } : {}
10193
+ });
10194
+ hub.setDashboardUrl(dashboard.url);
10195
+ }
10196
+ writeRuntimeState({
10197
+ pid: process.pid,
10198
+ socket: control.path,
10199
+ ...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
10200
+ startedAt,
10201
+ // SJ495: the daemon launcher sets this env on the detached child, so the
10202
+ // marker records whether this companion is backgrounded (foreground start
10203
+ // leaves it unset → false).
10204
+ daemon: process.env.CABANE_COMPANION_DAEMON === "1",
10205
+ instanceId
10173
10206
  });
10174
- hub.setDashboardUrl(dashboard.url);
10207
+ clearCrash();
10208
+ } catch (err) {
10209
+ clearRuntimeStateIfOurs(instanceId);
10210
+ process.removeListener("exit", clearRuntimeOnExit);
10211
+ await Promise.allSettled([
10212
+ supervisor.shutdown(),
10213
+ ...control ? [control.close()] : [],
10214
+ ...dashboard ? [dashboard.close()] : []
10215
+ ]);
10216
+ recordCrash({
10217
+ reason: err instanceof Error ? err.message : String(err),
10218
+ ...errorCode(err) ? { code: errorCode(err) } : {},
10219
+ origin: "startup",
10220
+ at: (/* @__PURE__ */ new Date()).toISOString()
10221
+ });
10222
+ throw err;
10175
10223
  }
10176
- writeRuntimeState({
10177
- pid: process.pid,
10178
- socket: control.path,
10179
- ...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
10180
- startedAt,
10181
- // SJ495: the daemon launcher sets this env on the detached child, so the
10182
- // marker records whether this companion is backgrounded (foreground start
10183
- // leaves it unset → false).
10184
- daemon: process.env.CABANE_COMPANION_DAEMON === "1",
10185
- instanceId
10186
- });
10187
- clearCrash();
10188
10224
  let stopped = false;
10189
10225
  const stop2 = async () => {
10190
10226
  if (stopped) return;
10191
10227
  stopped = true;
10192
10228
  clearRuntimeStateIfOurs(instanceId);
10229
+ process.removeListener("exit", clearRuntimeOnExit);
10193
10230
  try {
10194
10231
  await supervisor.shutdown();
10195
10232
  await closeSurfaces(control, dashboard);
package/dist/runtime.js CHANGED
@@ -1387,17 +1387,22 @@ function resolveStaticDir() {
1387
1387
 
1388
1388
  // src/control-socket.ts
1389
1389
  import { createHash } from "crypto";
1390
- import { existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
1390
+ import { chmodSync as chmodSync2, existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
1391
1391
  import { createServer, connect } from "net";
1392
1392
  import { join as join5 } from "path";
1393
1393
  var CONTROL_TIMEOUT_MS = 1e3;
1394
+ var MAX_UNIX_SOCKET_PATH_BYTES = 103;
1394
1395
  function controlSocketPath() {
1395
1396
  const dir2 = cabaneDir();
1397
+ const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
1396
1398
  if (process.platform === "win32") {
1397
- const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
1398
1399
  return `\\\\.\\pipe\\cabane-companion-${key}`;
1399
1400
  }
1400
- return join5(dir2, "companion.sock");
1401
+ const besideRuntime = join5(dir2, "companion.sock");
1402
+ if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
1403
+ const filename = `cabane-companion-${key}.sock`;
1404
+ const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync3(process.env.XDG_RUNTIME_DIR) ? join5(process.env.XDG_RUNTIME_DIR, filename) : null;
1405
+ return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join5("/tmp", filename);
1401
1406
  }
1402
1407
  async function startControlServer(handlers) {
1403
1408
  const path = controlSocketPath();
@@ -1418,6 +1423,18 @@ async function startControlServer(handlers) {
1418
1423
  resolve();
1419
1424
  });
1420
1425
  });
1426
+ if (process.platform !== "win32") {
1427
+ try {
1428
+ chmodSync2(path, 384);
1429
+ } catch (err) {
1430
+ await new Promise((resolve) => server.close(() => resolve()));
1431
+ try {
1432
+ rmSync2(path, { force: true });
1433
+ } catch {
1434
+ }
1435
+ throw err;
1436
+ }
1437
+ }
1421
1438
  server.on("error", () => {
1422
1439
  });
1423
1440
  return {
@@ -2162,7 +2179,7 @@ function errorMessage2(status, body) {
2162
2179
 
2163
2180
  // src/credentials.ts
2164
2181
  import {
2165
- chmodSync as chmodSync2,
2182
+ chmodSync as chmodSync3,
2166
2183
  existsSync as existsSync5,
2167
2184
  mkdirSync as mkdirSync5,
2168
2185
  readFileSync as readFileSync3,
@@ -2197,14 +2214,14 @@ function save(map) {
2197
2214
  const path = credentialsPath();
2198
2215
  mkdirSync5(dirname4(path), { recursive: true });
2199
2216
  try {
2200
- chmodSync2(cabaneDir(), 448);
2217
+ chmodSync3(cabaneDir(), 448);
2201
2218
  } catch {
2202
2219
  }
2203
2220
  const tmp = `${path}.${process.pid}.tmp`;
2204
2221
  try {
2205
2222
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
2206
2223
  try {
2207
- chmodSync2(tmp, 384);
2224
+ chmodSync3(tmp, 384);
2208
2225
  } catch {
2209
2226
  }
2210
2227
  renameSync2(tmp, path);
@@ -6687,7 +6704,7 @@ function resolveMcpSecrets(mcpServers, store) {
6687
6704
  }
6688
6705
 
6689
6706
  // src/transcript-writer.ts
6690
- import { appendFileSync, chmodSync as chmodSync3, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
6707
+ import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
6691
6708
  import { basename, dirname as dirname5, join as join13 } from "path";
6692
6709
  function transcriptsDir() {
6693
6710
  return join13(cabaneDir(), "transcripts");
@@ -6704,7 +6721,7 @@ var TranscriptWriter = class {
6704
6721
  try {
6705
6722
  mkdirSync9(dir2, { recursive: true });
6706
6723
  try {
6707
- chmodSync3(dir2, 448);
6724
+ chmodSync4(dir2, 448);
6708
6725
  } catch {
6709
6726
  }
6710
6727
  pruneOld(dir2, RETAIN);
@@ -6731,10 +6748,10 @@ var TranscriptWriter = class {
6731
6748
  try {
6732
6749
  const dir2 = join13(dirname5(this.path), "anomalies");
6733
6750
  mkdirSync9(dir2, { recursive: true, mode: 448 });
6734
- chmodSync3(dir2, 448);
6751
+ chmodSync4(dir2, 448);
6735
6752
  const target = join13(dir2, basename(this.path));
6736
6753
  copyFileSync(this.path, target);
6737
- chmodSync3(target, 384);
6754
+ chmodSync4(target, 384);
6738
6755
  pruneOld(dir2, ANOMALY_RETAIN);
6739
6756
  } catch (err) {
6740
6757
  this.fail(err);
@@ -9619,7 +9636,8 @@ async function createCompanionRuntime(opts = {}) {
9619
9636
  if (!claim.acquired) {
9620
9637
  return { ok: false, reason: "already-running", existing: claim.existing ?? null };
9621
9638
  }
9622
- process.on("exit", () => clearRuntimeStateIfOurs(instanceId));
9639
+ const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
9640
+ process.on("exit", clearRuntimeOnExit);
9623
9641
  const hub = new CompanionStateHub({
9624
9642
  // CT29: one device, one base URL — the cabane instance this device is paired
9625
9643
  // with. The dashboard's connection line shows it.
@@ -9654,41 +9672,60 @@ async function createCompanionRuntime(opts = {}) {
9654
9672
  if (!result.ok) return { ok: false, error: result.error };
9655
9673
  return { ok: true, message: connectedLine(runtime, verdict) };
9656
9674
  };
9657
- const control = await startControlServer({
9658
- status: () => hub.statusJson(),
9659
- connect: async (runtime, serverUrl) => {
9660
- const result = await connectHarness(runtime, serverUrl);
9661
- return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
9662
- },
9663
- stop: () => void supervisor.requestStop()
9664
- });
9675
+ let control = null;
9665
9676
  let dashboard = null;
9666
- if (opts.dashboard) {
9667
- const preferredPort = opts.port ?? cfg.dashboardPort;
9668
- dashboard = await startDashboard({
9669
- supervisor,
9670
- hub,
9671
- ...preferredPort !== void 0 ? { port: preferredPort } : {}
9677
+ try {
9678
+ control = await startControlServer({
9679
+ status: () => hub.statusJson(),
9680
+ connect: async (runtime, serverUrl) => {
9681
+ const result = await connectHarness(runtime, serverUrl);
9682
+ return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
9683
+ },
9684
+ stop: () => void supervisor.requestStop()
9685
+ });
9686
+ if (opts.dashboard) {
9687
+ const preferredPort = opts.port ?? cfg.dashboardPort;
9688
+ dashboard = await startDashboard({
9689
+ supervisor,
9690
+ hub,
9691
+ ...preferredPort !== void 0 ? { port: preferredPort } : {}
9692
+ });
9693
+ hub.setDashboardUrl(dashboard.url);
9694
+ }
9695
+ writeRuntimeState({
9696
+ pid: process.pid,
9697
+ socket: control.path,
9698
+ ...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
9699
+ startedAt,
9700
+ // SJ495: the daemon launcher sets this env on the detached child, so the
9701
+ // marker records whether this companion is backgrounded (foreground start
9702
+ // leaves it unset → false).
9703
+ daemon: process.env.CABANE_COMPANION_DAEMON === "1",
9704
+ instanceId
9672
9705
  });
9673
- hub.setDashboardUrl(dashboard.url);
9706
+ clearCrash();
9707
+ } catch (err) {
9708
+ clearRuntimeStateIfOurs(instanceId);
9709
+ process.removeListener("exit", clearRuntimeOnExit);
9710
+ await Promise.allSettled([
9711
+ supervisor.shutdown(),
9712
+ ...control ? [control.close()] : [],
9713
+ ...dashboard ? [dashboard.close()] : []
9714
+ ]);
9715
+ recordCrash({
9716
+ reason: err instanceof Error ? err.message : String(err),
9717
+ ...errorCode(err) ? { code: errorCode(err) } : {},
9718
+ origin: "startup",
9719
+ at: (/* @__PURE__ */ new Date()).toISOString()
9720
+ });
9721
+ throw err;
9674
9722
  }
9675
- writeRuntimeState({
9676
- pid: process.pid,
9677
- socket: control.path,
9678
- ...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
9679
- startedAt,
9680
- // SJ495: the daemon launcher sets this env on the detached child, so the
9681
- // marker records whether this companion is backgrounded (foreground start
9682
- // leaves it unset → false).
9683
- daemon: process.env.CABANE_COMPANION_DAEMON === "1",
9684
- instanceId
9685
- });
9686
- clearCrash();
9687
9723
  let stopped = false;
9688
9724
  const stop = async () => {
9689
9725
  if (stopped) return;
9690
9726
  stopped = true;
9691
9727
  clearRuntimeStateIfOurs(instanceId);
9728
+ process.removeListener("exit", clearRuntimeOnExit);
9692
9729
  try {
9693
9730
  await supervisor.shutdown();
9694
9731
  await closeSurfaces(control, dashboard);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.50",
3
+ "version": "0.6.51",
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",