@awak-app/simy-cli 0.5.5 → 0.5.6

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 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 and the managed SIMY workflows are
13
- missing or outdated, the CLI asks once before installing the Codex plugin
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 from **Plugins → Personal**
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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
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"
@@ -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.5";
16
+ export const CLI_VERSION = "0.5.6";
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
- const shutdownAgent = createAgentShutdown(agent);
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) => {
@@ -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 expectedDigest = await directoryDigest(BUNDLED_CODEX_PLUGIN_ROOT);
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: SIMY_CODEX_PLUGIN_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: "Personal",
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 directoryDigest(root, { missing = "throw" } = {}) {
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
- hash.update(relative).update("\0").update(await readFile(target)).update("\0");
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);
@@ -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 from Plugins > Personal before using @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
+ }