@commonlyai/cli 0.1.94 → 0.1.96

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.94",
3
+ "version": "0.1.96",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * OpenCode adapter — wraps `opencode run --format json` as a Commonly seat.
3
3
  *
4
- * Tested against opencode-ai 1.18.35. OpenCode stores conversation sessions
5
- * under XDG_DATA_HOME. Trusted seats use the operator's data home directly;
6
- * public seats get per-identity state without operator auth.json and a child
7
- * environment limited to safe launch/locale variables. Public provider keys
4
+ * Smoke-tested versions are recorded in TESTED_OPENCODE_VERSIONS below.
5
+ * OpenCode stores sessions under XDG_DATA_HOME. Trusted seats use the
6
+ * operator's data home directly; public seats get per-identity state without
7
+ * operator auth.json and a child environment limited to safe launch/locale
8
+ * variables. Public provider keys
8
9
  * stay in the host adapter and reach the configured provider through a
9
10
  * per-spawn loopback proxy; OpenCode receives only that proxy's bearer.
10
11
  * This protects the key, not provider spend: the model can use its seat's key
@@ -56,6 +57,10 @@ import { resolvePublicSandboxMode } from '../sandbox/mode.js';
56
57
 
57
58
  const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
58
59
  const TERMINATION_GRACE_MS = 5000;
