@commonlyai/cli 0.1.164 → 0.1.166

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.164",
3
+ "version": "0.1.166",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -17,6 +17,7 @@ import { homedir, tmpdir } from 'os';
17
17
  import { fileURLToPath } from 'url';
18
18
  import { createHash } from 'crypto';
19
19
 
20
+ import { requireSeatModel } from '../lib/adapters/seat-model.js';
20
21
  import { createClient } from '../lib/api.js';
21
22
  import { copyHandoffMemory } from '../lib/handoff-memory.js';
22
23
  import {
@@ -666,12 +667,29 @@ export const updateAgentConfiguration = async ({
666
667
  model = null,
667
668
  effort = null,
668
669
  envPath = null,
670
+ runtimeKey = false,
669
671
  parseEnv = parseEnvironmentFile,
670
672
  adapterRegistry = { getAdapter, listAdapterNames },
671
673
  }) => {
672
674
  if (!record?.agentName) {
673
675
  throw new Error('token record is missing agentName — re-attach the agent');
674
676
  }
677
+ if (runtimeKey) {
678
+ if (adapter || envPath) throw new Error('--adapter and --env require an owner login; run commonly login');
679
+ const controls = Object.fromEntries(Object.entries({ model, effort })
680
+ .filter(([, value]) => value !== null && value !== undefined));
681
+ if (!Object.keys(controls).length) throw new Error('provide --model or --effort');
682
+ const environment = { ...record.environment, ...controls };
683
+ const validation = validateEnvironmentSpec(environment);
684
+ if (!validation.ok) throw new Error(validation.errors.join('; '));
685
+ const result = await client.patch('/api/agents/runtime/configuration', {
686
+ agentName: record.agentName, instanceId: record.instanceId || 'default', ...controls,
687
+ });
688
+ return {
689
+ agentName: record.agentName, instanceId: record.instanceId || 'default', podId: result.podId,
690
+ changed: Object.keys(controls), environment,
691
+ };
692
+ }
675
693
  const instanceId = record.instanceId || 'default';
676
694
  const runtime = {};
677
695
  const environmentRuntime = {};
@@ -1253,6 +1271,7 @@ export const performRun = ({
1253
1271
  const effectiveTurnTimeoutMs = Number.isFinite(requestedTurnTimeout) && requestedTurnTimeout > 0
1254
1272
  ? Math.min(requestedTurnTimeout, 2_147_483_647) : 15 * 60 * 1000;
1255
1273
  const client = createClient({ instance: instanceUrl, token });
1274
+ const missingModelNotices = new Set();
1256
1275
  const handoffAgent = sanitizeHandoffAgent(refusalHandoff);
1257
1276
  if (refusalHandoff && !handoffAgent) {
1258
1277
  log(`--refusal-handoff ignored: '${String(refusalHandoff).slice(0, 64)}' is not an agent handle`);
@@ -1677,6 +1696,30 @@ export const performRun = ({
1677
1696
  ? `${recycleBriefing}\n\n${promptWithSkills}`
1678
1697
  : promptWithSkills;
1679
1698
 
1699
+ if (adapter.requiresModel) {
1700
+ try {
1701
+ requireSeatModel(seatEnvironment, adapter.name, true);
1702
+ } catch (error) {
1703
+ if (error.code === 'seat_model_missing' && eventPodId && !missingModelNotices.has(eventPodId)) {
1704
+ try {
1705
+ const delivery = await deliverChatReply({
1706
+ client,
1707
+ podId: eventPodId,
1708
+ text: "This agent has no model set; add environment.model to its configuration.",
1709
+ limit: chatCharLimit,
1710
+ maxChunks: maxChatChunks,
1711
+ log,
1712
+ });
1713
+ if (!delivery.skipped && !delivery.refused) missingModelNotices.add(eventPodId);
1714
+ } catch (noticeError) {
1715
+ log(`missing model notice could not be posted: ${noticeError.message}`);
1716
+ }
1717
+ }
1718
+ // Configuration failure is retryable: never turn a notice into an ack.
1719
+ throw error;
1720
+ }
1721
+ }
1722
+
1680
1723
  log(`[${event.type}] spawning ${adapter.name}`);
1681
1724
  // TASK-102 part B. The CHILD gets a scoped credential; the wrapper keeps
1682
1725
  // the seat token for its own polling, claiming and posting (`client` was
@@ -3748,13 +3791,21 @@ Use --local to find the name you'd pass to 'agent run' or 'agent detach'.
3748
3791
  process.exit(1);
3749
3792
  }
3750
3793
  const instance = opts.instance || record.instanceUrl;
3751
- const token = getToken(instance);
3794
+ const ownerToken = getToken(instance);
3795
+ const runtimeKey = !opts.adapter && !opts.env && !ownerToken && Boolean(record.runtimeToken);
3796
+ const token = runtimeKey ? record.runtimeToken : ownerToken;
3752
3797
  if (!token) { console.error('Not logged in. Run: commonly login'); process.exit(1); }
3753
- const client = createClient({ instance: resolveInstanceUrl(instance), token });
3754
3798
  try {
3799
+ const instanceUrl = resolveInstanceUrl(instance);
3800
+ if (runtimeKey && (!record.instanceUrl
3801
+ || new URL(instanceUrl).href.replace(/\/$/, '') !== new URL(record.instanceUrl).href.replace(/\/$/, ''))) {
3802
+ throw new Error('Changing --instance requires an owner login for that instance; run commonly login');
3803
+ }
3804
+ const client = createClient({ instance: instanceUrl, token });
3755
3805
  const result = await updateAgentConfiguration({
3756
3806
  client,
3757
3807
  record,
3808
+ runtimeKey,
3758
3809
  adapter: opts.adapter,
3759
3810
  model: opts.model,
3760
3811
  effort: opts.effort,
@@ -43,6 +43,7 @@
43
43
  * SIGKILL escalation is a post-v1 concern (ADR-005 §Spawning semantics).
44
44
  */
45
45
 
46
+ import { requireSeatModel, seatModelRequired } from './seat-model.js';
46
47
  import { spawn as childSpawn, spawnSync } from 'child_process';
47
48
  import { createHash, randomUUID } from 'crypto';
48
49
  import { accessSync, constants, realpathSync } from 'fs';
@@ -725,6 +726,7 @@ const prepareArgv = async (innerArgv, ctx) => {
725
726
  };
726
727
 
727
728
  export default {
729
+ get requiresModel() { return seatModelRequired(); },
728
730
  name: 'claude',
729
731
  // Identity-bearing runtime tag written into AgentInstallation.config.runtime
730
732
  // by `commonly agent attach`. The CLI command stays `commonly agent attach
@@ -759,6 +761,7 @@ export default {
759
761
  },
760
762
 
761
763
  async spawn(prompt, rawCtx = {}) {
764
+ const seatModel = requireSeatModel(rawCtx.environment, 'claude');
762
765
  // `sandbox.trust: 'internal'` is refused for new declarations and read as
763
766
  // `public` for a record that already carries it; such a seat is confined
764
767
  // where it can be and refuses to derive where it cannot (Wren 69585).
@@ -809,7 +812,7 @@ export default {
809
812
  // session-recovery retry, or a retry runs at a different effort than the
810
813
  // turn it replaces.
811
814
  const modelArgs = [
812
- ...(ctx.environment?.model ? ['--model', String(ctx.environment.model)] : []),
815
+ ...(seatModel ? ['--model', seatModel] : []),
813
816
  ...(ctx.environment?.effort ? ['--effort', String(ctx.environment.effort)] : []),
814
817
  ];
815
818
  const baseArgs = ['-p', fullPrompt, '--output-format', 'text', sessionFlag, sessionId, ...modelArgs];
@@ -37,6 +37,7 @@
37
37
  * SIGKILL escalation is a post-v1 concern (ADR-005 §Spawning semantics).
38
38
  */
39
39
 
40
+ import { requireSeatModel, seatModelRequired } from './seat-model.js';
40
41
  import { spawn as childSpawn, spawnSync } from 'child_process';
41
42
  import { createHash } from 'crypto';
42
43
  import { accessSync, constants } from 'fs';
@@ -616,6 +617,7 @@ const runCodex = ({ args, cwd, env, timeoutMs, credentials = [], spawnImpl = chi
616
617
  });
617
618
 
618
619
  export default {
620
+ get requiresModel() { return seatModelRequired(); },
619
621
  name: 'codex',
620
622
  // See claude.js for the rationale — identity-bearing runtime tag persisted
621
623
  // to AgentInstallation.config.runtime (paired with host:'byo') so a CLI-
@@ -642,6 +644,7 @@ export default {
642
644
  },
643
645
 
644
646
  async spawn(prompt, ctx = {}) {
647
+ const seatModel = requireSeatModel(ctx.environment, 'codex');
645
648
  // Passed through UNCOALESCED. `ctx.memoryLongTerm || ''` was here, and
646
649
  // `null || ''` is `''` — which routed the unreadable case straight into
647
650
  // the empty-memory branch and made the whole distinction unreachable from
@@ -698,7 +701,7 @@ export default {
698
701
  mcpFlags: mcp.flags,
699
702
  mcpReadDirectories: mcp.mcpReadDirectories,
700
703
  publicSandboxMode,
701
- model: ctx.environment?.model,
704
+ model: seatModel,
702
705
  effort: ctx.environment?.effort,
703
706
  commitAttributionEnv: ctx.commitAttributionEnv,
704
707
  });
@@ -1,4 +1,5 @@
1
1
  /** DeepSeek Harness 0.2.1-rc.2, private seats only. Configuration is per spawn. */
2
+ import { requireSeatModel } from './seat-model.js';
2
3
  import { spawn as childSpawn, spawnSync } from 'child_process';
3
4
  import { createHash } from 'crypto';
4
5
  import { existsSync } from 'fs';
@@ -218,6 +219,7 @@ export const runDsh = ({ binary, args, cwd, env, timeoutMs, credentials, spawnIm
218
219
  });
219
220
 
220
221
  export default {
222
+ requiresModel: true,
221
223
  name: 'dsh', runtimeType: 'dsh', supportsProvider: true,
222
224
 
223
225
  async validateEnvironment(environment, workspacePath) {
@@ -246,6 +248,7 @@ export default {
246
248
  },
247
249
 
248
250
  async spawn(prompt, ctx = {}) {
251
+ const seatModel = requireSeatModel(ctx.environment, 'dsh', true);
249
252
  assertPrivateDsh(ctx.environment);
250
253
  if (!ctx.environment?.provider || !ctx.environment?.model) {
251
254
  throw new Error('dsh requires environment.provider and environment.model');
@@ -275,7 +278,7 @@ export default {
275
278
  credentialFile: credential.path, instanceUrl: ctx.instanceUrl,
276
279
  });
277
280
  const patch = JSON.stringify(buildPatch({
278
- proxyBaseURL: proxy.baseURL, model: ctx.environment.model,
281
+ proxyBaseURL: proxy.baseURL, model: seatModel,
279
282
  cwd: ctx.cwd, mcp, effort: ctx.environment.effort,
280
283
  deepseekOfficial: new URL(ctx.environment.provider.baseURL).hostname === 'api.deepseek.com',
281
284
  }), null, 2);
@@ -1,4 +1,5 @@
1
1
  /** Goose 1.54.0 wrapper. Public seats wait for TASK-236's deny-by-default proof. */
2
+ import { requireSeatModel } from './seat-model.js';
2
3
  import { spawn as childSpawn, spawnSync } from 'node:child_process';
3
4
  import { mkdtemp, readFile, rm } from 'node:fs/promises';
4
5
  import { tmpdir } from 'node:os';
@@ -16,7 +17,6 @@ import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
16
17
  export const TESTED_GOOSE_VERSIONS = Object.freeze(['1.54.0']);
17
18
  const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
18
19
  const TERMINATION_GRACE_MS = 5000;
19
- const DEFAULT_MODEL = 'deepseek-v4-flash';
20
20
  const DEFAULT_PROVIDER_HOST = 'https://litellm.commonly.me';
21
21
  const DEFAULT_PROVIDER_KEY = 'COMMONLY_LITELLM_KEY';
22
22
  const PLACEHOLDER_RE = /\$\{(COMMONLY_[A-Z_]+)\}/g;
@@ -174,6 +174,7 @@ const runGoose = ({ binary, args, cwd, env, timeoutMs, credentials, spawnImpl =
174
174
  });
175
175
 
176
176
  export default {
177
+ requiresModel: true,
177
178
  name: 'goose',
178
179
  providerKeyEnv: DEFAULT_PROVIDER_KEY,
179
180
  supportsProvider: true,
@@ -193,6 +194,7 @@ export default {
193
194
  },
194
195
 
195
196
  async spawn(prompt, ctx = {}) {
197
+ const seatModel = requireSeatModel(ctx.environment, 'goose', true);
196
198
  const keyFile = await this.validateEnvironment(ctx.environment, ctx.cwd);
197
199
  const sourceEnv = ctx.env || process.env;
198
200
  const configured = ctx.environment?.provider;
@@ -203,7 +205,7 @@ export default {
203
205
  if (!/^https?:$/.test(baseUrl.protocol) || baseUrl.username || baseUrl.password || baseUrl.search || baseUrl.hash) {
204
206
  throw new Error('goose adapter: provider.baseURL must be a plain HTTP(S) URL');
205
207
  }
206
- const model = ctx.environment?.model || DEFAULT_MODEL;
208
+ const model = seatModel;
207
209
  const binary = ctx._binaryPath || 'goose';
208
210
  const version = ctx._version || versionOf(spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 5000 }).stdout);
209
211
  if (!TESTED_GOOSE_VERSIONS.includes(version)) {
@@ -1,4 +1,5 @@
1
1
  /** Hermes 0.22.2 local CLI wrapper. Hermes owns provider I/O; this file only spawns it. */
2
+ import { requireSeatModel, seatModelRequired } from './seat-model.js';
2
3
  import { spawn as childSpawn, spawnSync } from 'child_process';
3
4
  import { createHash } from 'crypto';
4
5
  import { mkdir } from 'fs/promises';
@@ -137,6 +138,7 @@ const runHermes = ({ args, prompt, cwd, env, timeoutMs, spawnImpl, credentials }
137
138
  });
138
139
 
139
140
  export default {
141
+ get requiresModel() { return seatModelRequired(); },
140
142
  name: 'hermes',
141
143
  runtimeType: 'hermes',
142
144
  supportsProvider: false,
@@ -161,6 +163,7 @@ export default {
161
163
  },
162
164
 
163
165
  async spawn(prompt, ctx = {}) {
166
+ const seatModel = requireSeatModel(ctx.environment, 'hermes');
164
167
  await this.validateEnvironment(ctx.environment);
165
168
  // Keep Hermes away from the operator's default profile, including any
166
169
  // Nous Portal/Cloud credentials there. The wrapper provides the sole key.
@@ -177,7 +180,7 @@ export default {
177
180
  }
178
181
  const args = ['chat', '--oneshot', '--query-file', '-', '--format', 'stream-json',
179
182
  '--provider', 'openrouter', '--source', 'tool', '--no-restore-cwd'];
180
- if (ctx.environment?.model) args.push('--model', ctx.environment.model);
183
+ if (seatModel) args.push('--model', seatModel);
181
184
  if (ctx.sessionId) args.push('--resume', ctx.sessionId);
182
185
  const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, {
183
186
  freshSession: !ctx.sessionId,
@@ -19,6 +19,7 @@
19
19
  * `text.part.text`, and token counts in `step_finish.part.tokens`.
20
20
  */
21
21
 
22
+ import { requireSeatModel, seatModelRequired } from './seat-model.js';
22
23
  import { spawn as childSpawn, spawnSync } from 'child_process';
23
24
  import { createHash } from 'crypto';
24
25
  import { realpathSync } from 'fs';
@@ -622,6 +623,7 @@ const describeOpenCodeVersionMismatch = (installedVersion) => {
622
623
  };
623
624
 
624
625
  export default {
626
+ get requiresModel() { return seatModelRequired(); },
625
627
  name: 'opencode',
626
628
  runtimeType: 'opencode',
627
629
  supportsProvider: true,
@@ -645,6 +647,7 @@ export default {
645
647
  },
646
648
 
647
649
  async spawn(prompt, rawCtx = {}) {
650
+ const seatModel = requireSeatModel(rawCtx.environment, 'opencode');
648
651
  const sandbox = normalizeSandboxTrust(rawCtx.environment?.sandbox);
649
652
  const ctx = rawCtx.environment
650
653
  ? { ...rawCtx, environment: { ...rawCtx.environment, sandbox } }
@@ -805,7 +808,7 @@ export default {
805
808
  prompt: fullPrompt,
806
809
  sessionId: ctx.sessionId,
807
810
  cwd: ctx.cwd,
808
- model: qualifiedProviderModel(provider, ctx.environment?.model),
811
+ model: qualifiedProviderModel(provider, seatModel),
809
812
  effort: ctx.environment?.effort,
810
813
  title: ctx.agentName,
811
814
  });
@@ -32,6 +32,7 @@
32
32
  * Contract (see stub.js): detect() and spawn(prompt, ctx) → { text, newSessionId }.
33
33
  */
34
34
 
35
+ import { requireSeatModel } from './seat-model.js';
35
36
  import { spawn as childSpawn, spawnSync } from 'child_process';
36
37
  import { createHash, randomUUID } from 'crypto';
37
38
  import { mkdir, readdir, writeFile } from 'fs/promises';
@@ -65,7 +66,6 @@ export const DEFAULT_PROVIDER = Object.freeze({
65
66
  api: 'openai-completions',
66
67
  apiKeyEnv: 'COMMONLY_LITELLM_KEY',
67
68
  });
68
- export const DEFAULT_MODEL = 'deepseek-v4-flash';
69
69
 
70
70
  // ADR-008 `effort` → pi `--thinking`. pi's ladder is off/minimal/low/medium/high/xhigh/max.
71
71
  const THINKING = { none: 'off', off: 'off', minimal: 'minimal', low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh', max: 'max' };
@@ -390,6 +390,7 @@ const runPi = ({ args, cwd, env, payload, timeoutMs, credentials = [], spawnImpl
390
390
  });
391
391
 
392
392
  export default {
393
+ requiresModel: true,
393
394
  name: 'pi',
394
395
 
395
396
  // TASK-049: the variable this adapter needs from the environment of whatever
@@ -409,12 +410,13 @@ export default {
409
410
  },
410
411
 
411
412
  async spawn(prompt, ctx = {}) {
413
+ const seatModel = requireSeatModel(ctx.environment, 'pi', true);
412
414
  // pi has no enforced sandbox. claude.js and codex.js refuse a public-trust
413
415
  // seat they cannot confine; so does this adapter, and it refuses any declared
414
416
  // sandbox mode rather than run unconfined under a spec that promised one.
415
417
  assertNoSandboxDeclared(ctx.environment);
416
418
  const provider = resolveProvider(ctx.environment);
417
- const model = ctx.environment?.model || DEFAULT_MODEL;
419
+ const model = seatModel;
418
420
  const thinking = thinkingFor(ctx.environment?.effort);
419
421
  const baseEnv = ctx.env || process.env;
420
422
  if (!baseEnv[provider.apiKeyEnv]) {
@@ -1,4 +1,5 @@
1
1
  /** Qwen Code 0.25.1 wrapper. Public turns have fresh HOME, config and keys. */
2
+ import { requireSeatModel } from './seat-model.js';
2
3
  import { spawn as childSpawn, spawnSync } from 'child_process';
3
4
  import { lstatSync, realpathSync } from 'fs';
4
5
  import { chmod, mkdtemp, readFile, rm, writeFile } from 'fs/promises';
@@ -208,6 +209,7 @@ const runQwen = ({ cmd, args, cwd, env, timeoutMs, spawnImpl, credentials }) =>
208
209
  });
209
210
 
210
211
  export default {
212
+ requiresModel: true,
211
213
  name: 'qwen',
212
214
  runtimeType: 'qwen',
213
215
  supportsProvider: true,
@@ -230,6 +232,7 @@ export default {
230
232
  },
231
233
 
232
234
  async spawn(prompt, ctx = {}) {
235
+ const seatModel = requireSeatModel(ctx.environment, 'qwen', true);
233
236
  const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
234
237
  const isPublic = sandbox?.trust === 'public';
235
238
  const mode = isPublic ? resolvePublicSandboxMode(sandbox) : null;
@@ -304,7 +307,7 @@ export default {
304
307
  freshSession: true,
305
308
  });
306
309
  const args = [
307
- '--bare', '--safe-mode', '--auth-type', 'openai', '-m', ctx.environment.model,
310
+ '--bare', '--safe-mode', '--auth-type', 'openai', '-m', seatModel,
308
311
  '--mcp-config', mcpPath, '--exclude-tools', [
309
312
  ...EXCLUDED_TOOLS, ...(isPublic ? PUBLIC_READ_TOOLS : []),
310
313
  ].join(','),
@@ -0,0 +1,15 @@
1
+ /** Fleet runners opt in; BYO users retain their own CLI's model default. */
2
+ export const seatModelRequired = () => process.env.COMMONLY_REQUIRE_SEAT_MODEL === '1';
3
+
4
+ export const requireSeatModel = (environment, adapterName, required = seatModelRequired()) => {
5
+ const model = environment?.model;
6
+ if (typeof model !== 'string' || !model.trim()) {
7
+ if (!required) return undefined;
8
+ const error = new Error(
9
+ `${adapterName} requires environment.model; set the seat model with commonly agent config <name> --model <model> before running`,
10
+ );
11
+ error.code = 'seat_model_missing';
12
+ throw error;
13
+ }
14
+ return model.trim();
15
+ };