@awak-app/simy-cli 0.5.5 → 0.5.7
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/README.md +24 -3
- package/package.json +2 -2
- package/src/cli-contract.js +1 -1
- package/src/index.js +8 -1
- package/src/sqm/agent-skill.js +26 -6
- package/src/sqm/bundle-store.js +9 -4
- package/src/sqm/index.js +1 -1
- package/src/sqm/proof.js +1 -0
- package/src/sqm/validation.js +15 -1
- package/src/sqm/workflow-onboarding.js +6 -1
- package/src/sqm/workflow-update.js +90 -0
package/README.md
CHANGED
|
@@ -9,14 +9,24 @@ npm install -g @awak-app/simy-cli
|
|
|
9
9
|
simy --daemon
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
When an interactive `simy --daemon` starts
|
|
13
|
-
|
|
12
|
+
When an interactive `simy --daemon` starts without managed SIMY workflows,
|
|
13
|
+
the CLI asks once before installing the Codex plugin
|
|
14
14
|
marketplace, Codex/Claude SQM skills, and PR hooks. Existing unrelated plugins
|
|
15
15
|
and hooks are preserved. Use `--install-workflows` for explicit non-interactive
|
|
16
16
|
installation or `--no-workflow-prompt` to start the daemon without asking.
|
|
17
|
-
After installation, restart Codex and install SIMY
|
|
17
|
+
After installation, restart Codex and find and install **SIMY** in **Plugins**
|
|
18
18
|
before using `@simy` in a new task.
|
|
19
19
|
|
|
20
|
+
After that first installation, the daemon checks installed workflows at startup
|
|
21
|
+
and every hour, including after a CLI automatic upgrade. It updates only existing
|
|
22
|
+
platform installations and refreshes an already enabled SIMY Codex plugin through
|
|
23
|
+
`codex plugin add`. Plugin versions include a content digest so changed bundles
|
|
24
|
+
receive a fresh cache. Use a new Codex task to pick up updated skills; running
|
|
25
|
+
tasks are not restarted. Missing or disabled plugins are not enabled automatically.
|
|
26
|
+
Failed refreshes retry on the next check without blocking daemon startup.
|
|
27
|
+
`--no-auto-update` or `SIMY_AUTO_UPDATE=0` disables these background updates too;
|
|
28
|
+
`--no-workflow-prompt` only suppresses the initial installation prompt.
|
|
29
|
+
|
|
20
30
|
This is the recommended installation because the CLI can safely install patch
|
|
21
31
|
and minor releases, and the daemon restarts only when no Agentic Loop task is active. To run
|
|
22
32
|
the interactive chat after installation, use `simy`.
|
|
@@ -370,3 +380,14 @@ Publishing is handled by GitHub Actions in `.github/workflows/publish.yml`.
|
|
|
370
380
|
`@awak-app/simy-cli`.
|
|
371
381
|
- The package check inspects the final npm tarball contents and fails if any
|
|
372
382
|
source map file would be published.
|
|
383
|
+
|
|
384
|
+
### Repository system scopes
|
|
385
|
+
|
|
386
|
+
Version 0.5.7 supports signed `system_scope` snapshots from Quality Monitor.
|
|
387
|
+
Each check pins the repository's connected component and graph revision. A new
|
|
388
|
+
check revalidates scoped bundles online; a stale or unavailable scope cannot fall
|
|
389
|
+
back to an older cached component. Minimum and full check proofs retain the
|
|
390
|
+
original snapshot. A scope revision change requires a fresh minimum check.
|
|
391
|
+
Deploy this CLI and the web capability proxy before enabling the backend scope
|
|
392
|
+
requirement. Older clients receive an explicit upgrade error, never an expanded
|
|
393
|
+
organization-wide bundle.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
|
|
39
39
|
"check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./src/local-task.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
40
40
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
|
|
41
|
-
"check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
41
|
+
"check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
42
42
|
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js",
|
|
43
43
|
"check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
|
|
44
44
|
"check:package": "node ./scripts/check-package-contents.js"
|
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.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.7";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
installAgentShutdownSignalHandlers,
|
|
21
21
|
} from "./shutdown.js";
|
|
22
22
|
import { prepareDaemonWorkflows } from "./sqm/workflow-onboarding.js";
|
|
23
|
+
import { startWorkflowUpdates } from "./sqm/workflow-update.js";
|
|
23
24
|
import { DEFAULT_WEB_ORIGIN, resolveWebOrigin } from "./web-origin.js";
|
|
24
25
|
|
|
25
26
|
const argv = process.argv.slice(2);
|
|
@@ -160,9 +161,15 @@ async function runLocalAgent() {
|
|
|
160
161
|
quiet: interactive,
|
|
161
162
|
updateManager,
|
|
162
163
|
});
|
|
163
|
-
|
|
164
|
+
let stopWorkflowUpdates = async () => {};
|
|
165
|
+
const closeAgent = createAgentShutdown(agent);
|
|
166
|
+
const shutdownAgent = async () => {
|
|
167
|
+
await stopWorkflowUpdates();
|
|
168
|
+
await closeAgent();
|
|
169
|
+
};
|
|
164
170
|
installAgentShutdownSignalHandlers({ shutdown: shutdownAgent });
|
|
165
171
|
await writeUpdateHandoffFromEnvironment({ version: packageInfo.version, port: agent.port });
|
|
172
|
+
stopWorkflowUpdates = startWorkflowUpdates({ enabled: daemon && !updatesDisabled });
|
|
166
173
|
updateManager.start({
|
|
167
174
|
registry: agent.registry,
|
|
168
175
|
restartDaemon: async (targetVersion) => {
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -117,7 +117,8 @@ export async function installSqmAgentSkill({ home = homedir(), platforms = Objec
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export async function inspectCodexPlugin({ home = homedir() } = {}) {
|
|
120
|
-
const
|
|
120
|
+
const manifest = await bundledPluginManifest();
|
|
121
|
+
const expectedDigest = await directoryDigest(BUNDLED_CODEX_PLUGIN_ROOT, { manifest });
|
|
121
122
|
const target = codexPluginPath(home);
|
|
122
123
|
const installedDigest = await directoryDigest(target, { missing: null });
|
|
123
124
|
const marketplacePath = codexMarketplacePath(home);
|
|
@@ -125,11 +126,12 @@ export async function inspectCodexPlugin({ home = homedir() } = {}) {
|
|
|
125
126
|
const entry = Array.isArray(marketplace?.plugins)
|
|
126
127
|
? marketplace.plugins.find((candidate) => candidate?.name === SIMY_CODEX_PLUGIN_ID)
|
|
127
128
|
: null;
|
|
128
|
-
const marketplaceMatches = isManagedMarketplaceEntry(entry)
|
|
129
|
+
const marketplaceMatches = isManagedMarketplaceEntry(entry) &&
|
|
130
|
+
!hasLegacyMarketplaceDisplayName(marketplace);
|
|
129
131
|
const filesMatch = installedDigest === expectedDigest;
|
|
130
132
|
return {
|
|
131
133
|
plugin_id: SIMY_CODEX_PLUGIN_ID,
|
|
132
|
-
version:
|
|
134
|
+
version: manifest.version,
|
|
133
135
|
path: target,
|
|
134
136
|
marketplace_path: marketplacePath,
|
|
135
137
|
digest: `sha256:${expectedDigest}`,
|
|
@@ -163,6 +165,8 @@ async function installCodexPlugin({ home }) {
|
|
|
163
165
|
await rm(staged, { recursive: true, force: true });
|
|
164
166
|
await rm(backup, { recursive: true, force: true });
|
|
165
167
|
await cp(BUNDLED_CODEX_PLUGIN_ROOT, staged, { recursive: true, errorOnExist: true });
|
|
168
|
+
await writeFile(path.join(staged, ".codex-plugin", "plugin.json"),
|
|
169
|
+
`${JSON.stringify(await bundledPluginManifest(), null, 2)}\n`);
|
|
166
170
|
let backedUp = false;
|
|
167
171
|
try {
|
|
168
172
|
if (await pathExists(target)) {
|
|
@@ -183,10 +187,11 @@ async function installCodexPlugin({ home }) {
|
|
|
183
187
|
? marketplace.name
|
|
184
188
|
: "personal",
|
|
185
189
|
interface: {
|
|
186
|
-
displayName: "
|
|
190
|
+
displayName: "Plugins",
|
|
187
191
|
...(marketplace.interface && typeof marketplace.interface === "object"
|
|
188
192
|
? marketplace.interface
|
|
189
193
|
: {}),
|
|
194
|
+
...(hasLegacyMarketplaceDisplayName(marketplace) ? { displayName: "Plugins" } : {}),
|
|
190
195
|
},
|
|
191
196
|
plugins: [
|
|
192
197
|
...plugins.filter((entry) => entry?.name !== SIMY_CODEX_PLUGIN_ID),
|
|
@@ -196,6 +201,11 @@ async function installCodexPlugin({ home }) {
|
|
|
196
201
|
await atomicText(marketplacePath, `${JSON.stringify(nextMarketplace, null, 2)}\n`, 0o600);
|
|
197
202
|
}
|
|
198
203
|
|
|
204
|
+
function hasLegacyMarketplaceDisplayName(marketplace) {
|
|
205
|
+
// Keep the marketplace identity stable so existing simy@personal installs work.
|
|
206
|
+
return marketplace?.name === "personal" && marketplace?.interface?.displayName === "Personal";
|
|
207
|
+
}
|
|
208
|
+
|
|
199
209
|
function managedMarketplaceEntry() {
|
|
200
210
|
return {
|
|
201
211
|
name: SIMY_CODEX_PLUGIN_ID,
|
|
@@ -307,7 +317,14 @@ async function fileDigest(target) {
|
|
|
307
317
|
}
|
|
308
318
|
}
|
|
309
319
|
|
|
310
|
-
async function
|
|
320
|
+
async function bundledPluginManifest() {
|
|
321
|
+
const manifest = JSON.parse(await readFile(path.join(BUNDLED_CODEX_PLUGIN_ROOT, ".codex-plugin", "plugin.json"), "utf8"));
|
|
322
|
+
// A stable content-derived version refreshes Codex's cache only when the bundle changes.
|
|
323
|
+
const digest = await directoryDigest(BUNDLED_CODEX_PLUGIN_ROOT);
|
|
324
|
+
return { ...manifest, version: `${manifest.version.split("+")[0]}+codex.${digest}` };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function directoryDigest(root, { missing = "throw", manifest } = {}) {
|
|
311
328
|
let rootStat;
|
|
312
329
|
try {
|
|
313
330
|
rootStat = await lstat(root);
|
|
@@ -331,7 +348,10 @@ async function directoryDigest(root, { missing = "throw" } = {}) {
|
|
|
331
348
|
continue;
|
|
332
349
|
}
|
|
333
350
|
if (!entry.isFile()) throw new Error(`${target} must be a regular file.`);
|
|
334
|
-
|
|
351
|
+
const content = manifest && relative === ".codex-plugin/plugin.json"
|
|
352
|
+
? `${JSON.stringify(manifest, null, 2)}\n`
|
|
353
|
+
: await readFile(target);
|
|
354
|
+
hash.update(relative).update("\0").update(content).update("\0");
|
|
335
355
|
}
|
|
336
356
|
};
|
|
337
357
|
await visit(root);
|
package/src/sqm/bundle-store.js
CHANGED
|
@@ -41,13 +41,13 @@ export async function loadKnowledgeBundle({
|
|
|
41
41
|
}
|
|
42
42
|
const cacheIdentity = normalizedCacheIdentity({ endpoint, accountId, organizationId, repository });
|
|
43
43
|
const location = cacheLocation(cacheRoot, cacheIdentity);
|
|
44
|
-
const validation = { cliVersion, publicKeys, organizationId };
|
|
44
|
+
const validation = { cliVersion, publicKeys, organizationId, repository };
|
|
45
45
|
const cached = await readVerifiedCache(location, validation, cacheIdentity);
|
|
46
46
|
const fresh =
|
|
47
47
|
cached &&
|
|
48
48
|
now - cached.fetchedAt < ttlMs &&
|
|
49
49
|
Date.parse(cached.bundle.expires_at) > now;
|
|
50
|
-
if (!offline && endpoint && (!fresh || refresh)) {
|
|
50
|
+
if (!offline && endpoint && (!fresh || refresh || cached?.bundle.system_scope)) {
|
|
51
51
|
try {
|
|
52
52
|
const fetched = await fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag: cached?.etag, fetchImpl });
|
|
53
53
|
if (fetched.notModified) {
|
|
@@ -56,6 +56,7 @@ export async function loadKnowledgeBundle({
|
|
|
56
56
|
await writeMetadata(location, { ...cached.metadata, fetched_at: new Date(now).toISOString(), etag: fetched.etag || cached.etag });
|
|
57
57
|
return result(cached.bundle, [], "cache_revalidated");
|
|
58
58
|
}
|
|
59
|
+
if (cached?.bundle.system_scope && !fetched.bundle?.system_scope) throw nonFallbackError("SQM Cloud attempted to remove the pinned system scope contract.");
|
|
59
60
|
const discoveredTrust = signingTrustForFetch({
|
|
60
61
|
bundle: fetched.bundle,
|
|
61
62
|
endpoint,
|
|
@@ -81,11 +82,12 @@ export async function loadKnowledgeBundle({
|
|
|
81
82
|
return result(bundle, [], "cloud");
|
|
82
83
|
} catch (error) {
|
|
83
84
|
if (error?.code === "incompatible_cli") throw error;
|
|
84
|
-
if (error?.fallbackAllowed !== false && cached && Date.parse(cached.bundle.expires_at) > now) return result(cached.bundle, [`SQM Cloud unavailable; using last verified bundle: ${message(error)}`], "stale_cache");
|
|
85
|
+
if (error?.fallbackAllowed !== false && cached && !cached.bundle.system_scope && Date.parse(cached.bundle.expires_at) > now) return result(cached.bundle, [`SQM Cloud unavailable; using last verified bundle: ${message(error)}`], "stale_cache");
|
|
85
86
|
return result(null, [`SQM knowledge could not be used; shadow checks were skipped: ${message(error)}`], "unavailable");
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
if (cached) {
|
|
90
|
+
if (cached.bundle.system_scope) return result(null, ["SQM system scope must be revalidated online before a new check."], "unavailable");
|
|
89
91
|
if (Date.parse(cached.bundle.expires_at) <= now) return result(null, ["The cached SQM knowledge bundle is expired; shadow checks were skipped."], "unavailable");
|
|
90
92
|
const warnings = [];
|
|
91
93
|
if (offline) warnings.push("SQM is offline; using the last verified knowledge bundle.");
|
|
@@ -96,10 +98,12 @@ export async function loadKnowledgeBundle({
|
|
|
96
98
|
return result(null, [`SQM has no verified cache (${reason}); shadow checks were skipped.`], "unavailable");
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
export function verifyAndValidate(bundle, { cliVersion, publicKeys, organizationId } = {}) {
|
|
101
|
+
export function verifyAndValidate(bundle, { cliVersion, publicKeys, organizationId, repository } = {}) {
|
|
100
102
|
try { validateKnowledgeBundle(bundle, { cliVersion }); } catch (error) { throw nonFallbackError(message(error), error); }
|
|
101
103
|
if (organizationId && bundle.organization_id !== organizationId) throw nonFallbackError(`SQM bundle organization mismatch: expected ${organizationId}.`);
|
|
104
|
+
if (bundle.system_scope && repository && bundle.system_scope.repository !== repository.toLowerCase()) throw nonFallbackError("SQM bundle repository system scope does not match this repository.");
|
|
102
105
|
for (const module of bundle.modules) {
|
|
106
|
+
if (bundle.system_scope && module.scope.visibility === "organization" && !module.scope.repositories.some((repo) => bundle.system_scope.repositories.includes(repo.toLowerCase()))) throw nonFallbackError(`SQM module ${module.id} is outside the repository system.`);
|
|
103
107
|
if (module.scope.visibility === "organization" && module.scope.organization_id !== bundle.organization_id) throw nonFallbackError(`SQM module ${module.id} is scoped to a different organization.`);
|
|
104
108
|
}
|
|
105
109
|
const keyId = bundle.integrity.key_id;
|
|
@@ -166,6 +170,7 @@ async function fetchBundle({ endpoint, repository, cliVersion, token, organizati
|
|
|
166
170
|
const url = new URL(endpoint);
|
|
167
171
|
url.searchParams.set("repository", repository);
|
|
168
172
|
url.searchParams.set("cli_version", cliVersion);
|
|
173
|
+
url.searchParams.set("scope_version", "1");
|
|
169
174
|
const headers = { Accept: "application/json" };
|
|
170
175
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
171
176
|
if (organizationId) headers["x-simy-org-id"] = organizationId;
|
package/src/sqm/index.js
CHANGED
|
@@ -132,7 +132,7 @@ function toLegacyCheck(minCheck, loaded) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function bundleIdentity(bundle) {
|
|
135
|
-
return { id: bundle.bundle_id, version: bundle.version, digest: bundle.integrity.digest, signing_key_id: bundle.integrity.key_id };
|
|
135
|
+
return { id: bundle.bundle_id, version: bundle.version, digest: bundle.integrity.digest, signing_key_id: bundle.integrity.key_id, ...(bundle.system_scope ? { system_scope: bundle.system_scope } : {}) };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function unavailable(warning, repository = null) {
|
package/src/sqm/proof.js
CHANGED
|
@@ -57,6 +57,7 @@ export function buildProof({ organizationId, repository, bundle, minCheck, evalu
|
|
|
57
57
|
version: bundle.version,
|
|
58
58
|
digest: bundle.integrity.digest,
|
|
59
59
|
signing_key_id: bundle.integrity.key_id,
|
|
60
|
+
...(bundle.system_scope ? { system_scope: bundle.system_scope } : {}),
|
|
60
61
|
},
|
|
61
62
|
min_check: {
|
|
62
63
|
result_digest: minCheck.result_digest,
|
package/src/sqm/validation.js
CHANGED
|
@@ -17,13 +17,27 @@ const SCENARIO_REQUIREMENTS = new Set(["local", "requires_human", "requires_post
|
|
|
17
17
|
export function validateKnowledgeBundle(bundle, { cliVersion }) {
|
|
18
18
|
const errors = [];
|
|
19
19
|
object(bundle, "$", errors);
|
|
20
|
-
exactKeys(bundle, ["schema_version", "bundle_id", "version", "organization_id", "generated_at", "expires_at", "modules", "integrity"], "$", errors);
|
|
20
|
+
exactKeys(bundle, ["schema_version", "bundle_id", "version", "organization_id", "generated_at", "expires_at", "modules", "integrity", "system_scope"], "$", errors);
|
|
21
21
|
equal(bundle?.schema_version, "1.0.0", "$.schema_version", errors);
|
|
22
22
|
stringPattern(bundle?.bundle_id, /^[a-z][a-z0-9._-]{2,127}$/, "$.bundle_id", errors);
|
|
23
23
|
stringPattern(bundle?.version, /^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$/, "$.version", errors);
|
|
24
24
|
nullableString(bundle?.organization_id, 200, "$.organization_id", errors);
|
|
25
25
|
dateTime(bundle?.generated_at, "$.generated_at", errors);
|
|
26
26
|
dateTime(bundle?.expires_at, "$.expires_at", errors);
|
|
27
|
+
if (bundle?.system_scope !== undefined) {
|
|
28
|
+
const scope = bundle.system_scope;
|
|
29
|
+
object(scope, "$.system_scope", errors);
|
|
30
|
+
exactKeys(scope, ["organization_id", "repository", "graph_revision", "repositories"], "$.system_scope", errors);
|
|
31
|
+
equal(scope?.organization_id, bundle.organization_id, "$.system_scope.organization_id", errors);
|
|
32
|
+
stringPattern(scope?.repository, REPOSITORY, "$.system_scope.repository", errors);
|
|
33
|
+
if (!Number.isSafeInteger(scope?.graph_revision) || scope.graph_revision < 0) errors.push("$.system_scope.graph_revision must be a nonnegative integer");
|
|
34
|
+
if (!Array.isArray(scope?.repositories) || !scope.repositories.length || !scope.repositories.includes(scope.repository)) {
|
|
35
|
+
errors.push("$.system_scope.repositories must contain the target repository");
|
|
36
|
+
} else {
|
|
37
|
+
for (const repo of scope.repositories) stringPattern(repo, REPOSITORY, "$.system_scope.repositories[]", errors);
|
|
38
|
+
if (new Set(scope.repositories).size !== scope.repositories.length) errors.push("$.system_scope.repositories must be unique");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
27
41
|
if (!Array.isArray(bundle?.modules)) errors.push("$.modules must be an array");
|
|
28
42
|
for (const [index, module] of (bundle?.modules || []).entries()) {
|
|
29
43
|
validateModule(module, `$.modules[${index}]`, errors, cliVersion);
|
|
@@ -36,6 +36,11 @@ export async function prepareDaemonWorkflows({
|
|
|
36
36
|
}
|
|
37
37
|
if (status.installed) return { outcome: "already_installed", status };
|
|
38
38
|
if (promptDisabled) return { outcome: "skipped", status };
|
|
39
|
+
if (!installRequested && Object.values(status.platforms).some((entry) => entry.state !== "missing")) {
|
|
40
|
+
// Existing installations are maintained by the daemon; do not prompt to add
|
|
41
|
+
// a missing platform or ask again after every bundled workflow update.
|
|
42
|
+
return { outcome: "update_pending", status };
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
if (!installRequested && (!stdinIsTTY || !stdoutIsTTY)) {
|
|
41
46
|
logger(
|
|
@@ -53,7 +58,7 @@ export async function prepareDaemonWorkflows({
|
|
|
53
58
|
try {
|
|
54
59
|
const installed = await install();
|
|
55
60
|
logger(
|
|
56
|
-
"SIMY workflows installed. Restart Codex, then install SIMY
|
|
61
|
+
"SIMY workflows installed. Restart Codex, then find and install SIMY in Plugins before using @simy.",
|
|
57
62
|
);
|
|
58
63
|
return { outcome: "installed", status: installed };
|
|
59
64
|
} catch (error) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, realpath, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { resolveBackendExecutable } from "../backend-executable.js";
|
|
8
|
+
import { inspectSqmAgentSkill, installSqmAgentSkill } from "./agent-skill.js";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
export function startWorkflowUpdates({ enabled, sync = syncInstalledWorkflows, intervalMs = CHECK_INTERVAL_MS,
|
|
14
|
+
errorLogger = console.error } = {}) {
|
|
15
|
+
let pending;
|
|
16
|
+
let stopped = !enabled;
|
|
17
|
+
const check = () => {
|
|
18
|
+
if (stopped || pending) return;
|
|
19
|
+
pending = Promise.resolve().then(sync).catch((error) => {
|
|
20
|
+
errorLogger(`SIMY workflow update failed; will retry later: ${error.message}`);
|
|
21
|
+
}).finally(() => { pending = null; });
|
|
22
|
+
};
|
|
23
|
+
const timer = enabled ? setInterval(check, intervalMs) : null;
|
|
24
|
+
timer?.unref?.();
|
|
25
|
+
check();
|
|
26
|
+
return async () => {
|
|
27
|
+
stopped = true;
|
|
28
|
+
clearInterval(timer);
|
|
29
|
+
await pending;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function syncInstalledWorkflows({ home = homedir(),
|
|
34
|
+
codexHome = process.env.CODEX_HOME || path.join(home, ".codex"),
|
|
35
|
+
inspect = inspectSqmAgentSkill, install = installSqmAgentSkill,
|
|
36
|
+
resolveExecutable = resolveBackendExecutable, run = execFileAsync,
|
|
37
|
+
} = {}) {
|
|
38
|
+
// Multiple daemons may share the same installation. Serialize updates and recover
|
|
39
|
+
// an abandoned lock after more than the bounded Codex command timeouts.
|
|
40
|
+
const lock = path.join(home, ".simy", "integrations", "workflow-update.lock");
|
|
41
|
+
await mkdir(path.dirname(lock), { recursive: true });
|
|
42
|
+
try {
|
|
43
|
+
if (Date.now() - (await stat(lock)).mtimeMs > 5 * 60 * 1000) await rm(lock, { recursive: true, force: true });
|
|
44
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
45
|
+
try { await mkdir(lock); } catch (error) {
|
|
46
|
+
if (error.code === "EEXIST") return { outcome: "busy" };
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
let status = await inspect({ home });
|
|
51
|
+
const platforms = Object.entries(status.platforms)
|
|
52
|
+
.filter(([, entry]) => entry.state === "outdated").map(([platform]) => platform);
|
|
53
|
+
if (platforms.length) status = await install({ home, platforms });
|
|
54
|
+
if (!status.platforms.codex.installed) return { outcome: platforms.length ? "updated" : "unchanged" };
|
|
55
|
+
|
|
56
|
+
const marketplace = JSON.parse(await readFile(status.codex_plugin.marketplace_path, "utf8"));
|
|
57
|
+
if (typeof marketplace.name !== "string" || !/^[A-Za-z0-9_-]+$/.test(marketplace.name)) {
|
|
58
|
+
throw new Error("Invalid SIMY marketplace name.");
|
|
59
|
+
}
|
|
60
|
+
const executable = await resolveExecutable("codex");
|
|
61
|
+
if (!executable) throw new Error("Codex CLI is unavailable; plugin cache refresh is pending.");
|
|
62
|
+
const options = { timeout: 10_000, maxBuffer: 1024 * 1024,
|
|
63
|
+
env: { ...process.env, HOME: home, CODEX_HOME: codexHome } };
|
|
64
|
+
const list = async () => JSON.parse((await run(executable,
|
|
65
|
+
["plugin", "list", "--marketplace", marketplace.name, "--json"], options)).stdout);
|
|
66
|
+
const selector = `simy@${marketplace.name}`;
|
|
67
|
+
const installed = (await list()).installed?.find((entry) => entry.pluginId === selector);
|
|
68
|
+
// Source installation is distinct from a user's enabled Codex plugin.
|
|
69
|
+
if (!installed?.installed || !installed.enabled) return { outcome: "not_enabled" };
|
|
70
|
+
if (installed.source?.source !== "local" ||
|
|
71
|
+
await realpath(installed.source.path) !== await realpath(status.codex_plugin.path)) {
|
|
72
|
+
throw new Error("Installed SIMY plugin points to a different source; cache refresh skipped.");
|
|
73
|
+
}
|
|
74
|
+
const cacheManifest = path.join(codexHome, "plugins", "cache", marketplace.name,
|
|
75
|
+
"simy", status.codex_plugin.version, ".codex-plugin", "plugin.json");
|
|
76
|
+
const cacheCurrent = async () => {
|
|
77
|
+
try { return JSON.parse(await readFile(cacheManifest, "utf8")).version === status.codex_plugin.version; }
|
|
78
|
+
catch (error) { if (error.code === "ENOENT" || error instanceof SyntaxError) return false; throw error; }
|
|
79
|
+
};
|
|
80
|
+
if (installed.version === status.codex_plugin.version && await cacheCurrent()) return { outcome: "current" };
|
|
81
|
+
await run(executable, ["plugin", "add", selector, "--json"], options);
|
|
82
|
+
const refreshed = (await list()).installed?.find((entry) => entry.pluginId === selector);
|
|
83
|
+
if (!refreshed?.enabled || refreshed.version !== status.codex_plugin.version || !await cacheCurrent()) {
|
|
84
|
+
throw new Error("Codex did not confirm the updated SIMY plugin cache; will retry.");
|
|
85
|
+
}
|
|
86
|
+
return { outcome: "updated", version: status.codex_plugin.version };
|
|
87
|
+
} finally {
|
|
88
|
+
await rm(lock, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|