60
+ // Bump only with a passing real sandbox-exec smoke against the new version.
61
+ const TESTED_OPENCODE_VERSIONS = Object.freeze(['1.18.35', '1.19.0']);
62
+ const ALLOW_UNTESTED_OPENCODE_VERSION_ENV = 'COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION';
63
+ const VERSION_CHECK_TIMEOUT_MS = 5000;
59
64
  const OPENCODE_CONFIG_GITIGNORE = [
60
65
  'node_modules',
61
66
  'package.json',
@@ -713,6 +718,52 @@ const spawnBinaryPath = (env) => {
713
718
  return 'opencode';
714
719
  };
715
720
 
721
+ const parseOpenCodeVersion = (output) => {
722
+ const match = String(output || '').trim().match(
723
+ /^(?:opencode\s+)?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/i,
724
+ );
725
+ return match?.[1] || null;
726
+ };
727
+
728
+ const readOpenCodeVersion = (binary, env) => {
729
+ try {
730
+ const probeEnv = { ...env };
731
+ for (const key of DISABLED_ENV) probeEnv[key] = '1';
732
+ const result = spawnSync(binary, ['--version'], {
733
+ encoding: 'utf8', env: probeEnv, timeout: VERSION_CHECK_TIMEOUT_MS,
734
+ });
735
+ if (result.error || result.status !== 0) return null;
736
+ return parseOpenCodeVersion(result.stdout);
737
+ } catch {
738
+ return null;
739
+ }
740
+ };
741
+
742
+ const compareOpenCodeVersions = (left, right) => {
743
+ const leftParts = left.split(/[+-]/, 1)[0].split('.').map(Number);
744
+ const rightParts = right.split(/[+-]/, 1)[0].split('.').map(Number);
745
+ for (let index = 0; index < 3; index += 1) {
746
+ if (leftParts[index] !== rightParts[index]) {
747
+ return leftParts[index] > rightParts[index] ? 1 : -1;
748
+ }
749
+ }
750
+ return 0;
751
+ };
752
+
753
+ const describeOpenCodeVersionMismatch = (installedVersion) => {
754
+ const tested = TESTED_OPENCODE_VERSIONS.join(', ');
755
+ if (!installedVersion) return `installed OpenCode version is unknown; smoke-tested versions: ${tested}`;
756
+ const oldest = TESTED_OPENCODE_VERSIONS[0];
757
+ const newest = TESTED_OPENCODE_VERSIONS[TESTED_OPENCODE_VERSIONS.length - 1];
758
+ if (compareOpenCodeVersions(installedVersion, newest) > 0) {
759
+ return `installed OpenCode ${installedVersion} is newer than smoke-tested versions ${tested}`;
760
+ }
761
+ if (compareOpenCodeVersions(installedVersion, oldest) < 0) {
762
+ return `installed OpenCode ${installedVersion} is older than smoke-tested versions ${tested}`;
763
+ }
764
+ return `installed OpenCode ${installedVersion} differs from smoke-tested versions ${tested}`;
765
+ };
766
+
716
767
  export default {
717
768
  name: 'opencode',
718
769
  runtimeType: 'opencode',
@@ -726,8 +777,7 @@ export default {
726
777
  const res = spawnSync('opencode', ['--version'], { encoding: 'utf8' });
727
778
  if (res.error || res.status !== 0) return null;
728
779
  const stdout = (res.stdout || '').trim();
729
- const versionMatch = stdout.match(/(\d+\.\d+(?:\.\d+)?)/);
730
- const version = versionMatch ? versionMatch[1] : (stdout || 'unknown');
780
+ const version = parseOpenCodeVersion(stdout) || stdout || 'unknown';
731
781
  const where = spawnSync('which', ['opencode'], { encoding: 'utf8' });
732
782
  const path = where.status === 0 ? (where.stdout || '').trim() || 'opencode' : 'opencode';
733
783
  return { path, version };
@@ -761,6 +811,29 @@ export default {
761
811
  }
762
812
  const providerKeyFile = await validateProviderEnvironment(ctx.environment, ctx.cwd);
763
813
 
814
+ const sourceEnv = ctx.env || process.env;
815
+ const childSourceEnv = Object.fromEntries(Object.entries(sourceEnv)
816
+ .filter(([key]) => key !== ALLOW_UNTESTED_OPENCODE_VERSION_ENV));
817
+ const runtimeEnv = isPublic
818
+ ? Object.fromEntries(Object.entries(childSourceEnv).filter(([key]) => PUBLIC_SAFE_ENV.test(key)))
819
+ : childSourceEnv;
820
+ const binary = ctx._binaryPath || spawnBinaryPath(runtimeEnv);
821
+ const installedVersion = readOpenCodeVersion(binary, runtimeEnv);
822
+ if (!TESTED_OPENCODE_VERSIONS.includes(installedVersion)) {
823
+ const mismatch = describeOpenCodeVersionMismatch(installedVersion);
824
+ const allowOverride = process.env[ALLOW_UNTESTED_OPENCODE_VERSION_ENV] === '1';
825
+ if (isPublic && !allowOverride) {
826
+ throw new Error(
827
+ `Public OpenCode seats refuse to run: ${mismatch}. `
828
+ + `To override from the host process, set ${ALLOW_UNTESTED_OPENCODE_VERSION_ENV}=1.`,
829
+ );
830
+ }
831
+ // eslint-disable-next-line no-console
832
+ console.warn(isPublic
833
+ ? `[opencode] ${mismatch}; continuing because the host process set ${ALLOW_UNTESTED_OPENCODE_VERSION_ENV}=1`
834
+ : `[opencode] ${mismatch}; trusted seat continues`);
835
+ }
836
+
764
837
  const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, {
765
838
  freshSession: !ctx.sessionId,
766
839
  });
@@ -834,10 +907,6 @@ export default {
834
907
  });
835
908
  await chmod(configPath, 0o600);
836
909
 
837
- const sourceEnv = ctx.env || process.env;
838
- const runtimeEnv = isPublic
839
- ? Object.fromEntries(Object.entries(sourceEnv).filter(([key]) => PUBLIC_SAFE_ENV.test(key)))
840
- : sourceEnv;
841
910
  const childEnv = makeChildEnvironment({
842
911
  source: runtimeEnv,
843
912
  tempDir,
@@ -858,7 +927,6 @@ export default {
858
927
  effort: ctx.environment?.effort,
859
928
  title: ctx.agentName,
860
929
  });
861
- const binary = ctx._binaryPath || spawnBinaryPath(childEnv);
862
930
  const platform = ctx._platform || process.platform;
863
931
  let cmd = binary;
864
932
  let spawnArgs = args;
@@ -928,13 +996,16 @@ export default {
928
996
  };
929
997
 
930
998
  export {
999
+ ALLOW_UNTESTED_OPENCODE_VERSION_ENV,
931
1000
  DISABLED_ENV,
1001
+ TESTED_OPENCODE_VERSIONS,
932
1002
  buildArgs,
933
1003
  buildMcpConfig,
934
1004
  buildProviderConfig,
935
1005
  makeEventParser,
936
1006
  prepareOpenCodeDataHome,
937
1007
  publicPermissions,
1008
+ readOpenCodeVersion,
938
1009
  qualifiedProviderModel,
939
1010
  resolveProviderKeyFile,
940
1011
  startProviderProxy,