@awak-app/simy-cli 0.6.1 → 0.6.2

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
@@ -104,6 +104,10 @@ Detached daemon and desktop-host diagnostics are saved to
104
104
  (`SIMY_HOME` defaults to `~/.simy`). Each log is limited to 1 MiB plus two
105
105
  backups; credentials, Cookie headers and URL queries are redacted. Installer
106
106
  handoff diagnostics remain in `~/.simy/installer/desktop-update.log`.
107
+ The macOS window also saves the latest nonzero background-host exit in
108
+ `$SIMY_HOME/logs/desktop-host-exit.log`, including its timestamp, exit status and
109
+ up to 16 KiB of stderr. Credential-bearing lines are omitted; each abnormal exit
110
+ replaces the previous record. This includes failures before JavaScript starts.
107
111
  `simy --version` or `simy -v` prints the package version without starting work.
108
112
 
109
113
  Closing the window keeps the agent running. Open the app again or choose
@@ -188,6 +192,16 @@ checks the bundled CLI in ChatGPT and Codex apps in system and user Applications
188
192
  Claude discovery likewise falls back to a compatible native or Desktop bundle
189
193
  when an older PATH installation cannot meet its version requirement.
190
194
 
195
+ On macOS and Linux, the desktop host reads the user's interactive login-shell
196
+ PATH once at startup, so providers installed through nvm, Volta or custom prefixes
197
+ can be found from GUI launches, including Finder or the Dock on macOS. Shell paths
198
+ take priority; existing launcher paths remain available. If the five-second shell
199
+ probe cannot read PATH, startup uses the launcher PATH. Windows keeps its existing PATH.
200
+
201
+ Installed Codex and Claude hooks use an absolute Node runtime path. When available,
202
+ the SIMY installer's `current` link or Homebrew's `opt` link replaces a versioned
203
+ runtime path, allowing hooks to follow updates without relying on the provider's PATH.
204
+
191
205
  Use `simy delivery activity --watch` for a read-only local snapshot every minute,
192
206
  or add `--json` for one JSON object per sample. It separates execution, waits,
193
207
  stops, failures and repeated attempts without new verified outcomes, with task
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
44
44
  "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/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
45
45
  "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 && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js",
46
- "check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.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/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && 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 ./src/coding-run-store.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 && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js",
46
+ "check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.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/login-shell-path.js && node --check ./src/hook-runtime.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/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && 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 ./src/coding-run-store.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 && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js",
47
47
  "check:package": "node ./scripts/check-package-contents.js",
48
48
  "build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
49
49
  "build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
@@ -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.1";
16
+ export const CLI_VERSION = "0.6.2";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -23,7 +23,8 @@ import { dirname, join } from "node:path";
23
23
  import { homedir } from "node:os";
24
24
  import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
25
25
  import { startAgent } from "./agent.js";
26
- import { createRuntimeDiagnostics } from "./runtime-diagnostics.js";
26
+ import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
27
+ import { applyLoginShellPath } from "./login-shell-path.js";
27
28
  import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
28
29
  import { createAgentShutdown } from "./shutdown.js";
29
30
  import { resolveWebOrigin } from "./web-origin.js";
