@commonlyai/cli 0.1.39 → 0.1.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -395,27 +395,46 @@ export const setWakeOnMessage = async ({ client, record, enabled }) => {
395
395
  export const updateAgentConfiguration = async ({
396
396
  client,
397
397
  record,
398
+ adapter = null,
398
399
  model = null,
399
400
  effort = null,
400
401
  envPath = null,
401
402
  parseEnv = parseEnvironmentFile,
403
+ adapterRegistry = { getAdapter, listAdapterNames },
402
404
  }) => {
403
405
  if (!record?.podId || !record?.agentName) {
404
406
  throw new Error('token record is missing podId/agentName — re-attach the agent');
405
407
  }
406
408
  const instanceId = record.instanceId || 'default';
407
409
  const runtime = {};
410
+ const environmentRuntime = {};
411
+ if (adapter !== null && adapter !== undefined) {
412
+ const normalizedAdapter = String(adapter).trim().toLowerCase();
413
+ const knownAdapters = adapterRegistry.listAdapterNames();
414
+ const selectedAdapter = adapterRegistry.getAdapter(normalizedAdapter);
415
+ if (!selectedAdapter || !knownAdapters.includes(normalizedAdapter)) {
416
+ throw new Error(
417
+ `Unknown adapter '${normalizedAdapter}'. Known: ${knownAdapters.join(', ')}`,
418
+ );
419
+ }
420
+ if (!await selectedAdapter.detect()) {
421
+ throw new Error(`Adapter '${normalizedAdapter}' not found on PATH. Install it and retry.`);
422
+ }
423
+ runtime.adapter = normalizedAdapter;
424
+ }
408
425
  if (model !== null && model !== undefined) {
409
426
  const normalizedModel = String(model);
410
427
  const validation = validateEnvironmentSpec({ model: normalizedModel });
411
428
  if (!validation.ok) throw new Error(validation.errors.join('; '));
412
429
  runtime.model = normalizedModel;
430
+ environmentRuntime.model = normalizedModel;
413
431
  }
414
432
  if (effort !== null && effort !== undefined) {
415
433
  const normalizedEffort = String(effort);
416
434
  const validation = validateEnvironmentSpec({ effort: normalizedEffort });
417
435
  if (!validation.ok) throw new Error(validation.errors.join('; '));
418
436
  runtime.effort = normalizedEffort;
437
+ environmentRuntime.effort = normalizedEffort;
419
438
  }
420
439
  const config = {};
421
440
  if (Object.keys(runtime).length) config.runtime = runtime;
@@ -429,15 +448,15 @@ export const updateAgentConfiguration = async ({
429
448
  // an ADR-008 environment, a model/effort flag must update that declaration
430
449
  // too; otherwise the daemon would correctly prefer the old explicit value
431
450
  // over the new legacy runtime overlay.
432
- if (environment && Object.keys(runtime).length) {
433
- environment = { ...environment, ...runtime };
451
+ if (environment && Object.keys(environmentRuntime).length) {
452
+ environment = { ...environment, ...environmentRuntime };
434
453
  }
435
- if (!environment && Object.keys(runtime).length) {
436
- environment = { ...runtime };
454
+ if (!environment && Object.keys(environmentRuntime).length) {
455
+ environment = { ...environmentRuntime };
437
456
  }
438
457
  if (environment) config.environment = environment;
439
458
  if (!Object.keys(config).length) {
440
- throw new Error('provide at least one of --model, --effort, or --env');
459
+ throw new Error('provide at least one of --adapter, --model, --effort, or --env');
441
460
  }
442
461
 
443
462
  await client.patch(
@@ -449,6 +468,7 @@ export const updateAgentConfiguration = async ({
449
468
  podId: record.podId,
450
469
  instanceId,
451
470
  changed: Object.keys(config),
471
+ ...(runtime.adapter ? { adapter: runtime.adapter } : {}),
452
472
  ...(environment ? { environment } : {}),
453
473
  };
454
474
  };
@@ -2077,6 +2097,7 @@ Examples:
2077
2097
 
2078
2098
  # List installed agents
2079
2099
  $ commonly agent list
2100
+ $ commonly agent config my-claude --adapter claude --model gpt-5.4 --effort high
2080
2101
  $ commonly agent config my-claude --model gpt-5.4 --effort high
2081
2102
 
2082
2103
  Docs:
@@ -2704,6 +2725,7 @@ Use --local to find the name you'd pass to 'agent run' or 'agent detach'.
2704
2725
  agent
2705
2726
  .command('config <name>')
2706
2727
  .description('Update an attached agent\'s server-side runtime configuration')
2728
+ .option('--adapter <name>', 'Local runtime adapter (must be installed on this machine)')
2707
2729
  .option('--model <id>', 'Model identifier to use on the next daemon restart')
2708
2730
  .option('--effort <level>', 'Reasoning effort (low|medium|high|xhigh|max)')
2709
2731
  .option('--env <path>', 'Replace the ADR-008 environment spec with this JSON file')
@@ -2722,12 +2744,17 @@ Use --local to find the name you'd pass to 'agent run' or 'agent detach'.
2722
2744
  const result = await updateAgentConfiguration({
2723
2745
  client,
2724
2746
  record,
2747
+ adapter: opts.adapter,
2725
2748
  model: opts.model,
2726
2749
  effort: opts.effort,
2727
2750
  envPath: opts.env ? pathResolve(opts.env) : null,
2728
2751
  });
2729
- if (result.environment) {
2730
- saveAgentToken(name, { ...record, environment: result.environment });
2752
+ if (result.environment || result.adapter) {
2753
+ saveAgentToken(name, {
2754
+ ...record,
2755
+ ...(result.environment ? { environment: result.environment } : {}),
2756
+ ...(result.adapter ? { adapter: result.adapter } : {}),
2757
+ });
2731
2758
  }
2732
2759
  console.log(`✓ Updated ${result.agentName} in pod ${result.podId} (${result.changed.join(', ')})`);
2733
2760
  console.log(' The daemon will apply the change on its next poll; a standalone agent run needs a restart.');
@@ -59,7 +59,7 @@ import {
59
59
  writeFile,
60
60
  } from 'fs/promises';
61
61
  import { homedir, tmpdir } from 'os';
62
- import { isAbsolute, join } from 'path';
62
+ import { delimiter, isAbsolute, join } from 'path';
63
63
 
64
64
  import { mountSkills } from '../environment.js';
65
65
  import { wrapArgvWithBwrap } from '../sandbox/bwrap.js';
@@ -337,17 +337,39 @@ const createMcpConfig = async (mcpServers, ctx = {}) => {
337
337
 
338
338
  // ── argv preparation — environment-aware ────────────────────────────────────
339
339
 
340
+ // Claude's npm installer commonly puts the binary in ~/.local/bin, while
341
+ // launchd/supervisor environments intentionally use a small PATH. Keep the
342
+ // command name for the legacy spawn contract, but add this directory to the
343
+ // child environment and to every detection lookup so a daemon does not report
344
+ // a false "adapter unavailable" (or hit spawn ENOENT) merely because it was
345
+ // started outside an interactive shell.
346
+ const withClaudePath = (input) => {
347
+ const output = { ...(input || process.env) };
348
+ const localBin = join(homedir(), '.local', 'bin');
349
+ const entries = String(output.PATH || '')
350
+ .split(delimiter)
351
+ .filter(Boolean);
352
+ if (!entries.includes(localBin)) entries.push(localBin);
353
+ output.PATH = entries.join(delimiter);
354
+ return output;
355
+ };
356
+
340
357
  // Resolve the absolute path of `claude` so bwrap's execvp doesn't depend on
341
358
  // PATH being correctly populated inside the sandbox namespace. Surfaced live
342
359
  // during the 2026-04-17 demo validation: bwrap silently inherits parent
343
360
  // PATH but cannot reach the user's `~/.local/bin` without an absolute path
344
361
  // argv[0], even when that directory is bound read-only into the sandbox.
345
- const resolveClaudePath = () => {
362
+ const resolveClaudePath = (env = process.env) => {
346
363
  // Defensive: spawnSync can return undefined under aggressive mocks (the
347
364
  // adapters.claude.environment.test.mjs suite stubs child_process so no real
348
365
  // process runs). Treat any failure mode as "use the bare command name."
349
366
  let which;
350
- try { which = spawnSync('which', ['claude'], { encoding: 'utf8' }); } catch { /* ignore */ }
367
+ try {
368
+ which = spawnSync('which', ['claude'], {
369
+ encoding: 'utf8',
370
+ env: withClaudePath(env),
371
+ });
372
+ } catch { /* ignore */ }
351
373
  if (which && which.status === 0) {
352
374
  const p = (which.stdout || '').trim();
353
375
  if (p) {
@@ -363,7 +385,8 @@ const resolveClaudePath = () => {
363
385
 
364
386
  const prepareArgv = async (innerArgv, ctx) => {
365
387
  const env = ctx.environment;
366
- if (!env) return { cmd: 'claude', args: innerArgv, env: ctx.claudeEnv };
388
+ const claudeEnv = withClaudePath(ctx.claudeEnv);
389
+ if (!env) return { cmd: 'claude', args: innerArgv, env: claudeEnv };
367
390
 
368
391
  let allowedPatterns = [];
369
392
  if (Array.isArray(env.mcp) && env.mcp.length > 0) {
@@ -407,7 +430,7 @@ const prepareArgv = async (innerArgv, ctx) => {
407
430
  ...innerArgv,
408
431
  ...buildPublicClaudePolicyArgs(allowedPatterns),
409
432
  ];
410
- const claudeBin = resolveClaudePath();
433
+ const claudeBin = resolveClaudePath(claudeEnv);
411
434
  const mcpExecutables = (env.mcp || [])
412
435
  .map((server) => server?.command?.[0])
413
436
  .filter((command) => isAbsolute(command));
@@ -422,7 +445,7 @@ const prepareArgv = async (innerArgv, ctx) => {
422
445
  return {
423
446
  cmd: wrapped[0],
424
447
  args: wrapped.slice(1),
425
- env: ctx.claudeEnv,
448
+ env: claudeEnv,
426
449
  };
427
450
  }
428
451
 
@@ -430,15 +453,15 @@ const prepareArgv = async (innerArgv, ctx) => {
430
453
  innerArgv = [...innerArgv, '--allowedTools', ...allowedPatterns];
431
454
  }
432
455
  if (sandboxMode === 'bwrap') {
433
- const claudeBin = resolveClaudePath();
456
+ const claudeBin = resolveClaudePath(claudeEnv);
434
457
  const wrapped = wrapArgvWithBwrap([claudeBin, ...innerArgv], env, {
435
458
  workspacePath: ctx.cwd,
436
459
  readOnlyPaths: ctx.mcpConfigDir ? [ctx.mcpConfigDir] : [],
437
460
  });
438
- return { cmd: wrapped[0], args: wrapped.slice(1), env: ctx.claudeEnv };
461
+ return { cmd: wrapped[0], args: wrapped.slice(1), env: claudeEnv };
439
462
  }
440
463
 
441
- return { cmd: 'claude', args: innerArgv, env: ctx.claudeEnv };
464
+ return { cmd: 'claude', args: innerArgv, env: claudeEnv };
442
465
  };
443
466
 
444
467
  export default {
@@ -453,14 +476,21 @@ export default {
453
476
 
454
477
  async detect() {
455
478
  try {
456
- const res = spawnSync('claude', ['--version'], { encoding: 'utf8' });
479
+ const claudeEnv = withClaudePath(process.env);
480
+ const res = spawnSync('claude', ['--version'], {
481
+ encoding: 'utf8',
482
+ env: claudeEnv,
483
+ });
457
484
  if (res.error || res.status !== 0) return null;
458
485
  // `claude --version` prints e.g. "2.5.1 (Claude Code)" — first token is enough
459
486
  const version = (res.stdout || '').trim().split(/\s+/)[0] || 'unknown';
460
487
  // Best-effort resolve of the binary path for clearer UX ("claude detected
461
488
  // at /usr/local/bin/claude"). Falls back to the bare command name on
462
489
  // platforms without `which` (e.g. Windows).
463
- const where = spawnSync('which', ['claude'], { encoding: 'utf8' });
490
+ const where = spawnSync('which', ['claude'], {
491
+ encoding: 'utf8',
492
+ env: claudeEnv,
493
+ });
464
494
  const path = where.status === 0 ? (where.stdout || '').trim() || 'claude' : 'claude';
465
495
  return { path, version };
466
496
  } catch {
@@ -133,6 +133,26 @@ export const createDaemonSupervisor = ({
133
133
  // once at boot). A row with NO declared model leaves the record alone —
134
134
  // never strip an operator's hand-set environment.
135
135
  const wanted = environmentFor(row);
136
+ const declaredAdapter = row.runtime && typeof row.runtime === 'object'
137
+ && typeof row.runtime.adapter === 'string'
138
+ ? row.runtime.adapter.trim().toLowerCase()
139
+ : null;
140
+ let adapterChanged = false;
141
+ let nextAdapter = existing.adapter;
142
+ if (declaredAdapter) {
143
+ const detectedAdapter = await resolveAdapter(row.runtime || null);
144
+ // resolveAdapterForRuntime historically probes fallbacks when a
145
+ // declared adapter is absent. A configuration edit must never accept
146
+ // that fallback: it would report claude while running codex (or vice
147
+ // versa). Keep the existing child/token untouched until the exact
148
+ // requested adapter is detected locally.
149
+ if (detectedAdapter !== declaredAdapter) {
150
+ log(`[${row.agentName}] requested adapter '${declaredAdapter}' is not available on this machine — keeping the current seat`);
151
+ return false;
152
+ }
153
+ nextAdapter = declaredAdapter;
154
+ adapterChanged = existing.adapter !== nextAdapter;
155
+ }
136
156
  if (wanted) {
137
157
  const nextEnvironment = wanted.declared
138
158
  ? wanted.value
@@ -140,18 +160,37 @@ export const createDaemonSupervisor = ({
140
160
  const workspacePath = workspacePathFor(nextEnvironment);
141
161
  const nextRecord = {
142
162
  ...existing,
163
+ ...(adapterChanged ? { adapter: nextAdapter } : {}),
143
164
  environment: nextEnvironment,
144
165
  ...(workspacePath ? { workspacePath } : {}),
145
166
  };
146
- if (!isDeepStrictEqual(existing.environment || null, nextEnvironment)
167
+ if (adapterChanged
168
+ || !isDeepStrictEqual(existing.environment || null, nextEnvironment)
147
169
  || (workspacePath && existing.workspacePath !== workspacePath)) {
148
170
  saveToken(row.agentName, nextRecord);
149
171
  log('runtime config changed — restarting the seat to load it');
150
172
  return 'changed';
151
173
  }
152
174
  }
175
+ if (adapterChanged) {
176
+ saveToken(row.agentName, { ...existing, adapter: nextAdapter });
177
+ log('runtime adapter changed — restarting the seat to load it');
178
+ return 'changed';
179
+ }
153
180
  return 'ready';
154
181
  }
182
+ const requestedAdapter = row.runtime && typeof row.runtime === 'object'
183
+ && typeof row.runtime.adapter === 'string'
184
+ ? row.runtime.adapter.trim().toLowerCase()
185
+ : null;
186
+ let adapter = null;
187
+ if (requestedAdapter) {
188
+ adapter = await resolveAdapter(row.runtime || null);
189
+ if (adapter !== requestedAdapter) {
190
+ log(`[${row.agentName}] requested adapter '${requestedAdapter}' is not available on this machine — skipping token mint`);
191
+ return false;
192
+ }
193
+ }
155
194
  const body = { agentName: row.agentName, instanceId: row.instanceId };
156
195
  let minted;
157
196
  try {
@@ -174,7 +213,7 @@ export const createDaemonSupervisor = ({
174
213
  log(`[${row.agentName}] mint returned no token — skipping`);
175
214
  return false;
176
215
  }
177
- const adapter = await resolveAdapter(row.runtime || null);
216
+ if (!adapter) adapter = await resolveAdapter(row.runtime || null);
178
217
  if (!adapter) {
179
218
  log(`[${row.agentName}] no usable CLI adapter on this machine — install claude or codex, or attach manually`);
180
219
  return false;