@geravant/sinain 1.6.7 → 1.6.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.
- package/launcher.js +140 -47
- package/package.json +2 -2
- package/sense_client/__main__.py +29 -14
- package/sense_client/requirements.txt +1 -0
- package/setup-overlay.js +39 -20
- package/sinain-agent/run.sh +10 -8
- package/sinain-core/src/agent/loop.ts +87 -4
- package/sinain-core/src/config.ts +4 -0
- package/sinain-core/src/escalation/escalator.ts +1 -1
- package/sinain-core/src/index.ts +17 -3
- package/sinain-core/src/overlay/commands.ts +11 -0
- package/sinain-core/src/overlay/ws-handler.ts +3 -1
package/launcher.js
CHANGED
|
@@ -35,11 +35,13 @@ let skipSense = false;
|
|
|
35
35
|
let skipOverlay = false;
|
|
36
36
|
let skipAgent = false;
|
|
37
37
|
let agentName = null;
|
|
38
|
+
let forceSetup = false;
|
|
38
39
|
|
|
39
40
|
for (const arg of args) {
|
|
40
41
|
if (arg === "--no-sense") { skipSense = true; continue; }
|
|
41
42
|
if (arg === "--no-overlay") { skipOverlay = true; continue; }
|
|
42
43
|
if (arg === "--no-agent") { skipAgent = true; continue; }
|
|
44
|
+
if (arg === "--setup") { forceSetup = true; continue; }
|
|
43
45
|
if (arg.startsWith("--agent=")) { agentName = arg.split("=")[1]; continue; }
|
|
44
46
|
console.error(`Unknown flag: ${arg}`);
|
|
45
47
|
process.exit(1);
|
|
@@ -60,15 +62,20 @@ async function main() {
|
|
|
60
62
|
await preflight();
|
|
61
63
|
console.log();
|
|
62
64
|
|
|
63
|
-
// Run setup wizard on first launch (no ~/.sinain/.env)
|
|
65
|
+
// Run setup wizard on first launch (no ~/.sinain/.env) or when --setup flag is passed
|
|
64
66
|
const userEnvPath = path.join(SINAIN_DIR, ".env");
|
|
65
|
-
if (!fs.existsSync(userEnvPath)) {
|
|
67
|
+
if (forceSetup || !fs.existsSync(userEnvPath)) {
|
|
66
68
|
await setupWizard(userEnvPath);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// Load user config
|
|
70
72
|
loadUserEnv();
|
|
71
73
|
|
|
74
|
+
// Ensure Ollama is running (if local vision enabled)
|
|
75
|
+
if (process.env.LOCAL_VISION_ENABLED === "true") {
|
|
76
|
+
await ensureOllama();
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
// Auto-detect transcription backend
|
|
73
80
|
detectTranscription();
|
|
74
81
|
|
|
@@ -127,7 +134,10 @@ async function main() {
|
|
|
127
134
|
const scDir = path.join(PKG_DIR, "sense_client");
|
|
128
135
|
// Check if key package is importable to skip pip
|
|
129
136
|
try {
|
|
130
|
-
|
|
137
|
+
const depCheck = IS_WINDOWS
|
|
138
|
+
? 'python3 -c "import PIL; import skimage"'
|
|
139
|
+
: 'python3 -c "import PIL; import skimage; import Quartz; import Vision"';
|
|
140
|
+
execSync(depCheck, { stdio: "pipe" });
|
|
131
141
|
} catch {
|
|
132
142
|
log("Installing sense_client Python dependencies...");
|
|
133
143
|
try {
|
|
@@ -213,7 +223,41 @@ async function main() {
|
|
|
213
223
|
warn("flutter not found — overlay source found but can't build");
|
|
214
224
|
}
|
|
215
225
|
} else {
|
|
216
|
-
|
|
226
|
+
// Auto-download overlay if not found
|
|
227
|
+
log("overlay not found — downloading from GitHub Releases...");
|
|
228
|
+
try {
|
|
229
|
+
const { downloadOverlay } = await import("./setup-overlay.js");
|
|
230
|
+
const success = await downloadOverlay({ silent: false });
|
|
231
|
+
if (success) {
|
|
232
|
+
// Re-find and launch the freshly downloaded overlay
|
|
233
|
+
const freshOverlay = findOverlay();
|
|
234
|
+
if (freshOverlay?.type === "prebuilt") {
|
|
235
|
+
if (!IS_WINDOWS) {
|
|
236
|
+
try {
|
|
237
|
+
execSync(`xattr -cr "${freshOverlay.path}"`, { stdio: "pipe" });
|
|
238
|
+
} catch { /* no quarantine */ }
|
|
239
|
+
}
|
|
240
|
+
log("Starting overlay (pre-built)...");
|
|
241
|
+
const binary = IS_WINDOWS
|
|
242
|
+
? freshOverlay.path
|
|
243
|
+
: path.join(freshOverlay.path, "Contents/MacOS/sinain_hud");
|
|
244
|
+
startProcess("overlay", binary, [], { color: MAGENTA });
|
|
245
|
+
await sleep(2000);
|
|
246
|
+
const overlayChild = children.find(c => c.name === "overlay");
|
|
247
|
+
if (overlayChild && !overlayChild.proc.killed && overlayChild.proc.exitCode === null) {
|
|
248
|
+
ok(`overlay running (pid:${overlayChild.pid})`);
|
|
249
|
+
overlayStatus = "running";
|
|
250
|
+
} else {
|
|
251
|
+
warn("overlay exited early — check logs above");
|
|
252
|
+
overlayStatus = "failed";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
warn("overlay auto-download failed — run: sinain setup-overlay");
|
|
257
|
+
}
|
|
258
|
+
} catch (e) {
|
|
259
|
+
warn(`overlay auto-download failed: ${e.message}`);
|
|
260
|
+
}
|
|
217
261
|
}
|
|
218
262
|
}
|
|
219
263
|
|
|
@@ -309,25 +353,38 @@ async function preflight() {
|
|
|
309
353
|
ok("port 9500 free");
|
|
310
354
|
}
|
|
311
355
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function ensureOllama() {
|
|
359
|
+
try {
|
|
360
|
+
const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(2000) });
|
|
361
|
+
if (resp.ok) {
|
|
362
|
+
ok("ollama server running");
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
} catch { /* not running */ }
|
|
366
|
+
|
|
367
|
+
// Try to start Ollama in background
|
|
368
|
+
log("Starting ollama server...");
|
|
369
|
+
try {
|
|
370
|
+
const { spawn: spawnProc } = await import("child_process");
|
|
371
|
+
spawnProc("ollama", ["serve"], { detached: true, stdio: "ignore" }).unref();
|
|
372
|
+
// Wait for it to become ready
|
|
373
|
+
for (let i = 0; i < 10; i++) {
|
|
374
|
+
await sleep(500);
|
|
323
375
|
try {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
376
|
+
const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(1000) });
|
|
377
|
+
if (resp.ok) {
|
|
378
|
+
ok("ollama server started");
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
} catch { /* not ready yet */ }
|
|
330
382
|
}
|
|
383
|
+
warn("ollama started but not responding — local vision may not work");
|
|
384
|
+
return false;
|
|
385
|
+
} catch {
|
|
386
|
+
warn("ollama not found — local vision disabled. Install: brew install ollama");
|
|
387
|
+
return false;
|
|
331
388
|
}
|
|
332
389
|
}
|
|
333
390
|
|
|
@@ -337,9 +394,20 @@ async function setupWizard(envPath) {
|
|
|
337
394
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
338
395
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
339
396
|
|
|
397
|
+
// Load existing .env values as defaults (for re-configuration)
|
|
398
|
+
const existing = {};
|
|
399
|
+
if (fs.existsSync(envPath)) {
|
|
400
|
+
for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
|
|
401
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
402
|
+
if (m) existing[m[1]] = m[2];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const hasExisting = Object.keys(existing).length > 0;
|
|
406
|
+
|
|
340
407
|
console.log();
|
|
341
|
-
console.log(`${BOLD}── First-time setup ────────────────────${RESET}`);
|
|
408
|
+
console.log(`${BOLD}── ${hasExisting ? "Re-configure" : "First-time setup"} ────────────────────${RESET}`);
|
|
342
409
|
console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
|
|
410
|
+
if (hasExisting) console.log(` ${DIM}Press Enter to keep current values shown in [brackets]${RESET}`);
|
|
343
411
|
console.log();
|
|
344
412
|
|
|
345
413
|
const vars = {};
|
|
@@ -391,10 +459,13 @@ async function setupWizard(envPath) {
|
|
|
391
459
|
|
|
392
460
|
// 2. OpenRouter API key (if cloud backend or for vision/OCR)
|
|
393
461
|
if (transcriptionBackend === "openrouter") {
|
|
462
|
+
const existingKey = existing.OPENROUTER_API_KEY;
|
|
463
|
+
const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
|
|
394
464
|
let key = "";
|
|
395
465
|
while (!key) {
|
|
396
|
-
key = await ask(` OpenRouter API key (sk-or-...): `);
|
|
466
|
+
key = await ask(` OpenRouter API key (sk-or-...)${keyHint}: `);
|
|
397
467
|
key = key.trim();
|
|
468
|
+
if (!key && existingKey) { key = existingKey; break; }
|
|
398
469
|
if (key && !key.startsWith("sk-or-")) {
|
|
399
470
|
console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
|
|
400
471
|
const retry = await ask(` Use this key anyway? [y/N]: `);
|
|
@@ -408,13 +479,17 @@ async function setupWizard(envPath) {
|
|
|
408
479
|
if (key) vars.OPENROUTER_API_KEY = key;
|
|
409
480
|
} else {
|
|
410
481
|
// Still ask for OpenRouter key (needed for vision/OCR)
|
|
411
|
-
const
|
|
482
|
+
const existingKey = existing.OPENROUTER_API_KEY;
|
|
483
|
+
const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
|
|
484
|
+
const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip)${keyHint}: `);
|
|
412
485
|
if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
|
|
486
|
+
else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
|
|
413
487
|
}
|
|
414
488
|
|
|
415
489
|
// 3. Agent selection
|
|
416
|
-
const
|
|
417
|
-
|
|
490
|
+
const defaultAgent = existing.SINAIN_AGENT || "claude";
|
|
491
|
+
const agentChoice = await ask(` Agent? [${BOLD}${defaultAgent}${RESET}/claude/codex/goose/junie/aider]: `);
|
|
492
|
+
vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || defaultAgent;
|
|
418
493
|
|
|
419
494
|
// 3b. Local vision (Ollama)
|
|
420
495
|
const IS_MACOS = os.platform() === "darwin";
|
|
@@ -423,20 +498,24 @@ async function setupWizard(envPath) {
|
|
|
423
498
|
const useVision = await ask(` Enable local vision AI? [Y/n] (Ollama — screen understanding without cloud API): `);
|
|
424
499
|
if (!useVision.trim() || useVision.trim().toLowerCase() === "y") {
|
|
425
500
|
vars.LOCAL_VISION_ENABLED = "true";
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
501
|
+
// Ensure ollama serve is running before list/pull
|
|
502
|
+
const ollamaReady = await ensureOllama();
|
|
503
|
+
if (ollamaReady) {
|
|
504
|
+
try {
|
|
505
|
+
const models = execSync("ollama list 2>/dev/null", { encoding: "utf-8" });
|
|
506
|
+
if (!models.includes("llava")) {
|
|
507
|
+
const pull = await ask(` Pull llava vision model (~4GB)? [Y/n]: `);
|
|
508
|
+
if (!pull.trim() || pull.trim().toLowerCase() === "y") {
|
|
509
|
+
console.log(` ${DIM}Pulling llava...${RESET}`);
|
|
510
|
+
execSync("ollama pull llava", { stdio: "inherit" });
|
|
511
|
+
ok("llava model pulled");
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
ok("llava model already available");
|
|
434
515
|
}
|
|
435
|
-
}
|
|
436
|
-
|
|
516
|
+
} catch {
|
|
517
|
+
warn("Could not check Ollama models");
|
|
437
518
|
}
|
|
438
|
-
} catch {
|
|
439
|
-
warn("Could not check Ollama models");
|
|
440
519
|
}
|
|
441
520
|
vars.LOCAL_VISION_MODEL = "llava";
|
|
442
521
|
}
|
|
@@ -451,6 +530,8 @@ async function setupWizard(envPath) {
|
|
|
451
530
|
console.log(` ${DIM}Installing Ollama...${RESET}`);
|
|
452
531
|
execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit" });
|
|
453
532
|
}
|
|
533
|
+
// Start ollama serve before pulling
|
|
534
|
+
await ensureOllama();
|
|
454
535
|
console.log(` ${DIM}Pulling llava vision model...${RESET}`);
|
|
455
536
|
execSync("ollama pull llava", { stdio: "inherit" });
|
|
456
537
|
vars.LOCAL_VISION_ENABLED = "true";
|
|
@@ -469,25 +550,37 @@ async function setupWizard(envPath) {
|
|
|
469
550
|
console.log(` selective — score-based (errors, questions trigger it)`);
|
|
470
551
|
console.log(` focus — always escalate every tick`);
|
|
471
552
|
console.log(` rich — always escalate with maximum context`);
|
|
472
|
-
const
|
|
473
|
-
|
|
553
|
+
const defaultEsc = existing.ESCALATION_MODE || "selective";
|
|
554
|
+
const escMode = await ask(` Escalation mode? [off/${BOLD}${defaultEsc}${RESET}/selective/focus/rich]: `);
|
|
555
|
+
vars.ESCALATION_MODE = escMode.trim().toLowerCase() || defaultEsc;
|
|
474
556
|
|
|
475
557
|
// 5. OpenClaw gateway
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
558
|
+
const hadGateway = !!(existing.OPENCLAW_WS_URL);
|
|
559
|
+
const gatewayDefault = hadGateway ? "Y" : "N";
|
|
560
|
+
const hasGateway = await ask(` Do you have an OpenClaw gateway? [${gatewayDefault === "Y" ? "Y/n" : "y/N"}]: `);
|
|
561
|
+
const wantsGateway = hasGateway.trim()
|
|
562
|
+
? hasGateway.trim().toLowerCase() === "y"
|
|
563
|
+
: hadGateway;
|
|
564
|
+
if (wantsGateway) {
|
|
565
|
+
const defaultWs = existing.OPENCLAW_WS_URL || "ws://localhost:18789";
|
|
566
|
+
const wsUrl = await ask(` Gateway WebSocket URL [${defaultWs}]: `);
|
|
567
|
+
vars.OPENCLAW_WS_URL = wsUrl.trim() || defaultWs;
|
|
568
|
+
|
|
569
|
+
const existingToken = existing.OPENCLAW_WS_TOKEN;
|
|
570
|
+
const tokenHint = existingToken ? ` [${existingToken.slice(0, 6)}...${existingToken.slice(-4)}]` : "";
|
|
571
|
+
const wsToken = await ask(` Gateway auth token (48-char hex)${tokenHint}: `);
|
|
482
572
|
if (wsToken.trim()) {
|
|
483
573
|
vars.OPENCLAW_WS_TOKEN = wsToken.trim();
|
|
484
574
|
vars.OPENCLAW_HTTP_TOKEN = wsToken.trim();
|
|
575
|
+
} else if (existingToken) {
|
|
576
|
+
vars.OPENCLAW_WS_TOKEN = existingToken;
|
|
577
|
+
vars.OPENCLAW_HTTP_TOKEN = existing.OPENCLAW_HTTP_TOKEN || existingToken;
|
|
485
578
|
}
|
|
486
579
|
|
|
487
580
|
// Derive HTTP URL from WS URL
|
|
488
581
|
const httpBase = vars.OPENCLAW_WS_URL.replace(/^ws/, "http");
|
|
489
582
|
vars.OPENCLAW_HTTP_URL = `${httpBase}/hooks/agent`;
|
|
490
|
-
vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
|
|
583
|
+
vars.OPENCLAW_SESSION_KEY = existing.OPENCLAW_SESSION_KEY || "agent:main:sinain";
|
|
491
584
|
} else {
|
|
492
585
|
// No gateway — disable WS connection attempts
|
|
493
586
|
vars.OPENCLAW_WS_URL = "";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geravant/sinain",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"description": "Ambient
|
|
3
|
+
"version": "1.6.9",
|
|
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": {
|
|
7
7
|
"sinain": "./cli.js",
|
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", {})
|
|
@@ -196,13 +198,16 @@ def main():
|
|
|
196
198
|
time.sleep(1)
|
|
197
199
|
continue
|
|
198
200
|
|
|
199
|
-
# First-frame log
|
|
201
|
+
# First-frame log + force initial context event
|
|
202
|
+
_is_first_frame = not _logged_first_frame
|
|
200
203
|
if not _logged_first_frame:
|
|
201
204
|
log(f"first frame: {frame.size[0]}x{frame.size[1]} (scale={config['capture']['scale']})")
|
|
202
205
|
_logged_first_frame = True
|
|
203
206
|
|
|
204
|
-
# 1. Check app/window change
|
|
207
|
+
# 1. Check app/window change (first frame always treated as app change)
|
|
205
208
|
app_changed, window_changed, app_name, window_title = app_detector.detect_change()
|
|
209
|
+
if _is_first_frame:
|
|
210
|
+
app_changed = True # force context event on startup
|
|
206
211
|
|
|
207
212
|
# Adaptive SSIM threshold
|
|
208
213
|
now_sec = time.time()
|
|
@@ -355,18 +360,28 @@ def main():
|
|
|
355
360
|
title=title, subtitle=subtitle, facts=facts,
|
|
356
361
|
)
|
|
357
362
|
|
|
358
|
-
# Vision scene analysis
|
|
363
|
+
# Vision scene analysis — async: send text event immediately, vision follows
|
|
359
364
|
if vision_provider and time.time() - last_vision_time >= vision_throttle_s:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
365
|
+
last_vision_time = time.time() # claim slot immediately to prevent concurrent calls
|
|
366
|
+
_v_frame = use_frame.copy() if isinstance(use_frame, np.ndarray) else use_frame.copy()
|
|
367
|
+
_v_meta = copy.copy(event.meta)
|
|
368
|
+
_v_ts = event.ts
|
|
369
|
+
_v_prompt = vision_prompt
|
|
370
|
+
def _do_vision(frame, meta, ts, prompt):
|
|
371
|
+
try:
|
|
372
|
+
from PIL import Image as PILImage
|
|
373
|
+
pil = PILImage.fromarray(frame) if isinstance(frame, np.ndarray) else frame
|
|
374
|
+
scene = vision_provider.describe(pil, prompt=prompt or None)
|
|
375
|
+
if scene:
|
|
376
|
+
log(f"vision: {scene[:80]}...")
|
|
377
|
+
ctx_ev = SenseEvent(type="context", ts=ts)
|
|
378
|
+
ctx_ev.observation = SenseObservation(scene=scene)
|
|
379
|
+
ctx_ev.meta = meta
|
|
380
|
+
ctx_ev.roi = package_full_frame(frame)
|
|
381
|
+
sender.send(ctx_ev)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
log(f"vision error: {e}")
|
|
384
|
+
vision_pool.submit(_do_vision, _v_frame, _v_meta, _v_ts, _v_prompt)
|
|
370
385
|
|
|
371
386
|
# Send small thumbnail for ALL event types (agent uses vision)
|
|
372
387
|
# Privacy matrix: gate image sending based on PRIVACY_IMAGES_OPENROUTER
|
|
@@ -4,6 +4,7 @@ numpy>=1.24
|
|
|
4
4
|
pytesseract>=0.3
|
|
5
5
|
requests>=2.31
|
|
6
6
|
pyobjc-framework-Quartz>=10.0; sys_platform == "darwin"
|
|
7
|
+
pyobjc-framework-Vision>=10.0; sys_platform == "darwin"
|
|
7
8
|
mss>=9.0; sys_platform == "win32"
|
|
8
9
|
psutil>=5.9; sys_platform == "win32"
|
|
9
10
|
winrt-Windows.Media.Ocr>=2.0; sys_platform == "win32"
|
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) ───────────────────────────────────────────────
|
package/sinain-agent/run.sh
CHANGED
|
@@ -49,20 +49,20 @@ invoke_agent() {
|
|
|
49
49
|
local prompt="$1"
|
|
50
50
|
case "$AGENT" in
|
|
51
51
|
claude)
|
|
52
|
-
claude --
|
|
52
|
+
claude --enable-auto-mode \
|
|
53
53
|
--mcp-config "$MCP_CONFIG" \
|
|
54
54
|
--max-turns 5 --output-format text \
|
|
55
|
-
-p "$prompt"
|
|
55
|
+
-p "$prompt"
|
|
56
56
|
;;
|
|
57
57
|
codex)
|
|
58
58
|
codex exec -s danger-full-access \
|
|
59
|
-
"$prompt"
|
|
59
|
+
"$prompt"
|
|
60
60
|
;;
|
|
61
61
|
junie)
|
|
62
62
|
if $JUNIE_HAS_MCP; then
|
|
63
63
|
junie --output-format text \
|
|
64
64
|
--mcp-location "$JUNIE_MCP_DIR" \
|
|
65
|
-
--task "$prompt"
|
|
65
|
+
--task "$prompt"
|
|
66
66
|
else
|
|
67
67
|
return 1
|
|
68
68
|
fi
|
|
@@ -70,7 +70,7 @@ invoke_agent() {
|
|
|
70
70
|
goose)
|
|
71
71
|
goose run --text "$prompt" \
|
|
72
72
|
--output-format text \
|
|
73
|
-
--max-turns 10
|
|
73
|
+
--max-turns 10
|
|
74
74
|
;;
|
|
75
75
|
aider)
|
|
76
76
|
# No MCP support — signal pipe mode
|
|
@@ -104,10 +104,10 @@ invoke_pipe() {
|
|
|
104
104
|
local msg="$1"
|
|
105
105
|
case "$AGENT" in
|
|
106
106
|
junie)
|
|
107
|
-
junie --output-format text --task "$msg"
|
|
107
|
+
junie --output-format text --task "$msg"
|
|
108
108
|
;;
|
|
109
109
|
aider)
|
|
110
|
-
aider --yes -m "$msg"
|
|
110
|
+
aider --yes -m "$msg"
|
|
111
111
|
;;
|
|
112
112
|
*)
|
|
113
113
|
# Generic: pipe message to stdin
|
|
@@ -131,7 +131,9 @@ if [ "$AGENT" = "junie" ]; then
|
|
|
131
131
|
if junie --help 2>&1 | grep -q "mcp-location"; then
|
|
132
132
|
JUNIE_HAS_MCP=true
|
|
133
133
|
mkdir -p "$JUNIE_MCP_DIR"
|
|
134
|
-
|
|
134
|
+
# Junie expects relative paths from the config file location.
|
|
135
|
+
# Since we moved the config into a sub-directory, we need to adjust ../ to ../../
|
|
136
|
+
sed 's|"\.\./|"../../|g' "$MCP_CONFIG" > "$JUNIE_MCP_DIR/mcp.json"
|
|
135
137
|
else
|
|
136
138
|
echo "NOTE: junie $(junie --version 2>&1 | grep -oE '[0-9.]+' | head -1) lacks --mcp-location, using pipe mode"
|
|
137
139
|
echo " Upgrade junie for MCP support: brew upgrade junie"
|
|
@@ -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
|
}
|
|
@@ -7,6 +7,9 @@ import { PRESETS } from "./privacy/presets.js";
|
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
|
|
10
|
+
/** The .env file path that was actually loaded (if any). */
|
|
11
|
+
export let loadedEnvPath: string | undefined;
|
|
12
|
+
|
|
10
13
|
function loadDotEnv(): void {
|
|
11
14
|
// Try sinain-core/.env first, then project root .env
|
|
12
15
|
const candidates = [
|
|
@@ -35,6 +38,7 @@ function loadDotEnv(): void {
|
|
|
35
38
|
process.env[key] = val;
|
|
36
39
|
}
|
|
37
40
|
}
|
|
41
|
+
loadedEnvPath = envPath;
|
|
38
42
|
console.log(`[config] loaded ${envPath}`);
|
|
39
43
|
return;
|
|
40
44
|
} catch { /* ignore */ }
|
|
@@ -52,7 +52,7 @@ export class Escalator {
|
|
|
52
52
|
private slot: EscalationSlot;
|
|
53
53
|
private httpPending: HttpPendingEscalation | null = null;
|
|
54
54
|
|
|
55
|
-
private lastEscalationTs =
|
|
55
|
+
private lastEscalationTs = 0;
|
|
56
56
|
private lastEscalatedDigest = "";
|
|
57
57
|
|
|
58
58
|
// Spawn deduplication state
|
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 {
|
|
@@ -139,6 +145,7 @@ async function main() {
|
|
|
139
145
|
escalator.pushSituationMd(content);
|
|
140
146
|
},
|
|
141
147
|
onHudUpdate: (text) => {
|
|
148
|
+
wsHandler.broadcastRaw({ type: "thinking", active: false } as any);
|
|
142
149
|
wsHandler.broadcast(text, "normal", "stream");
|
|
143
150
|
},
|
|
144
151
|
onTraceStart: tracer ? (tickId) => {
|
|
@@ -156,6 +163,13 @@ async function main() {
|
|
|
156
163
|
} : undefined,
|
|
157
164
|
traitEngine,
|
|
158
165
|
traitLogDir: config.traitConfig.logDir,
|
|
166
|
+
getKnowledgeDocPath: () => {
|
|
167
|
+
const workspace = resolveWorkspace();
|
|
168
|
+
const p = `${workspace}/memory/sinain-knowledge.md`;
|
|
169
|
+
try { if (existsSync(p)) return p; } catch {}
|
|
170
|
+
return null;
|
|
171
|
+
},
|
|
172
|
+
feedbackStore: feedbackStore ?? undefined,
|
|
159
173
|
});
|
|
160
174
|
|
|
161
175
|
// ── Wire learning signal collector (needs agentLoop) ──
|
|
@@ -400,13 +414,13 @@ async function main() {
|
|
|
400
414
|
|
|
401
415
|
// Knowledge graph integration
|
|
402
416
|
getKnowledgeDocPath: () => {
|
|
403
|
-
const workspace =
|
|
417
|
+
const workspace = resolveWorkspace();
|
|
404
418
|
const p = `${workspace}/memory/sinain-knowledge.md`;
|
|
405
419
|
try { if (existsSync(p)) return p; } catch {}
|
|
406
420
|
return null;
|
|
407
421
|
},
|
|
408
422
|
queryKnowledgeFacts: async (entities: string[], maxFacts: number) => {
|
|
409
|
-
const workspace =
|
|
423
|
+
const workspace = resolveWorkspace();
|
|
410
424
|
const dbPath = `${workspace}/memory/knowledge-graph.db`;
|
|
411
425
|
const scriptPath = `${workspace}/sinain-memory/graph_query.py`;
|
|
412
426
|
try {
|
|
@@ -43,6 +43,17 @@ export function setupCommands(deps: CommandDeps): void {
|
|
|
43
43
|
}
|
|
44
44
|
case "user_command": {
|
|
45
45
|
log(TAG, `user command received: "${msg.text.slice(0, 60)}"`);
|
|
46
|
+
// Echo user message to all overlay clients as a feed item
|
|
47
|
+
wsHandler.broadcastRaw({
|
|
48
|
+
type: "feed",
|
|
49
|
+
text: msg.text,
|
|
50
|
+
priority: "normal",
|
|
51
|
+
ts: Date.now(),
|
|
52
|
+
channel: "agent",
|
|
53
|
+
sender: "user",
|
|
54
|
+
} as any);
|
|
55
|
+
// Show thinking indicator
|
|
56
|
+
wsHandler.broadcastRaw({ type: "thinking", active: true } as any);
|
|
46
57
|
deps.onUserCommand(msg.text);
|
|
47
58
|
break;
|
|
48
59
|
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
FeedChannel,
|
|
12
12
|
} from "../types.js";
|
|
13
13
|
import { log, warn } from "../log.js";
|
|
14
|
+
import { loadedEnvPath } from "../config.js";
|
|
14
15
|
|
|
15
16
|
const TAG = "ws";
|
|
16
17
|
const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
@@ -135,13 +136,14 @@ export class WsHandler {
|
|
|
135
136
|
|
|
136
137
|
/** Send a status update to all connected overlays. */
|
|
137
138
|
broadcastStatus(): void {
|
|
138
|
-
const msg: StatusMessage = {
|
|
139
|
+
const msg: StatusMessage & { envPath?: string } = {
|
|
139
140
|
type: "status",
|
|
140
141
|
audio: this.state.audio,
|
|
141
142
|
mic: this.state.mic,
|
|
142
143
|
screen: this.state.screen,
|
|
143
144
|
connection: this.state.connection,
|
|
144
145
|
};
|
|
146
|
+
if (loadedEnvPath) msg.envPath = loadedEnvPath;
|
|
145
147
|
this.broadcastMessage(msg);
|
|
146
148
|
}
|
|
147
149
|
|