@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 +1 -1
- package/src/commands/agent.js +34 -7
- package/src/lib/adapters/claude.js +41 -11
- package/src/lib/daemon-supervisor.js +41 -2
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -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(
|
|
433
|
-
environment = { ...environment, ...
|
|
451
|
+
if (environment && Object.keys(environmentRuntime).length) {
|
|
452
|
+
environment = { ...environment, ...environmentRuntime };
|
|
434
453
|
}
|
|
435
|
-
if (!environment && Object.keys(
|
|
436
|
-
environment = { ...
|
|
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, {
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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:
|
|
461
|
+
return { cmd: wrapped[0], args: wrapped.slice(1), env: claudeEnv };
|
|
439
462
|
}
|
|
440
463
|
|
|
441
|
-
return { cmd: 'claude', args: innerArgv, env:
|
|
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
|
|
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'], {
|
|
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 (
|
|
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
|
-
|
|
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;
|