@geravant/sinain 1.0.18 → 1.0.19

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/install.js CHANGED
@@ -120,7 +120,8 @@ async function installNemoClaw({ sandboxName }) {
120
120
  cfg.agents.defaults ??= {};
121
121
  cfg.agents.defaults.sandbox ??= {};
122
122
  cfg.agents.defaults.sandbox.sessionToolsVisibility = "all";
123
- // NemoClaw: gateway bind/auth and compaction are managed by OpenShell — do not overwrite them
123
+ // NemoClaw: gateway bind/auth are managed by OpenShell — but compaction must be set explicitly
124
+ cfg.compaction = { mode: "safeguard", maxHistoryShare: 0.2, reserveTokensFloor: 40000 };
124
125
 
125
126
  const token = cfg.gateway?.auth?.token ?? "(see sandbox openclaw.json)";
126
127
 
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "sinain OpenClaw plugin — AI overlay for macOS",
5
5
  "type": "module",
6
6
  "bin": {
7
- "sinain": "./install.js"
7
+ "sinain": "./install.js",
8
+ "sinain-knowledge": "./sinain-knowledge/deploy/cli.ts"
8
9
  },
9
10
  "scripts": {
10
11
  "postinstall": "node install.js"
@@ -14,6 +15,7 @@
14
15
  "openclaw.plugin.json",
15
16
  "install.js",
16
17
  "sinain-memory",
18
+ "sinain-knowledge",
17
19
  "HEARTBEAT.md",
18
20
  "SKILL.md"
19
21
  ],
@@ -0,0 +1,103 @@
1
+ /**
2
+ * sinain-knowledge — Generic BackendAdapter
3
+ *
4
+ * Runs the knowledge system without OpenClaw — uses child_process for script
5
+ * execution, a configurable workspace path, and console logging for alerts.
6
+ */
7
+
8
+ import { execFile } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+
12
+ import type { ScriptResult } from "../../data/schema.js";
13
+ import type { BackendAdapter, BackendCapabilities, ScriptOptions } from "../interface.js";
14
+
15
+ // ============================================================================
16
+ // GenericAdapter
17
+ // ============================================================================
18
+
19
+ export class GenericAdapter implements BackendAdapter {
20
+ readonly name = "generic";
21
+ readonly capabilities: BackendCapabilities = {
22
+ hasHeartbeatTool: false,
23
+ hasRPC: false,
24
+ hasSessionHistory: false,
25
+ hasTranscriptAccess: false,
26
+ supportsPython: true,
27
+ hasTelegramAlerts: false,
28
+ };
29
+
30
+ constructor(private workspaceDir: string) {}
31
+
32
+ getWorkspaceDir(): string | null {
33
+ return this.workspaceDir;
34
+ }
35
+
36
+ async runScript(args: string[], opts: ScriptOptions): Promise<ScriptResult> {
37
+ // Try uv first, fall back to python3
38
+ const [cmd, ...rest] = this._resolveCommand(args);
39
+
40
+ return new Promise((resolve, reject) => {
41
+ const child = execFile(cmd, rest, {
42
+ cwd: opts.cwd,
43
+ timeout: opts.timeoutMs,
44
+ maxBuffer: 10 * 1024 * 1024,
45
+ }, (error, stdout, stderr) => {
46
+ if (error && (error as any).killed) {
47
+ reject(new Error(`Command timed out after ${opts.timeoutMs}ms`));
48
+ return;
49
+ }
50
+ resolve({
51
+ code: error ? (error as any).code ?? 1 : 0,
52
+ stdout: stdout ?? "",
53
+ stderr: stderr ?? "",
54
+ });
55
+ });
56
+ });
57
+ }
58
+
59
+ resolvePath(configPath: string): string {
60
+ return resolve(configPath);
61
+ }
62
+
63
+ // No session management in generic mode
64
+
65
+ // Alerts go to console
66
+ async sendAlert(type: string, title: string, body: string): Promise<void> {
67
+ console.log(`[sinain-alert:${type}] ${title}\n${body}`);
68
+ }
69
+
70
+ async initialize(): Promise<void> {
71
+ // Detect python availability
72
+ try {
73
+ await this.runScript(["python3", "--version"], { timeoutMs: 5000, cwd: this.workspaceDir });
74
+ } catch {
75
+ console.warn("sinain-knowledge: python3 not found — some features will be unavailable");
76
+ }
77
+ }
78
+
79
+ dispose(): void {
80
+ // Nothing to clean up
81
+ }
82
+
83
+ // ── Internal ──────────────────────────────────────────────────────────
84
+
85
+ private _resolveCommand(args: string[]): string[] {
86
+ // If args starts with "uv", try to use it, otherwise strip the uv prefix
87
+ if (args[0] === "uv") {
88
+ try {
89
+ // Check if uv is available
90
+ require("node:child_process").execFileSync("uv", ["--version"], { timeout: 3000 });
91
+ return args;
92
+ } catch {
93
+ // Strip "uv run --with <pkg>" prefix → just run python3 directly
94
+ const pythonIdx = args.indexOf("python3");
95
+ if (pythonIdx >= 0) {
96
+ return args.slice(pythonIdx);
97
+ }
98
+ return args;
99
+ }
100
+ }
101
+ return args;
102
+ }
103
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * sinain-knowledge — BackendAdapter interface
3
+ *
4
+ * Abstracts the runtime environment (OpenClaw, generic CLI, etc.) so the
5
+ * knowledge system (KnowledgeStore + CurationEngine) can run on any backend.
6
+ */
7
+
8
+ import type { ScriptResult } from "../data/schema.js";
9
+
10
+ // ============================================================================
11
+ // Capabilities
12
+ // ============================================================================
13
+
14
+ export interface BackendCapabilities {
15
+ hasHeartbeatTool: boolean;
16
+ hasRPC: boolean;
17
+ hasSessionHistory: boolean;
18
+ hasTranscriptAccess: boolean;
19
+ supportsPython: boolean;
20
+ hasTelegramAlerts: boolean;
21
+ }
22
+
23
+ // ============================================================================
24
+ // Script execution options
25
+ // ============================================================================
26
+
27
+ export interface ScriptOptions {
28
+ timeoutMs: number;
29
+ cwd: string;
30
+ }
31
+
32
+ // ============================================================================
33
+ // BackendAdapter
34
+ // ============================================================================
35
+
36
+ export interface BackendAdapter {
37
+ readonly name: string;
38
+ readonly capabilities: BackendCapabilities;
39
+
40
+ /** Get the current workspace directory, if available. */
41
+ getWorkspaceDir(): string | null;
42
+
43
+ /** Execute a command and return its result. */
44
+ runScript(args: string[], opts: ScriptOptions): Promise<ScriptResult>;
45
+
46
+ /** Resolve a config-relative path to an absolute path. */
47
+ resolvePath(configPath: string): string;
48
+
49
+ // ── Session management (optional) ─────────────────────────────────────
50
+
51
+ /** Path to sessions.json, if accessible. */
52
+ getSessionsJsonPath?(): string | null;
53
+
54
+ /** Get transcript file size for overflow detection. */
55
+ getTranscriptSize?(): { path: string; bytes: number } | null;
56
+
57
+ /** Truncate and archive the current session transcript. */
58
+ performTranscriptReset?(): boolean;
59
+
60
+ // ── Alerts (optional) ─────────────────────────────────────────────────
61
+
62
+ /** Send an alert notification (Telegram, webhook, etc.). */
63
+ sendAlert?(type: string, title: string, body: string): Promise<void>;
64
+
65
+ // ── Lifecycle ─────────────────────────────────────────────────────────
66
+
67
+ /** Initialize the adapter (called once at plugin startup). */
68
+ initialize(): Promise<void>;
69
+
70
+ /** Dispose resources (called at plugin shutdown). */
71
+ dispose(): void;
72
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * sinain-knowledge — OpenClaw BackendAdapter
3
+ *
4
+ * Wraps the OpenClaw plugin API to implement the BackendAdapter interface.
5
+ * Handles script execution (via uv), session file access, transcript management,
6
+ * and Telegram alerts using the bot token from openclaw.json.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, existsSync, statSync, copyFileSync } from "node:fs";
10
+ import { join, dirname } from "node:path";
11
+
12
+ import type { ScriptResult } from "../../data/schema.js";
13
+ import type { BackendAdapter, BackendCapabilities, ScriptOptions } from "../interface.js";
14
+
15
+ // ============================================================================
16
+ // Telegram alert helpers (self-contained within the adapter)
17
+ // ============================================================================
18
+
19
+ const ALERT_COOLDOWN_MS = 15 * 60_000;
20
+ const OVERFLOW_TRANSCRIPT_MIN_BYTES = 1_000_000;
21
+
22
+ let _cachedBotToken: string | null | undefined;
23
+ let _alertMissingConfigLogged = false;
24
+ const _alertCooldowns = new Map<string, number>();
25
+
26
+ function readBotToken(stateDir: string): string | null {
27
+ if (_cachedBotToken !== undefined) return _cachedBotToken;
28
+ try {
29
+ const openclawJson = join(stateDir, "openclaw.json");
30
+ const raw = JSON.parse(readFileSync(openclawJson, "utf-8"));
31
+ const token = raw?.channels?.telegram?.botToken ?? raw?.telegram?.botToken ?? null;
32
+ _cachedBotToken = typeof token === "string" && token.length > 10 ? token : null;
33
+ } catch {
34
+ _cachedBotToken = null;
35
+ }
36
+ return _cachedBotToken;
37
+ }
38
+
39
+ // ============================================================================
40
+ // OpenClawAdapter
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Minimal type for the OpenClaw plugin API.
45
+ * Only the methods we actually use — avoids importing the full SDK type.
46
+ */
47
+ export interface OpenClawApi {
48
+ pluginConfig: unknown;
49
+ config: unknown;
50
+ logger: { info(msg: string): void; warn(msg: string): void };
51
+ resolvePath(configPath: string): string;
52
+ runtime: {
53
+ system: {
54
+ runCommandWithTimeout(
55
+ args: string[],
56
+ opts: { timeoutMs: number; cwd: string },
57
+ ): Promise<ScriptResult>;
58
+ };
59
+ };
60
+ }
61
+
62
+ export class OpenClawAdapter implements BackendAdapter {
63
+ readonly name = "openclaw";
64
+ readonly capabilities: BackendCapabilities = {
65
+ hasHeartbeatTool: true,
66
+ hasRPC: true,
67
+ hasSessionHistory: true,
68
+ hasTranscriptAccess: true,
69
+ supportsPython: true,
70
+ hasTelegramAlerts: true,
71
+ };
72
+
73
+ private workspaceDir: string | null = null;
74
+
75
+ constructor(
76
+ private api: OpenClawApi,
77
+ private sessionKey: string | undefined,
78
+ ) {}
79
+
80
+ getWorkspaceDir(): string | null {
81
+ return this.workspaceDir;
82
+ }
83
+
84
+ setWorkspaceDir(dir: string): void {
85
+ this.workspaceDir = dir;
86
+ }
87
+
88
+ async runScript(args: string[], opts: ScriptOptions): Promise<ScriptResult> {
89
+ return this.api.runtime.system.runCommandWithTimeout(args, opts);
90
+ }
91
+
92
+ resolvePath(configPath: string): string {
93
+ return this.api.resolvePath(configPath);
94
+ }
95
+
96
+ // ── Session management ────────────────────────────────────────────────
97
+
98
+ getSessionsJsonPath(): string | null {
99
+ if (!this.workspaceDir) return null;
100
+ const sessionsDir = join(dirname(this.workspaceDir), "agents", "main", "sessions");
101
+ const p = join(sessionsDir, "sessions.json");
102
+ return existsSync(p) ? p : null;
103
+ }
104
+
105
+ getTranscriptSize(): { path: string; bytes: number } | null {
106
+ const sessionsJsonPath = this.getSessionsJsonPath();
107
+ if (!sessionsJsonPath || !this.sessionKey) return null;
108
+ try {
109
+ const sessionsData = JSON.parse(readFileSync(sessionsJsonPath, "utf-8"));
110
+ const session = sessionsData[this.sessionKey];
111
+ const transcriptPath = session?.sessionFile as string | undefined;
112
+ if (!transcriptPath || !existsSync(transcriptPath)) return null;
113
+ return { path: transcriptPath, bytes: statSync(transcriptPath).size };
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ performTranscriptReset(): boolean {
120
+ if (!this.sessionKey || !this.workspaceDir) {
121
+ this.api.logger.warn("sinain-hud: overflow reset aborted — no sessionKey or workspace dir");
122
+ return false;
123
+ }
124
+ const sessionsJsonPath = this.getSessionsJsonPath();
125
+ if (!sessionsJsonPath) {
126
+ this.api.logger.warn("sinain-hud: overflow reset aborted — sessions.json not found");
127
+ return false;
128
+ }
129
+ let sessionsData: Record<string, Record<string, unknown>>;
130
+ try {
131
+ sessionsData = JSON.parse(readFileSync(sessionsJsonPath, "utf-8"));
132
+ } catch (err) {
133
+ this.api.logger.warn(`sinain-hud: overflow reset aborted — cannot parse sessions.json: ${err}`);
134
+ return false;
135
+ }
136
+ const session = sessionsData[this.sessionKey];
137
+ const transcriptPath = session?.sessionFile as string | undefined;
138
+ if (!transcriptPath || !existsSync(transcriptPath)) {
139
+ this.api.logger.warn(`sinain-hud: overflow reset aborted — transcript not found: ${transcriptPath}`);
140
+ return false;
141
+ }
142
+ const size = statSync(transcriptPath).size;
143
+ if (size < OVERFLOW_TRANSCRIPT_MIN_BYTES) {
144
+ this.api.logger.info(
145
+ `sinain-hud: overflow reset skipped — transcript only ${Math.round(size / 1024)}KB (threshold: ${Math.round(OVERFLOW_TRANSCRIPT_MIN_BYTES / 1024)}KB)`,
146
+ );
147
+ return false;
148
+ }
149
+ const archivePath = transcriptPath.replace(/\.jsonl$/, `.archived.${Date.now()}.jsonl`);
150
+ try {
151
+ copyFileSync(transcriptPath, archivePath);
152
+ } catch (err) {
153
+ this.api.logger.warn(`sinain-hud: overflow reset aborted — archive failed: ${err}`);
154
+ return false;
155
+ }
156
+ writeFileSync(transcriptPath, "", "utf-8");
157
+ try {
158
+ session.contextTokens = 0;
159
+ writeFileSync(sessionsJsonPath, JSON.stringify(sessionsData, null, 2), "utf-8");
160
+ } catch {}
161
+ this.api.logger.info(
162
+ `sinain-hud: === OVERFLOW RESET === Transcript truncated (was ${Math.round(size / 1024)}KB). Archive: ${archivePath}`,
163
+ );
164
+ return true;
165
+ }
166
+
167
+ // ── Alerts ────────────────────────────────────────────────────────────
168
+
169
+ async sendAlert(alertType: string, title: string, body: string): Promise<void> {
170
+ const stateDir = this.getStateDir();
171
+ if (!stateDir) return;
172
+
173
+ const chatId = process.env.SINAIN_ALERT_CHAT_ID;
174
+ const token = readBotToken(stateDir);
175
+
176
+ if (!chatId || !token) {
177
+ if (!_alertMissingConfigLogged) {
178
+ _alertMissingConfigLogged = true;
179
+ console.log("sinain-hud: Telegram alerts disabled (missing SINAIN_ALERT_CHAT_ID or bot token)");
180
+ }
181
+ return;
182
+ }
183
+
184
+ const lastSent = _alertCooldowns.get(alertType) ?? 0;
185
+ if (Date.now() - lastSent < ALERT_COOLDOWN_MS) return;
186
+ _alertCooldowns.set(alertType, Date.now());
187
+
188
+ const text = `${title}\n${body}`;
189
+ fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json" },
192
+ body: JSON.stringify({ chat_id: chatId, text, parse_mode: "Markdown" }),
193
+ }).catch(() => {});
194
+ }
195
+
196
+ // ── Lifecycle ─────────────────────────────────────────────────────────
197
+
198
+ async initialize(): Promise<void> {
199
+ // Pre-initialize workspace from config
200
+ const cfgWorkspace = (this.api.config as any).agents?.defaults?.workspace as string | undefined;
201
+ if (cfgWorkspace && existsSync(cfgWorkspace)) {
202
+ this.workspaceDir = cfgWorkspace;
203
+ this.api.logger.info(`sinain-hud: workspace pre-initialized from config: ${this.workspaceDir}`);
204
+ }
205
+ }
206
+
207
+ dispose(): void {
208
+ // Nothing to clean up
209
+ }
210
+
211
+ resetAlertState(): void {
212
+ _alertCooldowns.clear();
213
+ _cachedBotToken = undefined;
214
+ _alertMissingConfigLogged = false;
215
+ }
216
+
217
+ // ── Internal helpers ──────────────────────────────────────────────────
218
+
219
+ private getStateDir(): string | null {
220
+ if (!this.workspaceDir) return null;
221
+ return dirname(this.workspaceDir);
222
+ }
223
+ }