@geravant/sinain 1.15.1 → 1.15.3

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/cli.js CHANGED
@@ -14,11 +14,18 @@ const SINAIN_DIR = path.join(HOME, ".sinain");
14
14
  const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
15
15
 
16
16
  // Self-update sentinel — query the npm registry, and if a newer version
17
- // of @geravant/sinain is available, re-exec via `npx --ignore-existing`
18
- // before doing anything else. Adds ~500ms startup but kills the
19
- // "stale npx cache silently serves 1.6.x" class of bugs permanently.
20
- // See self-update.js for details + opt-out env var.
21
- await checkForUpdate();
17
+ // of @geravant/sinain is available, re-exec before doing anything else.
18
+ // Adds ~500ms startup but kills the "stale npx cache silently serves 1.6.x"
19
+ // class of bugs permanently. See self-update.js for details + opt-out env var.
20
+ //
21
+ // Wrapped in try/catch so a bug here (or a missing module) can't silently
22
+ // kill the CLI — the user always sees the dispatch path run, even if the
23
+ // update check fails for any reason.
24
+ try {
25
+ await checkForUpdate();
26
+ } catch (err) {
27
+ process.stderr.write(` ⚠ self-update check failed: ${err.message}\n (continuing with installed version)\n`);
28
+ }
22
29
 
