@withtruing/companion 0.13.1 → 0.13.2

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +106 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -133,6 +133,10 @@ workspace-task settings; one companion process polls all of them independently.
133
133
  local development Console therefore does not replace or move projects already connected to the
134
134
  hosted Console.
135
135
 
136
+ If a saved backend is unavailable, `run` prints the exact setup fix and skips only the affected
137
+ Console, project, or workspace-task poll. Other runnable pairings keep working; `run` exits with
138
+ an error only when no configured work remains runnable.
139
+
136
140
  To remove one Console and all of its projects from this computer:
137
141
 
138
142
  ```bash
package/dist/cli.js CHANGED
@@ -404,6 +404,23 @@ export function backendRequirements(backends) {
404
404
  needsCli: backends.some((backend) => backend === 'cli'),
405
405
  };
406
406
  }
407
+ export function partitionPairingsByAvailability(pairings, availability) {
408
+ const partitions = pairings.map((pairing) => {
409
+ const unavailableConnections = new Set(pairing.connections
410
+ .filter((connection) => !availability[connection.backend])
411
+ .map((connection) => connection.connectionId));
412
+ const workspaceTasksUnavailable = Boolean(pairing.pollWorkspaceTasks && !availability[pairing.workspaceBackend ?? 'api-key']);
413
+ return { pairing, unavailableConnections, workspaceTasksUnavailable };
414
+ });
415
+ const isRunnable = ({ pairing }) => pairing.connections.some((connection) => connection.backend === 'codex' || availability[connection.backend])
416
+ || Boolean(pairing.pollWorkspaceTasks
417
+ && (pairing.workspaceBackend === 'codex' || availability[pairing.workspaceBackend ?? 'api-key']));
418
+ return {
419
+ pairings: partitions,
420
+ runnablePairings: partitions.filter(isRunnable),
421
+ unavailablePairings: partitions.filter((partition) => !isRunnable(partition)),
422
+ };
423
+ }
407
424
  export function createBackendRuntime(config, anthropic, createCodexTextModel = codexTextModel) {
408
425
  let usage = null;
409
426
  const codexOptions = config.backend === 'codex'
@@ -886,7 +903,7 @@ async function runPairing(pairing, options) {
886
903
  connection.connectionId,
887
904
  createBackendRuntime(connection, options.anthropic),
888
905
  ]));
889
- const workspaceRuntime = pairing.pollWorkspaceTasks && (workspaceBackend !== 'codex' || options.codexAvailable)
906
+ const workspaceRuntime = pairing.pollWorkspaceTasks && !options.workspaceTasksUnavailable
890
907
  ? createBackendRuntime({ backend: workspaceBackend, codexModel: pairing.codexModel }, options.anthropic)
891
908
  : null;
892
909
  const repositoryReadiness = new Map();
@@ -897,7 +914,7 @@ async function runPairing(pairing, options) {
897
914
  });
898
915
  }
899
916
  if (activeConnections.length !== pairing.connections.length && activeConnections.length > 0) {
900
- error('Skipping investigation polling while a configured Codex connection is unavailable, so the companion cannot claim an investigation it cannot run.');
917
+ error('Skipping investigation polling while a configured connection backend is unavailable, so the companion cannot claim an investigation it cannot run.');
901
918
  }
902
919
  const projectCount = activeConnections.length;
903
920
  const watching = workspaceRuntime
@@ -1116,53 +1133,106 @@ async function cmdRun(options = {}) {
1116
1133
  process.exitCode = 1;
1117
1134
  return;
1118
1135
  }
1119
- const runnablePairings = config?.pairings.filter((pairing) => pairing.connections.length > 0 || pairing.pollWorkspaceTasks) ?? [];
1120
- if (!config || runnablePairings.length === 0) {
1136
+ const configuredPairings = config?.pairings.filter((pairing) => pairing.connections.length > 0 || pairing.pollWorkspaceTasks) ?? [];
1137
+ if (!config || configuredPairings.length === 0) {
1121
1138
  console.error(options.safeStartupDiagnostics
1122
1139
  ? 'Nothing is set up yet. Run connect first.'
1123
1140
  : `Nothing set up yet — ${configPath} is missing or empty. Run "connect" first.`);
1124
1141
  process.exitCode = 1;
1125
1142
  return;
1126
1143
  }
1127
- const allBackends = runnablePairings.flatMap((pairing) => [
1144
+ const allBackends = configuredPairings.flatMap((pairing) => [
1128
1145
  ...pairing.connections.map((connection) => connection.backend),
1129
1146
  ...(pairing.pollWorkspaceTasks ? [pairing.workspaceBackend ?? 'api-key'] : []),
1130
1147
  ]);
1131
1148
  const { needsApiKey, needsCli } = backendRequirements(allBackends);
1132
1149
  const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
1133
- if (needsApiKey && !apiKey) {
1134
- console.error('No Anthropic API key found. Get one free at console.anthropic.com, then either run '
1135
- + '"connect" again adding --api-key <your key>, or set it yourself: '
1136
- + '"set ANTHROPIC_API_KEY=<your key>" (Windows) / "export ANTHROPIC_API_KEY=<your key>" (Mac/Linux), then run "run" again.');
1137
- process.exitCode = 1;
1138
- return;
1139
- }
1140
- if (needsCli && !await checkClaudeCliAvailable()) {
1141
- console.error('This needs Claude Code installed and you logged into it first: npm install -g @anthropic-ai/claude-code, then run `claude` and follow the login prompt.');
1142
- process.exitCode = 1;
1143
- return;
1144
- }
1145
- const unavailableConnections = new Set();
1150
+ const apiKeyMissingMessage = 'No Anthropic API key found. Get one free at console.anthropic.com, then either run '
1151
+ + '"connect" again adding --api-key <your key>, or set it yourself: '
1152
+ + '"set ANTHROPIC_API_KEY=<your key>" (Windows) / "export ANTHROPIC_API_KEY=<your key>" (Mac/Linux), then run "run" again.';
1153
+ const cliMissingMessage = 'This needs Claude Code installed and you logged into it first: npm install -g @anthropic-ai/claude-code, then run `claude` and follow the login prompt.';
1154
+ const cliAvailable = !needsCli || Boolean(await checkClaudeCliAvailable());
1146
1155
  let codexAvailable = true;
1156
+ let codexStatus = null;
1147
1157
  if (allBackends.includes('codex')) {
1148
1158
  const status = await checkCodexLogin(defaultCodexHome());
1149
1159
  codexAvailable = status === 'logged-in';
1150
- if (!codexAvailable) {
1151
- logCodexReadiness(status);
1152
- for (const pairing of runnablePairings) {
1153
- for (const connection of pairing.connections) {
1154
- if (connection.backend !== 'codex')
1155
- continue;
1156
- console.error(`Skipping ${connection.connectionId}: Codex is ${status === 'not-installed' ? 'not installed' : 'not logged in'}; no work will be claimed for this connection.`);
1157
- unavailableConnections.add(connection.connectionId);
1158
- }
1159
- if (pairing.pollWorkspaceTasks && (pairing.workspaceBackend ?? 'api-key') === 'codex') {
1160
- console.error(`Skipping workspace-level tasks for ${pairing.url}: Codex is ${status === 'not-installed' ? 'not installed' : 'not logged in'}; no work will be claimed.`);
1161
- }
1160
+ if (!codexAvailable)
1161
+ codexStatus = status;
1162
+ }
1163
+ const partitioned = partitionPairingsByAvailability(configuredPairings, {
1164
+ 'api-key': !needsApiKey || Boolean(apiKey),
1165
+ cli: cliAvailable,
1166
+ codex: codexAvailable,
1167
+ });
1168
+ const distinctBackends = new Set(allBackends);
1169
+ if (partitioned.runnablePairings.length === 0 && configuredPairings.length === 1 && distinctBackends.size === 1) {
1170
+ const [backend] = distinctBackends;
1171
+ if (backend === 'api-key')
1172
+ console.error(apiKeyMissingMessage);
1173
+ if (backend === 'cli')
1174
+ console.error(cliMissingMessage);
1175
+ if (backend === 'codex' && codexStatus) {
1176
+ logCodexReadiness(codexStatus);
1177
+ for (const connection of configuredPairings[0].connections) {
1178
+ console.error(`Skipping ${connection.connectionId}: Codex is ${codexStatus === 'not-installed' ? 'not installed' : 'not logged in'}; no work will be claimed for this connection.`);
1162
1179
  }
1180
+ if (configuredPairings[0].pollWorkspaceTasks) {
1181
+ console.error(`Skipping workspace-level tasks for ${configuredPairings[0].url}: Codex is ${codexStatus === 'not-installed' ? 'not installed' : 'not logged in'}; no work will be claimed.`);
1182
+ }
1183
+ }
1184
+ process.exitCode = 1;
1185
+ return;
1186
+ }
1187
+ if (codexStatus)
1188
+ logCodexReadiness(codexStatus);
1189
+ const backendUnavailableMessage = (backend) => {
1190
+ if (backend === 'api-key')
1191
+ return apiKeyMissingMessage;
1192
+ if (backend === 'cli')
1193
+ return cliMissingMessage;
1194
+ return `Codex is ${codexStatus === 'not-installed' ? 'not installed' : 'not logged in'}; no work will be claimed.`;
1195
+ };
1196
+ const fullyUnavailablePairings = new Set(partitioned.unavailablePairings);
1197
+ for (const partition of partitioned.pairings) {
1198
+ const { pairing, unavailableConnections, workspaceTasksUnavailable } = partition;
1199
+ const pairingLabel = `${pairing.url}${pairing.workspaceId ? ` (workspace ${pairing.workspaceId})` : ''}`;
1200
+ if (fullyUnavailablePairings.has(partition)) {
1201
+ const unavailableBackends = new Set(pairing.connections.map((connection) => connection.backend));
1202
+ if (pairing.pollWorkspaceTasks)
1203
+ unavailableBackends.add(pairing.workspaceBackend ?? 'api-key');
1204
+ for (const backend of unavailableBackends) {
1205
+ console.error(`Skipping ${pairingLabel}: ${backendUnavailableMessage(backend)}`);
1206
+ }
1207
+ continue;
1208
+ }
1209
+ for (const connection of pairing.connections) {
1210
+ if (!unavailableConnections.has(connection.connectionId))
1211
+ continue;
1212
+ console.error(connection.backend === 'codex'
1213
+ ? `Skipping ${connection.connectionId}: ${backendUnavailableMessage(connection.backend)}`
1214
+ : `Skipping ${connection.connectionId} for ${pairingLabel}: ${backendUnavailableMessage(connection.backend)}`);
1215
+ }
1216
+ if (workspaceTasksUnavailable) {
1217
+ const workspaceBackend = pairing.workspaceBackend ?? 'api-key';
1218
+ console.error(workspaceBackend === 'codex'
1219
+ ? `Skipping workspace-level tasks for ${pairing.url}: ${backendUnavailableMessage(workspaceBackend)}`
1220
+ : `Skipping workspace-level tasks for ${pairingLabel}: ${backendUnavailableMessage(workspaceBackend)}`);
1163
1221
  }
1164
1222
  }
1165
- const anthropic = needsApiKey ? new Anthropic({ apiKey }) : null;
1223
+ if (partitioned.runnablePairings.length === 0) {
1224
+ process.exitCode = 1;
1225
+ return;
1226
+ }
1227
+ const remainingBackends = partitioned.runnablePairings.flatMap(({ pairing, unavailableConnections, workspaceTasksUnavailable }) => [
1228
+ ...pairing.connections
1229
+ .filter((connection) => !unavailableConnections.has(connection.connectionId))
1230
+ .map((connection) => connection.backend),
1231
+ ...(pairing.pollWorkspaceTasks && !workspaceTasksUnavailable
1232
+ ? [pairing.workspaceBackend ?? 'api-key']
1233
+ : []),
1234
+ ]);
1235
+ const anthropic = backendRequirements(remainingBackends).needsApiKey ? new Anthropic({ apiKey }) : null;
1166
1236
  const usageLimitPause = new UsageLimitPause();
1167
1237
  let stopping = false;
1168
1238
  process.on('SIGINT', () => {
@@ -1174,21 +1244,21 @@ async function cmdRun(options = {}) {
1174
1244
  expectedExitReason = 'stopped by Ctrl+C';
1175
1245
  stopping = true;
1176
1246
  });
1177
- let activePairings = runnablePairings.length;
1178
- await Promise.all(runnablePairings.map(async (pairing) => {
1247
+ let activePairings = partitioned.runnablePairings.length;
1248
+ await Promise.all(partitioned.runnablePairings.map(async ({ pairing, unavailableConnections, workspaceTasksUnavailable }) => {
1179
1249
  try {
1180
1250
  return await runPairing(pairing, {
1181
1251
  ...options,
1182
- multiple: runnablePairings.length > 1,
1252
+ multiple: partitioned.runnablePairings.length > 1,
1183
1253
  anthropic,
1184
1254
  unavailableConnections,
1185
- codexAvailable,
1255
+ workspaceTasksUnavailable,
1186
1256
  usageLimitPause,
1187
1257
  shouldStop: () => stopping,
1188
1258
  });
1189
1259
  }
1190
1260
  catch (reason) {
1191
- if (runnablePairings.length === 1)
1261
+ if (partitioned.runnablePairings.length === 1)
1192
1262
  throw reason;
1193
1263
  console.error(`${pairingPrefix(pairing)} The Console at ${pairing.url} stopped because of an unexpected error: ${reason instanceof Error ? reason.message : reason}`);
1194
1264
  return 'stopped';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withtruing/companion",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "The Truing companion: reads your code and runs AI on your own computer, for Truing.",
5
5
  "type": "module",
6
6
  "license": "MIT",