@geravant/sinain 1.0.7 → 1.0.9

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.
Files changed (3) hide show
  1. package/install.js +204 -70
  2. package/package.json +1 -1
  3. package/HEARTBEAT.md +0 -62
package/install.js CHANGED
@@ -4,38 +4,36 @@ import path from "path";
4
4
  import os from "os";
5
5
  import { execSync } from "child_process";
6
6
 
7
- const HOME = os.homedir();
7
+ const HOME = os.homedir();
8
8
  const PLUGIN_DIR = path.join(HOME, ".openclaw/extensions/sinain-hud");
9
9
  const SOURCES_DIR = path.join(HOME, ".openclaw/sinain-sources");
10
10
  const OC_JSON = path.join(HOME, ".openclaw/openclaw.json");
11
11
  const WORKSPACE = path.join(HOME, ".openclaw/workspace");
12
12
 
13
13
  // PKG_DIR = sinain-hud-plugin/ inside the npm package
14
- const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
14
+ const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
15
15
  const MEMORY_SRC = path.join(PKG_DIR, "sinain-memory");
16
16
  const HEARTBEAT = path.join(PKG_DIR, "HEARTBEAT.md");
17
17
 
18
18
  console.log("\nInstalling sinain plugin...");
19
19
 
20
- // 1. Copy plugin files (remove stale extensions/sinain dir if present from old installs)
21
- const stalePluginDir = path.join(HOME, ".openclaw/extensions/sinain");
22
- if (fs.existsSync(stalePluginDir)) fs.rmSync(stalePluginDir, { recursive: true, force: true });
20
+ // 1. Stage plugin files to local ~/.openclaw (used by both paths)
23
21
  fs.mkdirSync(PLUGIN_DIR, { recursive: true });
24
22
  fs.copyFileSync(path.join(PKG_DIR, "index.ts"), path.join(PLUGIN_DIR, "index.ts"));
25
23
  fs.copyFileSync(path.join(PKG_DIR, "openclaw.plugin.json"), path.join(PLUGIN_DIR, "openclaw.plugin.json"));
26
24
  console.log(" ✓ Plugin files copied");
27
25
 
28
- // 2. Copy sinain-memory from bundled package files
26
+ // 2. Stage sinain-memory
29
27
  fs.mkdirSync(SOURCES_DIR, { recursive: true });
30
28
  const memoryDst = path.join(SOURCES_DIR, "sinain-memory");
31
29
  copyDir(MEMORY_SRC, memoryDst);
32
30
  console.log(" ✓ sinain-memory copied");
33
31
 
34
- // 3. Copy HEARTBEAT.md
32
+ // 3. Stage HEARTBEAT.md
35
33
  fs.copyFileSync(HEARTBEAT, path.join(SOURCES_DIR, "HEARTBEAT.md"));
36
34
  console.log(" ✓ HEARTBEAT.md copied");
37
35
 
38
- // 4. Install Python deps
36
+ // 4. Install Python deps (for local bare-metal path; harmless to run here)
39
37
  const reqFile = path.join(memoryDst, "requirements.txt");
40
38
  if (fs.existsSync(reqFile)) {
41
39
  console.log(" Installing Python dependencies...");
@@ -43,92 +41,228 @@ if (fs.existsSync(reqFile)) {
43
41
  execSync(`pip3 install -r "${reqFile}" --quiet --break-system-packages`, { stdio: "inherit" });
44
42
  console.log(" ✓ Python dependencies installed");
45
43
  } catch {
46
- console.warn(" ⚠ pip3 unavailable — Python eval features disabled");
44
+ try {
45
+ execSync(`pip3 install -r "${reqFile}" --quiet`, { stdio: "inherit" });
46
+ console.log(" ✓ Python dependencies installed");
47
+ } catch {
48
+ console.warn(" ⚠ pip3 unavailable — Python eval features disabled");
49
+ }
47
50
  }
48
51
  }
49
52
 
