@inetafrica/open-claudia 2.14.8 → 3.0.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 +30 -4
- package/CHANGELOG.md +21 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +105 -7
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +127 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/core/web-sessions.js +78 -0
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +51 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-delivery-contract.js +88 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-run-lock.js +63 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/test-web-sessions.js +74 -0
- package/web.js +159 -45
package/core/config.js
CHANGED
|
@@ -3,21 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
-
const {
|
|
7
|
-
const
|
|
6
|
+
const { resolveConfigDir, ensureConfigDir } = require("./config-dir");
|
|
7
|
+
const { atomicWriteFileSync } = require("./fsutil");
|
|
8
8
|
const { truthy: configTruthy } = require("../project-transcripts");
|
|
9
9
|
|
|
10
10
|
const BOT_DIR = path.resolve(__dirname, "..");
|
|
11
|
+
const CONFIG_DIR = resolveConfigDir();
|
|
11
12
|
const ENV_PATH = path.join(CONFIG_DIR, ".env");
|
|
12
13
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
console.error("No .env file found. Run: node setup.js");
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
const lines = fs.readFileSync(ENV_PATH, "utf-8").split("\n");
|
|
20
|
-
const env = { ...process.env };
|
|
14
|
+
function parseEnvText(text) {
|
|
15
|
+
const env = {};
|
|
16
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
21
17
|
for (const line of lines) {
|
|
22
18
|
const idx = line.indexOf("=");
|
|
23
19
|
if (idx > 0) env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
@@ -25,8 +21,16 @@ function loadEnv() {
|
|
|
25
21
|
return env;
|
|
26
22
|
}
|
|
27
23
|
|
|
24
|
+
function loadEnv(envPath = ENV_PATH, processEnvironment = process.env) {
|
|
25
|
+
const fileEnv = fs.existsSync(envPath)
|
|
26
|
+
? parseEnvText(fs.readFileSync(envPath, "utf-8"))
|
|
27
|
+
: {};
|
|
28
|
+
return { ...processEnvironment, ...fileEnv };
|
|
29
|
+
}
|
|
30
|
+
|
|
28
31
|
function saveEnvKey(key, value) {
|
|
29
|
-
|
|
32
|
+
ensureConfigDir(CONFIG_DIR);
|
|
33
|
+
const content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf-8") : "";
|
|
30
34
|
const lines = content.split("\n");
|
|
31
35
|
let found = false;
|
|
32
36
|
const updated = lines.map((line) => {
|
|
@@ -34,7 +38,7 @@ function saveEnvKey(key, value) {
|
|
|
34
38
|
return line;
|
|
35
39
|
});
|
|
36
40
|
if (!found) updated.push(`${key}=${value}`);
|
|
37
|
-
|
|
41
|
+
atomicWriteFileSync(ENV_PATH, updated.join("\n"), { backup: true, mode: 0o600 });
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
const config = loadEnv();
|
|
@@ -43,42 +47,73 @@ function hasPathSeparator(value) {
|
|
|
43
47
|
return value.includes("/") || value.includes("\\");
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const PROVIDER_COMMANDS = Object.freeze({ claude: "claude", codex: "codex" });
|
|
51
|
+
const DEFAULT_PROVIDER = String(config.DEFAULT_PROVIDER || "").trim().toLowerCase() || null;
|
|
52
|
+
const UTILITY_PROVIDER = String(config.UTILITY_PROVIDER || "active").trim().toLowerCase() || "active";
|
|
53
|
+
const PROVIDER_FALLBACKS = Object.freeze(String(config.PROVIDER_FALLBACKS || "")
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((value) => value.trim().toLowerCase())
|
|
56
|
+
.filter(Boolean));
|
|
57
|
+
const UTILITY_PURPOSE_PROVIDERS = Object.freeze({
|
|
58
|
+
dream: String(config.DREAM_PROVIDER || "").trim().toLowerCase() || null,
|
|
59
|
+
introspection: String(config.DREAM_PROVIDER || "").trim().toLowerCase() || null,
|
|
60
|
+
review: String(config.MEMORY_PROVIDER || "").trim().toLowerCase() || null,
|
|
61
|
+
recall: String(config.RECALL_PROVIDER || "").trim().toLowerCase() || null,
|
|
62
|
+
enforcer: String(config.ENFORCER_PROVIDER || "").trim().toLowerCase() || null,
|
|
63
|
+
subagent: String(config.SUBAGENT_PROVIDER || "").trim().toLowerCase() || null,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function configuredProviderPath(providerId) {
|
|
67
|
+
if (providerId === "claude") return String(config.CLAUDE_PATH || "").trim() || null;
|
|
68
|
+
if (providerId === "codex") return String(config.CODEX_PATH || "").trim() || null;
|
|
69
|
+
throw Object.assign(new Error(`Unknown provider: ${providerId}`), { code: "UNKNOWN_PROVIDER" });
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
function executableCandidate(command, env = process.env) {
|
|
73
|
+
if (!command) return null;
|
|
74
|
+
if (path.isAbsolute(command) || hasPathSeparator(command)) {
|
|
75
|
+
try {
|
|
76
|
+
fs.accessSync(command, fs.constants.X_OK);
|
|
77
|
+
return path.resolve(command);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return null;
|
|
60
80
|
}
|
|
61
|
-
return null;
|
|
62
81
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
process.
|
|
82
|
+
const suffixes = process.platform === "win32"
|
|
83
|
+
? String(env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")
|
|
84
|
+
: [""];
|
|
85
|
+
for (const directory of String(env.PATH || "").split(path.delimiter).filter(Boolean)) {
|
|
86
|
+
for (const suffix of suffixes) {
|
|
87
|
+
const candidate = path.join(directory, process.platform === "win32" ? command + suffix : command);
|
|
88
|
+
try {
|
|
89
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
90
|
+
return candidate;
|
|
91
|
+
} catch (error) { /* continue */ }
|
|
69
92
|
}
|
|
70
|
-
return null;
|
|
71
93
|
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
72
96
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
function resolveExecutablePath(configured, fallbackCommand, _label, _opts = {}) {
|
|
98
|
+
const raw = (configured || fallbackCommand || "").trim();
|
|
99
|
+
return raw ? executableCandidate(raw) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function discoverProviderExecutable(providerId, env = process.env) {
|
|
103
|
+
const configured = configuredProviderPath(providerId);
|
|
104
|
+
return executableCandidate(configured || PROVIDER_COMMANDS[providerId], env);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function providerExecutableStatus(providerId, env = process.env) {
|
|
108
|
+
const configured = configuredProviderPath(providerId);
|
|
109
|
+
const executable = discoverProviderExecutable(providerId, env);
|
|
110
|
+
if (executable) {
|
|
111
|
+
return { state: "available", executable, configured: !!configured, reason: null };
|
|
112
|
+
}
|
|
113
|
+
if (configured) {
|
|
114
|
+
return { state: "missing", executable: null, configured: true, reason: `Configured ${providerId} executable was not found` };
|
|
81
115
|
}
|
|
116
|
+
return { state: "unconfigured", executable: null, configured: false, reason: `${providerId} is not configured or installed` };
|
|
82
117
|
}
|
|
83
118
|
|
|
84
119
|
// Telegram (kept for backwards compat — channels list defaults to telegram)
|
|
@@ -86,11 +121,9 @@ const TOKEN = config.TELEGRAM_BOT_TOKEN;
|
|
|
86
121
|
const CHAT_IDS = (config.TELEGRAM_CHAT_ID || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
87
122
|
const CHAT_ID = CHAT_IDS[0];
|
|
88
123
|
|
|
89
|
-
const WORKSPACE = config.WORKSPACE;
|
|
90
|
-
const CLAUDE_PATH =
|
|
91
|
-
const CURSOR_PATH = config.CURSOR_PATH || null;
|
|
124
|
+
const WORKSPACE = config.WORKSPACE || null;
|
|
125
|
+
const CLAUDE_PATH = configuredProviderPath("claude");
|
|
92
126
|
const CODEX_PATH = config.CODEX_PATH || null;
|
|
93
|
-
const DEFAULT_CLAUDE_MODEL = config.CLAUDE_MODEL || process.env.CLAUDE_MODEL || "claude-opus-4-8";
|
|
94
127
|
const AUTO_COMPACT_TOKENS = parseInt(config.AUTO_COMPACT_TOKENS || process.env.AUTO_COMPACT_TOKENS || "380000", 10);
|
|
95
128
|
const MIN_COMPACT_INTERVAL_MS = parseInt(config.MIN_COMPACT_INTERVAL_MS || process.env.MIN_COMPACT_INTERVAL_MS || "1800000", 10);
|
|
96
129
|
const PROJECT_TRANSCRIPTS = configTruthy(config.PROJECT_TRANSCRIPTS || process.env.PROJECT_TRANSCRIPTS, true);
|
|
@@ -129,31 +162,22 @@ const MAX_VOICE_SIZE = 10 * 1024 * 1024;
|
|
|
129
162
|
const MAX_PROCESS_TIMEOUT = 360 * 60 * 1000;
|
|
130
163
|
const COMPACT_SUMMARY_TIMEOUT = 10 * 60 * 1000;
|
|
131
164
|
|
|
132
|
-
|
|
165
|
+
// Compatibility values for the pre-registry runner. They contain only an
|
|
166
|
+
// explicitly configured command; fallback PATH discovery remains lazy through
|
|
167
|
+
// discoverProviderExecutable().
|
|
168
|
+
const resolvedCodexPath = CODEX_PATH;
|
|
133
169
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} catch (e) {
|
|
139
|
-
console.error(`Failed to create workspace: ${e.message}`);
|
|
140
|
-
process.exit(1);
|
|
170
|
+
function ensureRuntimeDirectories() {
|
|
171
|
+
ensureConfigDir(CONFIG_DIR);
|
|
172
|
+
for (const directory of [WORKSPACE, TEMP_DIR, FILES_DIR]) {
|
|
173
|
+
if (directory) fs.mkdirSync(directory, { recursive: true });
|
|
141
174
|
}
|
|
175
|
+
return { configDir: CONFIG_DIR, workspace: WORKSPACE, tempDir: TEMP_DIR, filesDir: FILES_DIR };
|
|
142
176
|
}
|
|
143
177
|
|
|
144
|
-
let resolvedCursorPath = resolveExecutablePath(CURSOR_PATH, "agent", "Cursor Agent CLI");
|
|
145
|
-
if (resolvedCursorPath) console.error(`Cursor Agent CLI: ${resolvedCursorPath}`);
|
|
146
|
-
|
|
147
|
-
let resolvedCodexPath = resolveExecutablePath(CODEX_PATH, "codex", "Codex CLI");
|
|
148
|
-
if (resolvedCodexPath) console.error(`Codex CLI: ${resolvedCodexPath}`);
|
|
149
|
-
|
|
150
|
-
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
151
|
-
if (!fs.existsSync(FILES_DIR)) fs.mkdirSync(FILES_DIR, { recursive: true });
|
|
152
|
-
|
|
153
178
|
const FULL_PATH = [
|
|
154
|
-
path.dirname(CLAUDE_PATH),
|
|
155
|
-
|
|
156
|
-
resolvedCodexPath ? path.dirname(resolvedCodexPath) : null,
|
|
179
|
+
CLAUDE_PATH && hasPathSeparator(CLAUDE_PATH) ? path.dirname(CLAUDE_PATH) : null,
|
|
180
|
+
resolvedCodexPath && hasPathSeparator(resolvedCodexPath) ? path.dirname(resolvedCodexPath) : null,
|
|
157
181
|
path.dirname(process.execPath),
|
|
158
182
|
FFMPEG ? path.dirname(FFMPEG) : null,
|
|
159
183
|
WHISPER_CLI ? path.dirname(WHISPER_CLI) : null,
|
|
@@ -161,7 +185,8 @@ const FULL_PATH = [
|
|
|
161
185
|
? [process.env.APPDATA, process.env.LOCALAPPDATA].filter(Boolean).map((p) => path.join(p, "npm"))
|
|
162
186
|
: ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"]
|
|
163
187
|
),
|
|
164
|
-
|
|
188
|
+
...String(process.env.PATH || "").split(path.delimiter),
|
|
189
|
+
].filter(Boolean).filter((value, index, values) => values.indexOf(value) === index).join(path.delimiter);
|
|
165
190
|
|
|
166
191
|
// Kazee runs on one fixed host — like Telegram's api.telegram.org it's
|
|
167
192
|
// infrastructure, not a per-user setting. Hard-coded so onboarding only asks
|
|
@@ -229,30 +254,63 @@ function loadChannels() {
|
|
|
229
254
|
return channels;
|
|
230
255
|
}
|
|
231
256
|
|
|
257
|
+
// The bot's own control-plane secrets. No subprocess the bot spawns — the
|
|
258
|
+
// Claude/Codex CLIs, sub-agents, the recall walker, tools, and auth flows —
|
|
259
|
+
// ever needs these: the model CLI is authed via CLAUDE_CODE_OAUTH_TOKEN (added
|
|
260
|
+
// on top by claudeSubprocessEnv) or its own API key, and `open-claudia` CLI
|
|
261
|
+
// invocations re-read the .env file from disk. Stripping them from the ambient
|
|
262
|
+
// subprocess env closes the easiest exfil path — a prompt-injected agent (or any
|
|
263
|
+
// child) dumping its own environment can no longer read the bot's Telegram/Kazee
|
|
264
|
+
// tokens or the web control token.
|
|
265
|
+
const CONFINED_SECRET_KEYS = ["TELEGRAM_BOT_TOKEN", "KAZEE_BOT_TOKEN", "BOT_CONTROL_TOKEN"];
|
|
266
|
+
|
|
232
267
|
function botSubprocessEnv() {
|
|
233
268
|
// Credential confinement (5b): the operational keyring is deliberately NOT
|
|
234
269
|
// merged here. Agent shells, sub-agents, and every other bot subprocess run
|
|
235
270
|
// cred-free; keyring values enter ONLY tool-run subprocesses (tools.runEnv —
|
|
236
271
|
// the keys a tool declares via --requires) and the logged escape hatch
|
|
237
272
|
// (`keyring exec --reason`). See core/tool-guard.js for the deny-gate half.
|
|
238
|
-
|
|
273
|
+
const env = { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() };
|
|
274
|
+
for (const k of CONFINED_SECRET_KEYS) delete env[k];
|
|
275
|
+
return env;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Source material for provider-specific allowlist builders. This may contain
|
|
279
|
+
// sensitive config values; core/providers/env.js always reduces and scrubs it
|
|
280
|
+
// before a provider process receives anything.
|
|
281
|
+
function agentEnvSource() {
|
|
282
|
+
return {
|
|
283
|
+
...process.env,
|
|
284
|
+
...config,
|
|
285
|
+
PATH: FULL_PATH,
|
|
286
|
+
HOME: process.env.HOME || require("os").homedir(),
|
|
287
|
+
};
|
|
239
288
|
}
|
|
240
289
|
|
|
241
290
|
module.exports = {
|
|
242
291
|
config,
|
|
292
|
+
parseEnvText,
|
|
293
|
+
loadEnv,
|
|
243
294
|
saveEnvKey,
|
|
244
295
|
loadChannels,
|
|
245
296
|
botSubprocessEnv,
|
|
297
|
+
agentEnvSource,
|
|
298
|
+
configuredProviderPath,
|
|
299
|
+
discoverProviderExecutable,
|
|
300
|
+
providerExecutableStatus,
|
|
301
|
+
resolveExecutablePath,
|
|
302
|
+
DEFAULT_PROVIDER,
|
|
303
|
+
UTILITY_PROVIDER,
|
|
304
|
+
PROVIDER_FALLBACKS,
|
|
305
|
+
UTILITY_PURPOSE_PROVIDERS,
|
|
306
|
+
ensureRuntimeDirectories,
|
|
246
307
|
KAZEE_DEFAULT_URL,
|
|
247
308
|
BOT_DIR,
|
|
248
309
|
CONFIG_DIR,
|
|
249
310
|
TOKEN, CHAT_IDS, CHAT_ID,
|
|
250
311
|
WORKSPACE,
|
|
251
312
|
CLAUDE_PATH,
|
|
252
|
-
DEFAULT_CLAUDE_MODEL,
|
|
253
|
-
CURSOR_PATH,
|
|
254
313
|
CODEX_PATH,
|
|
255
|
-
resolvedCursorPath,
|
|
256
314
|
resolvedCodexPath,
|
|
257
315
|
AUTO_COMPACT_TOKENS,
|
|
258
316
|
MIN_COMPACT_INTERVAL_MS,
|
package/core/doctor.js
CHANGED
|
@@ -2,40 +2,21 @@
|
|
|
2
2
|
// before the user spends time on a task that's going to fail anyway.
|
|
3
3
|
|
|
4
4
|
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
5
6
|
const path = require("path");
|
|
6
|
-
const {
|
|
7
|
+
const { execFileSync } = require("child_process");
|
|
7
8
|
const {
|
|
8
|
-
CLAUDE_PATH, CURSOR_PATH, CODEX_PATH, resolvedCursorPath, resolvedCodexPath,
|
|
9
9
|
WHISPER_CLI, WHISPER_MODEL, FFMPEG, WORKSPACE, CONFIG_DIR,
|
|
10
10
|
botSubprocessEnv,
|
|
11
11
|
} = require("./config");
|
|
12
|
-
const { redactSensitive
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
function shellQuote(value) {
|
|
16
|
-
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function runCommandForDoctor(command, args = [], opts = {}) {
|
|
20
|
-
try {
|
|
21
|
-
const out = execSync([command, ...args].map(shellQuote).join(" "), {
|
|
22
|
-
cwd: opts.cwd || process.env.HOME || require("os").homedir(),
|
|
23
|
-
env: opts.env || botSubprocessEnv(),
|
|
24
|
-
encoding: "utf-8",
|
|
25
|
-
timeout: opts.timeout || 10000,
|
|
26
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
-
});
|
|
28
|
-
return { ok: true, output: out.trim(), code: 0 };
|
|
29
|
-
} catch (e) {
|
|
30
|
-
return { ok: false, output: `${e.stdout || ""}\n${e.stderr || ""}\n${e.message || ""}`.trim(), code: e.status ?? 1 };
|
|
31
|
-
}
|
|
32
|
-
}
|
|
12
|
+
const { redactSensitive } = require("./redact");
|
|
13
|
+
const { inspectProviderConfiguration } = require("./provider-status");
|
|
33
14
|
|
|
34
15
|
function binaryCheck(binPath, name) {
|
|
35
16
|
if (!binPath) return { ok: false, label: name, detail: "not configured/found" };
|
|
36
17
|
try {
|
|
37
18
|
if (fs.existsSync(binPath)) fs.accessSync(binPath, fs.constants.X_OK);
|
|
38
|
-
else
|
|
19
|
+
else execFileSync(process.platform === "win32" ? "where" : "which", [String(binPath)], { stdio: "ignore", env: botSubprocessEnv() });
|
|
39
20
|
return { ok: true, label: name, detail: binPath };
|
|
40
21
|
} catch (e) {
|
|
41
22
|
return { ok: false, label: name, detail: `not executable: ${binPath}` };
|
|
@@ -55,46 +36,119 @@ function checkWritableDir(dirPath, label) {
|
|
|
55
36
|
}
|
|
56
37
|
}
|
|
57
38
|
|
|
58
|
-
function
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
39
|
+
function defaultCodexHookProbe({ binary }) {
|
|
40
|
+
const { prepareCodexHook, probeCodexHookCompatibility } = require("./providers/codex-hook");
|
|
41
|
+
const { codexSubprocessEnv } = require("./auth-flow");
|
|
42
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-hook-probe-"));
|
|
43
|
+
try {
|
|
44
|
+
return probeCodexHookCompatibility({
|
|
45
|
+
binary,
|
|
46
|
+
prepared: prepareCodexHook({ cwd: temp }),
|
|
47
|
+
cwd: temp,
|
|
48
|
+
markerFile: path.join(temp, "probe.marker"),
|
|
49
|
+
auditFile: path.join(temp, "probe-audit.jsonl"),
|
|
50
|
+
env: codexSubprocessEnv(),
|
|
51
|
+
// A real model turn, not a fixture: allow for slow first-token latency.
|
|
52
|
+
timeoutMs: 45000,
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
56
|
+
}
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
function
|
|
59
|
+
function codexHookEnforcementCheck(providerSummary, options) {
|
|
60
|
+
const label = "Codex tool-hook enforcement (live probe)";
|
|
61
|
+
const codex = providerSummary.providers.find((provider) => provider.id === "codex");
|
|
62
|
+
const ready = codex
|
|
63
|
+
&& codex.availability.state === "available"
|
|
64
|
+
&& codex.compatibility.state === "compatible"
|
|
65
|
+
&& codex.auth.state === "authenticated";
|
|
66
|
+
if (!ready) {
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
warn: true,
|
|
70
|
+
label,
|
|
71
|
+
detail: codex
|
|
72
|
+
? `skipped — codex not ready (${codex.availability.state}/${codex.compatibility.state}/${codex.auth.state})`
|
|
73
|
+
: "skipped — codex is not registered",
|
|
74
|
+
action: "",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const runProbe = options.runCodexHookProbe || defaultCodexHookProbe;
|
|
78
|
+
try {
|
|
79
|
+
const probe = runProbe({ binary: codex.availability.executable });
|
|
80
|
+
return probe.ok
|
|
81
|
+
? { ok: true, label, detail: "deny rule enforced end-to-end against the real CLI" }
|
|
82
|
+
: {
|
|
83
|
+
ok: false,
|
|
84
|
+
label,
|
|
85
|
+
detail: redactSensitive(probe.diagnostic || "probe failed"),
|
|
86
|
+
action: "Do not run Codex tasks until this passes — update the Codex CLI and re-run /doctor.",
|
|
87
|
+
};
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
label,
|
|
92
|
+
detail: redactSensitive(e.message),
|
|
93
|
+
action: "Do not run Codex tasks until this passes — update the Codex CLI and re-run /doctor.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function runDoctorChecks(options = {}) {
|
|
65
99
|
const checks = [];
|
|
66
100
|
const nodeMajor = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
67
101
|
checks.push({ ok: nodeMajor >= 18, label: "Node.js", detail: process.version, action: nodeMajor >= 18 ? "" : "Install Node.js 18+." });
|
|
68
102
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
const providerSummary = inspectProviderConfiguration({
|
|
104
|
+
registry: options.registry,
|
|
105
|
+
executableStatus: options.executableStatus,
|
|
106
|
+
probeAuth: options.probeAuth !== false,
|
|
107
|
+
});
|
|
108
|
+
for (const provider of providerSummary.providers) {
|
|
109
|
+
const available = provider.availability.state === "available";
|
|
110
|
+
const compatible = provider.compatibility.state === "compatible";
|
|
111
|
+
const authenticated = provider.auth.state === "authenticated";
|
|
112
|
+
checks.push({
|
|
113
|
+
ok: available,
|
|
114
|
+
warn: !available,
|
|
115
|
+
label: `${provider.label} availability`,
|
|
116
|
+
detail: available ? provider.availability.executable : provider.availability.state,
|
|
117
|
+
action: available ? "" : provider.recovery,
|
|
118
|
+
});
|
|
119
|
+
checks.push({
|
|
120
|
+
ok: available && compatible,
|
|
121
|
+
warn: !available || !compatible,
|
|
122
|
+
label: `${provider.label} compatibility`,
|
|
123
|
+
detail: provider.compatibility.version
|
|
124
|
+
? `${provider.compatibility.state} (${provider.compatibility.version})`
|
|
125
|
+
: provider.compatibility.state,
|
|
126
|
+
action: available && !compatible ? provider.recovery : "",
|
|
127
|
+
});
|
|
128
|
+
checks.push({
|
|
129
|
+
ok: available && compatible && authenticated,
|
|
130
|
+
warn: !available || !compatible || !authenticated,
|
|
131
|
+
label: `${provider.label} auth`,
|
|
132
|
+
detail: provider.auth.state,
|
|
133
|
+
action: available && compatible && !authenticated ? provider.recovery : "",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// The live hook probe drives a real `codex exec` model turn (tokens,
|
|
137
|
+
// seconds), so it is strictly opt-in: only user-invoked /doctor passes it.
|
|
138
|
+
// Boot, health, and setup surfaces must never spawn a model prompt.
|
|
139
|
+
if (options.probeCodexHook === true) {
|
|
140
|
+
checks.push(codexHookEnforcementCheck(providerSummary, options));
|
|
141
|
+
}
|
|
87
142
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} else checks.push({ ok: true, warn: true, label: "Codex CLI", detail: "not configured/found (optional)", action: "Install only if you want /codex." });
|
|
143
|
+
checks.push({
|
|
144
|
+
ok: !!providerSummary.defaultProvider.providerId,
|
|
145
|
+
warn: !providerSummary.defaultProvider.providerId,
|
|
146
|
+
label: "Default provider",
|
|
147
|
+
detail: providerSummary.defaultProvider.label
|
|
148
|
+
? `${providerSummary.defaultProvider.label} (${providerSummary.defaultProvider.source})`
|
|
149
|
+
: "none",
|
|
150
|
+
action: providerSummary.defaultProvider.providerId ? "" : providerSummary.defaultProvider.reason,
|
|
151
|
+
});
|
|
98
152
|
|
|
99
153
|
if (FFMPEG || WHISPER_CLI || WHISPER_MODEL) {
|
|
100
154
|
const ff = binaryCheck(FFMPEG || "ffmpeg", "ffmpeg");
|