@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.
Files changed (157) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +21 -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 +105 -7
  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 +127 -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 +55 -0
  26. package/core/handlers.js +503 -217
  27. package/core/ideas.js +2 -1
  28. package/core/identity.js +8 -11
  29. package/core/io.js +24 -3
  30. package/core/jobs.js +589 -72
  31. package/core/lessons.js +3 -2
  32. package/core/loopback.js +45 -8
  33. package/core/memory-mutation-queue.js +241 -0
  34. package/core/migration-backup.js +1060 -0
  35. package/core/pack-review.js +295 -88
  36. package/core/packs.js +4 -3
  37. package/core/persona.js +2 -1
  38. package/core/process-tree.js +19 -2
  39. package/core/provider-migration.js +39 -0
  40. package/core/provider-status.js +80 -0
  41. package/core/providers/claude-events.js +117 -0
  42. package/core/providers/claude-hook.js +114 -0
  43. package/core/providers/claude.js +296 -0
  44. package/core/providers/codex-events.js +121 -0
  45. package/core/providers/codex-hook.js +280 -0
  46. package/core/providers/codex.js +286 -0
  47. package/core/providers/contract.js +153 -0
  48. package/core/providers/env.js +148 -0
  49. package/core/providers/events.js +170 -0
  50. package/core/providers/hook-command.js +22 -0
  51. package/core/providers/index.js +240 -0
  52. package/core/providers/utility-policy.js +269 -0
  53. package/core/recall/classic.js +2 -2
  54. package/core/recall/discoverer.js +71 -22
  55. package/core/recall/metrics.js +27 -1
  56. package/core/recall/tuning.js +4 -1
  57. package/core/recall/warm-walker.js +151 -108
  58. package/core/recall-filter.js +86 -61
  59. package/core/router.js +79 -7
  60. package/core/run-context.js +185 -0
  61. package/core/runner.js +1415 -1282
  62. package/core/scheduler.js +515 -210
  63. package/core/side-chat.js +346 -0
  64. package/core/state.js +1096 -95
  65. package/core/subagent.js +72 -98
  66. package/core/system-prompt.js +462 -279
  67. package/core/tool-guard.js +73 -39
  68. package/core/tools.js +22 -5
  69. package/core/transcripts.js +30 -1
  70. package/core/usage-log.js +19 -4
  71. package/core/utility-agent.js +654 -0
  72. package/core/web-auth.js +29 -13
  73. package/core/web-sessions.js +78 -0
  74. package/docs/CHANNEL_DESIGN.md +181 -0
  75. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  76. package/docs/PROVIDER_MIGRATION.md +67 -0
  77. package/health.js +50 -39
  78. package/package.json +51 -4
  79. package/setup.js +198 -38
  80. package/soul.md +39 -0
  81. package/test-ability-extraction.js +5 -0
  82. package/test-approval-async.js +63 -4
  83. package/test-cursor-removal.js +334 -0
  84. package/test-delivery-contract.js +88 -0
  85. package/test-enforcer.js +70 -27
  86. package/test-fixtures/current-provider-parity.json +132 -0
  87. package/test-fixtures/fake-agent-cli.js +477 -0
  88. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  89. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  90. package/test-fixtures/migrations/claude-only/state.json +5 -0
  91. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  92. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  93. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  94. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  95. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  96. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  97. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  98. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  99. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  100. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  101. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  102. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  103. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  104. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  105. package/test-fixtures/migrations/missing-project/state.json +4 -0
  106. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  107. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  108. package/test-fixtures/migrations/multi-user/state.json +6 -0
  109. package/test-fixtures/provider-event-samples.json +17 -0
  110. package/test-learning-e2e.js +5 -0
  111. package/test-memory-mutation-queue.js +191 -0
  112. package/test-provider-boot-matrix.js +399 -0
  113. package/test-provider-bootstrap.js +158 -0
  114. package/test-provider-capabilities.js +232 -0
  115. package/test-provider-claude.js +206 -0
  116. package/test-provider-codex.js +228 -0
  117. package/test-provider-compaction.js +371 -0
  118. package/test-provider-core-prompt.js +264 -0
  119. package/test-provider-coupling-audit.js +90 -0
  120. package/test-provider-dream.js +312 -0
  121. package/test-provider-enforcer.js +252 -0
  122. package/test-provider-env-isolation.js +150 -0
  123. package/test-provider-events.js +141 -0
  124. package/test-provider-fixture.js +332 -0
  125. package/test-provider-gateway.js +508 -0
  126. package/test-provider-language.js +89 -0
  127. package/test-provider-migration-backup.js +424 -0
  128. package/test-provider-pack-review.js +436 -0
  129. package/test-provider-parity-e2e.js +537 -0
  130. package/test-provider-prompt-parity.js +89 -0
  131. package/test-provider-recall.js +271 -0
  132. package/test-provider-registry.js +251 -0
  133. package/test-provider-resume-parity.js +41 -0
  134. package/test-provider-run-lifecycle.js +349 -0
  135. package/test-provider-runner.js +271 -0
  136. package/test-provider-scheduler.js +689 -0
  137. package/test-provider-session-commands.js +185 -0
  138. package/test-provider-session-history.js +205 -0
  139. package/test-provider-side-chat.js +337 -0
  140. package/test-provider-state-migration.js +316 -0
  141. package/test-provider-stream-decoder.js +69 -0
  142. package/test-provider-subagent.js +228 -0
  143. package/test-provider-tool-hooks.js +360 -0
  144. package/test-recall-discoverer.js +1 -0
  145. package/test-recall-engine.js +3 -3
  146. package/test-recall-evolution.js +18 -0
  147. package/test-run-lock.js +63 -0
  148. package/test-runner-watchdog-static.js +16 -8
  149. package/test-single-runtime.js +35 -0
  150. package/test-telegram-poll-recovery.js +56 -0
  151. package/test-tool-guard.js +56 -0
  152. package/test-tools.js +19 -0
  153. package/test-unified-identity.js +3 -1
  154. package/test-usage-accounting.js +92 -20
  155. package/test-utility-provider-policy.js +486 -0
  156. package/test-web-sessions.js +74 -0
  157. package/web.js +159 -45
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env node
2
+
3
+ const assert = require("assert");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawn } = require("child_process");
8
+
9
+ const fixture = path.join(__dirname, "test-fixtures", "fake-agent-cli.js");
10
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-provider-fixture-"));
11
+ const activeChildren = new Set();
12
+ let sequence = 0;
13
+
14
+ function fixturePath(label) {
15
+ sequence += 1;
16
+ return path.join(tmp, `${String(sequence).padStart(2, "0")}-${label}.json`);
17
+ }
18
+
19
+ function minimalChildEnv(controls) {
20
+ const env = {};
21
+ for (const key of ["PATH", "TMPDIR", "TMP", "TEMP", "SystemRoot", "COMSPEC", "PATHEXT"]) {
22
+ if (process.env[key] != null) env[key] = process.env[key];
23
+ }
24
+ env.HOME = tmp;
25
+ return { ...env, ...controls };
26
+ }
27
+
28
+ function runFixture({ kind, args = [], controls = {}, onSpawn, direct = false, timeoutMs = 3000 } = {}) {
29
+ const capturePath = controls.FAKE_AGENT_CAPTURE || fixturePath(`${kind || "unknown"}-capture`);
30
+ const env = minimalChildEnv({
31
+ FAKE_AGENT_KIND: kind,
32
+ FAKE_AGENT_CAPTURE: capturePath,
33
+ ...controls,
34
+ });
35
+
36
+ return new Promise((resolve, reject) => {
37
+ const child = spawn(direct ? fixture : process.execPath, direct ? args : [fixture, ...args], {
38
+ cwd: __dirname,
39
+ env,
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ });
42
+ activeChildren.add(child);
43
+ const stdoutChunks = [];
44
+ const stderrChunks = [];
45
+ let settled = false;
46
+ let forceTimer = null;
47
+ const deadline = setTimeout(() => {
48
+ child.kill("SIGTERM");
49
+ forceTimer = setTimeout(() => child.kill("SIGKILL"), 100);
50
+ }, timeoutMs);
51
+ child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
52
+ child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
53
+ child.on("error", (error) => {
54
+ if (settled) return;
55
+ settled = true;
56
+ clearTimeout(deadline);
57
+ if (forceTimer) clearTimeout(forceTimer);
58
+ activeChildren.delete(child);
59
+ reject(error);
60
+ });
61
+ child.on("close", (code, signal) => {
62
+ if (settled) return;
63
+ settled = true;
64
+ clearTimeout(deadline);
65
+ if (forceTimer) clearTimeout(forceTimer);
66
+ activeChildren.delete(child);
67
+ resolve({
68
+ code,
69
+ signal,
70
+ closedAt: Date.now(),
71
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
72
+ stdoutChunks,
73
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
74
+ capturePath,
75
+ capture: fs.existsSync(capturePath)
76
+ ? JSON.parse(fs.readFileSync(capturePath, "utf8"))
77
+ : null,
78
+ });
79
+ });
80
+ if (onSpawn) {
81
+ try {
82
+ onSpawn(child, stdoutChunks);
83
+ } catch (error) {
84
+ child.kill("SIGKILL");
85
+ reject(error);
86
+ }
87
+ }
88
+ });
89
+ }
90
+
91
+ function cleanup() {
92
+ for (const child of activeChildren) child.kill("SIGKILL");
93
+ activeChildren.clear();
94
+ fs.rmSync(tmp, { recursive: true, force: true });
95
+ }
96
+
97
+ function parseJsonLines(text) {
98
+ return text
99
+ .split(/\r?\n/)
100
+ .filter((line) => line.trim())
101
+ .map((line) => JSON.parse(line));
102
+ }
103
+
104
+ function waitForFile(filePath, timeoutMs = 1000) {
105
+ const started = Date.now();
106
+ return new Promise((resolve, reject) => {
107
+ const poll = () => {
108
+ if (fs.existsSync(filePath)) return resolve();
109
+ if (Date.now() - started >= timeoutMs) {
110
+ return reject(new Error(`timed out waiting for ${path.basename(filePath)}`));
111
+ }
112
+ setTimeout(poll, 5);
113
+ };
114
+ poll();
115
+ });
116
+ }
117
+
118
+ async function main() {
119
+ assert.ok(fs.existsSync(fixture), "fake provider CLI fixture must exist");
120
+ if (process.platform !== "win32") {
121
+ fs.accessSync(fixture, fs.constants.X_OK);
122
+ }
123
+
124
+ // Capture argv exactly, but reduce all requested environment inspection to
125
+ // boolean key presence. Credential values must never reach the capture file.
126
+ const secretValues = ["claude-secret-value", "openai-secret-value"];
127
+ const captured = await runFixture({
128
+ kind: "claude",
129
+ args: ["-p", "--resume", "claude-resume-1", "hello"],
130
+ controls: {
131
+ FAKE_AGENT_ENV_KEYS: "CLAUDE_CODE_OAUTH_TOKEN,OPENAI_API_KEY,ABSENT_PROVIDER_KEY",
132
+ CLAUDE_CODE_OAUTH_TOKEN: secretValues[0],
133
+ OPENAI_API_KEY: secretValues[1],
134
+ },
135
+ direct: process.platform !== "win32",
136
+ });
137
+ assert.strictEqual(captured.code, 0);
138
+ assert.deepStrictEqual(captured.capture.argv, ["-p", "--resume", "claude-resume-1", "hello"]);
139
+ assert.deepStrictEqual(captured.capture.envPresence, {
140
+ ABSENT_PROVIDER_KEY: false,
141
+ CLAUDE_CODE_OAUTH_TOKEN: true,
142
+ OPENAI_API_KEY: true,
143
+ });
144
+ const captureText = fs.readFileSync(captured.capturePath, "utf8");
145
+ for (const secret of secretValues) {
146
+ assert.ok(!captureText.includes(secret), "capture must never contain environment values");
147
+ }
148
+
149
+ // Claude success includes a fresh session, tool start/end, usage, and result.
150
+ const claude = await runFixture({
151
+ kind: "claude",
152
+ controls: {
153
+ FAKE_AGENT_TOOL: "1",
154
+ FAKE_AGENT_SESSION_ID: "claude-fresh-1",
155
+ FAKE_AGENT_TEXT: "fixture says café 👋",
156
+ },
157
+ });
158
+ const claudeEvents = parseJsonLines(claude.stdout);
159
+ assert.strictEqual(claudeEvents[0].type, "system");
160
+ assert.strictEqual(claudeEvents[0].session_id, "claude-fresh-1");
161
+ assert.ok(claudeEvents.some((event) => event.type === "assistant"
162
+ && event.message.content.some((block) => block.type === "tool_use")));
163
+ assert.ok(claudeEvents.some((event) => event.type === "user"
164
+ && event.message.content.some((block) => block.type === "tool_result")));
165
+ const claudeResult = claudeEvents.find((event) => event.type === "result");
166
+ assert.strictEqual(claudeResult.session_id, "claude-fresh-1");
167
+ assert.strictEqual(claudeResult.result, "fixture says café 👋");
168
+ assert.deepStrictEqual(claudeResult.usage, {
169
+ input_tokens: 12,
170
+ cache_read_input_tokens: 3,
171
+ cache_creation_input_tokens: 2,
172
+ output_tokens: 7,
173
+ });
174
+
175
+ // Codex success uses its native JSONL vocabulary and keeps a resumed thread id.
176
+ const codex = await runFixture({
177
+ kind: "codex",
178
+ args: ["exec", "resume", "codex-resume-1", "hello"],
179
+ controls: {
180
+ FAKE_AGENT_TOOL: "1",
181
+ FAKE_AGENT_SESSION_ID: "codex-resume-1",
182
+ FAKE_AGENT_TEXT: "codex fixture result",
183
+ },
184
+ });
185
+ const codexEvents = parseJsonLines(codex.stdout);
186
+ assert.deepStrictEqual(codexEvents[0], { type: "thread.started", thread_id: "codex-resume-1" });
187
+ assert.ok(codexEvents.some((event) => event.type === "item.started"
188
+ && event.item.type === "command_execution"));
189
+ assert.ok(codexEvents.some((event) => event.type === "item.completed"
190
+ && event.item.type === "command_execution" && event.item.exit_code === 0));
191
+ assert.ok(codexEvents.some((event) => event.type === "item.completed"
192
+ && event.item.type === "agent_message" && event.item.text === "codex fixture result"));
193
+ assert.deepStrictEqual(codexEvents.at(-1), {
194
+ type: "turn.completed",
195
+ usage: { input_tokens: 12, cached_input_tokens: 3, output_tokens: 7 },
196
+ });
197
+
198
+ // Resume parsing remains valid when Codex configuration overrides precede
199
+ // its positional thread id (the supported shape introduced in Task 7).
200
+ const optionAwareResume = await runFixture({
201
+ kind: "codex",
202
+ args: ["exec", "resume", "-c", 'sandbox_mode="read-only"', "codex-option-resume-1", "hello"],
203
+ });
204
+ assert.deepStrictEqual(parseJsonLines(optionAwareResume.stdout)[0], {
205
+ type: "thread.started",
206
+ thread_id: "codex-option-resume-1",
207
+ });
208
+
209
+ // Transport controls cover a byte split inside a multibyte character,
210
+ // coalesced JSONL, CRLF, and both trailing-newline states.
211
+ const split = await runFixture({
212
+ kind: "claude",
213
+ controls: {
214
+ FAKE_AGENT_TEXT: "split 👋 event",
215
+ FAKE_AGENT_CHUNK_MODE: "split",
216
+ FAKE_AGENT_CHUNK_DELAY_MS: "20",
217
+ },
218
+ });
219
+ assert.strictEqual(split.capture.writePlan.mode, "split");
220
+ assert.strictEqual(split.capture.writePlan.writes.length, 2);
221
+ assert.strictEqual(split.capture.writePlan.splitInsideUtf8, true,
222
+ "split mode must cut inside the multibyte marker");
223
+ assert.strictEqual(split.capture.writePlan.writes[0], split.capture.writePlan.splitOffset);
224
+ assert.strictEqual(parseJsonLines(split.stdout).find((event) => event.type === "result").result, "split 👋 event");
225
+
226
+ const coalesced = await runFixture({
227
+ kind: "codex",
228
+ controls: {
229
+ FAKE_AGENT_CHUNK_MODE: "coalesce",
230
+ FAKE_AGENT_LINE_ENDING: "crlf",
231
+ FAKE_AGENT_FINAL_NEWLINE: "0",
232
+ },
233
+ });
234
+ assert.ok(coalesced.stdout.includes("\r\n"), "CRLF mode must use CRLF framing");
235
+ assert.ok(!coalesced.stdout.endsWith("\n"), "final-newline control must omit the last newline");
236
+ assert.ok(parseJsonLines(coalesced.stdout).length >= 3, "coalesced mode must carry several JSONL records");
237
+ assert.strictEqual(coalesced.capture.writePlan.writes.length, 1,
238
+ "coalesced mode must perform one stdout write");
239
+ assert.strictEqual(coalesced.capture.writePlan.lineEnding, "crlf");
240
+ assert.strictEqual(coalesced.capture.writePlan.finalNewline, false);
241
+
242
+ const newline = await runFixture({
243
+ kind: "claude",
244
+ controls: { FAKE_AGENT_FINAL_NEWLINE: "1" },
245
+ });
246
+ assert.ok(newline.stdout.endsWith("\n"), "fixture must also support a final newline");
247
+
248
+ // Provider-auth failures and arbitrary non-zero exits are deterministic.
249
+ for (const kind of ["claude", "codex"]) {
250
+ const auth = await runFixture({
251
+ kind,
252
+ controls: { FAKE_AGENT_SCENARIO: "auth-failure" },
253
+ });
254
+ assert.strictEqual(auth.code, 1);
255
+ assert.match(auth.stderr, /authentication failed/i);
256
+ const authEvents = parseJsonLines(auth.stdout);
257
+ if (kind === "claude") {
258
+ assert.ok(authEvents.some((event) => event.type === "result"
259
+ && event.subtype === "error_during_execution" && event.is_error === true));
260
+ assert.ok(!authEvents.some((event) => event.subtype === "success"));
261
+ } else {
262
+ assert.ok(authEvents.some((event) => event.type === "error"
263
+ && /authentication failed/i.test(event.message)));
264
+ assert.ok(authEvents.some((event) => event.type === "turn.failed"));
265
+ assert.ok(!authEvents.some((event) => event.type === "turn.completed"));
266
+ }
267
+ }
268
+
269
+ const nonZero = await runFixture({
270
+ kind: "claude",
271
+ controls: {
272
+ FAKE_AGENT_SCENARIO: "non-zero",
273
+ FAKE_AGENT_EXIT_CODE: "23",
274
+ },
275
+ });
276
+ assert.strictEqual(nonZero.code, 23);
277
+
278
+ // A terminal event can precede child close; lifecycle tests depend on this.
279
+ let terminalAt = null;
280
+ let eofAt = null;
281
+ let aliveAtTerminal = false;
282
+ const delayed = await runFixture({
283
+ kind: "codex",
284
+ controls: { FAKE_AGENT_CLOSE_DELAY_MS: "80" },
285
+ onSpawn(child) {
286
+ let observed = "";
287
+ child.stdout.once("end", () => { eofAt = Date.now(); });
288
+ child.stdout.on("data", (chunk) => {
289
+ observed += chunk.toString("utf8");
290
+ if (terminalAt == null && observed.includes('"type":"turn.completed"')) {
291
+ terminalAt = Date.now();
292
+ aliveAtTerminal = child.exitCode === null && child.signalCode === null;
293
+ }
294
+ });
295
+ },
296
+ });
297
+ assert.strictEqual(delayed.code, 0);
298
+ assert.ok(terminalAt, "delayed-close scenario must emit a terminal event");
299
+ assert.ok(eofAt, "delayed-close scenario must explicitly close stdout before process close");
300
+ assert.strictEqual(aliveAtTerminal, true, "child must still be alive when terminal output arrives");
301
+ assert.ok(eofAt >= terminalAt, "stdout EOF must follow the terminal event");
302
+ assert.ok(delayed.closedAt - eofAt >= 65, "delayed-close control must hold after stdout EOF");
303
+
304
+ // Forced termination waits after output, records only the signal name, and
305
+ // then re-raises it so callers observe a genuinely signalled child.
306
+ const signalPath = fixturePath("signal");
307
+ let signalChild;
308
+ const forcedPromise = runFixture({
309
+ kind: "claude",
310
+ controls: {
311
+ FAKE_AGENT_SCENARIO: "forced-termination",
312
+ FAKE_AGENT_SIGNAL_CAPTURE: signalPath,
313
+ },
314
+ onSpawn(child) {
315
+ signalChild = child;
316
+ child.stdout.once("data", () => child.kill("SIGTERM"));
317
+ },
318
+ });
319
+ const forced = await forcedPromise;
320
+ assert.strictEqual(forced.code, null);
321
+ assert.strictEqual(forced.signal, "SIGTERM");
322
+ await waitForFile(signalPath);
323
+ assert.deepStrictEqual(JSON.parse(fs.readFileSync(signalPath, "utf8")), { signal: "SIGTERM" });
324
+ assert.ok(signalChild.pid > 0);
325
+
326
+ console.log("provider fixture OK");
327
+ }
328
+
329
+ main().catch((error) => {
330
+ console.error(error.stack || error.message);
331
+ process.exitCode = 1;
332
+ }).finally(cleanup);