@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/approvals.js
CHANGED
|
@@ -18,9 +18,15 @@ const fs = require("fs");
|
|
|
18
18
|
const path = require("path");
|
|
19
19
|
const crypto = require("crypto");
|
|
20
20
|
const CONFIG_DIR = require("../config-dir");
|
|
21
|
+
const { atomicWriteFileSync } = require("./fsutil");
|
|
21
22
|
|
|
22
23
|
const APPROVALS_DIR = process.env.APPROVALS_DIR || path.join(CONFIG_DIR, "approvals");
|
|
23
24
|
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
const LOCK_STALE_MS = 30 * 1000;
|
|
26
|
+
const LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
27
|
+
// Both approval UIs render the complete command. Keeping this comfortably
|
|
28
|
+
// below chat message limits prevents an unseen suffix from being approved.
|
|
29
|
+
const MAX_COMMAND_BYTES = 512;
|
|
24
30
|
|
|
25
31
|
function fileOf(id) {
|
|
26
32
|
const base = path.basename(String(id || "").trim());
|
|
@@ -28,13 +34,71 @@ function fileOf(id) {
|
|
|
28
34
|
return path.join(APPROVALS_DIR, `${base}.json`);
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
function commandHash(command) {
|
|
38
|
+
return crypto.createHash("sha256").update(String(command || "")).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function approvedCommand(command) {
|
|
42
|
+
const value = String(command || "");
|
|
43
|
+
if (Buffer.byteLength(value) > MAX_COMMAND_BYTES) {
|
|
44
|
+
const error = new Error(`approval command exceeds ${MAX_COMMAND_BYTES} bytes`);
|
|
45
|
+
error.code = "APPROVAL_PAYLOAD_TOO_LARGE";
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function quoteArg(value) {
|
|
52
|
+
const text = String(value);
|
|
53
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) return text;
|
|
54
|
+
return `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatApprovedCommand(argv) {
|
|
58
|
+
if (!Array.isArray(argv) || argv.some((arg) => typeof arg !== "string")) {
|
|
59
|
+
const error = new TypeError("approved command argv must be an array of strings");
|
|
60
|
+
error.code = "INVALID_APPROVAL_COMMAND";
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
return argv.map(quoteArg).join(" ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeRecord(rec) {
|
|
67
|
+
atomicWriteFileSync(fileOf(rec.id), `${JSON.stringify(rec, null, 2)}\n`, { mode: 0o600 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function acquireRecordLock(id) {
|
|
71
|
+
let lock;
|
|
72
|
+
try { lock = `${fileOf(id)}.lock`; } catch (_) { return null; }
|
|
73
|
+
for (let attempt = 0; attempt < 21; attempt += 1) {
|
|
74
|
+
try { return { fd: fs.openSync(lock, "wx", 0o600), lock }; }
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== "EEXIST") return null;
|
|
77
|
+
try {
|
|
78
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
79
|
+
fs.rmSync(lock, { force: true });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
} catch (_) { continue; }
|
|
83
|
+
Atomics.wait(LOCK_WAIT, 0, 0, 5);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function releaseRecordLock(held) {
|
|
90
|
+
if (!held) return;
|
|
91
|
+
try { fs.closeSync(held.fd); } catch (_) {}
|
|
92
|
+
try { fs.rmSync(held.lock, { force: true }); } catch (_) {}
|
|
93
|
+
}
|
|
94
|
+
|
|
31
95
|
// Best-effort GC so decided/expired records don't accumulate forever.
|
|
32
96
|
function prune() {
|
|
33
97
|
let entries;
|
|
34
98
|
try { entries = fs.readdirSync(APPROVALS_DIR); } catch (e) { return; }
|
|
35
99
|
const cutoff = Date.now() - MAX_AGE_MS;
|
|
36
100
|
for (const f of entries) {
|
|
37
|
-
if (!f.endsWith(".json")) continue;
|
|
101
|
+
if (!f.endsWith(".json") && !f.endsWith(".lock")) continue;
|
|
38
102
|
const p = path.join(APPROVALS_DIR, f);
|
|
39
103
|
try { if (fs.statSync(p).mtimeMs < cutoff) fs.rmSync(p, { force: true }); } catch (e) {}
|
|
40
104
|
}
|
|
@@ -49,6 +113,8 @@ function prune() {
|
|
|
49
113
|
// `target*` external channel, and person/entity pointers for Always-allow.
|
|
50
114
|
function create({
|
|
51
115
|
tool, verb = "", tier = "destructive", command = "", channelId = "", adapter = "",
|
|
116
|
+
canonicalUserId = "", userId = "", provider = "", providerSettings = {},
|
|
117
|
+
project = "", projectDir = "", sessionId = "", previousSessionId = "", originRunId = "",
|
|
52
118
|
kind = "tool", approverAdapter = "", approverChannel = "",
|
|
53
119
|
payloadText = "", targetAdapter = "", targetChannel = "",
|
|
54
120
|
personName = "", personId = "", entitySlug = "",
|
|
@@ -56,11 +122,18 @@ function create({
|
|
|
56
122
|
fs.mkdirSync(APPROVALS_DIR, { recursive: true, mode: 0o700 });
|
|
57
123
|
prune();
|
|
58
124
|
const id = `apr_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
125
|
+
const exactCommand = approvedCommand(command);
|
|
59
126
|
const rec = {
|
|
60
127
|
id, status: "pending",
|
|
61
128
|
tool: String(tool || ""), verb: String(verb || ""), tier: String(tier || ""),
|
|
62
|
-
command:
|
|
129
|
+
command: exactCommand,
|
|
130
|
+
commandHash: commandHash(exactCommand),
|
|
63
131
|
channelId: String(channelId || ""), adapter: String(adapter || ""),
|
|
132
|
+
canonicalUserId: String(canonicalUserId || ""), userId: String(userId || ""),
|
|
133
|
+
provider: String(provider || ""), providerSettings: providerSettings && typeof providerSettings === "object" ? structuredClone(providerSettings) : {},
|
|
134
|
+
project: String(project || ""), projectDir: String(projectDir || ""),
|
|
135
|
+
sessionId: String(sessionId || ""), previousSessionId: String(previousSessionId || ""),
|
|
136
|
+
originRunId: String(originRunId || ""),
|
|
64
137
|
kind: String(kind || "tool"),
|
|
65
138
|
approverAdapter: String(approverAdapter || ""), approverChannel: String(approverChannel || ""),
|
|
66
139
|
payloadText: String(payloadText || "").slice(0, 4000),
|
|
@@ -68,7 +141,7 @@ function create({
|
|
|
68
141
|
personName: String(personName || ""), personId: String(personId || ""), entitySlug: String(entitySlug || ""),
|
|
69
142
|
requestedAt: new Date().toISOString(),
|
|
70
143
|
};
|
|
71
|
-
|
|
144
|
+
writeRecord(rec);
|
|
72
145
|
return rec;
|
|
73
146
|
}
|
|
74
147
|
|
|
@@ -80,14 +153,18 @@ function read(id) {
|
|
|
80
153
|
// Flip a pending record. Idempotence guard: a second button press on the same
|
|
81
154
|
// message must not overturn the first decision.
|
|
82
155
|
function decide(id, status, by = "") {
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
156
|
+
const held = acquireRecordLock(id);
|
|
157
|
+
if (!held) return read(id);
|
|
158
|
+
try {
|
|
159
|
+
const rec = read(id);
|
|
160
|
+
if (!rec) return null;
|
|
161
|
+
if (rec.status !== "pending") return rec;
|
|
162
|
+
rec.status = status === "approved" ? "approved" : "denied";
|
|
163
|
+
rec.decidedAt = new Date().toISOString();
|
|
164
|
+
if (by) rec.decidedBy = String(by);
|
|
165
|
+
writeRecord(rec);
|
|
166
|
+
return rec;
|
|
167
|
+
} finally { releaseRecordLock(held); }
|
|
91
168
|
}
|
|
92
169
|
|
|
93
170
|
// Poll until decided or timeout. Timeout resolves as "timeout" — the caller
|
|
@@ -107,29 +184,59 @@ async function waitForDecision(id, { timeoutMs = 90000, pollMs = 1500 } = {}) {
|
|
|
107
184
|
// actions.js uses this flag to know a later button press must wake the agent
|
|
108
185
|
// (an attached waiter reacts by itself, a detached one cannot).
|
|
109
186
|
function detach(id) {
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
187
|
+
const held = acquireRecordLock(id);
|
|
188
|
+
if (!held) return read(id);
|
|
189
|
+
try {
|
|
190
|
+
const rec = read(id);
|
|
191
|
+
if (!rec || rec.status !== "pending") return rec;
|
|
192
|
+
rec.detached = true;
|
|
193
|
+
rec.detachedAt = new Date().toISOString();
|
|
194
|
+
writeRecord(rec);
|
|
195
|
+
return rec;
|
|
196
|
+
} finally { releaseRecordLock(held); }
|
|
116
197
|
}
|
|
117
198
|
|
|
118
199
|
// One-shot token redemption for the async path. Fail-closed on every branch:
|
|
119
200
|
// only an approved, un-consumed, un-expired record whose stored command is
|
|
120
201
|
// byte-identical to what is about to run can be consumed — and only once.
|
|
121
202
|
function consume(id, commandLine) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return { ok: false, reason: "
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
203
|
+
try { fileOf(id); }
|
|
204
|
+
catch (_) { return { ok: false, reason: "unknown or expired approval id" }; }
|
|
205
|
+
const held = acquireRecordLock(id);
|
|
206
|
+
if (!held) return { ok: false, reason: "approval redemption could not be locked" };
|
|
207
|
+
try {
|
|
208
|
+
const rec = read(id);
|
|
209
|
+
if (!rec) return { ok: false, reason: "unknown or expired approval id" };
|
|
210
|
+
if (rec.status !== "approved") return { ok: false, reason: `approval is ${rec.status}, not approved` };
|
|
211
|
+
if (rec.consumedAt) return { ok: false, reason: "approval already consumed (single use)" };
|
|
212
|
+
if (Date.now() - Date.parse(rec.requestedAt) > MAX_AGE_MS) return { ok: false, reason: "approval expired (24h)" };
|
|
213
|
+
let candidate;
|
|
214
|
+
try { candidate = approvedCommand(commandLine); }
|
|
215
|
+
catch (_) { return { ok: false, reason: "command does not match the approved payload" }; }
|
|
216
|
+
// Records created before commandHash was introduced remain redeemable only
|
|
217
|
+
// when their stored payload is fully byte-identical and small enough to
|
|
218
|
+
// have been displayed in full. Longer legacy records safely require a new
|
|
219
|
+
// approval instead of accepting a hidden or previously truncated suffix.
|
|
220
|
+
const matches = rec.commandHash
|
|
221
|
+
? candidate === rec.command && commandHash(candidate) === rec.commandHash
|
|
222
|
+
: Buffer.byteLength(String(rec.command || "")) <= MAX_COMMAND_BYTES
|
|
223
|
+
&& candidate === String(rec.command || "");
|
|
224
|
+
if (!matches) return { ok: false, reason: "command does not match the approved payload" };
|
|
225
|
+
rec.consumedAt = new Date().toISOString();
|
|
226
|
+
writeRecord(rec);
|
|
227
|
+
return { ok: true, record: rec };
|
|
228
|
+
} finally { releaseRecordLock(held); }
|
|
133
229
|
}
|
|
134
230
|
|
|
135
|
-
module.exports = {
|
|
231
|
+
module.exports = {
|
|
232
|
+
APPROVALS_DIR,
|
|
233
|
+
MAX_COMMAND_BYTES,
|
|
234
|
+
commandHash,
|
|
235
|
+
consume,
|
|
236
|
+
create,
|
|
237
|
+
decide,
|
|
238
|
+
detach,
|
|
239
|
+
formatApprovedCommand,
|
|
240
|
+
read,
|
|
241
|
+
waitForDecision,
|
|
242
|
+
};
|
package/core/auth-flow.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// core/handlers; this module owns the heavy lifting they call into.
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
|
-
const { spawn,
|
|
6
|
+
const { spawn, execFileSync } = require("child_process");
|
|
7
7
|
const {
|
|
8
8
|
config, saveEnvKey,
|
|
9
|
-
|
|
10
|
-
botSubprocessEnv,
|
|
9
|
+
discoverProviderExecutable,
|
|
10
|
+
botSubprocessEnv, agentEnvSource,
|
|
11
11
|
} = require("./config");
|
|
12
|
+
const { claudeAgentEnv, codexAgentEnv } = require("./providers/env");
|
|
12
13
|
const { vault } = require("./vault-store");
|
|
13
14
|
const { redactSensitive, stripTerminalControls, extractUrls } = require("./redact");
|
|
14
15
|
const { send } = require("./io");
|
|
@@ -28,10 +29,27 @@ function getClaudeOAuthToken() {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function claudeSubprocessEnv() {
|
|
31
|
-
const env = botSubprocessEnv();
|
|
32
32
|
const token = getClaudeOAuthToken().value;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
return claudeAgentEnv(agentEnvSource(), {
|
|
34
|
+
CLAUDE_CODE_OAUTH_TOKEN: token,
|
|
35
|
+
ANTHROPIC_API_KEY: config.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY,
|
|
36
|
+
CLAUDE_CONFIG_DIR: config.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function codexSubprocessEnv() {
|
|
41
|
+
return codexAgentEnv(agentEnvSource(), {
|
|
42
|
+
OPENAI_API_KEY: config.OPENAI_API_KEY || process.env.OPENAI_API_KEY,
|
|
43
|
+
CODEX_HOME: config.CODEX_HOME || process.env.CODEX_HOME,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function codexExecutable() {
|
|
48
|
+
return discoverProviderExecutable("codex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function claudeExecutable() {
|
|
52
|
+
return discoverProviderExecutable("claude");
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
function saveClaudeOAuthToken(token) {
|
|
@@ -179,8 +197,10 @@ function claudeUsageLimitMessage(details = "") {
|
|
|
179
197
|
}
|
|
180
198
|
|
|
181
199
|
function runClaudeAuthStatusDiagnostic() {
|
|
200
|
+
const binary = claudeExecutable();
|
|
201
|
+
if (!binary) return "Claude CLI not found";
|
|
182
202
|
try {
|
|
183
|
-
const output =
|
|
203
|
+
const output = execFileSync(binary, ["auth", "status"], {
|
|
184
204
|
cwd: process.env.HOME || require("os").homedir(),
|
|
185
205
|
env: claudeSubprocessEnv(),
|
|
186
206
|
encoding: "utf8",
|
|
@@ -193,6 +213,25 @@ function runClaudeAuthStatusDiagnostic() {
|
|
|
193
213
|
}
|
|
194
214
|
}
|
|
195
215
|
|
|
216
|
+
function claudeProviderAuthStatus(options = {}) {
|
|
217
|
+
const binary = options.binary || claudeExecutable();
|
|
218
|
+
if (!binary) return { state: "not_checked", reason: "Claude CLI not found" };
|
|
219
|
+
const tokenInfo = getClaudeOAuthToken();
|
|
220
|
+
if (tokenInfo.value) {
|
|
221
|
+
return { state: "authenticated", reason: null, method: "oauth_token", source: tokenInfo.source };
|
|
222
|
+
}
|
|
223
|
+
const runner = options.runCommand || runCommand;
|
|
224
|
+
const result = runner(binary, ["auth", "status"], {
|
|
225
|
+
env: claudeSubprocessEnv(),
|
|
226
|
+
timeout: 12000,
|
|
227
|
+
});
|
|
228
|
+
if (isClaudeAuthErrorText(result.output)) {
|
|
229
|
+
return { state: "unauthenticated", reason: summarizeAuthOutput(result.output, false) };
|
|
230
|
+
}
|
|
231
|
+
if (result.ok) return { state: "authenticated", reason: null, method: "cli_store", source: null };
|
|
232
|
+
return { state: "unknown", reason: summarizeAuthOutput(result.output, false) };
|
|
233
|
+
}
|
|
234
|
+
|
|
196
235
|
async function sendClaudeAuthStatusSummary(prefix = "Claude auth status") {
|
|
197
236
|
const output = runClaudeAuthStatusDiagnostic();
|
|
198
237
|
const tokenInfo = getClaudeOAuthToken();
|
|
@@ -219,8 +258,13 @@ async function runClaudeAuthCommand(args, label, opts = {}) {
|
|
|
219
258
|
await send(`Another Claude auth flow is already running (${state.pendingClaudeAuthLabel}). Send the requested code/token or wait for it to finish.`);
|
|
220
259
|
return;
|
|
221
260
|
}
|
|
261
|
+
const binary = claudeExecutable();
|
|
262
|
+
if (!binary) {
|
|
263
|
+
await send("Claude CLI not found. Install it or configure CLAUDE_PATH.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
222
266
|
await send(`${label} started...`);
|
|
223
|
-
const proc = spawn(
|
|
267
|
+
const proc = spawn(binary, args, {
|
|
224
268
|
cwd: process.env.HOME || require("os").homedir(),
|
|
225
269
|
env: claudeSubprocessEnv(),
|
|
226
270
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -298,7 +342,7 @@ function clearPendingCodexAuth(state = currentState()) {
|
|
|
298
342
|
|
|
299
343
|
function runCommand(command, args = [], opts = {}) {
|
|
300
344
|
try {
|
|
301
|
-
const out =
|
|
345
|
+
const out = execFileSync(command, args.map(String), {
|
|
302
346
|
cwd: opts.cwd || process.env.HOME || require("os").homedir(),
|
|
303
347
|
env: opts.env || botSubprocessEnv(),
|
|
304
348
|
encoding: "utf-8",
|
|
@@ -312,28 +356,61 @@ function runCommand(command, args = [], opts = {}) {
|
|
|
312
356
|
}
|
|
313
357
|
|
|
314
358
|
function runCodexLoginStatus() {
|
|
315
|
-
|
|
316
|
-
|
|
359
|
+
const binary = codexExecutable();
|
|
360
|
+
if (!binary) return { ok: false, code: 1, output: "Codex CLI not found" };
|
|
361
|
+
const result = runCommand(binary, ["login", "status"], { timeout: 12000, env: codexSubprocessEnv() });
|
|
317
362
|
return { ...result, ok: result.ok && !isCodexAuthErrorText(result.output) };
|
|
318
363
|
}
|
|
319
364
|
|
|
365
|
+
function codexProviderAuthStatus(options = {}) {
|
|
366
|
+
const binary = options.binary || codexExecutable();
|
|
367
|
+
if (!binary) return { state: "not_checked", reason: "Codex CLI not found" };
|
|
368
|
+
const runner = options.runCommand || runCommand;
|
|
369
|
+
const result = runner(binary, ["login", "status"], {
|
|
370
|
+
env: codexSubprocessEnv(),
|
|
371
|
+
timeout: 12000,
|
|
372
|
+
});
|
|
373
|
+
if (result.ok && !isCodexAuthErrorText(result.output)) {
|
|
374
|
+
return { state: "authenticated", reason: null, method: "cli_store" };
|
|
375
|
+
}
|
|
376
|
+
if (isCodexAuthErrorText(result.output)) {
|
|
377
|
+
return { state: "unauthenticated", reason: summarizeAuthOutput(result.output, false) };
|
|
378
|
+
}
|
|
379
|
+
return { state: "unknown", reason: summarizeAuthOutput(result.output, false) };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function codexAuthHelp(reason = "Codex authentication failed") {
|
|
383
|
+
return [
|
|
384
|
+
`Codex authentication needs attention: ${redactSensitive(reason)}`,
|
|
385
|
+
"",
|
|
386
|
+
"Try one of these from chat:",
|
|
387
|
+
"1. /codex_auth_status — check the local Codex login state",
|
|
388
|
+
"2. /codex_login — start device-code login",
|
|
389
|
+
"3. /codex_setup_token — store an existing OpenAI API key through the Codex CLI",
|
|
390
|
+
"",
|
|
391
|
+
"Or run `codex login` in a terminal on the bot host.",
|
|
392
|
+
].join("\n");
|
|
393
|
+
}
|
|
394
|
+
|
|
320
395
|
// Quick "is this provider usable non-interactively" check for the unified
|
|
321
396
|
// /login model picker. Claude is cheap (token presence is what the bot relies
|
|
322
397
|
// on). Codex spawns `codex login status` once — fine for a setup screen.
|
|
323
398
|
function providerAuthStatus() {
|
|
399
|
+
const binary = codexExecutable();
|
|
324
400
|
return {
|
|
325
401
|
claude: !!getClaudeOAuthToken().value,
|
|
326
|
-
codex:
|
|
402
|
+
codex: binary ? runCodexLoginStatus().ok : false,
|
|
327
403
|
};
|
|
328
404
|
}
|
|
329
405
|
|
|
330
406
|
async function sendCodexAuthStatusSummary(prefix = "Codex auth status") {
|
|
407
|
+
const binary = codexExecutable();
|
|
331
408
|
const status = runCodexLoginStatus();
|
|
332
|
-
const version =
|
|
409
|
+
const version = binary ? runCommand(binary, ["--version"], { env: codexSubprocessEnv() }) : { ok: false, output: "not found" };
|
|
333
410
|
await send([
|
|
334
411
|
prefix,
|
|
335
412
|
"",
|
|
336
|
-
`CLI: ${
|
|
413
|
+
`CLI: ${binary ? "found" : "not found"}`,
|
|
337
414
|
`Version: ${summarizeAuthOutput(version.output, version.ok)}`,
|
|
338
415
|
`Logged in: ${status.ok ? "yes" : "no/unknown"}`,
|
|
339
416
|
`Status: ${summarizeAuthOutput(status.output, status.ok)}`,
|
|
@@ -342,10 +419,12 @@ async function sendCodexAuthStatusSummary(prefix = "Codex auth status") {
|
|
|
342
419
|
}
|
|
343
420
|
|
|
344
421
|
async function saveCodexApiKeyWithCli(apiKey) {
|
|
422
|
+
const binary = codexExecutable();
|
|
423
|
+
if (!binary) return { ok: false, code: 1, output: "Codex CLI not found" };
|
|
345
424
|
return new Promise((resolve) => {
|
|
346
|
-
const proc = spawn(
|
|
425
|
+
const proc = spawn(binary, ["login", "--with-api-key"], {
|
|
347
426
|
cwd: process.env.HOME || require("os").homedir(),
|
|
348
|
-
env:
|
|
427
|
+
env: codexSubprocessEnv(),
|
|
349
428
|
stdio: ["pipe", "pipe", "pipe"],
|
|
350
429
|
});
|
|
351
430
|
let output = "";
|
|
@@ -360,12 +439,13 @@ async function saveCodexApiKeyWithCli(apiKey) {
|
|
|
360
439
|
|
|
361
440
|
async function runCodexDeviceLogin() {
|
|
362
441
|
const state = currentState();
|
|
363
|
-
|
|
442
|
+
const binary = codexExecutable();
|
|
443
|
+
if (!binary) return send("Codex CLI not found. Install: npm install -g @openai/codex");
|
|
364
444
|
if (state.pendingCodexAuthProcess) return send(`Another Codex auth flow is already running (${state.pendingCodexAuthLabel}). Send /cancel_codex_auth to cancel.`);
|
|
365
445
|
await send("Codex device login started. I'll send the URL/code if the CLI prints one.");
|
|
366
|
-
const proc = spawn(
|
|
446
|
+
const proc = spawn(binary, ["login", "--device-auth"], {
|
|
367
447
|
cwd: process.env.HOME || require("os").homedir(),
|
|
368
|
-
env:
|
|
448
|
+
env: codexSubprocessEnv(),
|
|
369
449
|
stdio: ["pipe", "pipe", "pipe"],
|
|
370
450
|
});
|
|
371
451
|
state.pendingCodexAuthProcess = proc;
|
|
@@ -416,6 +496,8 @@ module.exports = {
|
|
|
416
496
|
CLAUDE_OAUTH_TOKEN_KEY, CLAUDE_OAUTH_VAULT_KEY,
|
|
417
497
|
getClaudeOAuthToken,
|
|
418
498
|
claudeSubprocessEnv,
|
|
499
|
+
claudeProviderAuthStatus,
|
|
500
|
+
codexSubprocessEnv,
|
|
419
501
|
saveClaudeOAuthToken,
|
|
420
502
|
clearClaudeOAuthToken,
|
|
421
503
|
extractClaudeToken,
|
|
@@ -437,6 +519,8 @@ module.exports = {
|
|
|
437
519
|
clearPendingCodexAuth,
|
|
438
520
|
runCommand,
|
|
439
521
|
runCodexLoginStatus,
|
|
522
|
+
codexProviderAuthStatus,
|
|
523
|
+
codexAuthHelp,
|
|
440
524
|
providerAuthStatus,
|
|
441
525
|
sendCodexAuthStatusSummary,
|
|
442
526
|
saveCodexApiKeyWithCli,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
function resolveConfigDir(env = process.env) {
|
|
8
|
+
const configured = String(env.OPEN_CLAUDIA_CONFIG_DIR || "").trim();
|
|
9
|
+
if (configured) return path.resolve(configured);
|
|
10
|
+
const home = String(env.HOME || os.homedir()).trim();
|
|
11
|
+
return path.resolve(home, ".open-claudia");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureConfigDir(configDir = resolveConfigDir()) {
|
|
15
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
16
|
+
return configDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { resolveConfigDir, ensureConfigDir };
|