@inetafrica/open-claudia 2.15.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +87 -66
  4. package/bin/agent.js +44 -18
  5. package/bin/cli.js +30 -28
  6. package/bin/dream.js +5 -11
  7. package/bin/loopback-client.js +29 -5
  8. package/bin/schedule.js +19 -5
  9. package/bin/tool.js +23 -5
  10. package/bot-agent.js +5 -2350
  11. package/bot.js +66 -3
  12. package/channels/telegram/adapter.js +6 -1
  13. package/channels/voice/adapter.js +1 -0
  14. package/channels/voice/manage.js +43 -5
  15. package/config-dir.js +3 -11
  16. package/core/actions.js +149 -40
  17. package/core/approvals.js +136 -29
  18. package/core/auth-flow.js +103 -19
  19. package/core/config-dir.js +19 -0
  20. package/core/config.js +115 -69
  21. package/core/doctor.js +111 -57
  22. package/core/dream.js +219 -62
  23. package/core/enforcer.js +0 -0
  24. package/core/entities.js +2 -1
  25. package/core/fsutil.js +21 -4
  26. package/core/handlers.js +503 -217
  27. package/core/ideas.js +2 -1
  28. package/core/jobs.js +589 -72
  29. package/core/lessons.js +3 -2
  30. package/core/loopback.js +42 -6
  31. package/core/memory-mutation-queue.js +241 -0
  32. package/core/migration-backup.js +1060 -0
  33. package/core/pack-review.js +295 -88
  34. package/core/packs.js +4 -3
  35. package/core/persona.js +2 -1
  36. package/core/process-tree.js +19 -2
  37. package/core/provider-migration.js +39 -0
  38. package/core/provider-status.js +80 -0
  39. package/core/providers/claude-events.js +117 -0
  40. package/core/providers/claude-hook.js +114 -0
  41. package/core/providers/claude.js +296 -0
  42. package/core/providers/codex-events.js +121 -0
  43. package/core/providers/codex-hook.js +280 -0
  44. package/core/providers/codex.js +286 -0
  45. package/core/providers/contract.js +153 -0
  46. package/core/providers/env.js +148 -0
  47. package/core/providers/events.js +170 -0
  48. package/core/providers/hook-command.js +22 -0
  49. package/core/providers/index.js +240 -0
  50. package/core/providers/utility-policy.js +269 -0
  51. package/core/recall/classic.js +2 -2
  52. package/core/recall/discoverer.js +71 -22
  53. package/core/recall/metrics.js +27 -1
  54. package/core/recall/tuning.js +4 -1
  55. package/core/recall/warm-walker.js +151 -108
  56. package/core/recall-filter.js +86 -61
  57. package/core/router.js +79 -7
  58. package/core/run-context.js +185 -0
  59. package/core/runner.js +1414 -1290
  60. package/core/scheduler.js +515 -210
  61. package/core/side-chat.js +346 -0
  62. package/core/state.js +1084 -97
  63. package/core/subagent.js +72 -98
  64. package/core/system-prompt.js +462 -279
  65. package/core/tool-guard.js +73 -39
  66. package/core/tools.js +22 -5
  67. package/core/transcripts.js +30 -1
  68. package/core/usage-log.js +19 -4
  69. package/core/utility-agent.js +654 -0
  70. package/core/web-auth.js +29 -13
  71. package/docs/CHANNEL_DESIGN.md +181 -0
  72. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  73. package/docs/PROVIDER_MIGRATION.md +67 -0
  74. package/health.js +50 -39
  75. package/package.json +48 -4
  76. package/setup.js +198 -38
  77. package/soul.md +39 -0
  78. package/test-ability-extraction.js +5 -0
  79. package/test-approval-async.js +63 -4
  80. package/test-cursor-removal.js +334 -0
  81. package/test-enforcer.js +70 -27
  82. package/test-fixtures/current-provider-parity.json +132 -0
  83. package/test-fixtures/fake-agent-cli.js +477 -0
  84. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  85. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  86. package/test-fixtures/migrations/claude-only/state.json +5 -0
  87. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  88. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  89. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  90. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  91. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  92. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  93. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  94. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  95. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  96. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  97. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  98. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  99. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  100. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  101. package/test-fixtures/migrations/missing-project/state.json +4 -0
  102. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  103. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  104. package/test-fixtures/migrations/multi-user/state.json +6 -0
  105. package/test-fixtures/provider-event-samples.json +17 -0
  106. package/test-learning-e2e.js +5 -0
  107. package/test-memory-mutation-queue.js +191 -0
  108. package/test-provider-boot-matrix.js +399 -0
  109. package/test-provider-bootstrap.js +158 -0
  110. package/test-provider-capabilities.js +232 -0
  111. package/test-provider-claude.js +206 -0
  112. package/test-provider-codex.js +228 -0
  113. package/test-provider-compaction.js +371 -0
  114. package/test-provider-core-prompt.js +264 -0
  115. package/test-provider-coupling-audit.js +90 -0
  116. package/test-provider-dream.js +312 -0
  117. package/test-provider-enforcer.js +252 -0
  118. package/test-provider-env-isolation.js +150 -0
  119. package/test-provider-events.js +141 -0
  120. package/test-provider-fixture.js +332 -0
  121. package/test-provider-gateway.js +508 -0
  122. package/test-provider-language.js +89 -0
  123. package/test-provider-migration-backup.js +424 -0
  124. package/test-provider-pack-review.js +436 -0
  125. package/test-provider-parity-e2e.js +537 -0
  126. package/test-provider-prompt-parity.js +89 -0
  127. package/test-provider-recall.js +271 -0
  128. package/test-provider-registry.js +251 -0
  129. package/test-provider-resume-parity.js +41 -0
  130. package/test-provider-run-lifecycle.js +349 -0
  131. package/test-provider-runner.js +271 -0
  132. package/test-provider-scheduler.js +689 -0
  133. package/test-provider-session-commands.js +185 -0
  134. package/test-provider-session-history.js +205 -0
  135. package/test-provider-side-chat.js +337 -0
  136. package/test-provider-state-migration.js +316 -0
  137. package/test-provider-stream-decoder.js +69 -0
  138. package/test-provider-subagent.js +228 -0
  139. package/test-provider-tool-hooks.js +360 -0
  140. package/test-recall-discoverer.js +1 -0
  141. package/test-recall-engine.js +3 -3
  142. package/test-recall-evolution.js +18 -0
  143. package/test-runner-watchdog-static.js +16 -8
  144. package/test-single-runtime.js +35 -0
  145. package/test-telegram-poll-recovery.js +56 -0
  146. package/test-tool-guard.js +56 -0
  147. package/test-tools.js +19 -0
  148. package/test-unified-identity.js +3 -1
  149. package/test-usage-accounting.js +92 -20
  150. package/test-utility-provider-policy.js +486 -0
  151. package/web.js +130 -35
