@geravant/sinain 1.8.0 → 1.10.0
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/.env.example +14 -13
- package/HEARTBEAT.md +1 -1
- package/README.md +4 -7
- package/cli.js +16 -2
- package/config-shared.js +469 -0
- package/config.js +152 -0
- package/index.ts +1 -3
- package/launcher.js +7 -1
- package/onboard.js +345 -0
- package/package.json +8 -2
- package/sense_client/__main__.py +8 -4
- package/sense_client/gate.py +1 -0
- package/sense_client/ocr.py +58 -25
- package/sense_client/sender.py +2 -0
- package/sense_client/vision.py +31 -11
- package/sinain-agent/CLAUDE.md +0 -1
- package/sinain-agent/run.sh +2 -1
- package/sinain-core/src/agent/analyzer.ts +56 -58
- package/sinain-core/src/agent/loop.ts +37 -11
- package/sinain-core/src/audio/transcription.ts +20 -5
- package/sinain-core/src/config.ts +20 -16
- package/sinain-core/src/cost/tracker.ts +64 -0
- package/sinain-core/src/escalation/escalator.ts +31 -59
- package/sinain-core/src/index.ts +41 -45
- package/sinain-core/src/overlay/commands.ts +12 -0
- package/sinain-core/src/overlay/ws-handler.ts +27 -0
- package/sinain-core/src/server.ts +41 -0
- package/sinain-core/src/types.ts +46 -11
- package/sinain-knowledge/curation/engine.ts +0 -17
- package/sinain-knowledge/protocol/heartbeat.md +1 -1
- package/sinain-mcp-server/index.ts +4 -20
- package/sinain-memory/git_backup.sh +0 -19
package/config.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sinain config — interactive section-based config editor.
|
|
4
|
+
* Edit individual settings without re-running the full onboard wizard.
|
|
5
|
+
*/
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import {
|
|
8
|
+
c, guard, readEnv, writeEnv, summarizeConfig, runHealthCheck,
|
|
9
|
+
stepApiKey, stepTranscription, stepGateway, stepPrivacy, stepModel, stepAgent,
|
|
10
|
+
ENV_PATH, IS_WINDOWS, HOME, PKG_DIR,
|
|
11
|
+
} from "./config-shared.js";
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
|
|
15
|
+
// ── Section definitions ────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const SECTIONS = [
|
|
18
|
+
{ value: "apikey", label: "API Key", hint: "OpenRouter API key" },
|
|
19
|
+
{ value: "transcription", label: "Transcription", hint: "Cloud or local whisper" },
|
|
20
|
+
{ value: "model", label: "Model", hint: "AI model for analysis" },
|
|
21
|
+
{ value: "privacy", label: "Privacy", hint: "Standard / strict / paranoid" },
|
|
22
|
+
{ value: "gateway", label: "Gateway", hint: "OpenClaw connection" },
|
|
23
|
+
{ value: "agent", label: "Agent", hint: "Claude / Codex / Goose / ..." },
|
|
24
|
+
{ value: "health", label: "Health check", hint: "Test core + gateway status" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ── Section handlers ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
async function runSection(section, existing) {
|
|
30
|
+
switch (section) {
|
|
31
|
+
case "apikey": {
|
|
32
|
+
const key = await stepApiKey(existing);
|
|
33
|
+
return { OPENROUTER_API_KEY: key };
|
|
34
|
+
}
|
|
35
|
+
case "transcription": {
|
|
36
|
+
const backend = await stepTranscription(existing);
|
|
37
|
+
const vars = { TRANSCRIPTION_BACKEND: backend };
|
|
38
|
+
if (backend === "local") {
|
|
39
|
+
const modelDir = path.join(HOME, "models");
|
|
40
|
+
const modelPath = path.join(modelDir, "ggml-large-v3-turbo.bin");
|
|
41
|
+
if (fs.existsSync(modelPath)) {
|
|
42
|
+
vars.LOCAL_WHISPER_MODEL = modelPath;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return vars;
|
|
46
|
+
}
|
|
47
|
+
case "model": {
|
|
48
|
+
const model = await stepModel(existing);
|
|
49
|
+
return { AGENT_MODEL: model };
|
|
50
|
+
}
|
|
51
|
+
case "privacy": {
|
|
52
|
+
const mode = await stepPrivacy(existing);
|
|
53
|
+
return { PRIVACY_MODE: mode };
|
|
54
|
+
}
|
|
55
|
+
case "gateway": {
|
|
56
|
+
return await stepGateway(existing);
|
|
57
|
+
}
|
|
58
|
+
case "agent": {
|
|
59
|
+
const agent = await stepAgent(existing);
|
|
60
|
+
return { SINAIN_AGENT: agent };
|
|
61
|
+
}
|
|
62
|
+
case "health": {
|
|
63
|
+
await runHealthCheck();
|
|
64
|
+
return null; // no vars to write
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── List command ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function printConfigList(existing) {
|
|
72
|
+
const lines = summarizeConfig(existing);
|
|
73
|
+
if (lines.length === 0) {
|
|
74
|
+
console.log(c.dim(" No config found. Run: sinain onboard"));
|
|
75
|
+
} else {
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(c.bold(" Current config") + c.dim(` (${ENV_PATH})`));
|
|
78
|
+
console.log();
|
|
79
|
+
for (const line of lines) console.log(` ${line}`);
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
async function runConfigWizard(sectionsFilter) {
|
|
87
|
+
p.intro("sinain config");
|
|
88
|
+
|
|
89
|
+
const existing = readEnv();
|
|
90
|
+
if (Object.keys(existing).length === 0) {
|
|
91
|
+
p.log.warn("No config found. Run sinain onboard first.");
|
|
92
|
+
p.outro("sinain onboard");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
p.note(summarizeConfig(existing).join("\n"), "Current config");
|
|
97
|
+
|
|
98
|
+
// If specific sections requested via --sections, run just those
|
|
99
|
+
if (sectionsFilter && sectionsFilter.length > 0) {
|
|
100
|
+
for (const section of sectionsFilter) {
|
|
101
|
+
const vars = await runSection(section, existing);
|
|
102
|
+
if (vars) {
|
|
103
|
+
Object.assign(existing, vars);
|
|
104
|
+
writeEnv(existing);
|
|
105
|
+
p.log.success(`${section} updated.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
p.outro("Done.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Interactive loop
|
|
113
|
+
while (true) {
|
|
114
|
+
const choice = guard(await p.select({
|
|
115
|
+
message: "What to configure?",
|
|
116
|
+
options: [
|
|
117
|
+
...SECTIONS,
|
|
118
|
+
{ value: "__done", label: "Done", hint: "Save and exit" },
|
|
119
|
+
],
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
if (choice === "__done") break;
|
|
123
|
+
|
|
124
|
+
const vars = await runSection(choice, existing);
|
|
125
|
+
if (vars) {
|
|
126
|
+
Object.assign(existing, vars);
|
|
127
|
+
writeEnv(existing);
|
|
128
|
+
p.log.success(`${choice} updated.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
p.outro("Config saved.");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── CLI entry point ────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
const args = process.argv.slice(3); // skip node, cli.js, "config"
|
|
138
|
+
|
|
139
|
+
if (args[0] === "list" || args[0] === "ls") {
|
|
140
|
+
printConfigList(readEnv());
|
|
141
|
+
} else {
|
|
142
|
+
let sectionsFilter = null;
|
|
143
|
+
for (const arg of args) {
|
|
144
|
+
if (arg.startsWith("--sections=")) {
|
|
145
|
+
sectionsFilter = arg.slice(11).split(",").filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
runConfigWizard(sectionsFilter).catch((err) => {
|
|
149
|
+
console.error(c.red(` Error: ${err.message}`));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|
|
152
|
+
}
|
package/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Strips <private> tags from tool results before persistence
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync,
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, copyFileSync } from "node:fs";
|
|
12
12
|
import { join, dirname } from "node:path";
|
|
13
13
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
14
14
|
|
|
@@ -358,8 +358,6 @@ export default function sinainHudPlugin(api: OpenClawPluginApi): void {
|
|
|
358
358
|
const memorySource = cfg.memoryPath ? api.resolvePath(cfg.memoryPath) : undefined;
|
|
359
359
|
if (memorySource) {
|
|
360
360
|
store.deployDir(memorySource, "sinain-memory");
|
|
361
|
-
const gbPath = join(workspaceDir, "sinain-memory", "git_backup.sh");
|
|
362
|
-
if (existsSync(gbPath)) try { chmodSync(gbPath, 0o755); } catch {}
|
|
363
361
|
}
|
|
364
362
|
|
|
365
363
|
const modulesSource = cfg.modulesPath ? api.resolvePath(cfg.modulesPath) : undefined;
|
package/launcher.js
CHANGED
|
@@ -685,7 +685,13 @@ function loadUserEnv() {
|
|
|
685
685
|
const eq = trimmed.indexOf("=");
|
|
686
686
|
if (eq === -1) continue;
|
|
687
687
|
const key = trimmed.slice(0, eq).trim();
|
|
688
|
-
|
|
688
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
689
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
690
|
+
val = val.slice(1, -1);
|
|
691
|
+
} else {
|
|
692
|
+
const ci = val.search(/\s+#/);
|
|
693
|
+
if (ci !== -1) val = val.slice(0, ci).trimEnd();
|
|
694
|
+
}
|
|
689
695
|
// Don't override existing env vars
|
|
690
696
|
if (!process.env[key]) {
|
|
691
697
|
process.env[key] = val;
|
package/onboard.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sinain onboard — interactive setup wizard using @clack/prompts
|
|
4
|
+
* Modeled after `openclaw onboard` for a familiar, polished experience.
|
|
5
|
+
*/
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { execFileSync } from "child_process";
|
|
10
|
+
import {
|
|
11
|
+
c, guard, maskKey, readEnv, writeEnv, summarizeConfig, runHealthCheck,
|
|
12
|
+
stepApiKey, stepTranscription, stepGateway, stepPrivacy, stepModel,
|
|
13
|
+
HOME, SINAIN_DIR, ENV_PATH, PKG_DIR, IS_WINDOWS, IS_MAC,
|
|
14
|
+
} from "./config-shared.js";
|
|
15
|
+
|
|
16
|
+
// ── Header ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function printHeader() {
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(c.bold(" ┌─────────────────────────────────────────┐"));
|
|
21
|
+
console.log(c.bold(" │ │"));
|
|
22
|
+
console.log(c.bold(" │") + c.cyan(" ╔═╗╦╔╗╔╔═╗╦╔╗╔ ╦ ╦╦ ╦╔╦╗ ") + c.bold("│"));
|
|
23
|
+
console.log(c.bold(" │") + c.cyan(" ╚═╗║║║║╠═╣║║║║ ╠═╣║ ║ ║║ ") + c.bold("│"));
|
|
24
|
+
console.log(c.bold(" │") + c.cyan(" ╚═╝╩╝╚╝╩ ╩╩╝╚╝ ╩ ╩╚═╝═╩╝ ") + c.bold("│"));
|
|
25
|
+
console.log(c.bold(" │") + c.dim(" Privacy-first AI overlay ") + c.bold("│"));
|
|
26
|
+
console.log(c.bold(" │ │"));
|
|
27
|
+
console.log(c.bold(" └─────────────────────────────────────────┘"));
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Steps (imported from config-shared.js) ──────────────────────────────────
|
|
32
|
+
// stepApiKey, stepTranscription, stepGateway, stepPrivacy, stepModel
|
|
33
|
+
// are imported above and accept an optional label parameter.
|
|
34
|
+
|
|
35
|
+
async function stepOverlay(existing) {
|
|
36
|
+
// Check if overlay is already installed
|
|
37
|
+
const overlayPaths = [
|
|
38
|
+
path.join(SINAIN_DIR, "overlay", "SinainHUD.app"),
|
|
39
|
+
path.join(SINAIN_DIR, "overlay", "sinain_hud.exe"),
|
|
40
|
+
];
|
|
41
|
+
const overlayInstalled = overlayPaths.some((p) => fs.existsSync(p));
|
|
42
|
+
|
|
43
|
+
const choice = guard(await p.select({
|
|
44
|
+
message: "Install overlay",
|
|
45
|
+
options: [
|
|
46
|
+
{
|
|
47
|
+
value: "download",
|
|
48
|
+
label: "Download pre-built app (recommended)",
|
|
49
|
+
hint: "No Flutter SDK needed",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
value: "source",
|
|
53
|
+
label: "Build from source",
|
|
54
|
+
hint: "Requires Flutter SDK",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: "skip",
|
|
58
|
+
label: overlayInstalled ? "Skip (already installed)" : "Skip for now",
|
|
59
|
+
hint: overlayInstalled ? "SinainHUD.app detected" : "Install later: sinain setup-overlay",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
initialValue: overlayInstalled ? "skip" : "download",
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
if (choice === "download" || choice === "source") {
|
|
66
|
+
const s = p.spinner();
|
|
67
|
+
const label = choice === "download" ? "Downloading overlay..." : "Building overlay from source...";
|
|
68
|
+
s.start(label);
|
|
69
|
+
try {
|
|
70
|
+
// setup-overlay.js handles both modes via process.argv
|
|
71
|
+
if (choice === "source") process.argv.push("--from-source");
|
|
72
|
+
await import("./setup-overlay.js");
|
|
73
|
+
s.stop(c.green("Overlay installed."));
|
|
74
|
+
} catch (err) {
|
|
75
|
+
s.stop(c.yellow(`Failed: ${err.message}`));
|
|
76
|
+
p.note("Install manually: sinain setup-overlay", "Overlay");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export async function runOnboard(args = {}) {
|
|
84
|
+
printHeader();
|
|
85
|
+
p.intro("SinainHUD setup");
|
|
86
|
+
|
|
87
|
+
const existing = readEnv(ENV_PATH);
|
|
88
|
+
const hasExisting = Object.keys(existing).length > 0;
|
|
89
|
+
|
|
90
|
+
// ── Existing config handling ────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
let configAction = "fresh";
|
|
93
|
+
if (hasExisting) {
|
|
94
|
+
p.note(summarizeConfig(existing).join("\n"), "Existing config detected");
|
|
95
|
+
|
|
96
|
+
configAction = guard(await p.select({
|
|
97
|
+
message: "Config handling",
|
|
98
|
+
options: [
|
|
99
|
+
{ value: "keep", label: "Use existing values" },
|
|
100
|
+
{ value: "update", label: "Update values" },
|
|
101
|
+
{ value: "reset", label: "Reset (start fresh)" },
|
|
102
|
+
],
|
|
103
|
+
initialValue: "keep",
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
if (configAction === "keep") {
|
|
107
|
+
p.log.success("Using existing configuration.");
|
|
108
|
+
await stepOverlay(existing);
|
|
109
|
+
await runHealthCheck();
|
|
110
|
+
printOutro();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (configAction === "reset") {
|
|
115
|
+
fs.unlinkSync(ENV_PATH);
|
|
116
|
+
p.log.info("Config reset.");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const base = configAction === "update" ? existing : {};
|
|
121
|
+
|
|
122
|
+
// ── Flow selection ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
const flow = args.flow || guard(await p.select({
|
|
125
|
+
message: "Setup mode",
|
|
126
|
+
options: [
|
|
127
|
+
{
|
|
128
|
+
value: "quickstart",
|
|
129
|
+
label: "QuickStart",
|
|
130
|
+
hint: "Get running in 2 minutes. Configure details later.",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
value: "advanced",
|
|
134
|
+
label: "Advanced",
|
|
135
|
+
hint: "Full control over privacy, models, and connections.",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
initialValue: "quickstart",
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
const totalSteps = flow === "quickstart" ? 2 : 5;
|
|
142
|
+
|
|
143
|
+
// ── Collect vars ────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const vars = { ...base };
|
|
146
|
+
|
|
147
|
+
// Step 1: API key (both flows)
|
|
148
|
+
const apiKey = await stepApiKey(base, `[1/${totalSteps}] OpenRouter API key`);
|
|
149
|
+
vars.OPENROUTER_API_KEY = apiKey;
|
|
150
|
+
p.log.success("API key saved.");
|
|
151
|
+
|
|
152
|
+
if (flow === "quickstart") {
|
|
153
|
+
// QuickStart: sensible defaults
|
|
154
|
+
vars.TRANSCRIPTION_BACKEND = base.TRANSCRIPTION_BACKEND || "openrouter";
|
|
155
|
+
vars.PRIVACY_MODE = base.PRIVACY_MODE || "standard";
|
|
156
|
+
vars.AGENT_MODEL = base.AGENT_MODEL || "google/gemini-2.5-flash-lite";
|
|
157
|
+
vars.ESCALATION_MODE = base.ESCALATION_MODE || "off";
|
|
158
|
+
vars.SINAIN_AGENT = base.SINAIN_AGENT || "claude";
|
|
159
|
+
if (!vars.OPENCLAW_WS_URL) {
|
|
160
|
+
vars.OPENCLAW_WS_URL = "";
|
|
161
|
+
vars.OPENCLAW_HTTP_URL = "";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
p.note(
|
|
165
|
+
[
|
|
166
|
+
`Transcription: ${vars.TRANSCRIPTION_BACKEND}`,
|
|
167
|
+
`Privacy: ${vars.PRIVACY_MODE}`,
|
|
168
|
+
`Model: ${vars.AGENT_MODEL}`,
|
|
169
|
+
`Escalation: ${vars.ESCALATION_MODE}`,
|
|
170
|
+
"",
|
|
171
|
+
`Change later: sinain config`,
|
|
172
|
+
].join("\n"),
|
|
173
|
+
"QuickStart defaults",
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
// Advanced flow: steps 2-5
|
|
177
|
+
const transcription = await stepTranscription(base, "[2/5] Audio transcription");
|
|
178
|
+
vars.TRANSCRIPTION_BACKEND = transcription;
|
|
179
|
+
p.log.success(`Using ${transcription === "openrouter" ? "cloud" : "local"} transcription.`);
|
|
180
|
+
|
|
181
|
+
if (transcription === "local") {
|
|
182
|
+
const modelDir = path.join(HOME, "models");
|
|
183
|
+
const modelPath = path.join(modelDir, "ggml-large-v3-turbo.bin");
|
|
184
|
+
if (!fs.existsSync(modelPath)) {
|
|
185
|
+
const download = guard(await p.confirm({
|
|
186
|
+
message: "Download Whisper model (~1.5 GB)?",
|
|
187
|
+
initialValue: true,
|
|
188
|
+
}));
|
|
189
|
+
if (download) {
|
|
190
|
+
const s = p.spinner();
|
|
191
|
+
s.start("Downloading Whisper model...");
|
|
192
|
+
try {
|
|
193
|
+
fs.mkdirSync(modelDir, { recursive: true });
|
|
194
|
+
execFileSync("curl", [
|
|
195
|
+
"-L", "--progress-bar",
|
|
196
|
+
"-o", modelPath,
|
|
197
|
+
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin",
|
|
198
|
+
], { stdio: "inherit" });
|
|
199
|
+
s.stop(c.green("Model downloaded."));
|
|
200
|
+
vars.LOCAL_WHISPER_MODEL = modelPath;
|
|
201
|
+
} catch {
|
|
202
|
+
s.stop(c.yellow("Download failed. You can download manually later."));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
vars.LOCAL_WHISPER_MODEL = modelPath;
|
|
207
|
+
p.log.info(`Whisper model found: ${c.dim(modelPath)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const gatewayVars = await stepGateway(base, "[3/5] OpenClaw gateway");
|
|
212
|
+
Object.assign(vars, gatewayVars);
|
|
213
|
+
if (gatewayVars.ESCALATION_MODE === "off") {
|
|
214
|
+
p.log.info("Standalone mode (no gateway).");
|
|
215
|
+
} else {
|
|
216
|
+
p.log.success("Gateway configured.");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const privacy = await stepPrivacy(base, "[4/5] Privacy mode");
|
|
220
|
+
vars.PRIVACY_MODE = privacy;
|
|
221
|
+
p.log.success(`Privacy: ${privacy}.`);
|
|
222
|
+
|
|
223
|
+
const model = await stepModel(base, "[5/5] AI model for HUD analysis");
|
|
224
|
+
vars.AGENT_MODEL = model;
|
|
225
|
+
p.log.success(`Model: ${model}.`);
|
|
226
|
+
|
|
227
|
+
vars.SINAIN_AGENT = base.SINAIN_AGENT || "claude";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Common defaults ───────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
vars.SINAIN_CORE_URL = vars.SINAIN_CORE_URL || "http://localhost:9500";
|
|
233
|
+
vars.SINAIN_POLL_INTERVAL = vars.SINAIN_POLL_INTERVAL || "5";
|
|
234
|
+
vars.SINAIN_HEARTBEAT_INTERVAL = vars.SINAIN_HEARTBEAT_INTERVAL || "900";
|
|
235
|
+
vars.AUDIO_CAPTURE_CMD = vars.AUDIO_CAPTURE_CMD || "screencapturekit";
|
|
236
|
+
vars.AUDIO_AUTO_START = vars.AUDIO_AUTO_START || "true";
|
|
237
|
+
vars.PORT = vars.PORT || "9500";
|
|
238
|
+
|
|
239
|
+
// ── Write config ──────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
const s = p.spinner();
|
|
242
|
+
s.start("Writing configuration...");
|
|
243
|
+
writeEnv(vars);
|
|
244
|
+
s.stop(c.green(`Config saved: ${c.dim(ENV_PATH)}`));
|
|
245
|
+
|
|
246
|
+
// ── Overlay ───────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
await stepOverlay(base);
|
|
249
|
+
|
|
250
|
+
// ── Health check ──────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
await runHealthCheck();
|
|
253
|
+
|
|
254
|
+
// ── What now ──────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
printOutro();
|
|
257
|
+
|
|
258
|
+
// ── Start? ────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
const startNow = guard(await p.confirm({
|
|
261
|
+
message: "Start sinain now?",
|
|
262
|
+
initialValue: true,
|
|
263
|
+
}));
|
|
264
|
+
|
|
265
|
+
if (startNow) {
|
|
266
|
+
p.outro("Launching sinain...");
|
|
267
|
+
try {
|
|
268
|
+
await import("./launcher.js");
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.log(c.yellow(` Launch failed: ${err.message}`));
|
|
271
|
+
console.log(c.dim(" Try manually: sinain start"));
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
p.outro("Run when ready: sinain start");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function printOutro() {
|
|
279
|
+
const hotkey = IS_WINDOWS ? "Ctrl+Shift" : "Cmd+Shift";
|
|
280
|
+
p.note(
|
|
281
|
+
[
|
|
282
|
+
"Hotkeys:",
|
|
283
|
+
` ${hotkey}+Space — Show/hide overlay`,
|
|
284
|
+
` ${hotkey}+E — Cycle tabs (Stream → Agent → Tasks)`,
|
|
285
|
+
` ${hotkey}+C — Toggle click-through`,
|
|
286
|
+
` ${hotkey}+M — Cycle display mode`,
|
|
287
|
+
"",
|
|
288
|
+
"Docs: https://github.com/geravant/sinain-hud",
|
|
289
|
+
"Re-run: sinain onboard (or sinain onboard --advanced)",
|
|
290
|
+
].join("\n"),
|
|
291
|
+
"What now",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── CLI entry point ─────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
const cliArgs = process.argv.slice(2);
|
|
298
|
+
const flags = {};
|
|
299
|
+
for (const arg of cliArgs) {
|
|
300
|
+
if (arg === "--advanced") flags.flow = "advanced";
|
|
301
|
+
if (arg === "--quickstart") flags.flow = "quickstart";
|
|
302
|
+
if (arg.startsWith("--key=")) flags.key = arg.slice(6);
|
|
303
|
+
if (arg === "--non-interactive") flags.nonInteractive = true;
|
|
304
|
+
if (arg === "--reset") flags.reset = true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (flags.reset) {
|
|
308
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
309
|
+
fs.unlinkSync(ENV_PATH);
|
|
310
|
+
console.log(c.green(" Config reset."));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (flags.nonInteractive) {
|
|
315
|
+
const vars = {
|
|
316
|
+
OPENROUTER_API_KEY: flags.key || process.env.OPENROUTER_API_KEY || "",
|
|
317
|
+
TRANSCRIPTION_BACKEND: "openrouter",
|
|
318
|
+
PRIVACY_MODE: "standard",
|
|
319
|
+
AGENT_MODEL: "google/gemini-2.5-flash-lite",
|
|
320
|
+
ESCALATION_MODE: "off",
|
|
321
|
+
SINAIN_AGENT: "claude",
|
|
322
|
+
OPENCLAW_WS_URL: "",
|
|
323
|
+
OPENCLAW_HTTP_URL: "",
|
|
324
|
+
PORT: "9500",
|
|
325
|
+
AUDIO_CAPTURE_CMD: "screencapturekit",
|
|
326
|
+
AUDIO_AUTO_START: "true",
|
|
327
|
+
SINAIN_CORE_URL: "http://localhost:9500",
|
|
328
|
+
SINAIN_POLL_INTERVAL: "5",
|
|
329
|
+
SINAIN_HEARTBEAT_INTERVAL: "900",
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
if (!vars.OPENROUTER_API_KEY) {
|
|
333
|
+
console.error(c.red(" --key=<key> or OPENROUTER_API_KEY required for non-interactive mode."));
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
writeEnv(vars);
|
|
338
|
+
console.log(c.green(` Config written to ${ENV_PATH}`));
|
|
339
|
+
process.exit(0);
|
|
340
|
+
} else {
|
|
341
|
+
runOnboard(flags).catch((err) => {
|
|
342
|
+
console.error(c.red(` Error: ${err.message}`));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
});
|
|
345
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geravant/sinain",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
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,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"cli.js",
|
|
17
|
+
"config.js",
|
|
18
|
+
"config-shared.js",
|
|
19
|
+
"onboard.js",
|
|
17
20
|
"launcher.js",
|
|
18
21
|
"setup-overlay.js",
|
|
19
22
|
"setup-sck-capture.js",
|
|
@@ -47,5 +50,8 @@
|
|
|
47
50
|
"./index.ts"
|
|
48
51
|
]
|
|
49
52
|
},
|
|
50
|
-
"license": "MIT"
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@clack/prompts": "^1.1.0"
|
|
56
|
+
}
|
|
51
57
|
}
|
package/sense_client/__main__.py
CHANGED
|
@@ -371,13 +371,17 @@ def main():
|
|
|
371
371
|
try:
|
|
372
372
|
from PIL import Image as PILImage
|
|
373
373
|
pil = PILImage.fromarray(frame) if isinstance(frame, np.ndarray) else frame
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
result = vision_provider.describe(pil, prompt=prompt or None)
|
|
375
|
+
scene = result.text
|
|
376
|
+
v_cost = result.cost
|
|
377
|
+
if scene or v_cost:
|
|
378
|
+
if scene:
|
|
379
|
+
log(f"vision: {scene[:80]}...")
|
|
377
380
|
ctx_ev = SenseEvent(type="context", ts=ts)
|
|
378
|
-
ctx_ev.observation = SenseObservation(scene=scene)
|
|
381
|
+
ctx_ev.observation = SenseObservation(scene=scene or "")
|
|
379
382
|
ctx_ev.meta = meta
|
|
380
383
|
ctx_ev.roi = package_full_frame(frame)
|
|
384
|
+
ctx_ev.vision_cost = v_cost
|
|
381
385
|
sender.send(ctx_ev)
|
|
382
386
|
except Exception as e:
|
|
383
387
|
log(f"vision error: {e}")
|
package/sense_client/gate.py
CHANGED
|
@@ -43,6 +43,7 @@ class SenseEvent:
|
|
|
43
43
|
diff: dict | None = None
|
|
44
44
|
meta: SenseMeta = field(default_factory=SenseMeta)
|
|
45
45
|
observation: SenseObservation = field(default_factory=SenseObservation)
|
|
46
|
+
vision_cost: dict | None = None # {cost, tokens_in, tokens_out, model}
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
class DecisionGate:
|