@inetafrica/open-claudia 2.15.0 → 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 +12 -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 +66 -3
- 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 +115 -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 +21 -4
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +42 -6
- 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 +1414 -1290
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1084 -97
- 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/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 +48 -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-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-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/web.js +130 -35
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 };
|
|
81
112
|
}
|
|
113
|
+
if (configured) {
|
|
114
|
+
return { state: "missing", executable: null, configured: true, reason: `Configured ${providerId} executable was not found` };
|
|
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
|
|
@@ -230,7 +255,7 @@ function loadChannels() {
|
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
// The bot's own control-plane secrets. No subprocess the bot spawns — the
|
|
233
|
-
// Claude/Codex
|
|
258
|
+
// Claude/Codex CLIs, sub-agents, the recall walker, tools, and auth flows —
|
|
234
259
|
// ever needs these: the model CLI is authed via CLAUDE_CODE_OAUTH_TOKEN (added
|
|
235
260
|
// on top by claudeSubprocessEnv) or its own API key, and `open-claudia` CLI
|
|
236
261
|
// invocations re-read the .env file from disk. Stripping them from the ambient
|
|
@@ -250,21 +275,42 @@ function botSubprocessEnv() {
|
|
|
250
275
|
return env;
|
|
251
276
|
}
|
|
252
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
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
253
290
|
module.exports = {
|
|
254
291
|
config,
|
|
292
|
+
parseEnvText,
|
|
293
|
+
loadEnv,
|
|
255
294
|
saveEnvKey,
|
|
256
295
|
loadChannels,
|
|
257
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,
|
|
258
307
|
KAZEE_DEFAULT_URL,
|
|
259
308
|
BOT_DIR,
|
|
260
309
|
CONFIG_DIR,
|
|
261
310
|
TOKEN, CHAT_IDS, CHAT_ID,
|
|
262
311
|
WORKSPACE,
|
|
263
312
|
CLAUDE_PATH,
|
|
264
|
-
DEFAULT_CLAUDE_MODEL,
|
|
265
|
-
CURSOR_PATH,
|
|
266
313
|
CODEX_PATH,
|
|
267
|
-
resolvedCursorPath,
|
|
268
314
|
resolvedCodexPath,
|
|
269
315
|
AUTO_COMPACT_TOKENS,
|
|
270
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");
|