package/health.js CHANGED
@@ -3,6 +3,7 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const https = require("https");
5
5
  const CONFIG_DIR = require("./config-dir");
6
+ const { inspectProviderConfiguration } = require("./core/provider-status");
6
7
 
7
8
  const ENV_FILE = path.join(CONFIG_DIR, ".env");
8
9
  const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
@@ -12,7 +13,6 @@ const REQUIRED_ENV_KEYS = [
12
13
  "TELEGRAM_BOT_TOKEN",
13
14
  "TELEGRAM_CHAT_ID",
14
15
  "WORKSPACE",
15
- "CLAUDE_PATH",
16
16
  ];
17
17
 
18
18
  // Optional but recommended
@@ -60,24 +60,21 @@ function binaryExists(binPath) {
60
60
  }
61
61
 
62
62
  /**
63
- * Test if Claude CLI is authenticated
63
+ * Test Claude authentication using the provider's local auth-status command.
64
+ * This compatibility export deliberately never sends a model prompt.
64
65
  */
65
- function testClaudeAuth(claudePath) {
66
- try {
67
- execSync(`"${claudePath}" -p "say ok" --max-budget-usd 0.01 --output-format text 2>&1`, {
68
- encoding: "utf-8",
69
- timeout: 30000,
70
- stdio: "pipe",
71
- });
72
- return { ok: true };
73
- } catch (e) {
74
- const errMsg = (e.stderr || e.stdout || e.message || "").toLowerCase();
75
- if (errMsg.includes("auth") || errMsg.includes("login") || errMsg.includes("api key") || errMsg.includes("unauthorized")) {
76
- return { ok: false, reason: "not_authenticated", message: "Claude CLI is not authenticated. Run 'claude auth' or 'claude login'." };
77
- }
78
- // Other errors (like timeout) are OK - Claude is accessible
79
- return { ok: true };
80
- }
66
+ function testClaudeAuth(_claudePath, options = {}) {
67
+ const summary = inspectProviderConfiguration({
68
+ registry: options.registry,
69
+ probeAuth: true,
70
+ });
71
+ const claude = summary.providers.find((provider) => provider.id === "claude");
72
+ if (claude?.auth.state === "authenticated") return { ok: true };
73
+ return {
74
+ ok: false,
75
+ reason: claude?.auth.state || "not_checked",
76
+ message: claude?.recovery || "Claude Code authentication could not be confirmed.",
77
+ };
81
78
  }
82
79
 
83
80
  /**
@@ -228,12 +225,12 @@ function checkAuthFile() {
228
225
  /**
229
226
  * Run all health checks
230
227
  * @param {Object} options - Options
231
- * @param {boolean} options.quick - Skip slow checks (Claude auth, Telegram API)
228
+ * @param {boolean} options.quick - Skip auth and external channel checks
232
229
  * @param {boolean} options.fix - Attempt to fix issues (create directories)
233
230
  * @returns {Promise<Object>} Health check results
234
231
  */
235
232
  async function runHealthChecks(options = {}) {
236
- const { quick = false, fix = false } = options;
233
+ const { quick = false, fix = false, skipNetwork = false } = options;
237
234
  const results = {
238
235
  ok: true,
239
236
  checks: {},
@@ -267,27 +264,41 @@ async function runHealthChecks(options = {}) {
267
264
  results.checks.env = { ok: true };
268
265
  }
269
266
 
270
- // 3. Check Claude CLI exists
271
- const claudePath = env.CLAUDE_PATH;
272
- if (!binaryExists(claudePath)) {
273
- results.checks.claude_binary = { ok: false, reason: "not_found", message: `Claude CLI not found at: ${claudePath}` };
274
- results.ok = false;
275
- results.errors.push(results.checks.claude_binary.message);
276
- } else {
277
- results.checks.claude_binary = { ok: true, path: claudePath };
278
-
279
- // 4. Check Claude authentication (slow)
280
- if (!quick) {
281
- results.checks.claude_auth = testClaudeAuth(claudePath);
282
- if (!results.checks.claude_auth.ok) {
283
- results.ok = false;
284
- results.errors.push(results.checks.claude_auth.message);
285
- }
286
- }
267
+ // 3. Provider availability, compatibility, and authentication. Provider
268
+ // adapters use only local --version/help/auth-status commands here.
269
+ const providerSummary = inspectProviderConfiguration({
270
+ registry: options.registry,
271
+ probeAuth: !quick,
272
+ });
273
+ results.providerSummary = providerSummary;
274
+ for (const provider of providerSummary.providers) {
275
+ const compatible = provider.availability.state === "available"
276
+ && provider.compatibility.state === "compatible";
277
+ const authenticated = quick || provider.auth.state === "authenticated";
278
+ const ok = compatible && authenticated;
279
+ const key = `provider_${provider.id}`;
280
+ results.checks[key] = {
281
+ ok,
282
+ warn: !ok,
283
+ availability: provider.availability.state,
284
+ compatibility: provider.compatibility.state,
285
+ auth: provider.auth.state,
286
+ message: provider.recovery,
287
+ };
288
+ if (!ok && provider.recovery) results.warnings.push(`${provider.label}: ${provider.recovery.split("\n")[0]}`);
289
+ }
290
+ results.checks.default_provider = {
291
+ ok: !!providerSummary.defaultProvider.providerId,
292
+ warn: !providerSummary.defaultProvider.providerId,
293
+ providerId: providerSummary.defaultProvider.providerId,
294
+ message: providerSummary.defaultProvider.reason,
295
+ };
296
+ if (!providerSummary.defaultProvider.providerId) {
297
+ results.warnings.push(`${providerSummary.defaultProvider.reason}. Model turns are unavailable until a provider is configured.`);
287
298
  }
288
299
 
289
- // 5. Check Telegram token (slow)
290
- if (!quick && env.TELEGRAM_BOT_TOKEN) {
300
+ // 4. Check Telegram token (external network; never part of provider health).
301
+ if (!quick && !skipNetwork && env.TELEGRAM_BOT_TOKEN) {
291
302
  results.checks.telegram = await testTelegramToken(env.TELEGRAM_BOT_TOKEN);
292
303
  if (!results.checks.telegram.ok) {
293
304
  results.ok = false;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.15.0",
4
- "description": "Your always-on AI coding assistant Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
3
+ "version": "3.0.0",
4
+ "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
7
7
  "open-claudia": "./bin/cli.js"
@@ -9,7 +9,8 @@
9
9
  "scripts": {
10
10
  "setup": "node setup.js",
11
11
  "start": "node bot.js",
12
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-web-sessions.js"
12
+ "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js",
13
+ "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js"
13
14
  },
14
15
  "files": [
15
16
  "bot.js",
@@ -30,6 +31,8 @@
30
31
  ".env.example",
31
32
  "README.md",
32
33
  "CHANGELOG.md",
34
+ "soul.md",
35
+ "docs/",
33
36
  "test-runner-watchdog-static.js",
34
37
  "test-usage-accounting.js",
35
38
  "test-recall-engine.js",
@@ -64,10 +67,51 @@
64
67
  "test-kazee-message-id.js",
65
68
  "test-delivery-contract.js",
66
69
  "test-run-lock.js",
67
- "test-web-sessions.js"
70
+ "test-web-sessions.js",
71
+ "test-provider-fixture.js",
72
+ "test-provider-core-prompt.js",
73
+ "test-provider-prompt-parity.js",
74
+ "test-provider-env-isolation.js",
75
+ "test-provider-resume-parity.js",
76
+ "test-provider-bootstrap.js",
77
+ "test-provider-registry.js",
78
+ "test-provider-stream-decoder.js",
79
+ "test-provider-events.js",
80
+ "test-provider-claude.js",
81
+ "test-provider-codex.js",
82
+ "test-provider-runner.js",
83
+ "test-provider-run-lifecycle.js",
84
+ "test-provider-gateway.js",
85
+ "test-provider-side-chat.js",
86
+ "test-single-runtime.js",
87
+ "test-provider-migration-backup.js",
88
+ "test-provider-state-migration.js",
89
+ "test-provider-session-history.js",
90
+ "test-provider-session-commands.js",
91
+ "test-provider-compaction.js",
92
+ "test-provider-scheduler.js",
93
+ "test-cursor-removal.js",
94
+ "test-utility-provider-policy.js",
95
+ "test-provider-subagent.js",
96
+ "test-provider-recall.js",
97
+ "test-provider-pack-review.js",
98
+ "test-memory-mutation-queue.js",
99
+ "test-provider-dream.js",
100
+ "test-provider-enforcer.js",
101
+ "test-provider-tool-hooks.js",
102
+ "test-provider-boot-matrix.js",
103
+ "test-provider-capabilities.js",
104
+ "test-provider-language.js",
105
+ "test-provider-coupling-audit.js",
106
+ "test-provider-parity-e2e.js",
107
+ "test-telegram-poll-recovery.js",
108
+ "test-fixtures/"
68
109
  ],
69
110
  "keywords": [
70
111
  "claude",
112
+ "codex",
113
+ "coding-agent",
114
+ "provider-agnostic",
71
115
  "telegram",
72
116
  "ai",
73
117
  "assistant",
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
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
20
- const ask = (q) => new Promise((r) => rl.question(q, r));
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
- fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
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
- fs.writeFileSync(SETUP_STATE_FILE, JSON.stringify(state, null, 2));
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=Claude Code Telegram Bot
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
- fs.writeFileSync(ENV_FILE, updated.join("\n"));
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 Claude Code Telegram Bot — Setup\n");
577
+ console.log("\n Open Claudia — Setup\n");
508
578
 
509
579
  // Load or create state
510
- let state = loadSetupState() || { completedSteps: [], data: {} };
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 claudePath = findClaude();
529
- if (!claudePath) {
530
- console.log(" Claude Code CLI not found. Install it first:");
531
- console.log(" https://docs.anthropic.com/en/docs/claude-code");
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(` Claude CLI: ${claudePath}`);
605
+ console.log("");
535
606
 
536
- try {
537
- execSync(`"${claudePath}" -p "say ok" --max-budget-usd 0.01 --output-format text 2>&1`, {
538
- encoding: "utf-8", timeout: 30000,
539
- });
540
- console.log(" Claude auth: OK");
541
- } catch (e) {
542
- const errMsg = (e.stderr || e.stdout || e.message || "").toLowerCase();
543
- if (errMsg.includes("auth") || errMsg.includes("login") || errMsg.includes("api key") || errMsg.includes("unauthorized")) {
544
- console.log(" Claude auth: NOT LOGGED IN");
545
- console.log(" Run 'claude auth' or 'claude login' to authenticate first.");
546
- process.exit(1);
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
- console.log(" Claude auth: OK");
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 = 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 Claude Code bot is connected.");
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 env = envLines.join("\n");
767
-
768
- fs.writeFileSync(ENV_FILE, env);
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 = { runAuth, loadAuth, saveAuth, AUTH_FILE };
802
-
803
- main().catch((e) => {
804
- console.error(e);
805
- process.exit(1);
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();