@awak-app/simy-cli 0.6.31 → 0.6.32
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": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.32",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"test:macos-installer": "node --test packaging/test-macos-packaging.mjs",
|
|
51
51
|
"build:macos-installer:production": "bash packaging/build-macos-arm64.sh --production",
|
|
52
52
|
"test:e2e:claude-parity": "node ./scripts/claude-parity-runtime-e2e.js",
|
|
53
|
-
"test:e2e:claude-plugin": "node ./scripts/claude-plugin-runtime-e2e.js"
|
|
53
|
+
"test:e2e:claude-plugin": "node ./scripts/claude-plugin-runtime-e2e.js",
|
|
54
|
+
"test:e2e:sqm-faults": "node ./scripts/sqm-complete-loop-e2e.js"
|
|
54
55
|
},
|
|
55
56
|
"engines": {
|
|
56
57
|
"node": ">=20"
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.32";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -6,7 +6,7 @@ import { pinnedWorkflowSource } from './local-runtime.js';
|
|
|
6
6
|
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
7
7
|
// Exact shipped 0.6.23 and 0.6.24 entrypoints only. Customized hooks are retained.
|
|
8
8
|
const shipped = new Set(['8a454293d119eac7cdb46c2df70c6b54a431fcf575bbf4f48f3373ca14414f3b', '339dc64d7ac980f9bfdafd1b2feea47ef8e86b2895b9d6e040086b005a806a79']);
|
|
9
|
-
|
|
9
|
+
const originalLegacyRoutingShim = `// SIMY managed immutable-runtime route v1. Original exports retained for compatibility.
|
|
10
10
|
export * from './delivery-loop.pre-runtime-route.mjs';
|
|
11
11
|
import { readFile, readdir, lstat } from 'node:fs/promises';
|
|
12
12
|
import { join, resolve } from 'node:path';
|
|
@@ -34,6 +34,10 @@ export async function main(provider) {
|
|
|
34
34
|
} catch { process.stdout.write('{}\\n'); process.stderr.write('SIMY runtime route unavailable; original task state was preserved.\\n'); }
|
|
35
35
|
}
|
|
36
36
|
`;
|
|
37
|
+
export const legacyRoutingShim = originalLegacyRoutingShim.replace(
|
|
38
|
+
"for (const input of ['package.json', 'package-lock.json', 'README.md', 'src']) await visit(input);",
|
|
39
|
+
"for (const input of ['package.json', 'package-lock.json', 'README.md', 'src']) { try { await visit(input); } catch (error) { if (input === 'package-lock.json' && error.code === 'ENOENT') hash.update('packaged-without-lock\\0'); else throw error; } } if (pin.fingerprintVersion === 2) await visit('node_modules');",
|
|
40
|
+
);
|
|
37
41
|
export async function routeLegacyHooks({ root, codexHome = process.env.CODEX_HOME || join(homedir(), '.codex') } = {}) {
|
|
38
42
|
await pinnedWorkflowSource({ root });
|
|
39
43
|
const folder = join(codexHome, 'plugins/cache/personal/simy');
|
|
@@ -47,9 +51,10 @@ export async function routeLegacyHooks({ root, codexHome = process.env.CODEX_HOM
|
|
|
47
51
|
if (manifest.name !== 'simy' || manifest.version !== version || (await lstat(file)).isSymbolicLink()) continue;
|
|
48
52
|
const bytes = await readFile(file), before = digest(bytes), next = digest(legacyRoutingShim);
|
|
49
53
|
if (before === next) { receipts.push({ version, outcome: 'current', routeDigest: next }); continue; }
|
|
50
|
-
|
|
54
|
+
const managedV1 = before === digest(originalLegacyRoutingShim);
|
|
55
|
+
if (!shipped.has(before) && !managedV1) { receipts.push({ version, outcome: 'customized_retained', originalDigest: before }); continue; }
|
|
51
56
|
const backup = join(cache, 'hooks/delivery-loop.pre-runtime-route.mjs');
|
|
52
|
-
try { await writeFile(backup, bytes, { mode: 0o600, flag: 'wx' }); }
|
|
57
|
+
try { if (!managedV1) await writeFile(backup, bytes, { mode: 0o600, flag: 'wx' }); }
|
|
53
58
|
catch (error) { if (error.code !== 'EEXIST' || digest(await readFile(backup)) !== before) throw error; }
|
|
54
59
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
55
60
|
await writeFile(temporary, legacyRoutingShim, { mode: 0o600 }); await rename(temporary, file);
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
-
import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { acquireDesktopProcessLock } from '../desktop-process-lock.js';
|
|
8
|
+
import { compareVersions } from '../backend-executable.js';
|
|
7
9
|
import { stableHookRuntime } from '../hook-runtime.js';
|
|
8
10
|
|
|
9
11
|
const exec = promisify(execFile);
|
|
10
12
|
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
11
13
|
const inputs = ['package.json', 'package-lock.json', 'README.md', 'src'];
|
|
12
14
|
|
|
13
|
-
async function fingerprint(source, { allowMissingLock = false } = {}) {
|
|
15
|
+
async function fingerprint(source, { allowMissingLock = false, includeDependencies = false } = {}) {
|
|
14
16
|
const hash = createHash('sha256');
|
|
15
17
|
async function visit(relative) {
|
|
16
18
|
const target = join(source, relative);
|
|
@@ -29,6 +31,7 @@ async function fingerprint(source, { allowMissingLock = false } = {}) {
|
|
|
29
31
|
else throw error;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
34
|
+
if (includeDependencies) await visit('node_modules');
|
|
32
35
|
return hash.digest('hex');
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -40,12 +43,17 @@ async function atomicJson(file, value) {
|
|
|
40
43
|
|
|
41
44
|
function quote(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
|
|
42
45
|
|
|
43
|
-
export async function localRuntimeStatus({ root, source = packageRoot, allowMissingLock = false } = {}) {
|
|
46
|
+
export async function localRuntimeStatus({ root, source = packageRoot, allowMissingLock = false, includeDependencies } = {}) {
|
|
44
47
|
const pkg = JSON.parse(await readFile(join(source, 'package.json'), 'utf8'));
|
|
48
|
+
if (includeDependencies === undefined) {
|
|
49
|
+
try { includeDependencies = JSON.parse(await readFile(join(source, 'runtime-build.json'), 'utf8')).fingerprintVersion === 2; }
|
|
50
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; includeDependencies = false; }
|
|
51
|
+
}
|
|
52
|
+
if (includeDependencies) allowMissingLock = true;
|
|
45
53
|
let installation = null;
|
|
46
54
|
try { installation = JSON.parse(await readFile(join(root, 'delivery-runtime', 'current.json'), 'utf8')); }
|
|
47
55
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
48
|
-
return { version: pkg.version, source: resolve(source), sourceDigest: await fingerprint(source, { allowMissingLock }),
|
|
56
|
+
return { version: pkg.version, source: resolve(source), sourceDigest: await fingerprint(source, { allowMissingLock, includeDependencies }),
|
|
49
57
|
purposeVersion: 2, decisionPolicy: 'llm-first-hc102', stateContractVersion: 2, consistencyMonitorVersion: 1, installation };
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -58,14 +66,51 @@ export async function pinnedWorkflowSource({ root, source = packageRoot } = {})
|
|
|
58
66
|
if (resolve(current.release || '') !== expected) throw new Error('Local workflow pin points outside its immutable releases.');
|
|
59
67
|
const info = await lstat(expected);
|
|
60
68
|
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Pinned workflow source must be a regular directory.');
|
|
61
|
-
const status = await localRuntimeStatus({ root, source: expected });
|
|
69
|
+
const status = await localRuntimeStatus({ root, source: expected, allowMissingLock: true, includeDependencies: current.fingerprintVersion === 2 });
|
|
62
70
|
if (status.sourceDigest !== current.digest || status.version !== current.version) throw new Error('Pinned workflow runtime failed integrity verification.');
|
|
63
71
|
return resolve(source) === expected ? null : expected;
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
// Only the installer-selected bundle can advance an older local pin. Never
|
|
75
|
+
// adopt a checkout, downgrade a candidate, or touch task/provider state.
|
|
76
|
+
export async function adoptInstalledRuntime({ root, source = packageRoot, codexHome } = {}) {
|
|
77
|
+
let selected;
|
|
78
|
+
try { selected = await realpath(join(root, 'installer/current/Contents/Resources/cli')); }
|
|
79
|
+
catch (error) { if (error.code === 'ENOENT') return { adopted: false }; throw error; }
|
|
80
|
+
if (await realpath(source) !== selected) return { adopted: false };
|
|
81
|
+
const status = await localRuntimeStatus({ root, source, allowMissingLock: true, includeDependencies: false });
|
|
82
|
+
if (!status.installation || compareVersions(status.version, status.installation.version) <= 0) return { adopted: false };
|
|
83
|
+
// Validate the existing pin before replacing it; corruption is not an upgrade.
|
|
84
|
+
await pinnedWorkflowSource({ root, source });
|
|
85
|
+
const owner = await acquireDesktopProcessLock(root);
|
|
86
|
+
try {
|
|
87
|
+
const { routeLegacyHooks } = await import('./legacy-hook-routing.js');
|
|
88
|
+
await routeLegacyHooks({ root, ...(codexHome ? { codexHome } : {}) });
|
|
89
|
+
const installation = await installLocalRuntime({ root, source, allowMissingLock: true, includeDependencies: true,
|
|
90
|
+
expectedInstallationDigest: status.installation.digest,
|
|
91
|
+
verifyBeforeActivate: async () => {
|
|
92
|
+
if (await realpath(join(root, 'installer/current/Contents/Resources/cli')) !== selected) throw new Error('Installer selection changed during runtime adoption.');
|
|
93
|
+
},
|
|
94
|
+
installDependencies: async target => {
|
|
95
|
+
// Use the packaged dependencies without network or lifecycle scripts.
|
|
96
|
+
await cp(join(source, 'node_modules'), join(target, 'node_modules'), { recursive: true, dereference: true });
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
// A locally activated candidate is a separate explicit activation contract.
|
|
100
|
+
// Retain its receipt, but prevent watchdog replay of the superseded source.
|
|
101
|
+
const activeFile = join(root, 'desktop-runtime/active.json');
|
|
102
|
+
try {
|
|
103
|
+
const active = JSON.parse(await readFile(activeFile, 'utf8'));
|
|
104
|
+
if (active.digest !== installation.digest && !active.suspendedBy) await atomicJson(activeFile, { ...active, suspendedBy: 'runtime_upgrade_requires_activation', supersededBy: installation.digest });
|
|
105
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
106
|
+
return { adopted: true, installation };
|
|
107
|
+
} finally { await owner.release(); }
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
// Install a frozen candidate alongside the released app, without replacing a
|
|
67
111
|
// signed application, changing provider trust, or starting any registered task.
|
|
68
|
-
export async function installLocalRuntime({ root, source = packageRoot,
|
|
112
|
+
export async function installLocalRuntime({ root, source = packageRoot, allowMissingLock = false, includeDependencies = false,
|
|
113
|
+
expectedInstallationDigest, verifyBeforeActivate = async () => {},
|
|
69
114
|
installDependencies = async target => exec(process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
|
70
115
|
['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: target, timeout: 120_000, maxBuffer: 1024 * 1024 }),
|
|
71
116
|
} = {}) {
|
|
@@ -76,7 +121,8 @@ export async function installLocalRuntime({ root, source = packageRoot,
|
|
|
76
121
|
await mkdir(lock); // An existing install is not silently stolen.
|
|
77
122
|
const staged = join(storage, `.staged-${randomUUID()}`);
|
|
78
123
|
try {
|
|
79
|
-
const before = await localRuntimeStatus({ root, source });
|
|
124
|
+
const before = await localRuntimeStatus({ root, source, allowMissingLock, includeDependencies });
|
|
125
|
+
if (expectedInstallationDigest && before.installation?.digest !== expectedInstallationDigest) throw new Error('Runtime selection changed before adoption.');
|
|
80
126
|
const digest = before.sourceDigest;
|
|
81
127
|
const release = join(storage, 'releases', digest);
|
|
82
128
|
await mkdir(dirname(release), { recursive: true, mode: 0o700 });
|
|
@@ -88,16 +134,21 @@ export async function installLocalRuntime({ root, source = packageRoot,
|
|
|
88
134
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
89
135
|
if (!exists) {
|
|
90
136
|
await mkdir(staged, { mode: 0o700 });
|
|
91
|
-
for (const input of inputs)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
for (const input of inputs) {
|
|
138
|
+
try { await cp(join(source, input), join(staged, input), { recursive: true, errorOnExist: true }); }
|
|
139
|
+
catch (error) { if (!(allowMissingLock && input === 'package-lock.json' && error.code === 'ENOENT')) throw error; }
|
|
140
|
+
}
|
|
141
|
+
if (includeDependencies) await installDependencies(staged);
|
|
142
|
+
if (await fingerprint(staged, { allowMissingLock, includeDependencies }) !== digest || await fingerprint(source, { allowMissingLock, includeDependencies }) !== digest) throw new Error('Runtime source changed while copying; no candidate activated.');
|
|
143
|
+
if (!includeDependencies) await installDependencies(staged);
|
|
144
|
+
if (await fingerprint(staged, { allowMissingLock, includeDependencies }) !== digest) throw new Error('Dependency installation changed the runtime source.');
|
|
145
|
+
await atomicJson(join(staged, 'runtime-build.json'), { schema: 1, version: before.version, digest, fingerprintVersion: includeDependencies ? 2 : 1 });
|
|
96
146
|
await rename(staged, release);
|
|
97
147
|
} else {
|
|
98
148
|
const saved = JSON.parse(await readFile(join(release, 'runtime-build.json'), 'utf8'));
|
|
99
|
-
if (saved.schema !== 1 || saved.digest !== digest || await fingerprint(release) !== digest) throw new Error('Existing candidate failed integrity verification.');
|
|
149
|
+
if (saved.schema !== 1 || saved.digest !== digest || await fingerprint(release, { allowMissingLock, includeDependencies }) !== digest) throw new Error('Existing candidate failed integrity verification.');
|
|
100
150
|
}
|
|
151
|
+
await verifyBeforeActivate();
|
|
101
152
|
const bin = join(root, 'bin');
|
|
102
153
|
await mkdir(bin, { recursive: true, mode: 0o700 });
|
|
103
154
|
const launcher = join(bin, 'simy-delivery');
|
|
@@ -114,7 +165,7 @@ export async function installLocalRuntime({ root, source = packageRoot,
|
|
|
114
165
|
await writeFile(cliTemporary, `#!/bin/sh\nexec ${quote(stableHookRuntime())} ${quote(join(release, 'src/index.js'))} --no-auto-update "$@"\n`, { mode: 0o700 });
|
|
115
166
|
await rename(cliTemporary, cli);
|
|
116
167
|
} finally { await rm(cliTemporary, { force: true }); }
|
|
117
|
-
const current = { schema: 1, version: before.version, digest, release, launcher, cli,
|
|
168
|
+
const current = { schema: 1, version: before.version, digest, release, launcher, cli, fingerprintVersion: includeDependencies ? 2 : 1,
|
|
118
169
|
previous: before.installation?.digest === digest ? before.installation.previous
|
|
119
170
|
: before.installation ? { version: before.installation.version, digest: before.installation.digest, release: before.installation.release } : null };
|
|
120
171
|
await atomicJson(join(storage, 'current.json'), current);
|
package/src/desktop-host.js
CHANGED
|
@@ -33,7 +33,7 @@ import { startAgent } from "./agent.js";
|
|
|
33
33
|
import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
|
|
34
34
|
import { suspendDesktopWatchdog } from './delivery-hooks/runtime-watchdog.js';
|
|
35
35
|
import { attestDesktopRuntime } from './delivery-hooks/desktop-runtime.js';
|
|
36
|
-
import { pinnedWorkflowSource } from './delivery-hooks/local-runtime.js';
|
|
36
|
+
import { adoptInstalledRuntime, pinnedWorkflowSource } from './delivery-hooks/local-runtime.js';
|
|
37
37
|
import { applyLoginShellPath } from "./login-shell-path.js";
|
|
38
38
|
import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
|
|
39
39
|
import { createAgentShutdown } from "./shutdown.js";
|
|
@@ -1465,6 +1465,15 @@ export function isDesktopHostEntry(entry = process.argv[1]) {
|
|
|
1465
1465
|
catch { return false; }
|
|
1466
1466
|
}
|
|
1467
1467
|
|
|
1468
|
+
function desktopStartupError(error, fallbackMessage = "SIMY could not start. Check whether another SIMY window is already running, then reconnect.") {
|
|
1469
|
+
const conflict = error?.code === PROCESS_CONFLICT_CODE;
|
|
1470
|
+
return {
|
|
1471
|
+
type: "error",
|
|
1472
|
+
...(conflict ? { code: PROCESS_CONFLICT_CODE, conflicts: error.conflicts || [] } : {}),
|
|
1473
|
+
message: conflict ? PROCESS_CONFLICT_MESSAGE : fallbackMessage,
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1468
1477
|
export function startDesktopEntry() {
|
|
1469
1478
|
let host;
|
|
1470
1479
|
const stop = () => { void host?.close().finally(() => process.exit(0)); };
|
|
@@ -1475,14 +1484,7 @@ export function startDesktopEntry() {
|
|
|
1475
1484
|
captureRuntimeOutput(diagnostics, { stderr: process.stderr });
|
|
1476
1485
|
applyLoginShellPath({ diagnostics }).catch(() => {}).then(() => startDesktopHost()).then((value) => { host = value; }).catch((error) => {
|
|
1477
1486
|
diagnostics.write(`startup failed: ${error?.stack || error}`);
|
|
1478
|
-
|
|
1479
|
-
process.stdout.write(`${JSON.stringify({
|
|
1480
|
-
type: "error",
|
|
1481
|
-
...(conflict ? { code: PROCESS_CONFLICT_CODE, conflicts: error.conflicts || [] } : {}),
|
|
1482
|
-
message: conflict
|
|
1483
|
-
? PROCESS_CONFLICT_MESSAGE
|
|
1484
|
-
: "SIMY could not start. Check whether another SIMY window is already running, then reconnect.",
|
|
1485
|
-
})}\n`);
|
|
1487
|
+
process.stdout.write(`${JSON.stringify(desktopStartupError(error))}\n`);
|
|
1486
1488
|
process.exitCode = 1;
|
|
1487
1489
|
});
|
|
1488
1490
|
}
|
|
@@ -1490,14 +1492,20 @@ export function startDesktopEntry() {
|
|
|
1490
1492
|
if (isDesktopHostEntry()) {
|
|
1491
1493
|
try {
|
|
1492
1494
|
const root = process.env.SIMY_HOME?.trim() || join(homedir(), '.simy');
|
|
1495
|
+
try { await adoptInstalledRuntime({ root }); }
|
|
1496
|
+
catch (error) {
|
|
1497
|
+
if (error.code === PROCESS_CONFLICT_CODE) throw error;
|
|
1498
|
+
await pinnedWorkflowSource({ root }); // Only a verified previous runtime may recover.
|
|
1499
|
+
process.stderr.write('SIMY runtime upgrade deferred: ' + error.message + '\n');
|
|
1500
|
+
}
|
|
1493
1501
|
const selected = await pinnedWorkflowSource({ root });
|
|
1494
1502
|
if (selected) {
|
|
1495
1503
|
process.env.SIMY_AUTO_UPDATE = '0';
|
|
1496
1504
|
const selectedHost = await import(pathToFileURL(join(selected, 'src/desktop-host.js')).href);
|
|
1497
1505
|
selectedHost.startDesktopEntry();
|
|
1498
1506
|
} else startDesktopEntry();
|
|
1499
|
-
} catch {
|
|
1500
|
-
process.stdout.write(JSON.stringify(
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
process.stdout.write(JSON.stringify(desktopStartupError(error, 'Local desktop runtime integrity could not be verified. Inspect delivery runtime status; the previous task state was retained.')) + '\n');
|
|
1501
1509
|
process.exitCode = 1;
|
|
1502
1510
|
}
|
|
1503
1511
|
}
|