@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/web.js
CHANGED
|
@@ -4,12 +4,16 @@ const path = require("path");
|
|
|
4
4
|
const crypto = require("crypto");
|
|
5
5
|
const https = require("https");
|
|
6
6
|
const CONFIG_DIR = require("./config-dir");
|
|
7
|
+
const { ensureConfigDir } = require("./core/config-dir");
|
|
8
|
+
const { atomicWriteFileSync } = require("./core/fsutil");
|
|
7
9
|
const Vault = require("./vault");
|
|
8
10
|
const {
|
|
9
11
|
loadUsageHistory,
|
|
10
12
|
usageAlertPolicy,
|
|
11
13
|
usageTrend,
|
|
12
14
|
} = require("./core/usage-log");
|
|
15
|
+
const webSessions = require("./core/web-sessions");
|
|
16
|
+
const { inspectProviderConfiguration } = require("./core/provider-status");
|
|
13
17
|
|
|
14
18
|
const ENV_FILE = path.join(CONFIG_DIR, ".env");
|
|
15
19
|
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
@@ -28,7 +32,7 @@ function getPassword() {
|
|
|
28
32
|
}
|
|
29
33
|
// Use env-provided password (from K8s secret) or generate random
|
|
30
34
|
const initial = process.env.WEB_PASSWORD || crypto.randomBytes(16).toString("hex");
|
|
31
|
-
|
|
35
|
+
atomicWriteFileSync(WEB_PASSWORD_FILE, initial, { mode: 0o600 });
|
|
32
36
|
if (!process.env.WEB_PASSWORD) {
|
|
33
37
|
console.log(`\n Web UI initial password: ${initial}`);
|
|
34
38
|
console.log(` Change it in Settings after first login.\n`);
|
|
@@ -56,8 +60,8 @@ function validatePasswordComplexity(pw) {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function setPassword(newPassword) {
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
atomicWriteFileSync(WEB_PASSWORD_FILE, newPassword, { backup: true, mode: 0o600 });
|
|
64
|
+
atomicWriteFileSync(PASSWORD_CHANGED_FILE, new Date().toISOString(), { mode: 0o600 });
|
|
61
65
|
notifyAgentSpacePasswordChanged().then((r) => {
|
|
62
66
|
if (r && r.ok) {
|
|
63
67
|
try { fs.writeFileSync(PASSWORD_CHANGED_NOTIFIED_FILE, new Date().toISOString()); } catch (e) {}
|
|
@@ -136,12 +140,7 @@ function checkAuth(req) {
|
|
|
136
140
|
const cookie = (req.headers.cookie || "").split(";").find((c) => c.trim().startsWith("oc_session="));
|
|
137
141
|
if (!cookie) return false;
|
|
138
142
|
const token = cookie.split("=")[1]?.trim();
|
|
139
|
-
|
|
140
|
-
return token === expected;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function authToken() {
|
|
144
|
-
return crypto.createHash("sha256").update(getPassword()).digest("hex");
|
|
143
|
+
return webSessions.valid(token);
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
// ── Config helpers ─────────────────────────────────────────────────
|
|
@@ -158,7 +157,27 @@ function loadEnv() {
|
|
|
158
157
|
|
|
159
158
|
function saveEnv(env) {
|
|
160
159
|
const content = Object.entries(env).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
161
|
-
|
|
160
|
+
atomicWriteFileSync(ENV_FILE, content, { backup: true, mode: 0o600 });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// GET /api/config used to return the raw .env — every bot token, model API key
|
|
164
|
+
// and control-plane secret in plaintext to anyone with a dashboard session. The
|
|
165
|
+
// config UI only ever edits the non-secret SAFE_KEYS, so mask any secret-looking
|
|
166
|
+
// value before it leaves the server (keeping a 4-char tail so the owner can still
|
|
167
|
+
// tell a value is set / which one it is). Masked values can never round-trip back
|
|
168
|
+
// in: the POST handler rejects every key outside SAFE_KEYS.
|
|
169
|
+
const SECRET_KEY_RE = /(TOKEN|SECRET|PASSWORD|API_?KEY|_KEY|CREDENTIAL|WEBHOOK|SESSION|PRIVATE)/i;
|
|
170
|
+
function maskEnv(env) {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [k, v] of Object.entries(env)) {
|
|
173
|
+
if (v && SECRET_KEY_RE.test(k)) {
|
|
174
|
+
const s = String(v);
|
|
175
|
+
out[k] = s.length <= 8 ? "••••••••" : `••••${s.slice(-4)}`;
|
|
176
|
+
} else {
|
|
177
|
+
out[k] = v;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
function loadAuth() {
|
|
@@ -166,7 +185,7 @@ function loadAuth() {
|
|
|
166
185
|
}
|
|
167
186
|
|
|
168
187
|
function saveAuth(auth) {
|
|
169
|
-
|
|
188
|
+
atomicWriteFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), { backup: true, mode: 0o600 });
|
|
170
189
|
}
|
|
171
190
|
|
|
172
191
|
function loadSoul() {
|
|
@@ -180,12 +199,27 @@ function loadCrons() {
|
|
|
180
199
|
try {
|
|
181
200
|
const jobs = JSON.parse(fs.readFileSync(JOBS_FILE, "utf-8"));
|
|
182
201
|
if (Array.isArray(jobs)) return jobs.filter((j) => j && j.kind === "cron");
|
|
202
|
+
if (jobs && Array.isArray(jobs.jobs)) return jobs.jobs.filter((j) => j && j.kind === "cron" && !j.disabled);
|
|
183
203
|
} catch (e) { /* fall through to legacy */ }
|
|
184
204
|
try { return JSON.parse(fs.readFileSync(CRONS_FILE, "utf-8")); } catch (e) { return []; }
|
|
185
205
|
}
|
|
186
206
|
|
|
187
207
|
function loadSessions() {
|
|
188
|
-
|
|
208
|
+
let document;
|
|
209
|
+
try { document = JSON.parse(fs.readFileSync(SESSIONS_FILE, "utf-8")); } catch (e) { return []; }
|
|
210
|
+
const rows = [];
|
|
211
|
+
if (document && document.users && typeof document.users === "object") {
|
|
212
|
+
for (const [userId, projects] of Object.entries(document.users)) {
|
|
213
|
+
for (const [project, sessions] of Object.entries(projects || {})) {
|
|
214
|
+
for (const session of Array.isArray(sessions) ? sessions : []) rows.push({ userId, project, ...session });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return rows;
|
|
218
|
+
}
|
|
219
|
+
for (const [project, sessions] of Object.entries(document || {})) {
|
|
220
|
+
for (const session of Array.isArray(sessions) ? sessions : []) rows.push({ userId: null, project, ...session });
|
|
221
|
+
}
|
|
222
|
+
return rows;
|
|
189
223
|
}
|
|
190
224
|
|
|
191
225
|
function loadLogs(lines = 100) {
|
|
@@ -200,6 +234,17 @@ function isConfigured() {
|
|
|
200
234
|
return fs.existsSync(ENV_FILE);
|
|
201
235
|
}
|
|
202
236
|
|
|
237
|
+
function providerConfigurationStatus(options = {}) {
|
|
238
|
+
const env = loadEnv();
|
|
239
|
+
return inspectProviderConfiguration({
|
|
240
|
+
...options,
|
|
241
|
+
env: { ...process.env, ...env, ...(options.env || {}) },
|
|
242
|
+
defaultProvider: options.defaultProvider === undefined
|
|
243
|
+
? (env.DEFAULT_PROVIDER || null)
|
|
244
|
+
: options.defaultProvider,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
203
248
|
// Group usage records by a key (version / model) and return per-group
|
|
204
249
|
// averages so the dashboard can compare tokenomics across versions.
|
|
205
250
|
function aggregateUsage(records, keyName) {
|
|
@@ -246,7 +291,7 @@ function telegramGet(token, method) {
|
|
|
246
291
|
|
|
247
292
|
// ── API routes ─────────────────────────────────────────────────────
|
|
248
293
|
|
|
249
|
-
async function handleAPI(req, res, body) {
|
|
294
|
+
async function handleAPI(req, res, body, dependencies = {}) {
|
|
250
295
|
const url = req.url;
|
|
251
296
|
|
|
252
297
|
// Login — no auth required
|
|
@@ -255,7 +300,7 @@ async function handleAPI(req, res, body) {
|
|
|
255
300
|
if (password === getPassword()) {
|
|
256
301
|
res.writeHead(200, {
|
|
257
302
|
"Content-Type": "application/json",
|
|
258
|
-
"Set-Cookie": `oc_session=${
|
|
303
|
+
"Set-Cookie": `oc_session=${webSessions.issue()}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
259
304
|
});
|
|
260
305
|
return res.end(JSON.stringify({ ok: true, mustChangePassword: !isPasswordChanged() }));
|
|
261
306
|
}
|
|
@@ -291,14 +336,16 @@ async function handleAPI(req, res, body) {
|
|
|
291
336
|
if (url === "/api/status") {
|
|
292
337
|
const env = loadEnv();
|
|
293
338
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf-8"));
|
|
339
|
+
const providerStatus = providerConfigurationStatus();
|
|
294
340
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
295
341
|
return res.end(JSON.stringify({
|
|
296
342
|
configured: isConfigured(),
|
|
297
343
|
version: pkg.version,
|
|
298
344
|
workspace: env.WORKSPACE || "",
|
|
299
|
-
botToken: env.TELEGRAM_BOT_TOKEN ? "***" + env.TELEGRAM_BOT_TOKEN.slice(-
|
|
345
|
+
botToken: env.TELEGRAM_BOT_TOKEN ? "***" + env.TELEGRAM_BOT_TOKEN.slice(-4) : "",
|
|
300
346
|
chatId: env.TELEGRAM_CHAT_ID || "",
|
|
301
|
-
|
|
347
|
+
providers: providerStatus.providers,
|
|
348
|
+
defaultProvider: providerStatus.defaultProvider,
|
|
302
349
|
onboarded: env.ONBOARDED === "true",
|
|
303
350
|
}));
|
|
304
351
|
}
|
|
@@ -308,28 +355,44 @@ async function handleAPI(req, res, body) {
|
|
|
308
355
|
const data = JSON.parse(body);
|
|
309
356
|
|
|
310
357
|
// Verify token
|
|
311
|
-
const
|
|
358
|
+
const telegramProbe = dependencies.telegramGet || telegramGet;
|
|
359
|
+
const botInfo = await telegramProbe(data.botToken, "getMe");
|
|
312
360
|
if (!botInfo.ok) {
|
|
313
361
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
314
362
|
return res.end(JSON.stringify({ error: "Invalid Telegram bot token" }));
|
|
315
363
|
}
|
|
316
364
|
|
|
365
|
+
const existingEnv = loadEnv();
|
|
366
|
+
const requestedDefault = String(data.defaultProvider || "").trim().toLowerCase();
|
|
367
|
+
const requestedUtility = String(data.utilityProvider || "").trim().toLowerCase();
|
|
317
368
|
const env = {
|
|
369
|
+
...existingEnv,
|
|
318
370
|
TELEGRAM_BOT_TOKEN: data.botToken,
|
|
319
371
|
TELEGRAM_CHAT_ID: data.chatId,
|
|
320
372
|
WORKSPACE: data.workspace || path.join(CONFIG_DIR, "Workspace"),
|
|
321
|
-
CLAUDE_PATH: data.claudePath || "
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
373
|
+
CLAUDE_PATH: String(data.claudePath || "").trim() || existingEnv.CLAUDE_PATH || "",
|
|
374
|
+
CODEX_PATH: String(data.codexPath || "").trim() || existingEnv.CODEX_PATH || "",
|
|
375
|
+
DEFAULT_PROVIDER: new Set(["claude", "codex"]).has(requestedDefault)
|
|
376
|
+
? requestedDefault
|
|
377
|
+
: (existingEnv.DEFAULT_PROVIDER || ""),
|
|
378
|
+
UTILITY_PROVIDER: new Set(["active", "claude", "codex"]).has(requestedUtility)
|
|
379
|
+
? requestedUtility
|
|
380
|
+
: (existingEnv.UTILITY_PROVIDER || "active"),
|
|
381
|
+
PROVIDER_FALLBACKS: String(data.providerFallbacks || "").trim() || existingEnv.PROVIDER_FALLBACKS || "",
|
|
382
|
+
WHISPER_CLI: existingEnv.WHISPER_CLI || "",
|
|
383
|
+
WHISPER_MODEL: existingEnv.WHISPER_MODEL || "",
|
|
384
|
+
FFMPEG: existingEnv.FFMPEG || "",
|
|
326
385
|
VAULT_FILE: path.join(CONFIG_DIR, "vault.enc"),
|
|
327
386
|
SOUL_FILE: SOUL_FILE,
|
|
328
387
|
CRONS_FILE: CRONS_FILE,
|
|
329
388
|
AUTH_FILE: AUTH_FILE,
|
|
330
389
|
ONBOARDED: "false",
|
|
331
390
|
};
|
|
391
|
+
if (data.apiKey) env.ANTHROPIC_API_KEY = data.apiKey;
|
|
332
392
|
saveEnv(env);
|
|
393
|
+
const runtimeConfig = require("./core/config");
|
|
394
|
+
Object.assign(runtimeConfig.config, env);
|
|
395
|
+
require("./core/providers").registry.defaultProvider = env.DEFAULT_PROVIDER || null;
|
|
333
396
|
|
|
334
397
|
// Create workspace dir
|
|
335
398
|
const wsDir = env.WORKSPACE;
|
|
@@ -357,16 +420,25 @@ async function handleAPI(req, res, body) {
|
|
|
357
420
|
}
|
|
358
421
|
|
|
359
422
|
// Get config
|
|
360
|
-
if (url === "/api/config") {
|
|
423
|
+
if (url === "/api/config" && req.method === "GET") {
|
|
361
424
|
const env = loadEnv();
|
|
362
425
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
363
|
-
return res.end(JSON.stringify(env));
|
|
426
|
+
return res.end(JSON.stringify(maskEnv(env)));
|
|
364
427
|
}
|
|
365
428
|
|
|
366
429
|
// Update config (whitelist safe keys only)
|
|
367
430
|
if (url === "/api/config" && req.method === "POST") {
|
|
368
431
|
const SAFE_KEYS = new Set([
|
|
369
|
-
"WORKSPACE", "CLAUDE_PATH", "
|
|
432
|
+
"WORKSPACE", "CLAUDE_PATH", "CODEX_PATH", "DEFAULT_PROVIDER",
|
|
433
|
+
"UTILITY_PROVIDER", "PROVIDER_FALLBACKS",
|
|
434
|
+
"DREAM_PROVIDER", "MEMORY_PROVIDER", "RECALL_PROVIDER", "ENFORCER_PROVIDER", "SUBAGENT_PROVIDER",
|
|
435
|
+
"CLAUDE_MODEL", "CODEX_MODEL",
|
|
436
|
+
"DREAM_MODEL_CLAUDE", "DREAM_MODEL_CODEX",
|
|
437
|
+
"PACK_REVIEW_MODEL_CLAUDE", "PACK_REVIEW_MODEL_CODEX",
|
|
438
|
+
"RECALL_MODEL_CLAUDE", "RECALL_MODEL_CODEX",
|
|
439
|
+
"ENFORCER_MODEL_CLAUDE", "ENFORCER_MODEL_CODEX",
|
|
440
|
+
"SUBAGENT_MODEL_CLAUDE", "SUBAGENT_MODEL_CODEX",
|
|
441
|
+
"WHISPER_CLI", "FFMPEG", "WHISPER_MODEL",
|
|
370
442
|
"MEMORY_RECALL_MAX_CHARS",
|
|
371
443
|
"USAGE_ALERT_CONTEXT_TOKENS", "USAGE_ALERT_RATE_MULTIPLIER",
|
|
372
444
|
"USAGE_ALERT_BASELINE_TURNS", "USAGE_ALERT_MIN_BASELINE_TURNS",
|
|
@@ -379,6 +451,20 @@ async function handleAPI(req, res, body) {
|
|
|
379
451
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
380
452
|
return res.end(JSON.stringify({ error: `Cannot update key: ${key}` }));
|
|
381
453
|
}
|
|
454
|
+
const normalized = String(value == null ? "" : value).trim().toLowerCase();
|
|
455
|
+
if (key === "DEFAULT_PROVIDER" && normalized && !new Set(["claude", "codex"]).has(normalized)) {
|
|
456
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
457
|
+
return res.end(JSON.stringify({ error: "DEFAULT_PROVIDER must be claude, codex, or blank" }));
|
|
458
|
+
}
|
|
459
|
+
if (["UTILITY_PROVIDER", "DREAM_PROVIDER", "MEMORY_PROVIDER", "RECALL_PROVIDER", "ENFORCER_PROVIDER", "SUBAGENT_PROVIDER"].includes(key)
|
|
460
|
+
&& normalized && !new Set(["active", "claude", "codex"]).has(normalized)) {
|
|
461
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
462
|
+
return res.end(JSON.stringify({ error: `${key} must be active, claude, codex, or blank` }));
|
|
463
|
+
}
|
|
464
|
+
if (key === "PROVIDER_FALLBACKS" && normalized.split(",").filter(Boolean).some((id) => !new Set(["claude", "codex"]).has(id.trim()))) {
|
|
465
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
466
|
+
return res.end(JSON.stringify({ error: "PROVIDER_FALLBACKS accepts only claude and codex" }));
|
|
467
|
+
}
|
|
382
468
|
env[key] = value;
|
|
383
469
|
}
|
|
384
470
|
saveEnv(env);
|
|
@@ -431,7 +517,7 @@ async function handleAPI(req, res, body) {
|
|
|
431
517
|
}
|
|
432
518
|
|
|
433
519
|
// Soul
|
|
434
|
-
if (url === "/api/soul") {
|
|
520
|
+
if (url === "/api/soul" && req.method === "GET") {
|
|
435
521
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
436
522
|
return res.end(JSON.stringify({ content: loadSoul() }));
|
|
437
523
|
}
|
|
@@ -502,9 +588,12 @@ async function handleAPI(req, res, body) {
|
|
|
502
588
|
return res.end(JSON.stringify({ error: complexityError }));
|
|
503
589
|
}
|
|
504
590
|
setPassword(newPassword);
|
|
591
|
+
// Changing the password invalidates every outstanding session, then we
|
|
592
|
+
// re-issue one so the caller who just changed it stays logged in.
|
|
593
|
+
webSessions.revokeAll();
|
|
505
594
|
res.writeHead(200, {
|
|
506
595
|
"Content-Type": "application/json",
|
|
507
|
-
"Set-Cookie": `oc_session=${
|
|
596
|
+
"Set-Cookie": `oc_session=${webSessions.issue()}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
508
597
|
});
|
|
509
598
|
return res.end(JSON.stringify({ ok: true }));
|
|
510
599
|
}
|
|
@@ -705,7 +794,8 @@ async function loadTab() {
|
|
|
705
794
|
<div class="list-item"><span>Workspace</span><span>\${status.workspace}</span></div>
|
|
706
795
|
<div class="list-item"><span>Bot Token</span><span>\${status.botToken}</span></div>
|
|
707
796
|
<div class="list-item"><span>Chat ID</span><span>\${status.chatId}</span></div>
|
|
708
|
-
<div class="list-item"><span>
|
|
797
|
+
<div class="list-item"><span>Default provider</span><span>\${status.defaultProvider?.label || "None (model turns unavailable)"}</span></div>
|
|
798
|
+
\${(status.providers || []).map(p => '<div class="list-item"><span>' + p.label + '</span><span>' + p.availability.state + ' · ' + p.compatibility.state + ' · auth ' + p.auth.state + '</span></div>').join("")}
|
|
709
799
|
</div>\`;
|
|
710
800
|
}
|
|
711
801
|
else if (currentTab === "usage") {
|
|
@@ -770,7 +860,7 @@ async function loadTab() {
|
|
|
770
860
|
}
|
|
771
861
|
else if (currentTab === "credentials") {
|
|
772
862
|
el.innerHTML = \`
|
|
773
|
-
<div class="card" id="cred-status"><h2>
|
|
863
|
+
<div class="card" id="cred-status"><h2>Provider credentials</h2><p class="subtitle">Authenticate the coding-agent providers you use. Each provider keeps its own credentials.</p></div>
|
|
774
864
|
<div class="card">
|
|
775
865
|
<h2>Claude</h2>
|
|
776
866
|
<button onclick="startClaudeAuth()">Authenticate via browser</button>
|
|
@@ -824,18 +914,14 @@ async function loadTab() {
|
|
|
824
914
|
else if (currentTab === "sessions") {
|
|
825
915
|
const sessions = await api("/api/sessions");
|
|
826
916
|
if (!sessions) return;
|
|
827
|
-
const projects = Object.keys(sessions);
|
|
828
917
|
el.innerHTML = \`
|
|
829
918
|
<div class="card">
|
|
830
919
|
<h2>Conversation History</h2>
|
|
831
|
-
\${
|
|
832
|
-
<
|
|
833
|
-
|
|
834
|
-
<
|
|
835
|
-
|
|
836
|
-
<span style="color:#666;font-size:12px;">\${new Date(s.lastUsed).toLocaleDateString()}</span>
|
|
837
|
-
</div>\`).join("")}
|
|
838
|
-
\`).join("") || "<p>No sessions yet.</p>"}
|
|
920
|
+
\${sessions.slice().reverse().slice(0,25).map(s => \`
|
|
921
|
+
<div class="list-item">
|
|
922
|
+
<span>\${s.title || s.id} · \${s.provider || "legacy"} · \${s.project}</span>
|
|
923
|
+
<span style="color:#666;font-size:12px;">\${s.lastUsed ? new Date(s.lastUsed).toLocaleDateString() : "unknown"}</span>
|
|
924
|
+
</div>\`).join("") || "<p>No sessions yet.</p>"}
|
|
839
925
|
</div>\`;
|
|
840
926
|
}
|
|
841
927
|
else if (currentTab === "logs") {
|
|
@@ -871,7 +957,18 @@ async function loadTab() {
|
|
|
871
957
|
const groups = [
|
|
872
958
|
{ title: "General", fields: [
|
|
873
959
|
{ k: "WORKSPACE", label: "Workspace folder", desc: "Root directory where the bot reads and writes project files.", ph: "/data/Workspace" },
|
|
960
|
+
]},
|
|
961
|
+
{ title: "Providers", fields: [
|
|
874
962
|
{ k: "CLAUDE_PATH", label: "Claude CLI path", desc: "Path or command name for the Claude Code CLI.", ph: "claude" },
|
|
963
|
+
{ k: "CODEX_PATH", label: "Codex CLI path", desc: "Path or command name for the OpenAI Codex CLI.", ph: "codex" },
|
|
964
|
+
{ k: "DEFAULT_PROVIDER", label: "Default provider", desc: "claude or codex; blank chooses the first compatible provider deterministically.", ph: "claude" },
|
|
965
|
+
{ k: "UTILITY_PROVIDER", label: "Utility provider", desc: "active, claude, or codex for background intelligence.", ph: "active" },
|
|
966
|
+
{ k: "PROVIDER_FALLBACKS", label: "Utility fallbacks", desc: "Ordered comma-separated fallback providers. Foreground turns never use this list.", ph: "codex,claude" },
|
|
967
|
+
{ k: "DREAM_PROVIDER", label: "Dream provider", desc: "Optional per-purpose override.", ph: "codex" },
|
|
968
|
+
{ k: "MEMORY_PROVIDER", label: "Memory-review provider", desc: "Optional per-purpose override.", ph: "active" },
|
|
969
|
+
{ k: "RECALL_PROVIDER", label: "Recall provider", desc: "Optional per-purpose override.", ph: "active" },
|
|
970
|
+
{ k: "ENFORCER_PROVIDER", label: "Guard provider", desc: "Optional fail-closed relationship-guard provider.", ph: "claude" },
|
|
971
|
+
{ k: "SUBAGENT_PROVIDER", label: "Sub-agent provider", desc: "Optional /agent provider override.", ph: "active" },
|
|
875
972
|
]},
|
|
876
973
|
{ title: "Voice transcription", fields: [
|
|
877
974
|
{ k: "WHISPER_CLI", label: "Whisper CLI", desc: "Path to the whisper binary for transcribing voice notes. Leave blank to disable voice.", ph: "whisper" },
|
|
@@ -975,7 +1072,7 @@ async function loadCredStatus() {
|
|
|
975
1072
|
const s = await api("/api/auth/status");
|
|
976
1073
|
if (!s) return;
|
|
977
1074
|
const yn = (b) => b ? "<span style='color:#22c55e'>yes</span>" : "<span style='color:#dc2626'>no</span>";
|
|
978
|
-
$("#cred-status").innerHTML = \`<h2>
|
|
1075
|
+
$("#cred-status").innerHTML = \`<h2>Provider credentials</h2>
|
|
979
1076
|
<div class="list-item"><span>Claude OAuth token saved</span><span>\${yn(s.claude.configured)}</span></div>
|
|
980
1077
|
<div class="list-item"><span>Claude logged in</span><span>\${yn(s.claude.loggedIn)}</span></div>
|
|
981
1078
|
<div class="list-item"><span>Codex CLI present</span><span>\${yn(s.codex.cliFound)}</span></div>
|
|
@@ -1076,16 +1173,25 @@ async function saveConfig() {
|
|
|
1076
1173
|
}
|
|
1077
1174
|
|
|
1078
1175
|
function renderSetup() {
|
|
1176
|
+
const providerRows = (status.providers || []).map(p =>
|
|
1177
|
+
'<div class="list-item"><span>' + p.label + '</span><span>' + p.availability.state + ' · ' + p.compatibility.state + ' · auth ' + p.auth.state + '</span></div>' +
|
|
1178
|
+
(p.recovery ? '<div style="color:#888;font-size:11px;margin:-8px 0 10px;">' + p.recovery.split("\\n")[0] + '</div>' : "")
|
|
1179
|
+
).join("");
|
|
1079
1180
|
$("#app").innerHTML = \`
|
|
1080
1181
|
<div class="container">
|
|
1081
1182
|
<h1>${botName} Setup</h1>
|
|
1082
|
-
<p class="subtitle">Configure
|
|
1183
|
+
<p class="subtitle">Configure channels and coding-agent providers</p>
|
|
1083
1184
|
<div id="setup-msg"></div>
|
|
1185
|
+
<div class="card"><h2>Provider status</h2>\${providerRows || "<p>No provider detected. You can still complete setup and configure one later.</p>"}</div>
|
|
1084
1186
|
<div class="card">
|
|
1085
1187
|
<div class="form-group"><label>Telegram Bot Token (from @BotFather)</label><input id="s-token" placeholder="123456:ABC-DEF..."></div>
|
|
1086
1188
|
<div class="form-group"><label>Your Telegram Chat ID</label><input id="s-chatid" placeholder="e.g. 6251055967"></div>
|
|
1087
1189
|
<div class="form-group"><label>Anthropic API Key (or leave blank if Claude CLI is authed)</label><input id="s-apikey" type="password" placeholder="sk-ant-..."></div>
|
|
1088
|
-
<div class="form-group"><label>Claude CLI Path (
|
|
1190
|
+
<div class="form-group"><label>Claude CLI Path (optional; blank uses PATH discovery)</label><input id="s-claude" placeholder="claude"></div>
|
|
1191
|
+
<div class="form-group"><label>Codex CLI Path (optional; blank uses PATH discovery)</label><input id="s-codex" placeholder="codex"></div>
|
|
1192
|
+
<div class="form-group"><label>Default Provider (optional)</label><select id="s-default"><option value="">Auto-detect</option><option value="claude">Claude Code</option><option value="codex">OpenAI Codex</option></select></div>
|
|
1193
|
+
<div class="form-group"><label>Utility Provider</label><select id="s-utility"><option value="active">Active provider</option><option value="claude">Claude Code</option><option value="codex">OpenAI Codex</option></select></div>
|
|
1194
|
+
<div class="form-group"><label>Utility Fallbacks (optional)</label><input id="s-fallbacks" placeholder="codex,claude"></div>
|
|
1089
1195
|
<div class="form-group"><label>Workspace Path</label><input id="s-workspace" placeholder="Leave blank for default"></div>
|
|
1090
1196
|
<button onclick="doSetup()" style="width:100%">Complete Setup</button>
|
|
1091
1197
|
</div>
|
|
@@ -1098,6 +1204,10 @@ async function doSetup() {
|
|
|
1098
1204
|
chatId: $("#s-chatid").value,
|
|
1099
1205
|
apiKey: $("#s-apikey").value,
|
|
1100
1206
|
claudePath: $("#s-claude").value,
|
|
1207
|
+
codexPath: $("#s-codex").value,
|
|
1208
|
+
defaultProvider: $("#s-default").value,
|
|
1209
|
+
utilityProvider: $("#s-utility").value,
|
|
1210
|
+
providerFallbacks: $("#s-fallbacks").value,
|
|
1101
1211
|
workspace: $("#s-workspace").value,
|
|
1102
1212
|
}});
|
|
1103
1213
|
if (r?.ok) {
|
|
@@ -1119,8 +1229,9 @@ init();
|
|
|
1119
1229
|
|
|
1120
1230
|
let webServer = null;
|
|
1121
1231
|
|
|
1122
|
-
function startWebServer() {
|
|
1232
|
+
function startWebServer(options = {}) {
|
|
1123
1233
|
if (webServer) return webServer;
|
|
1234
|
+
ensureConfigDir(CONFIG_DIR);
|
|
1124
1235
|
|
|
1125
1236
|
const server = http.createServer(async (req, res) => {
|
|
1126
1237
|
// CORS for local dev
|
|
@@ -1137,7 +1248,7 @@ function startWebServer() {
|
|
|
1137
1248
|
if (consume(token)) {
|
|
1138
1249
|
res.writeHead(302, {
|
|
1139
1250
|
"Location": "/",
|
|
1140
|
-
"Set-Cookie": `oc_session=${
|
|
1251
|
+
"Set-Cookie": `oc_session=${webSessions.issue()}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
1141
1252
|
});
|
|
1142
1253
|
return res.end();
|
|
1143
1254
|
}
|
|
@@ -1148,7 +1259,7 @@ function startWebServer() {
|
|
|
1148
1259
|
if (req.url.startsWith("/api/")) {
|
|
1149
1260
|
let body = "";
|
|
1150
1261
|
req.on("data", (d) => { body += d; });
|
|
1151
|
-
req.on("end", () => handleAPI(req, res, body));
|
|
1262
|
+
req.on("end", () => handleAPI(req, res, body, options));
|
|
1152
1263
|
return;
|
|
1153
1264
|
}
|
|
1154
1265
|
|
|
@@ -1162,9 +1273,12 @@ function startWebServer() {
|
|
|
1162
1273
|
console.log("Admin password configured.");
|
|
1163
1274
|
reconcileGrandfatheredPasswordChange();
|
|
1164
1275
|
});
|
|
1276
|
+
server.once("close", () => {
|
|
1277
|
+
if (webServer === server) webServer = null;
|
|
1278
|
+
});
|
|
1165
1279
|
|
|
1166
1280
|
webServer = server;
|
|
1167
1281
|
return server;
|
|
1168
1282
|
}
|
|
1169
1283
|
|
|
1170
|
-
module.exports = { startWebServer, isConfigured };
|
|
1284
|
+
module.exports = { startWebServer, isConfigured, providerConfigurationStatus, handleAPI };
|