@cabane/companion 0.6.29 → 0.6.31

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
@@ -6475,7 +6475,7 @@ var ConnectorHealthStore = class {
6475
6475
 
6476
6476
  // src/dispatcher.ts
6477
6477
  import { randomUUID } from "crypto";
6478
- import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, statSync } from "fs";
6478
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, readdirSync as readdirSync2, statSync } from "fs";
6479
6479
  import { join as join15 } from "path";
6480
6480
 
6481
6481
  // src/summon.ts
@@ -7371,10 +7371,19 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
7371
7371
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
7372
7372
  var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
7373
7373
  function checkoutState(cwd) {
7374
- if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
7375
- if (!existsSync11(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
7374
+ if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
7375
+ if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
7376
+ let entries;
7377
+ try {
7378
+ entries = readdirSync2(cwd);
7379
+ } catch (error) {
7380
+ return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
7381
+ }
7382
+ if (entries.length === 0) {
7383
+ return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
7384
+ }
7376
7385
  const gitPath = join15(cwd, ".git");
7377
- if (!existsSync11(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
7386
+ if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
7378
7387
  let stat;
7379
7388
  try {
7380
7389
  stat = statSync(gitPath);
@@ -8588,7 +8597,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
8588
8597
  import {
8589
8598
  existsSync as existsSync12,
8590
8599
  mkdirSync as mkdirSync12,
8591
- readdirSync as readdirSync2,
8600
+ readdirSync as readdirSync3,
8592
8601
  readFileSync as readFileSync9,
8593
8602
  renameSync as renameSync3,
8594
8603
  rmSync as rmSync7,
@@ -8645,7 +8654,7 @@ var Outbox = class {
8645
8654
  if (!existsSync12(dir2)) return [];
8646
8655
  let names;
8647
8656
  try {
8648
- names = readdirSync2(dir2);
8657
+ names = readdirSync3(dir2);
8649
8658
  } catch {
8650
8659
  return [];
8651
8660
  }
@@ -8680,7 +8689,7 @@ var Outbox = class {
8680
8689
  const dir2 = this.dir();
8681
8690
  if (!existsSync12(dir2)) return 0;
8682
8691
  try {
8683
- return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
8692
+ return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
8684
8693
  } catch {
8685
8694
  return 0;
8686
8695
  }
@@ -8807,6 +8816,7 @@ var SseSubscriber = class {
8807
8816
  try {
8808
8817
  await this.connect();
8809
8818
  backoff = 500;
8819
+ if (!this.aborted) await sleep3(backoff);
8810
8820
  } catch (err) {
8811
8821
  if (this.aborted) return;
8812
8822
  if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
@@ -8832,8 +8842,8 @@ var SseSubscriber = class {
8832
8842
  }
8833
8843
  async connect() {
8834
8844
  const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
8835
- const query = this.lastEventId ? "" : "?replayPending=1";
8836
- const url = `${base}/api/workspaces/${this.opts.workspaceId}/events${query}`;
8845
+ const query = this.opts.path ? "" : this.lastEventId ? "" : "?replayPending=1";
8846
+ const url = this.opts.path ? `${base}${this.opts.path}` : `${base}/api/workspaces/${this.opts.workspaceId}/events${query}`;
8837
8847
  this.controller = new AbortController();
8838
8848
  const headers = {
8839
8849
  Authorization: `Bearer ${this.opts.token}`,
@@ -8925,6 +8935,7 @@ var CompanionSupervisor = class {
8925
8935
  reexecFn;
8926
8936
  dispatcherFactory;
8927
8937
  deviceApi = null;
8938
+ deviceSub = null;
8928
8939
  heartbeatTimer = null;
8929
8940
  inFlightHeartbeat = null;
8930
8941
  pollTimer = null;
@@ -8978,6 +8989,24 @@ var CompanionSupervisor = class {
8978
8989
  baseUrl: this.config.baseUrl,
8979
8990
  deviceToken: this.config.deviceToken
8980
8991
  });
8992
+ this.deviceSub = new SseSubscriber({
8993
+ baseUrl: this.config.baseUrl,
8994
+ workspaceId: "device-control",
8995
+ path: "/api/companion/events",
8996
+ token: this.config.deviceToken,
8997
+ log: this.log,
8998
+ lastEventId: null,
8999
+ onMessage: async (ev) => {
9000
+ if (ev.event === "assignments_changed") {
9001
+ if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
9002
+ await this.refreshAssignments();
9003
+ }
9004
+ },
9005
+ onAuthFailure: () => {
9006
+ this.log.error("companion: device control stream authentication failed");
9007
+ }
9008
+ });
9009
+ this.deviceSub.start();
8981
9010
  await this.refreshAssignments();
8982
9011
  this.kickHeartbeat();
8983
9012
  this.heartbeatTimer = setInterval(() => this.kickHeartbeat(), HEARTBEAT_INTERVAL_MS);
@@ -8990,7 +9019,10 @@ var CompanionSupervisor = class {
8990
9019
  // at least the loops keeping the event loop alive; the SSE promises let a
8991
9020
  // clean teardown resolve.
8992
9021
  finishedPromises() {
8993
- return [...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : []);
9022
+ return [
9023
+ ...this.deviceSub ? [this.deviceSub.finished] : [],
9024
+ ...[...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : [])
9025
+ ];
8994
9026
  }
8995
9027
  // ---- device-level loops ----
8996
9028
  kickHeartbeat() {
@@ -9674,13 +9706,15 @@ var CompanionSupervisor = class {
9674
9706
  this.stopped = true;
9675
9707
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
9676
9708
  if (this.pollTimer) clearInterval(this.pollTimer);
9709
+ this.deviceSub?.stop();
9677
9710
  for (const wr of this.workspaces.values()) {
9678
9711
  for (const a of wr.agents.values()) a.cancelDrain();
9679
9712
  if (wr.sub) wr.sub.stop();
9680
9713
  }
9681
- await Promise.all(
9682
- [...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9683
- );
9714
+ await Promise.all([
9715
+ ...this.deviceSub ? [this.deviceSub.finished] : [],
9716
+ ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9717
+ ]);
9684
9718
  }
9685
9719
  async drainForRestart(graceMs) {
9686
9720
  this.restartPending = true;
@@ -10033,7 +10067,7 @@ companion: deploy drain requested (${graceMs}ms grace)\u2026
10033
10067
  }
10034
10068
 
10035
10069
  // src/commands/status.ts
10036
- import { readdirSync as readdirSync3 } from "fs";
10070
+ import { readdirSync as readdirSync4 } from "fs";
10037
10071
  async function status(deps = {}) {
10038
10072
  const readService = deps.service ?? serviceStatus;
10039
10073
  const cfg = loadConfig();
@@ -10104,7 +10138,7 @@ function transcriptsLine() {
10104
10138
  let count = 0;
10105
10139
  let newest;
10106
10140
  try {
10107
- const files = readdirSync3(dir2).filter((f) => f.endsWith(".jsonl")).sort();
10141
+ const files = readdirSync4(dir2).filter((f) => f.endsWith(".jsonl")).sort();
10108
10142
  count = files.length;
10109
10143
  newest = files[files.length - 1];
10110
10144
  } catch {
@@ -10204,7 +10238,7 @@ function isAlive(kill, pid) {
10204
10238
  }
10205
10239
 
10206
10240
  // src/commands/transcript.ts
10207
- import { existsSync as existsSync14, readFileSync as readFileSync11, readdirSync as readdirSync4 } from "fs";
10241
+ import { existsSync as existsSync14, readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
10208
10242
  import { isAbsolute, join as join18 } from "path";
10209
10243
  async function transcript(opts = {}) {
10210
10244
  const dir2 = transcriptsDir();
@@ -10330,7 +10364,7 @@ async function followTranscripts(dir2) {
10330
10364
  }
10331
10365
  function listFiles(dir2) {
10332
10366
  try {
10333
- return readdirSync4(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
10367
+ return readdirSync5(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
10334
10368
  } catch {
10335
10369
  return [];
10336
10370
  }
package/dist/runtime.js CHANGED
@@ -5587,7 +5587,7 @@ var ConnectorHealthStore = class {
5587
5587
 
5588
5588
  // src/dispatcher.ts
5589
5589
  import { randomUUID } from "crypto";
5590
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync } from "fs";
5590
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync2, statSync } from "fs";
5591
5591
  import { join as join13 } from "path";
5592
5592
 
5593
5593
  // src/summon.ts
@@ -6483,10 +6483,19 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
6483
6483
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
6484
6484
  var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
6485
6485
  function checkoutState(cwd) {
6486
- if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
6487
- if (!existsSync9(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
6486
+ if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
6487
+ if (!existsSync9(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
6488
+ let entries;
6489
+ try {
6490
+ entries = readdirSync2(cwd);
6491
+ } catch (error) {
6492
+ return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
6493
+ }
6494
+ if (entries.length === 0) {
6495
+ return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
6496
+ }
6488
6497
  const gitPath = join13(cwd, ".git");
6489
- if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
6498
+ if (!existsSync9(gitPath)) return { ok: true, reason: "usable" };
6490
6499
  let stat;
6491
6500
  try {
6492
6501
  stat = statSync(gitPath);
@@ -7700,7 +7709,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
7700
7709
  import {
7701
7710
  existsSync as existsSync10,
7702
7711
  mkdirSync as mkdirSync10,
7703
- readdirSync as readdirSync2,
7712
+ readdirSync as readdirSync3,
7704
7713
  readFileSync as readFileSync8,
7705
7714
  renameSync as renameSync3,
7706
7715
  rmSync as rmSync6,
@@ -7757,7 +7766,7 @@ var Outbox = class {
7757
7766
  if (!existsSync10(dir2)) return [];
7758
7767
  let names;
7759
7768
  try {
7760
- names = readdirSync2(dir2);
7769
+ names = readdirSync3(dir2);
7761
7770
  } catch {
7762
7771
  return [];
7763
7772
  }
@@ -7792,7 +7801,7 @@ var Outbox = class {
7792
7801
  const dir2 = this.dir();
7793
7802
  if (!existsSync10(dir2)) return 0;
7794
7803
  try {
7795
- return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
7804
+ return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
7796
7805
  } catch {
7797
7806
  return 0;
7798
7807
  }
@@ -7919,6 +7928,7 @@ var SseSubscriber = class {
7919
7928
  try {
7920
7929
  await this.connect();
7921
7930
  backoff = 500;
7931
+ if (!this.aborted) await sleep2(backoff);
7922
7932
  } catch (err) {
7923
7933
  if (this.aborted) return;
7924
7934
  if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
@@ -7944,8 +7954,8 @@ var SseSubscriber = class {
7944
7954
  }
7945
7955
  async connect() {
7946
7956
  const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
7947
- const query = this.lastEventId ? "" : "?replayPending=1";
7948
- const url = `${base}/api/workspaces/${this.opts.workspaceId}/events${query}`;
7957
+ const query = this.opts.path ? "" : this.lastEventId ? "" : "?replayPending=1";
7958
+ const url = this.opts.path ? `${base}${this.opts.path}` : `${base}/api/workspaces/${this.opts.workspaceId}/events${query}`;
7949
7959
  this.controller = new AbortController();
7950
7960
  const headers = {
7951
7961
  Authorization: `Bearer ${this.opts.token}`,
@@ -8037,6 +8047,7 @@ var CompanionSupervisor = class {
8037
8047
  reexecFn;
8038
8048
  dispatcherFactory;
8039
8049
  deviceApi = null;
8050
+ deviceSub = null;
8040
8051
  heartbeatTimer = null;
8041
8052
  inFlightHeartbeat = null;
8042
8053
  pollTimer = null;
@@ -8090,6 +8101,24 @@ var CompanionSupervisor = class {
8090
8101
  baseUrl: this.config.baseUrl,
8091
8102
  deviceToken: this.config.deviceToken
8092
8103
  });
8104
+ this.deviceSub = new SseSubscriber({
8105
+ baseUrl: this.config.baseUrl,
8106
+ workspaceId: "device-control",
8107
+ path: "/api/companion/events",
8108
+ token: this.config.deviceToken,
8109
+ log: this.log,
8110
+ lastEventId: null,
8111
+ onMessage: async (ev) => {
8112
+ if (ev.event === "assignments_changed") {
8113
+ if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
8114
+ await this.refreshAssignments();
8115
+ }
8116
+ },
8117
+ onAuthFailure: () => {
8118
+ this.log.error("companion: device control stream authentication failed");
8119
+ }
8120
+ });
8121
+ this.deviceSub.start();
8093
8122
  await this.refreshAssignments();
8094
8123
  this.kickHeartbeat();
8095
8124
  this.heartbeatTimer = setInterval(() => this.kickHeartbeat(), HEARTBEAT_INTERVAL_MS);
@@ -8102,7 +8131,10 @@ var CompanionSupervisor = class {
8102
8131
  // at least the loops keeping the event loop alive; the SSE promises let a
8103
8132
  // clean teardown resolve.
8104
8133
  finishedPromises() {
8105
- return [...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : []);
8134
+ return [
8135
+ ...this.deviceSub ? [this.deviceSub.finished] : [],
8136
+ ...[...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : [])
8137
+ ];
8106
8138
  }
8107
8139
  // ---- device-level loops ----
8108
8140
  kickHeartbeat() {
@@ -8786,13 +8818,15 @@ var CompanionSupervisor = class {
8786
8818
  this.stopped = true;
8787
8819
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
8788
8820
  if (this.pollTimer) clearInterval(this.pollTimer);
8821
+ this.deviceSub?.stop();
8789
8822
  for (const wr of this.workspaces.values()) {
8790
8823
  for (const a of wr.agents.values()) a.cancelDrain();
8791
8824
  if (wr.sub) wr.sub.stop();
8792
8825
  }
8793
- await Promise.all(
8794
- [...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
8795
- );
8826
+ await Promise.all([
8827
+ ...this.deviceSub ? [this.deviceSub.finished] : [],
8828
+ ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
8829
+ ]);
8796
8830
  }
8797
8831
  async drainForRestart(graceMs) {
8798
8832
  this.restartPending = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.29",
3
+ "version": "0.6.31",
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",