@@ -735,7 +736,7 @@ export async function openProviderLogin(provider, root, {
735
736
  const directory = await mkdtemp(join(root, `${provider}-login-`));
736
737
  const script = join(directory, `Sign in to ${label}.command`);
737
738
  const quote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
738
- await writeFile(script, `#!/bin/bash\nexport PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH"\n${quote(executable.executable)} ${args.join(" ")}\nresult=$?\nrm -f -- ${quote(script)}\nrmdir -- ${quote(directory)}\nexit "$result"\n`, { mode: 0o700 });
739
+ await writeFile(script, `#!/bin/bash\nexport PATH=${quote(env.PATH || "/usr/bin:/bin")}\n${quote(executable.executable)} ${args.join(" ")}\nresult=$?\nrm -f -- ${quote(script)}\nrmdir -- ${quote(directory)}\nexit "$result"\n`, { mode: 0o700 });
739
740
  await chmod(script, 0o700);
740
741
  try { await execFileImpl("/usr/bin/open", ["-a", "Terminal", script], { timeout: 10_000 }); }
741
742
  catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
@@ -750,8 +751,12 @@ if (isDesktopHostEntry()) {
750
751
  let host;
751
752
  const stop = () => { void host?.close().finally(() => process.exit(0)); };
752
753
  process.once("SIGTERM", stop); process.once("SIGINT", stop);
753
- startDesktopHost().then((value) => { host = value; }).catch((error) => {
754
- createRuntimeDiagnostics(process.env.SIMY_HOME?.trim() || join(homedir(), ".simy"), { name: "desktop-host" }).write(`startup failed: ${error?.stack || error}`);
754
+ const diagnostics = createRuntimeDiagnostics(process.env.SIMY_HOME?.trim() || join(homedir(), ".simy"), { name: "desktop-host" });
755
+ // stdout is the native JSONL channel; stderr is the only unstructured output
756
+ // and the native window does not display it, so keep it for support.
757
+ captureRuntimeOutput(diagnostics, { stderr: process.stderr });
758
+ applyLoginShellPath({ diagnostics }).catch(() => {}).then(() => startDesktopHost()).then((value) => { host = value; }).catch((error) => {
759
+ diagnostics.write(`startup failed: ${error?.stack || error}`);
755
760
  const conflict = error?.code === PROCESS_CONFLICT_CODE;
756
761
  process.stdout.write(`${JSON.stringify({
757
762
  type: "error",
@@ -0,0 +1,29 @@
1
+ import { existsSync } from "node:fs";
2
+
3
+ // Version-specific runtimes are replaced by the stable alias that follows
4
+ // upgrades: the installer's `current` release link and Homebrew's `opt` link.
5
+ const STABLE_RUNTIME_ALIASES = [
6
+ [/^(.*)\/installer\/releases\/[^/]+\/[^/]+\.app\/(Contents\/Resources\/runtime\/bin\/node)$/, "$1/installer/current/$2"],
7
+ [/^(.*)\/Cellar\/([^/]+)\/[^/]+\/(bin\/node)$/, "$1/opt/$2/$3"],
8
+ ];
9
+
10
+ // Provider hooks are persisted into Codex/Claude configuration and run long
11
+ // after this process exits, often from GUI apps without the user's shell PATH.
12
+ // They must therefore name an absolute runtime that survives SIMY and Node updates.
13
+ export function stableHookRuntime({ execPath = process.execPath, fileExists = existsSync } = {}) {
14
+ for (const [pattern, replacement] of STABLE_RUNTIME_ALIASES) {
15
+ if (!pattern.test(execPath)) continue;
16
+ const alias = execPath.replace(pattern, replacement);
17
+ if (fileExists(alias)) return alias;
18
+ }
19
+ return execPath;
20
+ }
21
+
22
+ export function hookRuntimeCommand(command, { platform = process.platform, ...options } = {}) {
23
+ if (typeof command !== "string" || !command.startsWith("node ")) return command;
24
+ return `${quoteRuntime(stableHookRuntime(options), platform)} ${command.slice("node ".length)}`;
25
+ }
26
+
27
+ function quoteRuntime(value, platform) {
28
+ return platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
29
+ }
@@ -0,0 +1,67 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const START = "__SIMY_LOGIN_PATH_START__";
6
+ const END = "__SIMY_LOGIN_PATH_END__";
7
+
8
+ // Apps opened from Finder or the Dock inherit launchd's minimal PATH, not the
9
+ // user's shell profile. Provider CLIs installed through nvm, volta, fnm, asdf,
10
+ // pnpm or custom prefixes are therefore invisible unless we ask the user's own
11
+ // login shell once. A slow or broken profile must never prevent startup.
12
+ export async function resolveLoginShellPath({
13
+ platform = process.platform,
14
+ env = process.env,
15
+ execFileImpl = execFile,
16
+ fileExists = existsSync,
17
+ timeoutMs = 5_000,
18
+ } = {}) {
19
+ if (platform === "win32") return null;
20
+ const shell = loginShell({ platform, env, fileExists });
21
+ if (!shell) return null;
22
+ const output = await new Promise((resolve) => {
23
+ // Read the exported environment rather than expanding $PATH in the shell:
24
+ // fish expands a quoted $PATH with spaces, but exports it colon-separated.
25
+ const child = execFileImpl(shell, ["-l", "-i", "-c", `echo ${START}; /usr/bin/env; echo ${END}`], {
26
+ env: { ...env, TERM: "dumb", DISABLE_AUTO_UPDATE: "true" },
27
+ timeout: timeoutMs,
28
+ killSignal: "SIGKILL",
29
+ maxBuffer: 1024 * 1024,
30
+ }, (_error, stdout) => resolve(String(stdout || "")));
31
+ child?.stdin?.end();
32
+ });
33
+ const start = output.lastIndexOf(START);
34
+ const end = output.indexOf(END, start);
35
+ if (start < 0 || end < 0) return null;
36
+ const line = output.slice(start + START.length, end).split(/\r?\n/).find((item) => item.startsWith("PATH="));
37
+ return line?.slice("PATH=".length) || null;
38
+ }
39
+
40
+ // Login-shell entries take priority, matching what the user sees in Terminal.
41
+ // Existing entries (including the launcher's fallback directories) are kept.
42
+ export function mergePathEntries(...values) {
43
+ const entries = [];
44
+ for (const value of values) {
45
+ for (const entry of String(value || "").split(path.delimiter)) {
46
+ if (path.isAbsolute(entry) && !entries.includes(entry)) entries.push(entry);
47
+ }
48
+ }
49
+ return entries.join(path.delimiter);
50
+ }
51
+
52
+ export async function applyLoginShellPath({ env = process.env, platform = process.platform, diagnostics, ...options } = {}) {
53
+ if (platform === "win32") return false;
54
+ const loginPath = await resolveLoginShellPath({ env, platform, ...options });
55
+ if (!loginPath) {
56
+ diagnostics?.write("login shell PATH unavailable; using launcher PATH");
57
+ return false;
58
+ }
59
+ env.PATH = mergePathEntries(loginPath, env.PATH);
60
+ diagnostics?.write(`login shell PATH applied (${env.PATH.split(path.delimiter).length} entries)`);
61
+ return true;
62
+ }
63
+
64
+ function loginShell({ platform, env, fileExists }) {
65
+ const candidates = [env.SHELL, platform === "darwin" ? "/bin/zsh" : null, "/bin/sh"];
66
+ return candidates.find((candidate) => candidate && path.isAbsolute(candidate) && fileExists(candidate)) || null;
67
+ }
@@ -3,6 +3,7 @@ import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { hookRuntimeCommand } from "../hook-runtime.js";
6
7
 
7
8
  import { inspectClaudePlugin, installClaudePlugin } from "./claude-plugin.js";
8
9
 
@@ -328,25 +329,14 @@ async function bundledPluginManifest({ overrides = new Map() } = {}) {
328
329
  async function installedPluginOverrides() {
329
330
  const relative = "hooks/hooks.json";
330
331
  const source = JSON.parse(await readFile(path.join(BUNDLED_CODEX_PLUGIN_ROOT, relative), "utf8"));
331
- const commandPrefix = "node ";
332
- const runtime = shellQuote(process.execPath);
333
332
  for (const groups of Object.values(source.hooks || {})) {
334
333
  for (const group of groups || []) {
335
- for (const hook of group.hooks || []) {
336
- if (typeof hook.command === "string" && hook.command.startsWith(commandPrefix)) {
337
- hook.command = `${runtime} ${hook.command.slice(commandPrefix.length)}`;
338
- }
339
- }
334
+ for (const hook of group.hooks || []) hook.command = hookRuntimeCommand(hook.command);
340
335
  }
341
336
  }
342
337
  return new Map([[relative, `${JSON.stringify(source, null, 2)}\n`]]);
343
338
  }
344
339
 
345
- function shellQuote(value) {
346
- if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
347
- return `'${value.replaceAll("'", "'\\''")}'`;
348
- }
349
-
350
340
  async function directoryDigest(root, { missing = "throw", manifest, overrides = new Map() } = {}) {
351
341
  let rootStat;
352
342
  try {
@@ -3,6 +3,7 @@ import { mkdir, readFile, readdir, rename, rm, lstat, writeFile } from "node:fs/
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { hookRuntimeCommand } from "../hook-runtime.js";
6
7
 
7
8
  const SOURCE = fileURLToPath(new URL("../codex-plugins/simy/", import.meta.url));
8
9
  export const CLAUDE_MARKETPLACE = "simy-local";
@@ -43,7 +44,7 @@ export async function claudePluginFiles() {
43
44
  } else if (event === "Notification") {
44
45
  hook.statusMessage = "SIMY Delivery Loop permission-notification tracking";
45
46
  }
46
- hook.command = hook.command.replace(/^node /, `${quoteRuntime(process.execPath)} `)
47
+ hook.command = hookRuntimeCommand(hook.command)
47
48
  .replaceAll("process.env.PLUGIN_ROOT", "process.env.CLAUDE_PLUGIN_ROOT")
48
49
  .replaceAll("hook.main('codex')", "hook.main('claude')");
49
50
  }
@@ -136,6 +137,3 @@ function json(value) { return `${JSON.stringify(value, null, 2)}\n`; }
136
137
  async function optionalRead(file) {
137
138
  try { return await readFile(file, "utf8"); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
138
139
  }
139
- function quoteRuntime(value) {
140
- return process.platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
141
- }