@geravant/sinain 1.6.1 → 1.6.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/launcher.js +39 -2
- package/package.json +2 -1
- package/sense_client/__main__.py +24 -12
- package/setup-overlay.js +39 -20
- package/setup-sck-capture.js +190 -0
- package/sinain-core/src/agent/loop.ts +87 -4
- package/sinain-core/src/index.ts +16 -3
package/launcher.js
CHANGED
|
@@ -127,7 +127,10 @@ async function main() {
|
|
|
127
127
|
const scDir = path.join(PKG_DIR, "sense_client");
|
|
128
128
|
// Check if key package is importable to skip pip
|
|
129
129
|
try {
|
|
130
|
-
|
|
130
|
+
const depCheck = IS_WINDOWS
|
|
131
|
+
? 'python3 -c "import PIL; import skimage"'
|
|
132
|
+
: 'python3 -c "import PIL; import skimage; import Quartz"';
|
|
133
|
+
execSync(depCheck, { stdio: "pipe" });
|
|
131
134
|
} catch {
|
|
132
135
|
log("Installing sense_client Python dependencies...");
|
|
133
136
|
try {
|
|
@@ -213,7 +216,41 @@ async function main() {
|
|
|
213
216
|
warn("flutter not found — overlay source found but can't build");
|
|
214
217
|
}
|
|
215
218
|
} else {
|
|
216
|
-
|
|
219
|
+
// Auto-download overlay if not found
|
|
220
|
+
log("overlay not found — downloading from GitHub Releases...");
|
|
221
|
+
try {
|
|
222
|
+
const { downloadOverlay } = await import("./setup-overlay.js");
|
|
223
|
+
const success = await downloadOverlay({ silent: false });
|
|
224
|
+
if (success) {
|
|
225
|
+
// Re-find and launch the freshly downloaded overlay
|
|
226
|
+
const freshOverlay = findOverlay();
|
|
227
|
+
if (freshOverlay?.type === "prebuilt") {
|
|
228
|
+
if (!IS_WINDOWS) {
|
|
229
|
+
try {
|
|
230
|
+
execSync(`xattr -cr "${freshOverlay.path}"`, { stdio: "pipe" });
|
|
231
|
+
} catch { /* no quarantine */ }
|
|
232
|
+
}
|
|
233
|
+
log("Starting overlay (pre-built)...");
|
|
234
|
+
const binary = IS_WINDOWS
|
|
235
|
+
? freshOverlay.path
|
|
236
|
+
: path.join(freshOverlay.path, "Contents/MacOS/sinain_hud");
|
|
237
|
+
startProcess("overlay", binary, [], { color: MAGENTA });
|
|
238
|
+
await sleep(2000);
|
|
239
|
+
const overlayChild = children.find(c => c.name === "overlay");
|
|
240
|
+
if (overlayChild && !overlayChild.proc.killed && overlayChild.proc.exitCode === null) {
|
|
241
|
+
ok(`overlay running (pid:${overlayChild.pid})`);
|
|
242
|
+
overlayStatus = "running";
|
|
243
|
+
} else {
|
|
244
|
+
warn("overlay exited early — check logs above");
|
|
245
|
+
overlayStatus = "failed";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
warn("overlay auto-download failed — run: sinain setup-overlay");
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
warn(`overlay auto-download failed: ${e.message}`);
|
|
253
|
+
}
|
|
217
254
|
}
|
|
218
255
|
}
|
|
219
256
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geravant/sinain",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "Ambient AI overlay invisible to screen capture — real-time insights from audio + screen context",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"cli.js",
|
|
17
17
|
"launcher.js",
|
|
18
18
|
"setup-overlay.js",
|
|
19
|
+
"setup-sck-capture.js",
|
|
19
20
|
"pack-prepare.js",
|
|
20
21
|
"install.js",
|
|
21
22
|
"index.ts",
|
package/sense_client/__main__.py
CHANGED
|
@@ -12,6 +12,7 @@ if sys.platform == "win32":
|
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
14
|
import concurrent.futures
|
|
15
|
+
import copy
|
|
15
16
|
import json
|
|
16
17
|
import os
|
|
17
18
|
import time
|
|
@@ -28,7 +29,7 @@ from .capture import ScreenCapture, create_capture
|
|
|
28
29
|
from .change_detector import ChangeDetector
|
|
29
30
|
from .roi_extractor import ROIExtractor
|
|
30
31
|
from .ocr import OCRResult, create_ocr
|
|
31
|
-
from .gate import DecisionGate, SenseObservation
|
|
32
|
+
from .gate import DecisionGate, SenseEvent, SenseObservation, SenseMeta
|
|
32
33
|
from .sender import SenseSender, package_full_frame, package_roi
|
|
33
34
|
from .app_detector import AppDetector
|
|
34
35
|
from .config import load_config
|
|
@@ -128,6 +129,7 @@ def main():
|
|
|
128
129
|
)
|
|
129
130
|
app_detector = AppDetector()
|
|
130
131
|
ocr_pool = concurrent.futures.ThreadPoolExecutor(max_workers=4)
|
|
132
|
+
vision_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1, thread_name_prefix="vision")
|
|
131
133
|
|
|
132
134
|
# Vision provider — routes to Ollama (local) or OpenRouter (cloud) based on config/privacy
|
|
133
135
|
vision_cfg = config.get("vision", {})
|
|
@@ -355,18 +357,28 @@ def main():
|
|
|
355
357
|
title=title, subtitle=subtitle, facts=facts,
|
|
356
358
|
)
|
|
357
359
|
|
|
358
|
-
# Vision scene analysis
|
|
360
|
+
# Vision scene analysis — async: send text event immediately, vision follows
|
|
359
361
|
if vision_provider and time.time() - last_vision_time >= vision_throttle_s:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
362
|
+
last_vision_time = time.time() # claim slot immediately to prevent concurrent calls
|
|
363
|
+
_v_frame = use_frame.copy() if isinstance(use_frame, np.ndarray) else use_frame.copy()
|
|
364
|
+
_v_meta = copy.copy(event.meta)
|
|
365
|
+
_v_ts = event.ts
|
|
366
|
+
_v_prompt = vision_prompt
|
|
367
|
+
def _do_vision(frame, meta, ts, prompt):
|
|
368
|
+
try:
|
|
369
|
+
from PIL import Image as PILImage
|
|
370
|
+
pil = PILImage.fromarray(frame) if isinstance(frame, np.ndarray) else frame
|
|
371
|
+
scene = vision_provider.describe(pil, prompt=prompt or None)
|
|
372
|
+
if scene:
|
|
373
|
+
log(f"vision: {scene[:80]}...")
|
|
374
|
+
ctx_ev = SenseEvent(type="context", ts=ts)
|
|
375
|
+
ctx_ev.observation = SenseObservation(scene=scene)
|
|
376
|
+
ctx_ev.meta = meta
|
|
377
|
+
ctx_ev.roi = package_full_frame(frame)
|
|
378
|
+
sender.send(ctx_ev)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
log(f"vision error: {e}")
|
|
381
|
+
vision_pool.submit(_do_vision, _v_frame, _v_meta, _v_ts, _v_prompt)
|
|
370
382
|
|
|
371
383
|
# Send small thumbnail for ALL event types (agent uses vision)
|
|
372
384
|
# Privacy matrix: gate image sending based on PRIVACY_IMAGES_OPENROUTER
|
package/setup-overlay.js
CHANGED
|
@@ -37,25 +37,36 @@ function ok(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${GREEN}✓${RE
|
|
|
37
37
|
function warn(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${YELLOW}⚠${RESET} ${msg}`); }
|
|
38
38
|
function fail(msg) { console.error(`${BOLD}[setup-overlay]${RESET} ${RED}✗${RESET} ${msg}`); process.exit(1); }
|
|
39
39
|
|
|
40
|
-
// ──
|
|
40
|
+
// ── Entry point (only when run directly, not when imported) ──────────────────
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const isMain = process.argv[1] && (
|
|
43
|
+
import.meta.url === `file://${process.argv[1]}` ||
|
|
44
|
+
import.meta.url === new URL(process.argv[1], "file://").href
|
|
45
|
+
);
|
|
45
46
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (isMain) {
|
|
48
|
+
const args = process.argv.slice(2);
|
|
49
|
+
const fromSource = args.includes("--from-source");
|
|
50
|
+
const forceUpdate = args.includes("--update");
|
|
51
|
+
|
|
52
|
+
if (fromSource) {
|
|
53
|
+
await buildFromSource();
|
|
54
|
+
} else {
|
|
55
|
+
await downloadOverlay({ forceUpdate });
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
// ── Download pre-built .app ──────────────────────────────────────────────────
|
|
53
60
|
|
|
54
|
-
async function
|
|
61
|
+
export async function downloadOverlay({ silent = false, forceUpdate = false } = {}) {
|
|
62
|
+
const _log = silent ? () => {} : log;
|
|
63
|
+
const _ok = silent ? () => {} : ok;
|
|
64
|
+
const _warn = silent ? () => {} : warn;
|
|
65
|
+
|
|
55
66
|
fs.mkdirSync(APP_DIR, { recursive: true });
|
|
56
67
|
|
|
57
68
|
// Find latest overlay release
|
|
58
|
-
|
|
69
|
+
_log("Checking for latest overlay release...");
|
|
59
70
|
let release;
|
|
60
71
|
try {
|
|
61
72
|
const res = await fetch(`${RELEASES_API}?per_page=20`, {
|
|
@@ -67,6 +78,7 @@ async function downloadPrebuilt() {
|
|
|
67
78
|
release = releases.find(r => r.tag_name?.startsWith("overlay-v"));
|
|
68
79
|
if (!release) throw new Error("No overlay release found");
|
|
69
80
|
} catch (e) {
|
|
81
|
+
if (silent) return false;
|
|
70
82
|
fail(`Failed to fetch releases: ${e.message}\n Try: sinain setup-overlay --from-source`);
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -78,21 +90,22 @@ async function downloadPrebuilt() {
|
|
|
78
90
|
try {
|
|
79
91
|
const local = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
|
|
80
92
|
if (local.tag === tag) {
|
|
81
|
-
|
|
82
|
-
return;
|
|
93
|
+
_ok(`Overlay already up-to-date (${version})`);
|
|
94
|
+
return true;
|
|
83
95
|
}
|
|
84
|
-
|
|
96
|
+
_log(`Updating: ${local.tag} → ${tag}`);
|
|
85
97
|
} catch { /* corrupt version file — re-download */ }
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
// Find the .zip asset for this platform
|
|
89
101
|
const zipAsset = release.assets?.find(a => a.name === ASSET_NAME);
|
|
90
102
|
if (!zipAsset) {
|
|
103
|
+
if (silent) return false;
|
|
91
104
|
fail(`Release ${tag} has no ${ASSET_NAME} asset.\n Try: sinain setup-overlay --from-source`);
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
// Download with progress
|
|
95
|
-
|
|
108
|
+
_log(`Downloading overlay ${version} for ${IS_WINDOWS ? "Windows" : "macOS"} (${formatBytes(zipAsset.size)})...`);
|
|
96
109
|
const zipPath = path.join(APP_DIR, ASSET_NAME);
|
|
97
110
|
|
|
98
111
|
try {
|
|
@@ -112,17 +125,18 @@ async function downloadPrebuilt() {
|
|
|
112
125
|
if (done) break;
|
|
113
126
|
chunks.push(value);
|
|
114
127
|
downloaded += value.length;
|
|
115
|
-
if (total > 0) {
|
|
128
|
+
if (total > 0 && !silent) {
|
|
116
129
|
const pct = Math.round((downloaded / total) * 100);
|
|
117
130
|
process.stdout.write(`\r${BOLD}[setup-overlay]${RESET} ${DIM}${pct}% (${formatBytes(downloaded)} / ${formatBytes(total)})${RESET}`);
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
|
-
process.stdout.write("\n");
|
|
133
|
+
if (!silent) process.stdout.write("\n");
|
|
121
134
|
|
|
122
135
|
const buffer = Buffer.concat(chunks);
|
|
123
136
|
fs.writeFileSync(zipPath, buffer);
|
|
124
|
-
|
|
137
|
+
_ok(`Downloaded ${formatBytes(buffer.length)}`);
|
|
125
138
|
} catch (e) {
|
|
139
|
+
if (silent) return false;
|
|
126
140
|
fail(`Download failed: ${e.message}`);
|
|
127
141
|
}
|
|
128
142
|
|
|
@@ -132,7 +146,7 @@ async function downloadPrebuilt() {
|
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
// Extract
|
|
135
|
-
|
|
149
|
+
_log("Extracting...");
|
|
136
150
|
if (IS_WINDOWS) {
|
|
137
151
|
try {
|
|
138
152
|
execSync(
|
|
@@ -140,6 +154,7 @@ async function downloadPrebuilt() {
|
|
|
140
154
|
{ stdio: "pipe" }
|
|
141
155
|
);
|
|
142
156
|
} catch (e) {
|
|
157
|
+
if (silent) return false;
|
|
143
158
|
fail(`Extraction failed: ${e.message}`);
|
|
144
159
|
}
|
|
145
160
|
} else {
|
|
@@ -150,6 +165,7 @@ async function downloadPrebuilt() {
|
|
|
150
165
|
try {
|
|
151
166
|
execSync(`unzip -o -q "${zipPath}" -d "${APP_DIR}"`, { stdio: "pipe" });
|
|
152
167
|
} catch (e) {
|
|
168
|
+
if (silent) return false;
|
|
153
169
|
fail(`Extraction failed: ${e.message}`);
|
|
154
170
|
}
|
|
155
171
|
}
|
|
@@ -170,12 +186,15 @@ async function downloadPrebuilt() {
|
|
|
170
186
|
// Clean up zip
|
|
171
187
|
fs.unlinkSync(zipPath);
|
|
172
188
|
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
_ok(`Overlay ${version} installed`);
|
|
190
|
+
if (!silent) {
|
|
191
|
+
console.log(`
|
|
175
192
|
${GREEN}✓${RESET} Overlay ready!
|
|
176
193
|
Location: ${APP_PATH}
|
|
177
194
|
The overlay will auto-start with: ${BOLD}sinain start${RESET}
|
|
178
195
|
`);
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
179
198
|
}
|
|
180
199
|
|
|
181
200
|
// ── Build from source (legacy) ───────────────────────────────────────────────
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sinain setup-sck-capture — download pre-built sck-capture binary from GitHub Releases
|
|
3
|
+
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import os from "os";
|
|
8
|
+
|
|
9
|
+
const HOME = os.homedir();
|
|
10
|
+
const SINAIN_DIR = path.join(HOME, ".sinain");
|
|
11
|
+
const INSTALL_DIR = path.join(SINAIN_DIR, "sck-capture");
|
|
12
|
+
const BINARY_PATH = path.join(INSTALL_DIR, "sck-capture");
|
|
13
|
+
const VERSION_FILE = path.join(INSTALL_DIR, "version.json");
|
|
14
|
+
|
|
15
|
+
const REPO = "anthillnet/sinain-hud";
|
|
16
|
+
const RELEASES_API = `https://api.github.com/repos/${REPO}/releases`;
|
|
17
|
+
const TAG_PREFIX = "sck-capture-v";
|
|
18
|
+
const ASSET_NAME = "sck-capture-macos.zip";
|
|
19
|
+
|
|
20
|
+
const BOLD = "\x1b[1m";
|
|
21
|
+
const GREEN = "\x1b[32m";
|
|
22
|
+
const YELLOW = "\x1b[33m";
|
|
23
|
+
const RED = "\x1b[31m";
|
|
24
|
+
const DIM = "\x1b[2m";
|
|
25
|
+
const RESET = "\x1b[0m";
|
|
26
|
+
|
|
27
|
+
function log(msg) { console.log(`${BOLD}[setup-sck-capture]${RESET} ${msg}`); }
|
|
28
|
+
function ok(msg) { console.log(`${BOLD}[setup-sck-capture]${RESET} ${GREEN}✓${RESET} ${msg}`); }
|
|
29
|
+
function warn(msg) { console.log(`${BOLD}[setup-sck-capture]${RESET} ${YELLOW}⚠${RESET} ${msg}`); }
|
|
30
|
+
function fail(msg) { console.error(`${BOLD}[setup-sck-capture]${RESET} ${RED}✗${RESET} ${msg}`); process.exit(1); }
|
|
31
|
+
|
|
32
|
+
// ── Entry point (only when run directly, not when imported) ──────────────────
|
|
33
|
+
|
|
34
|
+
const isMain = process.argv[1] && (
|
|
35
|
+
import.meta.url === `file://${process.argv[1]}` ||
|
|
36
|
+
import.meta.url === new URL(process.argv[1], "file://").href
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (isMain) {
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const forceUpdate = args.includes("--update");
|
|
42
|
+
|
|
43
|
+
if (os.platform() === "win32") {
|
|
44
|
+
log("sck-capture is macOS-only (Windows uses win-audio-capture.exe)");
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await downloadBinary({ forceUpdate });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Download pre-built binary ────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export async function downloadBinary({ silent = false, forceUpdate = false } = {}) {
|
|
54
|
+
const _log = silent ? () => {} : log;
|
|
55
|
+
const _ok = silent ? () => {} : ok;
|
|
56
|
+
const _warn = silent ? () => {} : warn;
|
|
57
|
+
|
|
58
|
+
fs.mkdirSync(INSTALL_DIR, { recursive: true });
|
|
59
|
+
|
|
60
|
+
// Find latest sck-capture release
|
|
61
|
+
_log("Checking for latest sck-capture release...");
|
|
62
|
+
let release;
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${RELEASES_API}?per_page=30`, {
|
|
65
|
+
signal: AbortSignal.timeout(10000),
|
|
66
|
+
headers: { "Accept": "application/vnd.github+json" },
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) throw new Error(`GitHub API returned ${res.status}`);
|
|
69
|
+
const releases = await res.json();
|
|
70
|
+
release = releases.find(r => r.tag_name?.startsWith(TAG_PREFIX));
|
|
71
|
+
if (!release) throw new Error("No sck-capture release found");
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (silent) {
|
|
74
|
+
_warn(`Failed to fetch sck-capture release: ${e.message}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
fail(`Failed to fetch releases: ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const tag = release.tag_name;
|
|
81
|
+
const version = tag.replace(TAG_PREFIX, "");
|
|
82
|
+
|
|
83
|
+
// Check if already up-to-date
|
|
84
|
+
if (!forceUpdate && fs.existsSync(VERSION_FILE) && fs.existsSync(BINARY_PATH)) {
|
|
85
|
+
try {
|
|
86
|
+
const local = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
|
|
87
|
+
if (local.tag === tag) {
|
|
88
|
+
_ok(`sck-capture already up-to-date (${version})`);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
_log(`Updating: ${local.tag} → ${tag}`);
|
|
92
|
+
} catch { /* corrupt version file — re-download */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Find the .zip asset
|
|
96
|
+
const zipAsset = release.assets?.find(a => a.name === ASSET_NAME);
|
|
97
|
+
if (!zipAsset) {
|
|
98
|
+
if (silent) {
|
|
99
|
+
_warn(`Release ${tag} has no ${ASSET_NAME} asset`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
fail(`Release ${tag} has no ${ASSET_NAME} asset`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Download
|
|
106
|
+
_log(`Downloading sck-capture ${version} (${formatBytes(zipAsset.size)})...`);
|
|
107
|
+
const zipPath = path.join(INSTALL_DIR, ASSET_NAME);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(zipAsset.browser_download_url, {
|
|
111
|
+
signal: AbortSignal.timeout(60000),
|
|
112
|
+
redirect: "follow",
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
115
|
+
|
|
116
|
+
const total = parseInt(res.headers.get("content-length") || "0");
|
|
117
|
+
const chunks = [];
|
|
118
|
+
let downloaded = 0;
|
|
119
|
+
|
|
120
|
+
const reader = res.body.getReader();
|
|
121
|
+
while (true) {
|
|
122
|
+
const { done, value } = await reader.read();
|
|
123
|
+
if (done) break;
|
|
124
|
+
chunks.push(value);
|
|
125
|
+
downloaded += value.length;
|
|
126
|
+
if (!silent && total > 0) {
|
|
127
|
+
const pct = Math.round((downloaded / total) * 100);
|
|
128
|
+
process.stdout.write(`\r${BOLD}[setup-sck-capture]${RESET} ${DIM}${pct}% (${formatBytes(downloaded)} / ${formatBytes(total)})${RESET}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!silent) process.stdout.write("\n");
|
|
132
|
+
|
|
133
|
+
const buffer = Buffer.concat(chunks);
|
|
134
|
+
fs.writeFileSync(zipPath, buffer);
|
|
135
|
+
_ok(`Downloaded ${formatBytes(buffer.length)}`);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (silent) {
|
|
138
|
+
_warn(`Download failed: ${e.message}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
fail(`Download failed: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Remove old binary if present
|
|
145
|
+
if (fs.existsSync(BINARY_PATH)) {
|
|
146
|
+
fs.unlinkSync(BINARY_PATH);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Extract
|
|
150
|
+
_log("Extracting...");
|
|
151
|
+
try {
|
|
152
|
+
execSync(`ditto -x -k "${zipPath}" "${INSTALL_DIR}"`, { stdio: "pipe" });
|
|
153
|
+
} catch {
|
|
154
|
+
try {
|
|
155
|
+
execSync(`unzip -o -q "${zipPath}" -d "${INSTALL_DIR}"`, { stdio: "pipe" });
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (silent) {
|
|
158
|
+
_warn(`Extraction failed: ${e.message}`);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
fail(`Extraction failed: ${e.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Make executable
|
|
166
|
+
try {
|
|
167
|
+
fs.chmodSync(BINARY_PATH, 0o755);
|
|
168
|
+
} catch { /* may not exist if zip structure differs */ }
|
|
169
|
+
|
|
170
|
+
// Write version marker
|
|
171
|
+
fs.writeFileSync(VERSION_FILE, JSON.stringify({
|
|
172
|
+
tag,
|
|
173
|
+
version,
|
|
174
|
+
installedAt: new Date().toISOString(),
|
|
175
|
+
}, null, 2));
|
|
176
|
+
|
|
177
|
+
// Clean up zip
|
|
178
|
+
fs.unlinkSync(zipPath);
|
|
179
|
+
|
|
180
|
+
_ok(`sck-capture ${version} installed → ${BINARY_PATH}`);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function formatBytes(bytes) {
|
|
187
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
188
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
189
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
190
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import type { FeedBuffer } from "../buffers/feed-buffer.js";
|
|
3
4
|
import type { SenseBuffer } from "../buffers/sense-buffer.js";
|
|
4
|
-
import type { AgentConfig, AgentEntry, ContextWindow, EscalationMode, ContextRichness, RecorderStatus } from "../types.js";
|
|
5
|
+
import type { AgentConfig, AgentEntry, ContextWindow, EscalationMode, ContextRichness, RecorderStatus, SenseEvent, FeedbackRecord } from "../types.js";
|
|
5
6
|
import type { Profiler } from "../profiler.js";
|
|
6
|
-
import { buildContextWindow } from "./context-window.js";
|
|
7
|
+
import { buildContextWindow, RICHNESS_PRESETS } from "./context-window.js";
|
|
7
8
|
import { analyzeContext } from "./analyzer.js";
|
|
8
9
|
import { writeSituationMd } from "./situation-writer.js";
|
|
9
10
|
import { calculateEscalationScore } from "../escalation/scorer.js";
|
|
@@ -35,6 +36,10 @@ export interface AgentLoopDeps {
|
|
|
35
36
|
traitEngine?: TraitEngine;
|
|
36
37
|
/** Directory to write per-day trait log JSONL files. */
|
|
37
38
|
traitLogDir?: string;
|
|
39
|
+
/** Optional: path to sinain-knowledge.md for startup recap. */
|
|
40
|
+
getKnowledgeDocPath?: () => string | null;
|
|
41
|
+
/** Optional: feedback store for startup recap context. */
|
|
42
|
+
feedbackStore?: { queryRecent(n: number): FeedbackRecord[] };
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export interface TraceContext {
|
|
@@ -69,6 +74,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
69
74
|
private lastRunTs = 0;
|
|
70
75
|
private running = false;
|
|
71
76
|
private started = false;
|
|
77
|
+
private firstTick = true;
|
|
72
78
|
|
|
73
79
|
private lastPushedHud = "";
|
|
74
80
|
private agentNextId = 1;
|
|
@@ -112,6 +118,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
112
118
|
}, this.deps.agentConfig.maxIntervalMs);
|
|
113
119
|
|
|
114
120
|
log(TAG, `loop started (debounce=${this.deps.agentConfig.debounceMs}ms, max=${this.deps.agentConfig.maxIntervalMs}ms, cooldown=${this.deps.agentConfig.cooldownMs}ms, model=${this.deps.agentConfig.model})`);
|
|
121
|
+
|
|
122
|
+
// Fire recap tick: immediate HUD from persistent knowledge (no sense data needed)
|
|
123
|
+
this.fireRecapTick().catch(e => debug(TAG, "recap skipped:", String(e)));
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
/** Stop the agent loop. */
|
|
@@ -131,12 +140,13 @@ export class AgentLoop extends EventEmitter {
|
|
|
131
140
|
onNewContext(): void {
|
|
132
141
|
if (!this.started) return;
|
|
133
142
|
|
|
134
|
-
//
|
|
143
|
+
// Fast first tick: 500ms debounce on startup, normal debounce after
|
|
144
|
+
const delay = this.firstTick ? 500 : this.deps.agentConfig.debounceMs;
|
|
135
145
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
136
146
|
this.debounceTimer = setTimeout(() => {
|
|
137
147
|
this.debounceTimer = null;
|
|
138
148
|
this.run().catch(err => error(TAG, "debounce tick error:", err.message));
|
|
139
|
-
},
|
|
149
|
+
}, delay);
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
/** Get agent results history (newest first). */
|
|
@@ -398,7 +408,80 @@ export class AgentLoop extends EventEmitter {
|
|
|
398
408
|
traceCtx?.finish({ totalLatencyMs: Date.now() - Date.now(), llmLatencyMs: 0, llmInputTokens: 0, llmOutputTokens: 0, llmCost: 0, escalated: false, escalationScore: 0, contextScreenEvents: 0, contextAudioEntries: 0, contextRichness: richness, digestLength: 0, hudChanged: false });
|
|
399
409
|
} finally {
|
|
400
410
|
this.running = false;
|
|
411
|
+
this.firstTick = false;
|
|
401
412
|
this.lastRunTs = Date.now();
|
|
402
413
|
}
|
|
403
414
|
}
|
|
415
|
+
|
|
416
|
+
// ── Private: startup recap tick from persistent knowledge ──
|
|
417
|
+
|
|
418
|
+
private async fireRecapTick(): Promise<void> {
|
|
419
|
+
if (this.running) return;
|
|
420
|
+
this.running = true;
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const sections: string[] = [];
|
|
424
|
+
const startTs = Date.now();
|
|
425
|
+
|
|
426
|
+
// 1. sinain-knowledge.md (established patterns, user preferences)
|
|
427
|
+
const knowledgePath = this.deps.getKnowledgeDocPath?.();
|
|
428
|
+
if (knowledgePath) {
|
|
429
|
+
const content = await fs.promises.readFile(knowledgePath, "utf-8").catch(() => "");
|
|
430
|
+
if (content.length > 50) sections.push(content.slice(0, 2000));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 2. SITUATION.md digest (if fresh — less than 5 minutes old)
|
|
434
|
+
try {
|
|
435
|
+
const stat = await fs.promises.stat(this.deps.situationMdPath);
|
|
436
|
+
if (Date.now() - stat.mtimeMs < 5 * 60_000) {
|
|
437
|
+
const sit = await fs.promises.readFile(this.deps.situationMdPath, "utf-8");
|
|
438
|
+
const digestMatch = sit.match(/## Digest\n([\s\S]*?)(?=\n##|$)/);
|
|
439
|
+
if (digestMatch?.[1]?.trim()) {
|
|
440
|
+
sections.push(`Last session digest:\n${digestMatch[1].trim()}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch { /* SITUATION.md missing — fine */ }
|
|
444
|
+
|
|
445
|
+
// 3. Recent feedback records (last 5 escalation summaries)
|
|
446
|
+
const records = this.deps.feedbackStore?.queryRecent(5) ?? [];
|
|
447
|
+
if (records.length > 0) {
|
|
448
|
+
const recaps = records.slice(0, 5).map(r => `- ${r.currentApp}: ${r.hud}`).join("\n");
|
|
449
|
+
sections.push(`Recent activity:\n${recaps}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (sections.length === 0) { return; }
|
|
453
|
+
|
|
454
|
+
const recapContext = sections.join("\n\n");
|
|
455
|
+
|
|
456
|
+
// Build synthetic ContextWindow with knowledge as screen entry
|
|
457
|
+
const recapWindow: ContextWindow = {
|
|
458
|
+
audio: [],
|
|
459
|
+
screen: [{
|
|
460
|
+
ts: Date.now(),
|
|
461
|
+
ocr: recapContext,
|
|
462
|
+
meta: { app: "sinain-recap", windowTitle: "startup" },
|
|
463
|
+
type: "context",
|
|
464
|
+
} as unknown as SenseEvent],
|
|
465
|
+
images: [],
|
|
466
|
+
currentApp: "sinain-recap",
|
|
467
|
+
appHistory: [],
|
|
468
|
+
audioCount: 0,
|
|
469
|
+
screenCount: 1,
|
|
470
|
+
windowMs: 0,
|
|
471
|
+
newestEventTs: Date.now(),
|
|
472
|
+
preset: RICHNESS_PRESETS.lean,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const result = await analyzeContext(recapWindow, this.deps.agentConfig, null);
|
|
476
|
+
if (result?.hud && result.hud !== "—" && result.hud !== "Idle") {
|
|
477
|
+
this.deps.onHudUpdate(result.hud);
|
|
478
|
+
log(TAG, `recap tick (${Date.now() - startTs}ms, ${result.tokensIn}in+${result.tokensOut}out tok) hud="${result.hud}"`);
|
|
479
|
+
}
|
|
480
|
+
} catch (err: any) {
|
|
481
|
+
debug(TAG, "recap tick error:", err.message || err);
|
|
482
|
+
} finally {
|
|
483
|
+
this.running = false;
|
|
484
|
+
// Do NOT update lastRunTs — normal cooldown should not be affected by recap
|
|
485
|
+
}
|
|
486
|
+
}
|
|
404
487
|
}
|
package/sinain-core/src/index.ts
CHANGED
|
@@ -25,6 +25,12 @@ import { initPrivacy, levelFor, applyLevel } from "./privacy/index.js";
|
|
|
25
25
|
|
|
26
26
|
const TAG = "core";
|
|
27
27
|
|
|
28
|
+
/** Resolve workspace path, expanding leading ~ to HOME. */
|
|
29
|
+
function resolveWorkspace(): string {
|
|
30
|
+
const raw = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
|
|
31
|
+
return raw.startsWith("~") ? raw.replace("~", process.env.HOME || "") : raw;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
async function main() {
|
|
29
35
|
log(TAG, "sinain-core starting...");
|
|
30
36
|
|
|
@@ -80,7 +86,7 @@ async function main() {
|
|
|
80
86
|
profiler,
|
|
81
87
|
feedbackStore: feedbackStore ?? undefined,
|
|
82
88
|
queryKnowledgeFacts: async (entities: string[], maxFacts: number) => {
|
|
83
|
-
const workspace =
|
|
89
|
+
const workspace = resolveWorkspace();
|
|
84
90
|
const dbPath = `${workspace}/memory/knowledge-graph.db`;
|
|
85
91
|
const scriptPath = `${workspace}/sinain-memory/graph_query.py`;
|
|
86
92
|
try {
|
|
@@ -156,6 +162,13 @@ async function main() {
|
|
|
156
162
|
} : undefined,
|
|
157
163
|
traitEngine,
|
|
158
164
|
traitLogDir: config.traitConfig.logDir,
|
|
165
|
+
getKnowledgeDocPath: () => {
|
|
166
|
+
const workspace = resolveWorkspace();
|
|
167
|
+
const p = `${workspace}/memory/sinain-knowledge.md`;
|
|
168
|
+
try { if (existsSync(p)) return p; } catch {}
|
|
169
|
+
return null;
|
|
170
|
+
},
|
|
171
|
+
feedbackStore: feedbackStore ?? undefined,
|
|
159
172
|
});
|
|
160
173
|
|
|
161
174
|
// ── Wire learning signal collector (needs agentLoop) ──
|
|
@@ -400,13 +413,13 @@ async function main() {
|
|
|
400
413
|
|
|
401
414
|
// Knowledge graph integration
|
|
402
415
|
getKnowledgeDocPath: () => {
|
|
403
|
-
const workspace =
|
|
416
|
+
const workspace = resolveWorkspace();
|
|
404
417
|
const p = `${workspace}/memory/sinain-knowledge.md`;
|
|
405
418
|
try { if (existsSync(p)) return p; } catch {}
|
|
406
419
|
return null;
|
|
407
420
|
},
|
|
408
421
|
queryKnowledgeFacts: async (entities: string[], maxFacts: number) => {
|
|
409
|
-
const workspace =
|
|
422
|
+
const workspace = resolveWorkspace();
|
|
410
423
|
const dbPath = `${workspace}/memory/knowledge-graph.db`;
|
|
411
424
|
const scriptPath = `${workspace}/sinain-memory/graph_query.py`;
|
|
412
425
|
try {
|