@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
normalizeDecoderError,
|
|
5
|
+
normalizedUsage,
|
|
6
|
+
providerErrorFlags,
|
|
7
|
+
} = require("./events");
|
|
8
|
+
|
|
9
|
+
function createClaudeParserState() {
|
|
10
|
+
return {
|
|
11
|
+
sessionId: null,
|
|
12
|
+
sawText: false,
|
|
13
|
+
terminal: false,
|
|
14
|
+
tools: new Map(),
|
|
15
|
+
seenUsage: new Set(),
|
|
16
|
+
totalCostUsd: 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sessionEvent(sessionId, state) {
|
|
21
|
+
if (!sessionId || state.sessionId === sessionId) return null;
|
|
22
|
+
state.sessionId = sessionId;
|
|
23
|
+
return { type: "session", sessionId };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function claudeErrorMessage(rawEvent) {
|
|
27
|
+
if (typeof rawEvent.result === "string" && rawEvent.result.trim()) return rawEvent.result.trim();
|
|
28
|
+
if (Array.isArray(rawEvent.errors) && rawEvent.errors.length) return rawEvent.errors.map(String).join("; ");
|
|
29
|
+
if (typeof rawEvent.message === "string" && rawEvent.message.trim()) return rawEvent.message.trim();
|
|
30
|
+
return "Claude provider failed";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeClaudeEvent(rawEvent, state = createClaudeParserState()) {
|
|
34
|
+
if (!rawEvent || typeof rawEvent !== "object") return [];
|
|
35
|
+
const decoderFailure = normalizeDecoderError(rawEvent);
|
|
36
|
+
if (decoderFailure) return [decoderFailure];
|
|
37
|
+
|
|
38
|
+
if (rawEvent.type === "system" && rawEvent.subtype === "init") {
|
|
39
|
+
const event = sessionEvent(rawEvent.session_id, state);
|
|
40
|
+
return event ? [event] : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (rawEvent.type === "stream_event") {
|
|
44
|
+
const delta = rawEvent.event?.type === "content_block_delta" ? rawEvent.event.delta : null;
|
|
45
|
+
if (delta?.type === "text_delta" && typeof delta.text === "string" && delta.text) {
|
|
46
|
+
state.sawText = true;
|
|
47
|
+
return [{ type: "text_delta", text: delta.text }];
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (rawEvent.type === "assistant") {
|
|
53
|
+
const events = [];
|
|
54
|
+
for (const block of rawEvent.message?.content || []) {
|
|
55
|
+
if (block?.type === "text" && typeof block.text === "string" && block.text) {
|
|
56
|
+
state.sawText = true;
|
|
57
|
+
events.push({ type: "text_final", text: block.text });
|
|
58
|
+
} else if (block?.type === "tool_use" && block.id && block.name) {
|
|
59
|
+
state.tools.set(block.id, block.name);
|
|
60
|
+
events.push({
|
|
61
|
+
type: "tool_start",
|
|
62
|
+
id: block.id,
|
|
63
|
+
name: block.name,
|
|
64
|
+
detail: block.input ?? null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const usage = normalizedUsage(rawEvent.message?.usage, "provider_call", state.seenUsage);
|
|
69
|
+
if (usage) events.push(usage);
|
|
70
|
+
return events;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (rawEvent.type === "user") {
|
|
74
|
+
const events = [];
|
|
75
|
+
for (const block of rawEvent.message?.content || []) {
|
|
76
|
+
if (block?.type !== "tool_result" || !block.tool_use_id) continue;
|
|
77
|
+
const name = state.tools.get(block.tool_use_id) || "unknown";
|
|
78
|
+
events.push({
|
|
79
|
+
type: "tool_end",
|
|
80
|
+
id: block.tool_use_id,
|
|
81
|
+
name,
|
|
82
|
+
detail: block.content ?? null,
|
|
83
|
+
exitCode: block.is_error ? 1 : 0,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return events;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (rawEvent.type === "result") {
|
|
90
|
+
if (state.terminal) return [];
|
|
91
|
+
state.terminal = true;
|
|
92
|
+
if (Number.isFinite(rawEvent.total_cost_usd)) state.totalCostUsd = rawEvent.total_cost_usd;
|
|
93
|
+
if (rawEvent.is_error || /^error/.test(String(rawEvent.subtype || ""))) {
|
|
94
|
+
const message = claudeErrorMessage(rawEvent);
|
|
95
|
+
return [{ type: "error", message, ...providerErrorFlags(message) }];
|
|
96
|
+
}
|
|
97
|
+
const events = [];
|
|
98
|
+
const session = sessionEvent(rawEvent.session_id, state);
|
|
99
|
+
if (session) events.push(session);
|
|
100
|
+
const usage = normalizedUsage(rawEvent.usage, "turn", state.seenUsage);
|
|
101
|
+
if (usage) events.push(usage);
|
|
102
|
+
const hasStructuredOutput = Object.prototype.hasOwnProperty.call(rawEvent, "structured_output");
|
|
103
|
+
const structuredText = hasStructuredOutput ? JSON.stringify(rawEvent.structured_output) : "";
|
|
104
|
+
events.push({
|
|
105
|
+
type: "result",
|
|
106
|
+
text: hasStructuredOutput
|
|
107
|
+
? (structuredText === undefined ? "" : structuredText)
|
|
108
|
+
: (state.sawText ? "" : (typeof rawEvent.result === "string" ? rawEvent.result : "")),
|
|
109
|
+
sessionId: rawEvent.session_id || state.sessionId || null,
|
|
110
|
+
});
|
|
111
|
+
return events;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { createClaudeParserState, normalizeClaudeEvent };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { resolveConfigDir } = require("../config-dir");
|
|
7
|
+
const { atomicWriteFileSync } = require("../fsutil");
|
|
8
|
+
const { buildDenyGateCommand } = require("./hook-command");
|
|
9
|
+
|
|
10
|
+
const HOOK_TIMEOUT_SECONDS = 10;
|
|
11
|
+
|
|
12
|
+
function hookError(code, message, cause) {
|
|
13
|
+
const error = new Error(message);
|
|
14
|
+
error.name = "ToolHookInitializationError";
|
|
15
|
+
error.code = code;
|
|
16
|
+
if (cause) error.cause = cause;
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildClaudeHookSettings(options = {}) {
|
|
21
|
+
return {
|
|
22
|
+
hooks: {
|
|
23
|
+
PreToolUse: [
|
|
24
|
+
{
|
|
25
|
+
matcher: "Bash",
|
|
26
|
+
hooks: [
|
|
27
|
+
{
|
|
28
|
+
type: "command",
|
|
29
|
+
command: buildDenyGateCommand(options),
|
|
30
|
+
timeout: HOOK_TIMEOUT_SECONDS,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function settingsBody(options = {}) {
|
|
40
|
+
return `${JSON.stringify(buildClaudeHookSettings(options), null, 2)}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultSettingsFile() {
|
|
44
|
+
return process.env.DENY_GATE_SETTINGS
|
|
45
|
+
? path.resolve(process.env.DENY_GATE_SETTINGS)
|
|
46
|
+
: path.join(resolveConfigDir(), "deny-gate.settings.json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertSafeTarget(file) {
|
|
50
|
+
let stat;
|
|
51
|
+
try { stat = fs.lstatSync(file); }
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === "ENOENT") return;
|
|
54
|
+
throw hookError("TOOL_HOOK_INIT_FAILED", "Could not inspect Claude tool-hook settings", error);
|
|
55
|
+
}
|
|
56
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
57
|
+
throw hookError("UNSAFE_TOOL_HOOK_PATH", "Refusing to replace a non-regular Claude tool-hook settings path");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function verifySettingsFile(file, expected) {
|
|
62
|
+
assertSafeTarget(file);
|
|
63
|
+
let actual;
|
|
64
|
+
try { actual = fs.readFileSync(file, "utf8"); }
|
|
65
|
+
catch (error) { throw hookError("TOOL_HOOK_INIT_FAILED", "Could not verify Claude tool-hook settings", error); }
|
|
66
|
+
if (actual !== expected) {
|
|
67
|
+
throw hookError("TOOL_HOOK_CONFIG_UNVERIFIED", "Generated Claude tool-hook settings failed content verification");
|
|
68
|
+
}
|
|
69
|
+
try { fs.chmodSync(file, 0o600); }
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (process.platform !== "win32") {
|
|
72
|
+
throw hookError("TOOL_HOOK_INIT_FAILED", "Could not make Claude tool-hook settings private", error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ensureClaudeHookSettings(options = {}) {
|
|
78
|
+
const file = path.resolve(options.settingsFile || defaultSettingsFile());
|
|
79
|
+
const expected = settingsBody(options);
|
|
80
|
+
assertSafeTarget(file);
|
|
81
|
+
try {
|
|
82
|
+
if (fs.readFileSync(file, "utf8") === expected) {
|
|
83
|
+
verifySettingsFile(file, expected);
|
|
84
|
+
return file;
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error.code && !["ENOENT", "EACCES", "EPERM", "EISDIR"].includes(error.code)) throw error;
|
|
88
|
+
}
|
|
89
|
+
try { atomicWriteFileSync(file, expected, { mode: 0o600 }); }
|
|
90
|
+
catch (error) { throw hookError("TOOL_HOOK_INIT_FAILED", "Could not generate Claude tool-hook settings", error); }
|
|
91
|
+
verifySettingsFile(file, expected);
|
|
92
|
+
return file;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function prepareClaudeHook(options = {}) {
|
|
96
|
+
const settingsFile = ensureClaudeHookSettings(options);
|
|
97
|
+
const fingerprint = crypto.createHash("sha256").update(settingsBody(options)).digest("hex");
|
|
98
|
+
return Object.freeze({ providerId: "claude", settingsFile, fingerprint });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createClaudeHookTransport(options = {}) {
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
prepare() { return prepareClaudeHook(options); },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
HOOK_TIMEOUT_SECONDS,
|
|
109
|
+
buildClaudeHookSettings,
|
|
110
|
+
createClaudeHookTransport,
|
|
111
|
+
defaultSettingsFile,
|
|
112
|
+
ensureClaudeHookSettings,
|
|
113
|
+
prepareClaudeHook,
|
|
114
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const config = require("../config");
|
|
4
|
+
const auth = require("../auth-flow");
|
|
5
|
+
const { createJsonlDecoder } = require("./events");
|
|
6
|
+
const { createClaudeParserState, normalizeClaudeEvent } = require("./claude-events");
|
|
7
|
+
const { createClaudeHookTransport } = require("./claude-hook");
|
|
8
|
+
|
|
9
|
+
const CLAUDE_MODELS = Object.freeze([
|
|
10
|
+
"claude-fable-5",
|
|
11
|
+
"claude-opus-4-8",
|
|
12
|
+
"claude-opus-4-7",
|
|
13
|
+
"claude-opus-4-6",
|
|
14
|
+
"claude-sonnet-4-6",
|
|
15
|
+
"claude-haiku-4-5-20251001",
|
|
16
|
+
]);
|
|
17
|
+
const CLAUDE_EFFORT_VALUES = Object.freeze(["low", "medium", "high", "xhigh", "max"]);
|
|
18
|
+
const CLAUDE_DEFAULT_MODEL = config.config.CLAUDE_MODEL || process.env.CLAUDE_MODEL || "claude-opus-4-8";
|
|
19
|
+
const REQUIRED_HELP_FLAGS = Object.freeze([
|
|
20
|
+
"--append-system-prompt",
|
|
21
|
+
"--output-format",
|
|
22
|
+
"--resume",
|
|
23
|
+
"--permission-mode",
|
|
24
|
+
"--settings",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const capabilities = Object.freeze({
|
|
28
|
+
resume: { support: "native", reason: null },
|
|
29
|
+
readOnlyMode: { support: "native", reason: null },
|
|
30
|
+
effort: { support: "native", values: [...CLAUDE_EFFORT_VALUES], reason: null },
|
|
31
|
+
budget: { support: "native", reason: null },
|
|
32
|
+
worktree: { support: "native", reason: null },
|
|
33
|
+
partialStreaming: { support: "native", reason: null },
|
|
34
|
+
maxTurns: { support: "native", reason: null },
|
|
35
|
+
imageFlag: { support: "unsupported", reason: "Claude Code has no dedicated image CLI flag; image ingress is represented in the prepared turn prompt" },
|
|
36
|
+
persistentUtilityStream: { support: "native", reason: null },
|
|
37
|
+
preToolHook: { support: "native", reason: null },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function providerError(code, message, details = {}) {
|
|
41
|
+
const error = new Error(message);
|
|
42
|
+
error.name = "ProviderInvocationError";
|
|
43
|
+
error.code = code;
|
|
44
|
+
error.details = details;
|
|
45
|
+
return error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requiredString(value, code, label) {
|
|
49
|
+
if (typeof value !== "string" || !value.trim()) throw providerError(code, `${label} is required`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function settingsOf(context) {
|
|
54
|
+
return context.providerSettings || context.settings || {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function preparedPrompt(context) {
|
|
58
|
+
const userPrompt = typeof context.userPrompt === "string"
|
|
59
|
+
? context.userPrompt
|
|
60
|
+
: (typeof context.prompt === "string" ? context.prompt : null);
|
|
61
|
+
if (userPrompt === null) throw providerError("INVALID_PROVIDER_CONTEXT", "userPrompt must be a string");
|
|
62
|
+
const dynamic = typeof context.dynamicContext === "string" ? context.dynamicContext : "";
|
|
63
|
+
return dynamic ? `${dynamic}\n\n${userPrompt}` : userPrompt;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function appendSessionArgs(args, context) {
|
|
67
|
+
if (context.resumeSessionId && context.continueSession) {
|
|
68
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "resumeSessionId and continueSession are mutually exclusive");
|
|
69
|
+
}
|
|
70
|
+
if (context.fresh) return;
|
|
71
|
+
if (context.resumeSessionId) args.push("--resume", String(context.resumeSessionId));
|
|
72
|
+
else if (context.continueSession) args.push("--continue");
|
|
73
|
+
else if (context.sessionId) args.push("--resume", String(context.sessionId));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function appendModelAndLimits(args, context, defaultModel, utility) {
|
|
77
|
+
const settings = settingsOf(context);
|
|
78
|
+
let model = context.model || settings.model || null;
|
|
79
|
+
if (!model && utility && context.tier) {
|
|
80
|
+
model = context.tier === "low"
|
|
81
|
+
? "claude-haiku-4-5-20251001"
|
|
82
|
+
: (context.tier === "medium" ? "claude-sonnet-4-6" : defaultModel);
|
|
83
|
+
}
|
|
84
|
+
args.push("--model", String(model || defaultModel));
|
|
85
|
+
|
|
86
|
+
const maxTurns = context.maxTurns ?? null;
|
|
87
|
+
if (maxTurns !== null) {
|
|
88
|
+
if (!Number.isSafeInteger(maxTurns) || maxTurns < 1) {
|
|
89
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "maxTurns must be a positive integer");
|
|
90
|
+
}
|
|
91
|
+
args.push("--max-turns", String(maxTurns));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const effort = context.effort || settings.effort || null;
|
|
95
|
+
if (effort) {
|
|
96
|
+
if (!CLAUDE_EFFORT_VALUES.includes(effort)) {
|
|
97
|
+
throw providerError("UNSUPPORTED_PROVIDER_SETTING", `Unsupported Claude effort: ${effort}`, { setting: "effort", value: effort });
|
|
98
|
+
}
|
|
99
|
+
args.push("--effort", effort);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const budget = context.budget ?? settings.budget ?? null;
|
|
103
|
+
if (budget !== null) {
|
|
104
|
+
const numericBudget = Number(budget);
|
|
105
|
+
if (!Number.isFinite(numericBudget) || numericBudget <= 0) {
|
|
106
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "budget must be a positive number");
|
|
107
|
+
}
|
|
108
|
+
args.push("--max-budget-usd", String(numericBudget));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function appendPermissionArgs(args, context, utility) {
|
|
113
|
+
const settings = settingsOf(context);
|
|
114
|
+
let permissionMode = context.permissionMode ?? settings.permissionMode ?? null;
|
|
115
|
+
if (utility && context.readOnly) permissionMode = "plan";
|
|
116
|
+
if (permissionMode === "ask") permissionMode = "plan";
|
|
117
|
+
if (permissionMode && permissionMode !== "direct") {
|
|
118
|
+
const supported = new Set(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]);
|
|
119
|
+
if (!supported.has(permissionMode)) {
|
|
120
|
+
throw providerError("UNSUPPORTED_PROVIDER_SETTING", `Unsupported Claude permission mode: ${permissionMode}`, { setting: "permissionMode", value: permissionMode });
|
|
121
|
+
}
|
|
122
|
+
args.push("--permission-mode", permissionMode);
|
|
123
|
+
} else {
|
|
124
|
+
args.push("--dangerously-skip-permissions");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function appendToolLists(args, context) {
|
|
129
|
+
for (const [field, flag] of [["allowedTools", "--allowedTools"], ["disallowedTools", "--disallowedTools"]]) {
|
|
130
|
+
if (context[field] === undefined || context[field] === null) continue;
|
|
131
|
+
if (!Array.isArray(context[field]) || context[field].some((tool) => typeof tool !== "string" || !tool.trim())) {
|
|
132
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", `${field} must be an array of non-empty strings`);
|
|
133
|
+
}
|
|
134
|
+
args.push(flag, context[field].join(","));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createClaudeProvider(options = {}) {
|
|
139
|
+
const resolveExecutable = options.resolveExecutable || (() => config.discoverProviderExecutable("claude"));
|
|
140
|
+
const defaultModel = options.defaultModel || CLAUDE_DEFAULT_MODEL;
|
|
141
|
+
const buildEnv = options.buildEnv || auth.claudeSubprocessEnv;
|
|
142
|
+
const getAuthStatus = options.getAuthStatus || ((binary) => auth.claudeProviderAuthStatus({ binary }));
|
|
143
|
+
const runCommand = options.runCommand || auth.runCommand;
|
|
144
|
+
// Factories are safe by default. `false` is reserved for hermetic argv tests
|
|
145
|
+
// that do not launch a real provider process.
|
|
146
|
+
const hookTransport = options.hookTransport === false
|
|
147
|
+
? null
|
|
148
|
+
: (options.hookTransport || createClaudeHookTransport());
|
|
149
|
+
|
|
150
|
+
function executable() {
|
|
151
|
+
return resolveExecutable() || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function requireExecutable() {
|
|
155
|
+
const binary = executable();
|
|
156
|
+
if (!binary) throw providerError("NO_AVAILABLE_PROVIDER", "Claude Code is not configured or installed", { providerId: "claude" });
|
|
157
|
+
return binary;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildInvocation(context, utility) {
|
|
161
|
+
if (!context || typeof context !== "object") throw providerError("INVALID_PROVIDER_CONTEXT", "provider context must be an object");
|
|
162
|
+
const coreInstructions = requiredString(context.coreInstructions, "MISSING_CORE_INSTRUCTIONS", "coreInstructions");
|
|
163
|
+
if ((context.images || context.imagePaths)?.length) {
|
|
164
|
+
throw providerError("UNSUPPORTED_PROVIDER_CAPABILITY", capabilities.imageFlag.reason, { capability: "imageFlag" });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const binary = requireExecutable();
|
|
168
|
+
const env = { ...buildEnv() };
|
|
169
|
+
let toolHookSettings = context.toolHookSettings || null;
|
|
170
|
+
if (hookTransport) {
|
|
171
|
+
try {
|
|
172
|
+
const prepared = hookTransport.prepare({ binary, env, cwd: context.cwd || process.cwd(), runCommand });
|
|
173
|
+
toolHookSettings = prepared.settingsFile;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw providerError(
|
|
176
|
+
"MANDATORY_TOOL_HOOK_UNAVAILABLE",
|
|
177
|
+
error?.message || "Claude's mandatory tool hook could not be initialized",
|
|
178
|
+
{ providerId: "claude", causeCode: error?.code || null },
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const args = ["-p"];
|
|
184
|
+
if (utility) appendToolLists(args, context);
|
|
185
|
+
args.push(
|
|
186
|
+
"--verbose",
|
|
187
|
+
"--output-format", "stream-json",
|
|
188
|
+
"--append-system-prompt", coreInstructions,
|
|
189
|
+
);
|
|
190
|
+
if (utility) args.push("--no-session-persistence");
|
|
191
|
+
|
|
192
|
+
if (toolHookSettings) args.push("--settings", String(toolHookSettings));
|
|
193
|
+
const transcriptDirectory = context.transcriptDirectory
|
|
194
|
+
|| context.transcriptPaths?.transcriptsDir
|
|
195
|
+
|| context.transcriptPaths?.directory
|
|
196
|
+
|| null;
|
|
197
|
+
if (transcriptDirectory) args.push("--add-dir", String(transcriptDirectory));
|
|
198
|
+
|
|
199
|
+
if (!utility) appendSessionArgs(args, context);
|
|
200
|
+
appendModelAndLimits(args, context, defaultModel, utility);
|
|
201
|
+
|
|
202
|
+
if (context.outputSchema) {
|
|
203
|
+
let schema;
|
|
204
|
+
try { schema = JSON.stringify(context.outputSchema); }
|
|
205
|
+
catch (error) { throw providerError("INVALID_PROVIDER_CONTEXT", "outputSchema must be JSON-serializable"); }
|
|
206
|
+
if (!schema) throw providerError("INVALID_PROVIDER_CONTEXT", "outputSchema must be JSON-serializable");
|
|
207
|
+
args.push("--json-schema", schema);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
appendPermissionArgs(args, context, utility);
|
|
211
|
+
const settings = settingsOf(context);
|
|
212
|
+
if (!utility && (context.worktree ?? settings.worktree)) args.push("--worktree");
|
|
213
|
+
if (!utility && context.voiceStream) args.push("--include-partial-messages");
|
|
214
|
+
const prompt = preparedPrompt(context);
|
|
215
|
+
if (!utility) args.push(prompt);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
binary,
|
|
219
|
+
args,
|
|
220
|
+
env,
|
|
221
|
+
// Utility prompts can contain memory, mandates, or reviewer payloads.
|
|
222
|
+
// Keep them out of argv/process listings; `claude -p` reads stdin when
|
|
223
|
+
// no positional prompt is supplied. Foreground compatibility is kept
|
|
224
|
+
// unchanged until its separately-scoped migration.
|
|
225
|
+
stdin: utility ? prompt : null,
|
|
226
|
+
parserState: createClaudeParserState(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const provider = {
|
|
231
|
+
id: "claude",
|
|
232
|
+
label: "Claude Code",
|
|
233
|
+
capabilities,
|
|
234
|
+
|
|
235
|
+
isAvailable: () => !!executable(),
|
|
236
|
+
executable,
|
|
237
|
+
defaultModel: () => defaultModel,
|
|
238
|
+
modelChoices: () => [...CLAUDE_MODELS],
|
|
239
|
+
|
|
240
|
+
compatibilityStatus() {
|
|
241
|
+
const binary = executable();
|
|
242
|
+
if (!binary) return { state: "not_checked", version: null, reason: "Claude CLI not found" };
|
|
243
|
+
const versionResult = runCommand(binary, ["--version"], { env: buildEnv(), timeout: 10000 });
|
|
244
|
+
if (!versionResult.ok) return { state: "unknown", version: null, reason: "Claude version probe failed" };
|
|
245
|
+
const version = (String(versionResult.output).match(/\b(\d+\.\d+\.\d+)\b/) || [null, null])[1];
|
|
246
|
+
const helpResult = runCommand(binary, ["--help"], { env: buildEnv(), timeout: 10000 });
|
|
247
|
+
if (!helpResult.ok) return { state: "unknown", version, reason: "Claude compatibility probe failed" };
|
|
248
|
+
const missing = REQUIRED_HELP_FLAGS.filter((flag) => !String(helpResult.output).includes(flag));
|
|
249
|
+
if (missing.length) {
|
|
250
|
+
return { state: "incompatible", version, reason: `Claude CLI is missing required options: ${missing.join(", ")}` };
|
|
251
|
+
}
|
|
252
|
+
return { state: "compatible", version, reason: null };
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
buildMainInvocation: (context) => buildInvocation(context, false),
|
|
256
|
+
buildCompactionInvocation(context) {
|
|
257
|
+
if (!context?.fresh || !context.readOnly || context.maxTurns !== 1) {
|
|
258
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "Claude compaction seed requires a fresh read-only one-turn context");
|
|
259
|
+
}
|
|
260
|
+
return buildInvocation({ ...context, permissionMode: "plan" }, false);
|
|
261
|
+
},
|
|
262
|
+
buildUtilityInvocation: (context) => buildInvocation({ ...context, fresh: true }, true),
|
|
263
|
+
createEventDecoder: (decoderOptions) => createJsonlDecoder(decoderOptions),
|
|
264
|
+
normalizeEvent: normalizeClaudeEvent,
|
|
265
|
+
authStatus: () => getAuthStatus(executable()),
|
|
266
|
+
authHelp: () => auth.claudeAuthRecoveryMessage(),
|
|
267
|
+
preflightAuth() {
|
|
268
|
+
const status = provider.authStatus();
|
|
269
|
+
if (status.state !== "unauthenticated") return null;
|
|
270
|
+
return {
|
|
271
|
+
code: "PROVIDER_UNAUTHENTICATED",
|
|
272
|
+
providerId: "claude",
|
|
273
|
+
message: auth.claudeAuthRecoveryMessage(status.reason || "Claude Code authentication failed"),
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
nativeToolsNote: "Claude Code exposes its native tools; Open Claudia's tool hook and approval policy remain authoritative.",
|
|
277
|
+
tierModel(tier) {
|
|
278
|
+
if (tier === "low") return "claude-haiku-4-5-20251001";
|
|
279
|
+
if (tier === "medium") return "claude-sonnet-4-6";
|
|
280
|
+
return defaultModel;
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return provider;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const claudeProvider = createClaudeProvider();
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
CLAUDE_EFFORT_VALUES,
|
|
291
|
+
CLAUDE_MODELS,
|
|
292
|
+
REQUIRED_HELP_FLAGS,
|
|
293
|
+
capabilities,
|
|
294
|
+
claudeProvider,
|
|
295
|
+
createClaudeProvider,
|
|
296
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
normalizeDecoderError,
|
|
5
|
+
normalizedUsage,
|
|
6
|
+
providerErrorFlags,
|
|
7
|
+
} = require("./events");
|
|
8
|
+
|
|
9
|
+
function createCodexParserState() {
|
|
10
|
+
return {
|
|
11
|
+
sessionId: null,
|
|
12
|
+
sawText: false,
|
|
13
|
+
terminal: false,
|
|
14
|
+
tools: new Map(),
|
|
15
|
+
completedItems: new Set(),
|
|
16
|
+
seenUsage: new Set(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sessionEvent(sessionId, state) {
|
|
21
|
+
if (!sessionId || state.sessionId === sessionId) return null;
|
|
22
|
+
state.sessionId = sessionId;
|
|
23
|
+
return { type: "session", sessionId };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toolInfo(item) {
|
|
27
|
+
if (!item || typeof item !== "object") return null;
|
|
28
|
+
if (item.type === "command_execution") {
|
|
29
|
+
return { name: "Shell", startDetail: item.command ?? null, endDetail: item.aggregated_output ?? item.command ?? null };
|
|
30
|
+
}
|
|
31
|
+
if (item.type === "file_change") {
|
|
32
|
+
return { name: "Edit", startDetail: item.path ?? item.file_path ?? item.changes ?? null, endDetail: item.changes ?? item.path ?? item.file_path ?? null };
|
|
33
|
+
}
|
|
34
|
+
if (item.type === "web_search") {
|
|
35
|
+
return { name: "Web", startDetail: item.query ?? null, endDetail: item.result ?? item.query ?? null };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function codexErrorMessage(rawEvent) {
|
|
41
|
+
if (typeof rawEvent.message === "string" && rawEvent.message.trim()) return rawEvent.message.trim();
|
|
42
|
+
if (typeof rawEvent.error?.message === "string" && rawEvent.error.message.trim()) return rawEvent.error.message.trim();
|
|
43
|
+
return "Codex provider failed";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeCodexEvent(rawEvent, state = createCodexParserState()) {
|
|
47
|
+
if (!rawEvent || typeof rawEvent !== "object") return [];
|
|
48
|
+
const decoderFailure = normalizeDecoderError(rawEvent);
|
|
49
|
+
if (decoderFailure) return [decoderFailure];
|
|
50
|
+
|
|
51
|
+
if (rawEvent.type === "thread.started") {
|
|
52
|
+
const event = sessionEvent(rawEvent.thread_id, state);
|
|
53
|
+
return event ? [event] : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (rawEvent.type === "item.started") {
|
|
57
|
+
const info = toolInfo(rawEvent.item);
|
|
58
|
+
if (!info || !rawEvent.item.id) return [];
|
|
59
|
+
state.tools.set(rawEvent.item.id, info.name);
|
|
60
|
+
return [{
|
|
61
|
+
type: "tool_start",
|
|
62
|
+
id: rawEvent.item.id,
|
|
63
|
+
name: info.name,
|
|
64
|
+
detail: info.startDetail,
|
|
65
|
+
}];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (rawEvent.type === "item.updated" && rawEvent.item?.type === "agent_message") {
|
|
69
|
+
const text = rawEvent.delta?.text ?? rawEvent.item.delta ?? null;
|
|
70
|
+
if (typeof text !== "string" || !text) return [];
|
|
71
|
+
state.sawText = true;
|
|
72
|
+
return [{ type: "text_delta", text }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (rawEvent.type === "item.completed") {
|
|
76
|
+
const item = rawEvent.item;
|
|
77
|
+
if (!item || !item.id || state.completedItems.has(item.id)) return [];
|
|
78
|
+
state.completedItems.add(item.id);
|
|
79
|
+
if (item.type === "agent_message" && typeof item.text === "string" && item.text) {
|
|
80
|
+
state.sawText = true;
|
|
81
|
+
return [{ type: "text_final", text: item.text }];
|
|
82
|
+
}
|
|
83
|
+
const info = toolInfo(item);
|
|
84
|
+
if (!info) return [];
|
|
85
|
+
const exitCode = Number.isInteger(item.exit_code)
|
|
86
|
+
? item.exit_code
|
|
87
|
+
: (/failed|error/i.test(String(item.status || "")) ? 1 : 0);
|
|
88
|
+
return [{
|
|
89
|
+
type: "tool_end",
|
|
90
|
+
id: item.id,
|
|
91
|
+
name: state.tools.get(item.id) || info.name,
|
|
92
|
+
detail: info.endDetail,
|
|
93
|
+
exitCode,
|
|
94
|
+
}];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (rawEvent.type === "turn.completed") {
|
|
98
|
+
if (state.terminal) return [];
|
|
99
|
+
state.terminal = true;
|
|
100
|
+
const events = [];
|
|
101
|
+
const usage = normalizedUsage(rawEvent.usage, "session_total", state.seenUsage);
|
|
102
|
+
if (usage) events.push(usage);
|
|
103
|
+
events.push({
|
|
104
|
+
type: "result",
|
|
105
|
+
text: "",
|
|
106
|
+
sessionId: rawEvent.thread_id || state.sessionId || null,
|
|
107
|
+
});
|
|
108
|
+
return events;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (rawEvent.type === "error" || rawEvent.type === "turn.failed") {
|
|
112
|
+
if (state.terminal) return [];
|
|
113
|
+
state.terminal = true;
|
|
114
|
+
const message = codexErrorMessage(rawEvent);
|
|
115
|
+
return [{ type: "error", message, ...providerErrorFlags(message) }];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { createCodexParserState, normalizeCodexEvent };
|