23
30
  switch (cmd) {
24
31
  case "start":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.15.1",
3
+ "version": "1.15.3",
4
4
  "description": "Ambient intelligence that sees what you see, hears what you hear, and acts on your behalf",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "cli.js",
17
+ "self-update.js",
17
18
  "config.js",
18
19
  "config-shared.js",
19
20
  "onboard.js",
@@ -39,6 +40,7 @@
39
40
  "sinain-mcp-server/tsconfig.json",
40
41
  "sinain-agent/run.sh",
41
42
  "sinain-agent/mcp-config.json",
43
+ "sinain-agent/agents.example.json",
42
44
  "sinain-agent/.env.example",
43
45
  "sinain-agent/CLAUDE.md",
44
46
  "sense_client",
package/self-update.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Self-update sentinel for @geravant/sinain.
3
+ *
4
+ * Why this exists: when users run `npx @geravant/sinain start` (without a
5
+ * version pin), npx caches the *unversioned spec* and reuses that cache
6
+ * forever — never re-checking the registry. Users end up running 1.6.x
7
+ * for months while the latest is 1.15+. The cache-key is the spec, not
8
+ * the version, so once the slot is populated, `npx` considers the spec
9
+ * "satisfied" indefinitely.
10
+ *
11
+ * Fix: at the top of cli.js, BEFORE any command dispatches, query the
12
+ * npm registry for the current `latest` dist-tag. If it's newer than
13
+ * the version of this file, re-exec ourselves with
14
+ * `npx --ignore-existing @geravant/sinain@<latest> <argv>`. The user sees
15
+ * one extra "updating..." line and then the up-to-date wizard runs.
16
+ *
17
+ * Cost: ~500ms startup latency (single HTTPS GET to registry.npmjs.org).
18
+ * Soft-fail on network errors so offline use still works.
19
+ *
20
+ * Loop protection: SINAIN_SELF_UPDATED=1 is set on the relaunched process
21
+ * so it can't recurse if the registry returns nonsense. Users can opt out
22
+ * entirely with SINAIN_NO_AUTO_UPDATE=1.
23
+ */
24
+
25
+ import { spawnSync } from "child_process";
26
+ import fs from "fs";
27
+ import path from "path";
28
+ import { fileURLToPath } from "url";
29
+
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
+ const PKG_NAME = "@geravant/sinain";
32
+ const REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
33
+ const FETCH_TIMEOUT_MS = 2000;
34
+
35
+ /**
36
+ * Compare two semver versions. Returns >0 if a>b, <0 if a<b, 0 if equal.
37
+ * Handles only X.Y.Z (no pre-release tags). Tolerates leading 'v'.
38
+ */
39
+ function semverCompare(a, b) {
40
+ const parse = (v) => v.replace(/^v/, "").split(/[.+-]/).map((p) => parseInt(p, 10) || 0);
41
+ const [a1, a2, a3] = parse(a);
42
+ const [b1, b2, b3] = parse(b);
43
+ if (a1 !== b1) return a1 - b1;
44
+ if (a2 !== b2) return a2 - b2;
45
+ return a3 - b3;
46
+ }
47
+
48
+ function readLocalVersion() {
49
+ try {
50
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf-8"));
51
+ return pkg.version;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ async function fetchRemoteVersion() {
58
+ const ctrl = new AbortController();
59
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
60
+ try {
61
+ const res = await fetch(REGISTRY_URL, { signal: ctrl.signal });
62
+ if (!res.ok) return null;
63
+ const data = await res.json();
64
+ return data?.version ?? null;
65
+ } catch {
66
+ return null;
67
+ } finally {
68
+ clearTimeout(timer);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Commands that should NOT trigger a self-update check:
74
+ * - update : explicit user action (already updating)
75
+ * - version : status query, must be fast and offline-safe
76
+ * - help : ditto
77
+ * - stop : tearing things down, don't introduce a network call
78
+ * - status : status query, fast + offline-safe
79
+ * - completion / shell helpers : trivial, must not block
80
+ *
81
+ * `start`, `setup-overlay`, `onboard`, `config` (i.e. anything that runs
82
+ * the wizard or core install path) DO trigger the check, because that's
83
+ * where stale-cache UX hits hardest.
84
+ */
85
+ const SKIP_COMMANDS = new Set([
86
+ "update", "version", "--version", "-v",
87
+ "help", "--help", "-h",
88
+ "stop", "status", "completion",
89
+ ]);
90
+
91
+ /**
92
+ * Run the self-update check. If a newer version is available, exec ourselves
93
+ * via `npx --ignore-existing @geravant/sinain@<latest>` and exit with the
94
+ * relaunched process's status. If everything's fine (or the check fails
95
+ * soft), this function returns and the caller proceeds normally.
96
+ */
97
+ export async function checkForUpdate() {
98
+ // Loop guard — set on the relaunched process so we can't recurse.
99
+ if (process.env.SINAIN_SELF_UPDATED === "1") return;
100
+ // Opt-out for users who don't want the network call.
101
+ if (process.env.SINAIN_NO_AUTO_UPDATE === "1") return;
102
+ // Skip commands that shouldn't pay the network cost.
103
+ const cmd = process.argv[2];
104
+ if (cmd && SKIP_COMMANDS.has(cmd)) return;
105
+
106
+ const local = readLocalVersion();
107
+ if (!local) return;
108
+
109
+ const remote = await fetchRemoteVersion();
110
+ if (!remote) return;
111
+
112
+ if (semverCompare(remote, local) <= 0) return;
113
+
114
+ // Stale — print + relaunch.
115
+ process.stderr.write(
116
+ `\n ⚠ sinain ${local} is stale; latest is ${remote}\n` +
117
+ ` Auto-updating... (set SINAIN_NO_AUTO_UPDATE=1 to disable)\n\n`,
118
+ );
119
+
120
+ const args = process.argv.slice(2);
121
+ // --yes: skip npx's "are you sure" prompt for unfamiliar packages.
122
+ // (Note: do NOT pass --ignore-existing — npm 7+ removed it. The
123
+ // version-pinned spec `@geravant/sinain@<remote>` already keys to a
124
+ // separate npx cache slot, so a fresh `<remote>` triggers a download
125
+ // regardless of what's in the unversioned slot.)
126
+ const result = spawnSync(
127
+ "npx",
128
+ ["--yes", `${PKG_NAME}@${remote}`, ...args],
129
+ {
130
+ stdio: "inherit",
131
+ env: { ...process.env, SINAIN_SELF_UPDATED: "1" },
132
+ },
133
+ );
134
+ process.exit(result.status ?? 1);
135
+ }
@@ -0,0 +1,148 @@
1
+ {
2
+ "_doc": [
3
+ "Roster + bare-agent + OpenClaw config for sinain. This is the single",
4
+ "source of truth for everything agent-shaped:",
5
+ " - top-level fields → bare-agent infra (poll, turns, tool whitelists)",
6
+ " - escalation block → routing policy (was ESCALATION_* in .env)",
7
+ " - analyzer block → loop pacing (was AGENT_DEBOUNCE_MS / _MAX_INTERVAL_MS)",
8
+ " - profiles → one entry per CLI binary or routing target",
9
+ " (claude, openclaude, codex, ..., openclaw)",
10
+ "",
11
+ "This is the COMMITTED example. The actual file run.sh and sinain-core",
12
+ "read is sinain-agent/agents.json (gitignored). On first run, run.sh",
13
+ "copies this example into agents.json so a fresh checkout works without",
14
+ "manual setup. sinain-core falls back to this file too if agents.json",
15
+ "doesn't exist yet.",
16
+ "",
17
+ "Path resolution in sinain-core: AGENTS_CONFIG_PATH env override, or",
18
+ "default: <repo-root>/sinain-agent/agents.json (resolved relative to",
19
+ "the sinain-core/dist directory).",
20
+ "",
21
+ "Profile fields (all optional except `type`):",
22
+ " type Determines dispatch path AND CLI flag layout.",
23
+ " Local CLIs (HTTP-dispatched): claude, openclaude, codex,",
24
+ " goose, junie, aider — bare agent invokes the binary.",
25
+ " Gateway profiles (WS-dispatched): openclaw — sinain-core",
26
+ " sends via WS RPC. Multiple gateway profiles with different",
27
+ " names are allowed (e.g. nemoclaw, nanoclaw-prod) — any",
28
+ " profile with `type: \"openclaw\"` is treated as a gateway.",
29
+ " All gateway profiles auto-register in the overlay roster.",
30
+ " bin Path or name of a real binary on PATH (default: profile",
31
+ " name). Shell aliases are invisible here — replicate them",
32
+ " as bin (the underlying binary) + env (the vars the alias",
33
+ " was setting). See the pclaude example.",
34
+ " settings Path to a Claude-Code-style settings.json (default:",
35
+ " sinain-agent/.claude/settings.json — the hook-bearing one).",
36
+ " model OPENAI_MODEL override (only meaningful for openclaude /",
37
+ " CLAUDE_CODE_USE_OPENAI=1 paths).",
38
+ " env Per-profile env overrides applied only for this binary's",
39
+ " invocation. Values may use ${VAR} indirection (anywhere in",
40
+ " the string) to read from the parent shell's environment —",
41
+ " keep secrets in .env, not here.",
42
+ "",
43
+ "openclaw-profile-only fields (consumed by sinain-core):",
44
+ " wsUrl, wsToken, httpUrl, httpToken, sessionKey, phase1TimeoutMs,",
45
+ " phase2TimeoutMs, pingIntervalMs",
46
+ "",
47
+ "Examples beyond the built-ins live under `_examples`. Move any of them",
48
+ "into `profiles` to activate; the runtime ignores keys it doesn't know."
49
+ ],
50
+
51
+ "default": "openclaude",
52
+ "pollIntervalSec": 5,
53
+ "agentMaxTurns": 8,
54
+ "spawnMaxTurns": 25,
55
+
56
+ "allowedTools": "mcp__sinain",
57
+ "escAllowedTools": "${allowedTools} Bash(git:*) Edit Write Read Glob Grep LS",
58
+ "spawnAllowedTools": "${allowedTools} Bash(git:*) Edit Write Read Glob Grep LS",
59
+ "autoApproveTools": "Read Glob Grep Ls Cat mcp__sinain*",
60
+
61
+ "analyzer": {
62
+ "debounceMs": 6000,
63
+ "maxIntervalMs": 60000
64
+ },
65
+
66
+ "escalation": {
67
+ "mode": "rich",
68
+ "cooldownMs": 30000,
69
+ "staleMs": 90000
70
+ },
71
+
72
+ "profiles": {
73
+ "claude": { "type": "claude" },
74
+ "openclaude": {
75
+ "_comment": "Routes through the local OpenRouter proxy (auto-launched on :11435 by run.sh) → DeepSeek V4 Flash. Proxy preserves reasoning_content across multi-turn MCP flows. To use Anthropic API directly, drop the env block (or just pick the `claude` profile).",
76
+ "type": "openclaude",
77
+ "model": "deepseek/deepseek-v4-flash",
78
+ "env": {
79
+ "CLAUDE_CODE_USE_OPENAI": "1",
80
+ "OPENAI_BASE_URL": "http://localhost:11435/api/v1",
81
+ "OPENAI_API_KEY": "${OPENROUTER_API_KEY}"
82
+ }
83
+ },
84
+ "openclaw": {
85
+ "_comment": "Remote gateway routing. Selecting openclaw for a lane sends that lane's traffic to the OpenClaw gateway via WS RPC instead of the local bare agent. sinain-core reads these fields directly at startup to construct the WS client. Removing this profile disables the gateway path entirely (no WS client, no openclaw chip in the overlay roster).",
86
+ "type": "openclaw",
87
+ "wsUrl": "ws://localhost:18789",
88
+ "wsToken": "${OPENCLAW_WS_TOKEN}",
89
+ "httpUrl": "http://localhost:18789/hooks/agent",
90
+ "httpToken": "${OPENCLAW_HTTP_TOKEN}",
91
+ "sessionKey": "agent:main:sinain",
92
+ "phase1TimeoutMs": 30000,
93
+ "phase2TimeoutMs": 120000,
94
+ "pingIntervalMs": 30000
95
+ },
96
+ "codex": { "type": "codex" },
97
+ "goose": { "type": "goose" },
98
+ "junie": { "type": "junie" },
99
+ "aider": { "type": "aider" }
100
+ },
101
+
102
+ "_examples": {
103
+ "pclaude": {
104
+ "_comment": "Personal claude config. `bin` must be a real PATH binary — replicate a shell alias as bin+env (the alias `pclaude=CLAUDE_CONFIG_DIR=$HOME/.claude-personal claude` becomes the entry below).",
105
+ "type": "claude",
106
+ "bin": "claude",
107
+ "env": {
108
+ "CLAUDE_CONFIG_DIR": "${HOME}/.claude-personal"
109
+ }
110
+ },
111
+ "pclaude-with-personal-endpoint": {
112
+ "_comment": "Personal claude pointing at a private OpenAI-compat endpoint.",
113
+ "type": "claude",
114
+ "bin": "claude",
115
+ "env": {
116
+ "OPENAI_API_KEY": "${PERSONAL_OPENAI_API_KEY}",
117
+ "OPENAI_BASE_URL": "https://my-personal-endpoint/v1"
118
+ },
119
+ "settings": "~/.pclaude/settings.json"
120
+ },
121
+ "openclaude-spawn": {
122
+ "_comment": "Different settings.json for the spawn lane vs the escalation lane.",
123
+ "type": "openclaude",
124
+ "bin": "openclaude",
125
+ "settings": "/Users/your.name/IdeaProjects/sinain-hud/sinain-agent/.claude/spawn-settings.json"
126
+ },
127
+ "openclaude-gpt5": {
128
+ "_comment": "Same binary, different model + key for a different OpenRouter route.",
129
+ "type": "openclaude",
130
+ "bin": "openclaude",
131
+ "model": "openai/gpt-5",
132
+ "env": {
133
+ "CLAUDE_CODE_USE_OPENAI": "1",
134
+ "OPENAI_BASE_URL": "http://localhost:11435/api/v1",
135
+ "OPENAI_API_KEY": "${OPENROUTER_API_KEY}"
136
+ }
137
+ },
138
+ "nemoclaw": {
139
+ "_comment": "Server-side NemoClaw (or any openclaw-fork) on a different host. `type: openclaw` makes it a WS-dispatched gateway alongside the canonical openclaw profile. Currently, only the first gateway profile gets a live WS client at startup — selecting nemoclaw in the overlay routes via WS, but the connection still uses openclaw's URL until per-profile WS clients ship.",
140
+ "type": "openclaw",
141
+ "wsUrl": "ws://nemoclaw-host:18789",
142
+ "wsToken": "${NEMOCLAW_WS_TOKEN}",
143
+ "httpUrl": "http://nemoclaw-host:18789/hooks/agent",
144
+ "httpToken": "${NEMOCLAW_HTTP_TOKEN}",
145
+ "sessionKey": "agent:main:sinain"
146
+ }
147
+ }
148
+ }