@commonlyai/cli 0.1.189 → 0.1.190

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.
@@ -1,7 +1,7 @@
1
- /** Hermes 0.22.2 local CLI wrapper. Hermes owns provider I/O; this file only spawns it. */
1
+ /** Hermes 0.22 local CLI wrapper. Hermes owns provider I/O; this file only spawns it. */
2
2
  import { spawn as childSpawn, spawnSync } from 'child_process';
3
- import { createHash } from 'crypto';
4
- import { mkdir } from 'fs/promises';
3
+ import { createHash, randomBytes } from 'crypto';
4
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'fs/promises';
5
5
  import { homedir } from 'os';
6
6
  import { join } from 'path';
7
7
  import { StringDecoder } from 'string_decoder';
@@ -9,11 +9,64 @@ import { observeSpawn, controlsFromArguments } from '../runtime-observation.js';
9
9
  import { requireSeatModel } from '../seat-model.js';
10
10
  import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
11
11
 
12
- export const TESTED_HERMES_VERSION = '0.22.2';
12
+ export const SUPPORTED_HERMES_VERSION_RANGE = '>=0.22.5 <0.23.0';
13
+ const supportsHermesVersion = (version) => {
14
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version || '');
15
+ if (!match) return false;
16
+ return Number(match[1]) === 0 && Number(match[2]) === 22 && Number(match[3]) >= 5;
17
+ };
13
18
  const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
14
19
  const VERSION_TIMEOUT_MS = 15000;
15
20
  const MAX_STDERR_CHARS = 4096;
16
21
  const MAX_LINE_CHARS = 1024 * 1024;
22
+ // Keys the wrapper owns in the profile's .env. Every other line is the user's.
23
+ const PROFILE_ENV_KEYS = ['OPENROUTER_API_KEY', 'OPENROUTER_BASE_URL'];
24
+ const PLAIN_ENV_VALUE = /^[A-Za-z0-9_\-.:/+=@%~]+$/;
25
+
26
+ const definedKey = (line) => {
27
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
28
+ return match ? match[1] : null;
29
+ };
30
+
31
+ // From 0.22.5 the detached gateway serves a profiles/<hash> home with a child
32
+ // env scrubbed of credentials and overlaid with that profile's own .env
33
+ // (tools/environments/local.py, served_profile_child_env), so a key in the
34
+ // spawn env never reaches the model. Hermes documents <profile>/.env as where
35
+ // a profile's API keys live (website/docs/user-guide/profiles.md). The file
36
+ // stays after the turn: the gateway outlives the spawn and rereads it.
37
+ export const syncProfileEnv = async (home, env) => {
38
+ const owned = PROFILE_ENV_KEYS.filter((key) => env[key]);
39
+ for (const key of owned) {
40
+ if (!PLAIN_ENV_VALUE.test(env[key])) {
41
+ throw new Error(`${key} contains characters a Hermes .env line cannot carry unquoted; refusing to write it`);
42
+ }
43
+ }
44
+ const path = join(home, '.env');
45
+ let current = '';
46
+ try {
47
+ current = await readFile(path, 'utf8');
48
+ } catch (error) {
49
+ if (error?.code !== 'ENOENT') throw new Error(`Cannot read the Hermes profile .env (${error?.code || 'read failed'})`);
50
+ }
51
+ const kept = current.split(/\r?\n/).filter((line, index, lines) => (
52
+ !owned.includes(definedKey(line)) && !(index === lines.length - 1 && line === '')
53
+ ));
54
+ const next = `${[...kept, ...owned.map((key) => `${key}=${env[key]}`)].join('\n')}\n`;
55
+ await chmod(home, 0o700);
56
+ if (next === current) {
57
+ await chmod(path, 0o600);
58
+ return;
59
+ }
60
+ const temp = join(home, `.env.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
61
+ try {
62
+ await writeFile(temp, next, { mode: 0o600, flag: 'wx' });
63
+ await chmod(temp, 0o600);
64
+ await rename(temp, path);
65
+ } catch (error) {
66
+ await rm(temp, { force: true });
67
+ throw new Error(`Cannot write the Hermes profile .env (${error?.code || 'write failed'})`);
68
+ }
69
+ };
17
70
 
18
71
  const readVersion = (env, spawnSyncImpl = spawnSync) => {
19
72
  let probe;
@@ -24,8 +77,8 @@ const readVersion = (env, spawnSyncImpl = spawnSync) => {
24
77
  if (probe.error?.code === 'ENOENT') return null;
25
78
  if (probe.error || probe.status !== 0) continue;
26
79
  const version = String(probe.stdout || '').match(/Hermes Agent v([^\s]+)/i)?.[1];
27
- if (version !== TESTED_HERMES_VERSION) {
28
- throw new Error(`Hermes ${version || 'unknown'} is unsupported; this adapter requires ${TESTED_HERMES_VERSION}`);
80
+ if (!supportsHermesVersion(version)) {
81
+ throw new Error(`Hermes ${version || 'unknown'} is unsupported; this adapter supports ${SUPPORTED_HERMES_VERSION_RANGE}. Run hermes update if yours is older than 0.22.5; releases 0.23 and later are not supported yet`);
29
82
  }
30
83
  return version;
31
84
  }
@@ -182,13 +235,18 @@ export const createHermesAdapter = ({
182
235
  await mkdir(home, { recursive: true, mode: 0o700 });
183
236
  const env = { ...(ctx.env || process.env), HERMES_HOME: home };
184
237
  if (!readVersion(env, ctx._spawnSyncImpl)) {
185
- throw new Error('Hermes Agent 0.22.2 is not on PATH');
238
+ throw new Error(`Hermes Agent ${SUPPORTED_HERMES_VERSION_RANGE} is not on PATH; install for your OS: https://hermes-agent.nousresearch.com/docs/getting-started/installation`);
186
239
  }
187
240
  if (!env.OPENROUTER_API_KEY) {
188
241
  throw new Error('Supply the operator OPENROUTER_API_KEY through Secret Manager before running Hermes');
189
242
  }
243
+ await syncProfileEnv(home, env);
244
+ const cwd = ctx.cwd || process.cwd();
245
+ // 0.22.5's gateway rejects --no-restore-cwd; --in pins the workspace on
246
+ // both fresh and resumed turns. 0.22.2-0.22.4 are refused above, so no
247
+ // version-keyed flag is needed.
190
248
  const args = ['chat', '--oneshot', '--query-file', '-', '--format', 'stream-json',
191
- '--provider', 'openrouter', '--source', 'tool', '--no-restore-cwd'];
249
+ '--provider', 'openrouter', '--source', 'tool', '--in', cwd];
192
250
  if (seatModel) args.push('--model', seatModel);
