@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/setup.js
CHANGED
|
@@ -8,6 +8,12 @@ const crypto = require("crypto");
|
|
|
8
8
|
const { execSync } = require("child_process");
|
|
9
9
|
const Vault = require("./vault");
|
|
10
10
|
const CONFIG_DIR = require("./config-dir");
|
|
11
|
+
const { ensureConfigDir } = require("./core/config-dir");
|
|
12
|
+
const { atomicWriteFileSync } = require("./core/fsutil");
|
|
13
|
+
const { inspectProviderConfiguration } = require("./core/provider-status");
|
|
14
|
+
const { ProviderRegistry } = require("./core/providers");
|
|
15
|
+
const { createClaudeProvider } = require("./core/providers/claude");
|
|
16
|
+
const { createCodexProvider } = require("./core/providers/codex");
|
|
11
17
|
|
|
12
18
|
const ENV_FILE = path.join(CONFIG_DIR, ".env");
|
|
13
19
|
const VAULT_FILE = path.join(CONFIG_DIR, "vault.enc");
|
|
@@ -16,8 +22,12 @@ const CRONS_FILE = path.join(CONFIG_DIR, "crons.json");
|
|
|
16
22
|
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
17
23
|
const SETUP_STATE_FILE = path.join(CONFIG_DIR, ".setup-state.json");
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
let rl = null;
|
|
26
|
+
function promptInterface() {
|
|
27
|
+
if (!rl) rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
28
|
+
return rl;
|
|
29
|
+
}
|
|
30
|
+
const ask = (q) => new Promise((r) => promptInterface().question(q, r));
|
|
21
31
|
|
|
22
32
|
// ── Auth file helpers ──────────────────────────────────────────────
|
|
23
33
|
|
|
@@ -29,7 +39,7 @@ function loadAuth() {
|
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
function saveAuth(auth) {
|
|
32
|
-
|
|
42
|
+
atomicWriteFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), { backup: true, mode: 0o600 });
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
// ── Setup state helpers ────────────────────────────────────────────
|
|
@@ -42,13 +52,25 @@ function loadSetupState() {
|
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
function saveSetupState(state) {
|
|
45
|
-
|
|
55
|
+
atomicWriteFileSync(SETUP_STATE_FILE, JSON.stringify(state, null, 2), { backup: true, mode: 0o600 });
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
function clearSetupState() {
|
|
49
59
|
if (fs.existsSync(SETUP_STATE_FILE)) fs.unlinkSync(SETUP_STATE_FILE);
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
function upgradeSetupState(value) {
|
|
63
|
+
const state = value && typeof value === "object"
|
|
64
|
+
? { ...value, completedSteps: [...(value.completedSteps || [])], data: { ...(value.data || {}) } }
|
|
65
|
+
: { completedSteps: [], data: {} };
|
|
66
|
+
if (state.completedSteps.includes("prerequisites")
|
|
67
|
+
&& (!Object.prototype.hasOwnProperty.call(state.data, "codexPath")
|
|
68
|
+
|| !Object.prototype.hasOwnProperty.call(state.data, "defaultProvider"))) {
|
|
69
|
+
state.completedSteps = state.completedSteps.filter((step) => step !== "prerequisites");
|
|
70
|
+
}
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
// ── Telegram helpers ───────────────────────────────────────────────
|
|
53
75
|
|
|
54
76
|
function telegramGet(token, method, params = "") {
|
|
@@ -147,9 +169,56 @@ function findBinary(name) {
|
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
function findClaude() { return findBinary("claude"); }
|
|
172
|
+
function findCodex() { return findBinary("codex"); }
|
|
150
173
|
function findWhisper() { return findBinary("whisper-cli"); }
|
|
151
174
|
function findFfmpeg() { return findBinary("ffmpeg"); }
|
|
152
175
|
|
|
176
|
+
function inspectProviderSetup(options = {}) {
|
|
177
|
+
if (!options.paths) return inspectProviderConfiguration(options);
|
|
178
|
+
const resolved = {};
|
|
179
|
+
for (const providerId of ["claude", "codex"]) {
|
|
180
|
+
const value = String(options.paths[providerId] || "").trim();
|
|
181
|
+
if (!value) {
|
|
182
|
+
resolved[providerId] = null;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
fs.accessSync(value, fs.constants.X_OK);
|
|
187
|
+
resolved[providerId] = path.resolve(value);
|
|
188
|
+
} catch (_) {
|
|
189
|
+
resolved[providerId] = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const registry = new ProviderRegistry({ defaultProvider: options.defaultProvider || null });
|
|
193
|
+
registry.registerProvider(createClaudeProvider({ resolveExecutable: () => resolved.claude }));
|
|
194
|
+
registry.registerProvider(createCodexProvider({ resolveExecutable: () => resolved.codex }));
|
|
195
|
+
return inspectProviderConfiguration({
|
|
196
|
+
...options,
|
|
197
|
+
registry,
|
|
198
|
+
executableStatus(providerId) {
|
|
199
|
+
const configured = String(options.paths[providerId] || "").trim();
|
|
200
|
+
if (resolved[providerId]) {
|
|
201
|
+
return { state: "available", executable: resolved[providerId], configured: true, reason: null };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
state: configured ? "missing" : "unconfigured",
|
|
205
|
+
executable: null,
|
|
206
|
+
configured: !!configured,
|
|
207
|
+
reason: configured
|
|
208
|
+
? `Configured ${providerId} executable was not found`
|
|
209
|
+
: `${providerId} is not configured or installed`,
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function providerStatusLine(provider) {
|
|
216
|
+
const availability = provider.availability.state;
|
|
217
|
+
if (availability !== "available") return `${provider.label}: ${availability}`;
|
|
218
|
+
const version = provider.compatibility.version ? ` ${provider.compatibility.version}` : "";
|
|
219
|
+
return `${provider.label}: ${provider.compatibility.state}${version}; auth ${provider.auth.state}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
153
222
|
function findWhisperModel() {
|
|
154
223
|
const candidates = [
|
|
155
224
|
"/opt/homebrew/share/whisper-cpp/models/ggml-base.en.bin",
|
|
@@ -285,7 +354,7 @@ async function setupDaemon(platform) {
|
|
|
285
354
|
const serviceName = "claude-telegram-bot";
|
|
286
355
|
const serviceFile = `/etc/systemd/system/${serviceName}.service`;
|
|
287
356
|
const unit = `[Unit]
|
|
288
|
-
Description=
|
|
357
|
+
Description=Open Claudia Provider Bot
|
|
289
358
|
After=network.target
|
|
290
359
|
|
|
291
360
|
[Service]
|
|
@@ -483,7 +552,7 @@ function updateEnvKey(key, value) {
|
|
|
483
552
|
return line;
|
|
484
553
|
});
|
|
485
554
|
if (!found) updated.push(`${key}=${value}`);
|
|
486
|
-
|
|
555
|
+
atomicWriteFileSync(ENV_FILE, updated.join("\n"), { backup: true, mode: 0o600 });
|
|
487
556
|
}
|
|
488
557
|
|
|
489
558
|
// ── Main setup (resumable) ─────────────────────────────────────────
|
|
@@ -497,6 +566,7 @@ const { validateKazee } = require("./core/kazee-probe");
|
|
|
497
566
|
const KAZEE_DEFAULT_URL = "https://chat.inet.africa";
|
|
498
567
|
|
|
499
568
|
async function main() {
|
|
569
|
+
ensureConfigDir(CONFIG_DIR);
|
|
500
570
|
// Check if this is an auth subcommand
|
|
501
571
|
const args = process.argv.slice(2);
|
|
502
572
|
if (args.includes("--auth") || args.includes("auth")) {
|
|
@@ -504,10 +574,12 @@ async function main() {
|
|
|
504
574
|
return;
|
|
505
575
|
}
|
|
506
576
|
|
|
507
|
-
console.log("\n
|
|
577
|
+
console.log("\n Open Claudia — Setup\n");
|
|
508
578
|
|
|
509
579
|
// Load or create state
|
|
510
|
-
|
|
580
|
+
// Resume files from the Claude-only wizard must pass through the provider
|
|
581
|
+
// step once before the new config keys can be serialized safely.
|
|
582
|
+
let state = upgradeSetupState(loadSetupState());
|
|
511
583
|
const resuming = state.completedSteps.length > 0;
|
|
512
584
|
|
|
513
585
|
if (resuming) {
|
|
@@ -525,27 +597,71 @@ async function main() {
|
|
|
525
597
|
if (!state.completedSteps.includes("prerequisites")) {
|
|
526
598
|
console.log("Checking prerequisites...\n");
|
|
527
599
|
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
console.log(
|
|
531
|
-
console.log(
|
|
532
|
-
process.exit(1);
|
|
600
|
+
const providerReport = inspectProviderSetup();
|
|
601
|
+
for (const provider of providerReport.providers) {
|
|
602
|
+
console.log(` ${providerStatusLine(provider)}`);
|
|
603
|
+
if (provider.recovery) console.log(` Recovery: ${provider.recovery.split("\n")[0]}`);
|
|
533
604
|
}
|
|
534
|
-
console.log(
|
|
605
|
+
console.log("");
|
|
535
606
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
607
|
+
const autoProviders = providerReport.providers
|
|
608
|
+
.filter((provider) => provider.availability.state === "available"
|
|
609
|
+
&& provider.compatibility.state === "compatible")
|
|
610
|
+
.map((provider) => provider.id);
|
|
611
|
+
const choice = (await ask(" Providers to configure (auto/claude/codex/both/none) [auto]: "))
|
|
612
|
+
.trim().toLowerCase() || "auto";
|
|
613
|
+
const wanted = choice === "none" ? []
|
|
614
|
+
: choice === "both" ? ["claude", "codex"]
|
|
615
|
+
: choice === "claude" || choice === "codex" ? [choice]
|
|
616
|
+
: autoProviders;
|
|
617
|
+
const detectedById = Object.fromEntries(providerReport.providers.map((provider) => [
|
|
618
|
+
provider.id,
|
|
619
|
+
provider.availability.executable || null,
|
|
620
|
+
]));
|
|
621
|
+
const providerPaths = { claude: null, codex: null };
|
|
622
|
+
for (const providerId of wanted) {
|
|
623
|
+
let detected = detectedById[providerId]
|
|
624
|
+
|| state.data?.[providerId === "claude" ? "claudePath" : "codexPath"]
|
|
625
|
+
|| (providerId === "claude" ? findClaude() : findCodex());
|
|
626
|
+
if (!detected) {
|
|
627
|
+
const envKey = providerId === "claude" ? "CLAUDE_PATH" : "CODEX_PATH";
|
|
628
|
+
const entered = (await ask(` ${envKey} (blank to leave unconfigured): `)).trim();
|
|
629
|
+
if (entered) {
|
|
630
|
+
try {
|
|
631
|
+
fs.accessSync(entered, fs.constants.X_OK);
|
|
632
|
+
detected = path.resolve(entered);
|
|
633
|
+
} catch (_) {
|
|
634
|
+
console.log(` ${entered} is not executable; ${providerId} remains unconfigured.`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
providerPaths[providerId] = detected || null;
|
|
639
|
+
}
|
|
640
|
+
const finalProviderReport = inspectProviderSetup({ paths: providerPaths });
|
|
641
|
+
for (const provider of finalProviderReport.providers) {
|
|
642
|
+
if (provider.availability.state === "available" && provider.compatibility.state !== "compatible") {
|
|
643
|
+
console.log(` ${providerStatusLine(provider)}`);
|
|
644
|
+
if (provider.recovery) console.log(` Recovery: ${provider.recovery.split("\n")[0]}`);
|
|
547
645
|
}
|
|
548
|
-
|
|
646
|
+
}
|
|
647
|
+
const configuredProviders = finalProviderReport.providers
|
|
648
|
+
.filter((provider) => provider.availability.state === "available"
|
|
649
|
+
&& provider.compatibility.state === "compatible")
|
|
650
|
+
.map((provider) => provider.id);
|
|
651
|
+
let defaultProvider = configuredProviders[0] || null;
|
|
652
|
+
if (configuredProviders.length > 1) {
|
|
653
|
+
const reportedDefault = providerReport.defaultProvider.providerId;
|
|
654
|
+
const suggestedDefault = configuredProviders.includes(reportedDefault)
|
|
655
|
+
? reportedDefault
|
|
656
|
+
: configuredProviders[0];
|
|
657
|
+
const answer = (await ask(` Default provider (claude/codex) [${suggestedDefault}]: `))
|
|
658
|
+
.trim().toLowerCase();
|
|
659
|
+
const proposed = answer || suggestedDefault;
|
|
660
|
+
defaultProvider = configuredProviders.includes(proposed) ? proposed : configuredProviders[0];
|
|
661
|
+
}
|
|
662
|
+
if (!configuredProviders.length) {
|
|
663
|
+
console.log(" No model provider configured. Setup, doctor, health, and the web UI will still work;");
|
|
664
|
+
console.log(" model turns will return NO_AVAILABLE_PROVIDER until Claude Code or OpenAI Codex is configured.\n");
|
|
549
665
|
}
|
|
550
666
|
|
|
551
667
|
const platform = detectPlatform();
|
|
@@ -592,7 +708,9 @@ async function main() {
|
|
|
592
708
|
}
|
|
593
709
|
}
|
|
594
710
|
|
|
595
|
-
state.data.claudePath =
|
|
711
|
+
state.data.claudePath = providerPaths.claude || "";
|
|
712
|
+
state.data.codexPath = providerPaths.codex || "";
|
|
713
|
+
state.data.defaultProvider = defaultProvider || "";
|
|
596
714
|
state.data.platform = platform;
|
|
597
715
|
state.data.ffmpegPath = ffmpegPath || "";
|
|
598
716
|
state.data.whisperPath = whisperPath || "";
|
|
@@ -648,7 +766,7 @@ async function main() {
|
|
|
648
766
|
|
|
649
767
|
// Send confirmation
|
|
650
768
|
console.log(" Sending test message...");
|
|
651
|
-
const sent = await sendMessage(token, userInfo.chatId, "Setup verified! Your
|
|
769
|
+
const sent = await sendMessage(token, userInfo.chatId, "Setup verified! Your Open Claudia bot is connected.");
|
|
652
770
|
if (sent) {
|
|
653
771
|
console.log(" Test message sent! Check your Telegram.\n");
|
|
654
772
|
} else {
|
|
@@ -747,6 +865,15 @@ async function main() {
|
|
|
747
865
|
`TELEGRAM_CHAT_ID=${d.chatId}`,
|
|
748
866
|
`WORKSPACE=${d.workspace}`,
|
|
749
867
|
`CLAUDE_PATH=${d.claudePath}`,
|
|
868
|
+
`CODEX_PATH=${d.codexPath}`,
|
|
869
|
+
`DEFAULT_PROVIDER=${d.defaultProvider}`,
|
|
870
|
+
"UTILITY_PROVIDER=active",
|
|
871
|
+
"PROVIDER_FALLBACKS=",
|
|
872
|
+
"DREAM_PROVIDER=",
|
|
873
|
+
"MEMORY_PROVIDER=",
|
|
874
|
+
"RECALL_PROVIDER=",
|
|
875
|
+
"ENFORCER_PROVIDER=",
|
|
876
|
+
"SUBAGENT_PROVIDER=",
|
|
750
877
|
`WHISPER_CLI=${d.whisperPath}`,
|
|
751
878
|
`WHISPER_MODEL=${d.whisperModel}`,
|
|
752
879
|
`FFMPEG=${d.ffmpegPath}`,
|
|
@@ -763,9 +890,30 @@ async function main() {
|
|
|
763
890
|
envLines.push(`KAZEE_BOT_USER_ID=${d.kazee.botUserId || ""}`);
|
|
764
891
|
envLines.push(`KAZEE_OWNER_USER_ID=${d.kazee.ownerUserId || ""}`);
|
|
765
892
|
}
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
893
|
+
const existingEnv = fs.existsSync(ENV_FILE)
|
|
894
|
+
? fs.readFileSync(ENV_FILE, "utf8").split(/\r?\n/).reduce((result, line) => {
|
|
895
|
+
const index = line.indexOf("=");
|
|
896
|
+
if (index > 0) result[line.slice(0, index).trim()] = line.slice(index + 1).trim();
|
|
897
|
+
return result;
|
|
898
|
+
}, {})
|
|
899
|
+
: {};
|
|
900
|
+
const mergedEnv = { ...existingEnv };
|
|
901
|
+
const preserveExistingPolicy = new Set([
|
|
902
|
+
"UTILITY_PROVIDER", "PROVIDER_FALLBACKS", "DREAM_PROVIDER", "MEMORY_PROVIDER",
|
|
903
|
+
"RECALL_PROVIDER", "ENFORCER_PROVIDER", "SUBAGENT_PROVIDER",
|
|
904
|
+
]);
|
|
905
|
+
for (const line of envLines) {
|
|
906
|
+
const index = line.indexOf("=");
|
|
907
|
+
const key = line.slice(0, index);
|
|
908
|
+
const value = line.slice(index + 1);
|
|
909
|
+
// A resumed/second setup must never erase the other provider's existing
|
|
910
|
+
// credential/config merely because its prompt was left blank.
|
|
911
|
+
if (preserveExistingPolicy.has(key) && Object.prototype.hasOwnProperty.call(existingEnv, key)) continue;
|
|
912
|
+
if (!value && Object.prototype.hasOwnProperty.call(existingEnv, key)) continue;
|
|
913
|
+
mergedEnv[key] = value;
|
|
914
|
+
}
|
|
915
|
+
const env = Object.entries(mergedEnv).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
916
|
+
atomicWriteFileSync(ENV_FILE, env, { backup: true, mode: 0o600 });
|
|
769
917
|
console.log(` Config saved: ${ENV_FILE}\n`);
|
|
770
918
|
|
|
771
919
|
if (!fs.existsSync(CRONS_FILE)) fs.writeFileSync(CRONS_FILE, "[]");
|
|
@@ -794,13 +942,25 @@ async function main() {
|
|
|
794
942
|
// Done — clean up state file
|
|
795
943
|
clearSetupState();
|
|
796
944
|
console.log("\n Setup complete! Start chatting with your bot in Telegram.\n");
|
|
797
|
-
rl.close();
|
|
945
|
+
if (rl) rl.close();
|
|
798
946
|
}
|
|
799
947
|
|
|
800
948
|
// Allow running auth directly: node setup.js auth
|
|
801
|
-
module.exports = {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
949
|
+
module.exports = {
|
|
950
|
+
runAuth,
|
|
951
|
+
loadAuth,
|
|
952
|
+
saveAuth,
|
|
953
|
+
AUTH_FILE,
|
|
954
|
+
inspectProviderSetup,
|
|
955
|
+
providerStatusLine,
|
|
956
|
+
upgradeSetupState,
|
|
957
|
+
main,
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
if (require.main === module) {
|
|
961
|
+
main().catch((e) => {
|
|
962
|
+
if (rl) rl.close();
|
|
963
|
+
console.error(e);
|
|
964
|
+
process.exit(1);
|
|
965
|
+
});
|
|
966
|
+
}
|
package/soul.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Soul
|
|
2
|
+
|
|
3
|
+
You are Sumeet's personal AI assistant: Open Claudia running the selected coding-agent provider, available 24/7 through his configured chat channels.
|
|
4
|
+
|
|
5
|
+
## Identity
|
|
6
|
+
- Name: Open Claudia
|
|
7
|
+
- Tone: Direct, concise, technical. Like a sharp senior engineer who's also a good friend.
|
|
8
|
+
- Never over-explain. Sumeet is technical — he gets it.
|
|
9
|
+
- Mobile-first: keep messages short. Send files for anything long.
|
|
10
|
+
|
|
11
|
+
## About Sumeet
|
|
12
|
+
- Full-stack developer / founder
|
|
13
|
+
- Works across many projects in ~/Workspace
|
|
14
|
+
- Tech stack: Node.js, React/Next.js, MongoDB, Kubernetes, Docker
|
|
15
|
+
- Uses Cursor IDE and Claude Code CLI daily
|
|
16
|
+
- Communicates via Telegram on mobile when away from desk
|
|
17
|
+
|
|
18
|
+
## Key Projects
|
|
19
|
+
- **crm** — Kazee CRM platform (monorepo: crm-backend, frontend, helpdesk-frontend, spaces-backend, spaces-frontend, workflow-kanban). MongoDB, Node.js backend, Next.js frontends.
|
|
20
|
+
- **metriq / metriq-api** — Metrics/analytics platform
|
|
21
|
+
- **ticket-central** — Ticketing system
|
|
22
|
+
- **hr-hub** — HR management
|
|
23
|
+
- **kazee-connect-subscription-service** — Subscription/billing service
|
|
24
|
+
- **k8s / kubernetes-applications / kazee-infratructure** — Kubernetes deployment configs
|
|
25
|
+
- **janus-devops / janus-webrtc** — WebRTC infrastructure
|
|
26
|
+
|
|
27
|
+
## Working Style
|
|
28
|
+
- Prefers action over discussion. Do the thing, then explain what you did.
|
|
29
|
+
- Likes plans for big changes, but hates over-planning small ones.
|
|
30
|
+
- Appreciates when you proactively flag issues or suggest improvements.
|
|
31
|
+
- Send files directly via Telegram when output is long.
|
|
32
|
+
|
|
33
|
+
## What You Can Do
|
|
34
|
+
- Write, edit, and refactor code across all projects
|
|
35
|
+
- Run commands, tests, builds, deployments
|
|
36
|
+
- Send files, images, code snippets directly to Telegram
|
|
37
|
+
- Run scheduled checks and report results
|
|
38
|
+
- Monitor git status, PRs, deployments
|
|
39
|
+
- Help with architecture decisions and code reviews
|
|
@@ -86,6 +86,11 @@ const reused = review.applyAction({
|
|
|
86
86
|
});
|
|
87
87
|
assert.strictEqual(reused.appliedTo, "chat-mobile", "update records the cross-project reuse");
|
|
88
88
|
assert.deepStrictEqual(packs.readPack("mobile-app-deploy").applied_on, ["spaces", "chat-mobile"], "applied_on grew");
|
|
89
|
+
const abilityFile = path.join(process.env.PACKS_DIR, "mobile-app-deploy", "PACK.md");
|
|
90
|
+
assert.ok(fs.existsSync(`${abilityFile}.bak`), "reviewer writes retain the previous complete pack backup");
|
|
91
|
+
assert.strictEqual(fs.statSync(abilityFile).mode & 0o777, 0o600, "reviewer memory files stay owner-only");
|
|
92
|
+
assert.deepStrictEqual(fs.readdirSync(path.dirname(abilityFile)).filter((name) => name.endsWith(".tmp")), [],
|
|
93
|
+
"atomic reviewer writes leave no partial temporary file");
|
|
89
94
|
|
|
90
95
|
graph.syncFromCorpus(packs, entStub);
|
|
91
96
|
edges = graph.allEdges();
|
package/test-approval-async.js
CHANGED
|
@@ -7,6 +7,7 @@ const assert = require("assert");
|
|
|
7
7
|
const fs = require("fs");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
const path = require("path");
|
|
10
|
+
const { spawn } = require("child_process");
|
|
10
11
|
|
|
11
12
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "approval-async-test-"));
|
|
12
13
|
process.env.APPROVALS_DIR = path.join(tmp, "approvals");
|
|
@@ -48,6 +49,30 @@ const approvals = require("./core/approvals");
|
|
|
48
49
|
assert.strictEqual(replay.ok, false, "second consume refused (single use)");
|
|
49
50
|
assert.ok(/already consumed/.test(replay.reason));
|
|
50
51
|
|
|
52
|
+
const concurrent = approvals.create({ tool: "fleet", command: CMD, channelId: "c1", adapter: "tg" });
|
|
53
|
+
approvals.decide(concurrent.id, "approved", "c1");
|
|
54
|
+
const consumeInChild = () => new Promise((resolve, reject) => {
|
|
55
|
+
const source = [
|
|
56
|
+
`const approvals = require(${JSON.stringify(path.join(__dirname, "core", "approvals.js"))});`,
|
|
57
|
+
`process.stdout.write(JSON.stringify(approvals.consume(${JSON.stringify(concurrent.id)}, ${JSON.stringify(CMD)})));`,
|
|
58
|
+
].join("\n");
|
|
59
|
+
const child = spawn(process.execPath, ["-e", source], {
|
|
60
|
+
env: { ...process.env, APPROVALS_DIR: approvals.APPROVALS_DIR },
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
+
});
|
|
63
|
+
let stdout = "";
|
|
64
|
+
let stderr = "";
|
|
65
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
66
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
67
|
+
child.once("error", reject);
|
|
68
|
+
child.once("close", (code) => {
|
|
69
|
+
if (code !== 0) reject(new Error(stderr || `approval child exited ${code}`));
|
|
70
|
+
else resolve(JSON.parse(stdout));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
const concurrentResults = await Promise.all([consumeInChild(), consumeInChild()]);
|
|
74
|
+
assert.strictEqual(concurrentResults.filter((result) => result.ok).length, 1, "concurrent redemption has exactly one winner");
|
|
75
|
+
|
|
51
76
|
// ── unknown id fails closed ──
|
|
52
77
|
assert.strictEqual(approvals.consume("apr_nope", CMD).ok, false, "unknown id refused");
|
|
53
78
|
|
|
@@ -62,11 +87,45 @@ const approvals = require("./core/approvals");
|
|
|
62
87
|
assert.strictEqual(expired.ok, false, "expired token refused");
|
|
63
88
|
assert.ok(/expired/.test(expired.reason));
|
|
64
89
|
|
|
65
|
-
// ──
|
|
66
|
-
|
|
67
|
-
const
|
|
90
|
+
// ── command binding: the whole visible payload is hashed. Commands too long
|
|
91
|
+
// to display completely are rejected rather than approving a hidden suffix.
|
|
92
|
+
const boundCmd = "open-claudia tool run fleet purge " + "x".repeat(400);
|
|
93
|
+
const lrec = approvals.create({ tool: "fleet", verb: "purge", tier: "destructive", command: boundCmd, channelId: "c1", adapter: "tg" });
|
|
94
|
+
assert.strictEqual(lrec.command, boundCmd, "the exact approved command is retained");
|
|
95
|
+
assert.match(lrec.commandHash, /^[a-f0-9]{64}$/, "the approval carries a full-payload digest");
|
|
68
96
|
approvals.decide(lrec.id, "approved", "c1");
|
|
69
|
-
|
|
97
|
+
const changedSuffix = `${boundCmd.slice(0, -8)}DIFFERENT`;
|
|
98
|
+
assert.strictEqual(approvals.consume(lrec.id, changedSuffix).ok, false, "a suffix mismatch is refused");
|
|
99
|
+
assert.strictEqual(approvals.consume(lrec.id, boundCmd).ok, true, "the full exact command still redeems");
|
|
100
|
+
assert.throws(
|
|
101
|
+
() => approvals.create({ tool: "fleet", command: "open-claudia tool run fleet purge " + "x".repeat(1200) }),
|
|
102
|
+
(error) => error && error.code === "APPROVAL_PAYLOAD_TOO_LARGE",
|
|
103
|
+
"an approval never hides an undisplayed command suffix",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const legacy = approvals.create({ tool: "fleet", command: CMD, channelId: "c1", adapter: "tg" });
|
|
107
|
+
approvals.decide(legacy.id, "approved", "c1");
|
|
108
|
+
const legacyFile = path.join(approvals.APPROVALS_DIR, `${legacy.id}.json`);
|
|
109
|
+
const legacyRecord = JSON.parse(fs.readFileSync(legacyFile, "utf8"));
|
|
110
|
+
delete legacyRecord.commandHash;
|
|
111
|
+
fs.writeFileSync(legacyFile, JSON.stringify(legacyRecord));
|
|
112
|
+
assert.strictEqual(approvals.consume(legacy.id, CMD).ok, true, "safe short pre-hash approvals remain redeemable");
|
|
113
|
+
|
|
114
|
+
const unsafeLegacy = approvals.create({ tool: "fleet", command: CMD, channelId: "c1", adapter: "tg" });
|
|
115
|
+
approvals.decide(unsafeLegacy.id, "approved", "c1");
|
|
116
|
+
const unsafeLegacyFile = path.join(approvals.APPROVALS_DIR, `${unsafeLegacy.id}.json`);
|
|
117
|
+
const unsafeLegacyRecord = JSON.parse(fs.readFileSync(unsafeLegacyFile, "utf8"));
|
|
118
|
+
delete unsafeLegacyRecord.commandHash;
|
|
119
|
+
unsafeLegacyRecord.command = "x".repeat(600);
|
|
120
|
+
fs.writeFileSync(unsafeLegacyFile, JSON.stringify(unsafeLegacyRecord));
|
|
121
|
+
assert.strictEqual(
|
|
122
|
+
approvals.consume(unsafeLegacy.id, unsafeLegacyRecord.command).ok,
|
|
123
|
+
false,
|
|
124
|
+
"legacy approvals with an undisplayed suffix require a fresh decision",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const quotedCommand = approvals.formatApprovedCommand(["open-claudia", "tool", "run", "fleet", "purge", "node one", "it's-safe"]);
|
|
128
|
+
assert.strictEqual(quotedCommand, "open-claudia tool run fleet purge 'node one' 'it'\"'\"'s-safe'");
|
|
70
129
|
|
|
71
130
|
// ── waitForDecision default fast-path window is 90s (not the old 5 min) ──
|
|
72
131
|
const src = fs.readFileSync(path.join(__dirname, "core", "approvals.js"), "utf-8");
|