@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/index.ts +163 -1257
- package/install.js +2 -1
- package/package.json +4 -2
- package/sinain-knowledge/adapters/generic/adapter.ts +103 -0
- package/sinain-knowledge/adapters/interface.ts +72 -0
- package/sinain-knowledge/adapters/openclaw/adapter.ts +223 -0
- package/sinain-knowledge/curation/engine.ts +493 -0
- package/sinain-knowledge/curation/resilience.ts +336 -0
- package/sinain-knowledge/data/git-store.ts +310 -0
- package/sinain-knowledge/data/schema.ts +89 -0
- package/sinain-knowledge/data/snapshot.ts +226 -0
- package/sinain-knowledge/data/store.ts +488 -0
- package/sinain-knowledge/deploy/cli.ts +214 -0
- package/sinain-knowledge/deploy/manifest.ts +80 -0
- package/sinain-knowledge/protocol/bindings/generic.md +5 -0
- package/sinain-knowledge/protocol/bindings/openclaw.md +5 -0
- package/sinain-knowledge/protocol/heartbeat.md +62 -0
- package/sinain-knowledge/protocol/renderer.ts +56 -0
- package/sinain-knowledge/protocol/skill.md +335 -0
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
|
|
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.
|
|
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
|
+
}
|