50
- // 5. Patch openclaw.json
51
- let cfg = {};
52
- if (fs.existsSync(OC_JSON)) {
53
- try { cfg = JSON.parse(fs.readFileSync(OC_JSON, "utf8")); } catch {}
54
- }
55
- cfg.plugins ??= {};
56
- cfg.plugins.entries ??= {};
57
- cfg.plugins.entries["sinain-hud"] = {
58
- enabled: true,
59
- config: {
60
- heartbeatPath: path.join(SOURCES_DIR, "HEARTBEAT.md"),
61
- memoryPath: memoryDst,
62
- sessionKey: "agent:main:sinain"
63
- }
64
- };
65
- // Remove stale "sinain" entry if present from a previous install
66
- delete cfg.plugins.entries["sinain"];
67
- cfg.plugins.allow ??= [];
68
- if (!cfg.plugins.allow.includes("sinain-hud")) cfg.plugins.allow.push("sinain-hud");
69
- cfg.agents ??= {};
70
- cfg.agents.defaults ??= {};
71
- cfg.agents.defaults.sandbox ??= {};
72
- cfg.agents.defaults.sandbox.sessionToolsVisibility = "all";
73
- cfg.gateway ??= {};
74
- cfg.gateway.mode = "local"; // required for gateway to start
75
- cfg.gateway.bind = "lan"; // allow remote Mac to connect
76
- cfg.gateway.auth ??= {};
77
- cfg.gateway.auth.mode ??= "token";
78
- if (!cfg.gateway.auth.token) {
79
- cfg.gateway.auth.token = require("crypto").randomBytes(24).toString("hex");
53
+ // ── Detect environment and branch ───────────────────────────────────────────
54
+
55
+ const nemoClaw = detectNemoClaw();
56
+ if (nemoClaw) {
57
+ await installNemoClaw(nemoClaw);
58
+ } else {
59
+ await installLocal();
80
60
  }
81
- const authToken = cfg.gateway.auth.token;
82
61
 
83
- fs.mkdirSync(path.dirname(OC_JSON), { recursive: true });
84
- fs.writeFileSync(OC_JSON, JSON.stringify(cfg, null, 2));
85
- console.log(" openclaw.json patched");
62
+ // ── NemoClaw path: upload staged files into the OpenShell sandbox ────────────
63
+ //
64
+ // NemoClaw runs OpenClaw inside an OpenShell sandbox pod. The sandbox has
65
+ // outbound network policy (npm registry is blocked at the binary level) so
66
+ // `npx` / `npm install` cannot reach registry.npmjs.org from inside the pod.
67
+ // Instead we:
68
+ // 1. Stage files locally (done above — steps 1-3)
69
+ // 2. Upload them into the sandbox via `openshell sandbox upload`
70
+ // 3. Download sandbox openclaw.json, patch it, upload back
71
+ // 4. Reload the openclaw gateway inside the sandbox over SSH
72
+ // 5. Print the sandbox's auth token so setup-nemoclaw.sh can use it
73
+
74
+ async function installNemoClaw({ sandboxName }) {
75
+ console.log(`\n NemoClaw sandbox detected: '${sandboxName}'`);
86
76
 
87
- // 6. Memory restore from backup repo (if SINAIN_BACKUP_REPO is set)
88
- const backupUrl = process.env.SINAIN_BACKUP_REPO;
89
- if (backupUrl) {
77
+ // Ensure SSH config has an entry for this sandbox
90
78
  try {
91
- await checkRepoPrivacy(backupUrl);
92
- if (!fs.existsSync(path.join(WORKSPACE, ".git"))) {
93
- console.log(" Restoring memory from backup repo...");
94
- execSync(`git clone "${backupUrl}" "${WORKSPACE}" --quiet`, { stdio: "inherit" });
79
+ const sshEntry = run_capture(`openshell sandbox ssh-config ${sandboxName}`);
80
+ const sshFile = path.join(HOME, ".ssh/config");
81
+ const existing = fs.existsSync(sshFile) ? fs.readFileSync(sshFile, "utf8") : "";
82
+ if (!existing.includes(`Host openshell-${sandboxName}`)) {
83
+ fs.mkdirSync(path.join(HOME, ".ssh"), { recursive: true, mode: 0o700 });
84
+ fs.appendFileSync(sshFile, "\n" + sshEntry + "\n");
85
+ }
86
+ } catch { /* ssh-config is optional; connect may still work */ }
87
+
88
+ // Upload plugin dir into sandbox
89
+ run(`openshell sandbox upload ${sandboxName} "${PLUGIN_DIR}" /sandbox/.openclaw/extensions/sinain-hud`);
90
+ console.log(" ✓ Plugin uploaded to sandbox");
91
+
92
+ // Upload sinain-sources dir into sandbox
93
+ run(`openshell sandbox upload ${sandboxName} "${SOURCES_DIR}" /sandbox/.openclaw/sinain-sources`);
94
+ console.log(" ✓ sinain-sources uploaded to sandbox");
95
+
96
+ // Download sandbox openclaw.json, patch, re-upload
97
+ const tmpJson = path.join(os.tmpdir(), `sinain-openclaw-${Date.now()}.json`);
98
+ run(`openshell sandbox download ${sandboxName} /sandbox/.openclaw/openclaw.json "${tmpJson}"`);
99
+
100
+ let cfg = {};
101
+ try { cfg = JSON.parse(fs.readFileSync(tmpJson, "utf8")); } catch {}
102
+
103
+ cfg.plugins ??= {};
104
+ cfg.plugins.entries ??= {};
105
+ cfg.plugins.allow ??= [];
106
+ cfg.plugins.entries["sinain-hud"] = {
107
+ enabled: true,
108
+ config: {
109
+ heartbeatPath: "/sandbox/.openclaw/sinain-sources/HEARTBEAT.md",
110
+ memoryPath: "/sandbox/.openclaw/sinain-sources/sinain-memory",
111
+ sessionKey: "agent:main:sinain"
112
+ }
113
+ };
114
+ if (!cfg.plugins.allow.includes("sinain-hud")) cfg.plugins.allow.push("sinain-hud");
115
+ cfg.agents ??= {};
116
+ cfg.agents.defaults ??= {};
117
+ cfg.agents.defaults.sandbox ??= {};
118
+ cfg.agents.defaults.sandbox.sessionToolsVisibility = "all";
119
+ cfg.compaction = { mode: "safeguard", maxHistoryShare: 0.2, reserveTokensFloor: 40000 };
120
+ // NemoClaw: gateway bind/auth are managed by OpenShell — do not overwrite them
121
+
122
+ const token = cfg.gateway?.auth?.token ?? "(see sandbox openclaw.json)";
123
+
124
+ fs.writeFileSync(tmpJson, JSON.stringify(cfg, null, 2));
125
+ run(`openshell sandbox upload ${sandboxName} "${tmpJson}" /sandbox/.openclaw/openclaw.json`);
126
+ fs.rmSync(tmpJson, { force: true });
127
+ console.log(" ✓ openclaw.json patched in sandbox");
128
+
129
+ // Memory restore from backup repo (workspace lives inside sandbox)
130
+ const backupUrl = process.env.SINAIN_BACKUP_REPO;
131
+ if (backupUrl) {
132
+ try {
133
+ await checkRepoPrivacy(backupUrl);
134
+ run(`ssh -T openshell-${sandboxName} 'if [ ! -d /sandbox/.openclaw/workspace/.git ]; then git clone "${backupUrl}" /sandbox/.openclaw/workspace --quiet; fi'`);
95
135
  console.log(" ✓ Memory restored from", backupUrl);
96
- } else {
97
- execSync(`git -C "${WORKSPACE}" remote set-url origin "${backupUrl}"`, { stdio: "pipe" });
98
- console.log(" ✓ Workspace git remote updated");
136
+ } catch (e) {
137
+ console.error("\n ✗ Memory restore aborted:", e.message, "\n");
138
+ process.exit(1);
99
139
  }
100
- } catch (e) {
101
- console.error("\n ✗ Memory restore aborted:", e.message, "\n");
102
- process.exit(1);
103
140
  }
141
+
142
+ // Reload openclaw gateway inside sandbox
143
+ try {
144
+ run_capture(`ssh -T openshell-${sandboxName} 'openclaw reload'`);
145
+ console.log(" ✓ Gateway reloaded");
146
+ } catch {
147
+ console.warn(" ⚠ Could not reload gateway — it will pick up changes on next start");
148
+ }
149
+
150
+ console.log(`
151
+ ✓ sinain installed successfully.
152
+ Sandbox: ${sandboxName}
153
+ Auth token: ${token}
154
+
155
+ Next steps:
156
+ 1. Note the token above
157
+ 2. Run ./setup-nemoclaw.sh on your Mac
158
+ NemoClaw URL: ws://YOUR-BREV-IP:18789
159
+ Auth token: ${token}
160
+ `);
104
161
  }
105
162
 
106
- // 7. Start / restart gateway
107
- try {
108
- execSync("openclaw gateway restart --background", { stdio: "pipe" });
109
- console.log(" ✓ Gateway restarted");
110
- } catch {
163
+ // ── Standard path: bare-metal OpenClaw (e.g. strato server) ─────────────────
164
+
165
+ async function installLocal() {
166
+ // Patch openclaw.json
167
+ let cfg = {};
168
+ if (fs.existsSync(OC_JSON)) {
169
+ try { cfg = JSON.parse(fs.readFileSync(OC_JSON, "utf8")); } catch {}
170
+ }
171
+ cfg.plugins ??= {};
172
+ cfg.plugins.entries ??= {};
173
+ cfg.plugins.entries["sinain-hud"] = {
174
+ enabled: true,
175
+ config: {
176
+ heartbeatPath: path.join(SOURCES_DIR, "HEARTBEAT.md"),
177
+ memoryPath: memoryDst,
178
+ sessionKey: "agent:main:sinain"
179
+ }
180
+ };
181
+ cfg.agents ??= {};
182
+ cfg.agents.defaults ??= {};
183
+ cfg.agents.defaults.sandbox ??= {};
184
+ cfg.agents.defaults.sandbox.sessionToolsVisibility = "all";
185
+ cfg.compaction = { mode: "safeguard", maxHistoryShare: 0.2, reserveTokensFloor: 40000 };
186
+ cfg.gateway ??= {};
187
+ cfg.gateway.bind = "lan"; // allow remote Mac to connect
188
+
189
+ fs.mkdirSync(path.dirname(OC_JSON), { recursive: true });
190
+ fs.writeFileSync(OC_JSON, JSON.stringify(cfg, null, 2));
191
+ console.log(" ✓ openclaw.json patched");
192
+
193
+ // Memory restore from backup repo
194
+ const backupUrl = process.env.SINAIN_BACKUP_REPO;
195
+ if (backupUrl) {
196
+ try {
197
+ await checkRepoPrivacy(backupUrl);
198
+ if (!fs.existsSync(path.join(WORKSPACE, ".git"))) {
199
+ console.log(" Restoring memory from backup repo...");
200
+ execSync(`git clone "${backupUrl}" "${WORKSPACE}" --quiet`, { stdio: "inherit" });
201
+ console.log(" ✓ Memory restored from", backupUrl);
202
+ } else {
203
+ execSync(`git -C "${WORKSPACE}" remote set-url origin "${backupUrl}"`, { stdio: "pipe" });
204
+ console.log(" ✓ Workspace git remote updated");
205
+ }
206
+ } catch (e) {
207
+ console.error("\n ✗ Memory restore aborted:", e.message, "\n");
208
+ process.exit(1);
209
+ }
210
+ }
211
+
212
+ // Reload gateway
111
213
  try {
112
- execSync("openclaw gateway start --background", { stdio: "pipe" });
113
- console.log(" ✓ Gateway started");
214
+ execSync("openclaw reload", { stdio: "pipe" });
215
+ console.log(" ✓ Gateway reloaded");
114
216
  } catch {
115
- console.warn(" ⚠ Could not start gateway — run: openclaw gateway");
217
+ try {
218
+ execSync("openclaw stop && sleep 1 && openclaw start --background", { stdio: "pipe" });
219
+ console.log(" ✓ Gateway restarted");
220
+ } catch {
221
+ console.warn(" ⚠ Could not start gateway — run: openclaw gateway");
222
+ }
116
223
  }
224
+
225
+ console.log(`
226
+ ✓ sinain installed successfully.
227
+ Plugin config: ~/.openclaw/openclaw.json
228
+ Auth token: check your Brev dashboard → 'Gateway Token'
229
+ Then run ./setup-nemoclaw.sh on your Mac.
230
+ `);
117
231
  }
118
232
 
119
- console.log("\n✓ sinain installed successfully.");
120
- console.log(" Plugin config: ~/.openclaw/openclaw.json");
121
- console.log(` Auth token: ${authToken}`);
122
- console.log(" Next: run 'openclaw gateway' in a new terminal, then run ./setup-nemoclaw.sh on your Mac.\n");
233
+ // ── Detection ────────────────────────────────────────────────────────────────
234
+
235
+ function detectNemoClaw() {
236
+ // NemoClaw writes ~/.nemoclaw/sandboxes.json with a defaultSandbox field
237
+ const sandboxesJson = path.join(HOME, ".nemoclaw/sandboxes.json");
238
+ if (!fs.existsSync(sandboxesJson)) return null;
239
+ try {
240
+ const data = JSON.parse(fs.readFileSync(sandboxesJson, "utf8"));
241
+ const sandboxName = data.defaultSandbox;
242
+ if (!sandboxName || !data.sandboxes?.[sandboxName]) return null;
243
+ // Verify openshell CLI is reachable
244
+ try { execSync("which openshell", { stdio: "pipe" }); } catch { return null; }
245
+ return { sandboxName };
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
123
250
 
124
251
  // ── Helpers ──────────────────────────────────────────────────────────────────
125
252
 
253
+ function run(cmd) {
254
+ execSync(cmd, { stdio: "inherit" });
255
+ }
256
+
257
+ function run_capture(cmd) {
258
+ return execSync(cmd, { encoding: "utf-8", stdio: ["pipe","pipe","pipe"] }).trim();
259
+ }
260
+
126
261
  function copyDir(src, dst) {
127
262
  fs.mkdirSync(dst, { recursive: true });
128
263
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
129
264
  const s = path.join(src, entry.name);
130
265
  const d = path.join(dst, entry.name);
131
- // Skip __pycache__ and .pytest_cache to keep the deploy lean
132
266
  if (entry.name === "__pycache__" || entry.name === ".pytest_cache") continue;
133
267
  entry.isDirectory() ? copyDir(s, d) : fs.copyFileSync(s, d);
134
268
  }
@@ -163,5 +297,5 @@ async function checkRepoPrivacy(url) {
163
297
  } else if (res.status !== 404) {
164
298
  throw new Error(`Cannot verify repo privacy (HTTP ${res.status}). Aborting for safety.`);
165
299
  }
166
- // 404 = private (unauthenticated can't see it) — OK
300
+ // 404 = private repo (unauthenticated can't see it) — OK
167
301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "sinain OpenClaw plugin — AI overlay for macOS",
5
5
  "type": "module",
6
6
  "bin": {
package/HEARTBEAT.md DELETED
@@ -1,62 +0,0 @@
1
- # HEARTBEAT.md
2
-
3
- > **Execution contract:**
4
- > 1. Setup → call `sinain_heartbeat_tick` → act on result → HEARTBEAT_OK
5
- > 2. You MUST call the tool. Do NOT skip it. Do NOT reply HEARTBEAT_OK without calling it first.
6
-
7
- ---
8
-
9
- ## Setup
10
-
11
- 1. `sessions_history({ sessionKey: "agent:main:sinain", limit: 20, includeTools: false })`
12
- 2. Determine **IDLE** status: no user activity in the last 30 minutes
13
- 3. Compose **SESSION_SUMMARY**: 2–3 sentences describing the current state (what the user is doing, key topics, errors seen)
14
-
15
- ---
16
-
17
- ## Execute Tick
18
-
19
- Call the `sinain_heartbeat_tick` tool:
20
-
21
- ```
22
- sinain_heartbeat_tick({ sessionSummary: "...", idle: true|false })
23
- ```
24
-
25
- The tool runs all scripts (git backup, signal analysis, insight synthesis) and writes the log entry automatically.
26
-
27
- ---
28
-
29
- ## Act on Result
30
-
31
- - If `recommendedAction.action === "sessions_spawn"` → spawn a subagent with `.task`
32
- - If `recommendedAction.action === "telegram_tip"` → send `.task` to Telegram (chat_id: 59835117)
33
- - If `output` is not null → send to Telegram (chat_id: 59835117): `Suggestion: {suggestion}\n\nInsight: {insight}`
34
-
35
- ---
36
-
37
- ## Finish
38
-
39
- Reply **HEARTBEAT_OK**
40
-
41
- ---
42
-
43
- ## Rules
44
-
45
- - **Proactivity quota:** on active days, at least 2 ticks MUST produce output
46
- - Max 2 subagents. NEVER repeat a recent action. Prefer depth over breadth.
47
- - Memory mining, feedback, and curation run via plugin timer — do NOT invoke manually.
48
- - Module management is on-demand, not per-tick — see SKILL.md for module commands.
49
-
50
- ---
51
-
52
- ## NemoClaw Provisioning
53
-
54
- New Brev instance setup (run in Code-Server terminal):
55
- ```
56
- SINAIN_BACKUP_REPO=<git-url> npx sinain
57
- ```
58
- - Copies plugin files, patches `openclaw.json`, installs Python deps, restores memory from backup repo, reloads gateway
59
- - Token printed at end (or visible in Brev dashboard → Gateway Token)
60
- - Mac side: `./setup-nemoclaw.sh` → 5 prompts → overlay starts
61
-
62
- Memory is git-backed via `git_backup.sh` on every heartbeat tick. New instances restore instantly via `SINAIN_BACKUP_REPO`.