193
251
  if (ctx.sessionId) args.push('--resume', ctx.sessionId);
194
252
  const fullPrompt = memoryPreamble(prompt, ctx.memoryLongTerm, {
@@ -198,8 +256,8 @@ export const createHermesAdapter = ({
198
256
  const timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout > 0
199
257
  ? Math.min(requestedTimeout, DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
200
258
  return runHermes({
201
- args, prompt: fullPrompt, cwd: ctx.cwd, env, timeoutMs,
202
- spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(args, { modelFlag: '--model', valueFlags: ['--query-file', '--format', '--provider', '--source', '--resume'] }),
259
+ args, prompt: fullPrompt, cwd, env, timeoutMs,
260
+ spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(args, { modelFlag: '--model', valueFlags: ['--query-file', '--format', '--provider', '--source', '--in', '--resume'] }),
203
261
  credentials: spawnCredentials(ctx, ['OPENROUTER_API_KEY']),
204
262
  });
205
263
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.189",
3
+ "version": "0.1.190",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -5,7 +5,7 @@ import { buildMemoryPreamble } from '../memory-bridge.js';
5
5
  import { effectiveSandboxTrust } from '../environment.js';
6
6
  import { seatModelRequired } from './seat-model.js';
7
7
 
8
- export { TESTED_HERMES_VERSION, readHermesJsonl } from '@commonlyai/seats/adapters/hermes';
8
+ export { SUPPORTED_HERMES_VERSION_RANGE, readHermesJsonl, syncProfileEnv } from '@commonlyai/seats/adapters/hermes';
9
9
 
10
10
  // Commonly's policy for the generic Hermes adapter: its memory preamble, its
11
11
  // legacy sandbox-trust names, the fleet seat-model switch, and the profile