actoviq-agent-sdk 0.4.2 → 0.4.6
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/README-zh.md +58 -62
- package/README.md +72 -22
- package/assets/actoviq-icon.png +0 -0
- package/bin/actoviq-gui.js +24 -0
- package/dist/src/cli/actoviq-react.js +31 -14
- package/dist/src/cli/actoviq-react.js.map +1 -1
- package/dist/src/cli/actoviq-tui.js +9 -0
- package/dist/src/cli/actoviq-tui.js.map +1 -1
- package/dist/src/cli/bridge-interactive-agent.js +94 -15
- package/dist/src/cli/bridge-interactive-agent.js.map +1 -1
- package/dist/src/cli/version.d.ts +3 -0
- package/dist/src/cli/version.d.ts.map +1 -0
- package/dist/src/cli/version.js +28 -0
- package/dist/src/cli/version.js.map +1 -0
- package/dist/src/gui/actoviqGui.d.ts +29 -0
- package/dist/src/gui/actoviqGui.d.ts.map +1 -0
- package/dist/src/gui/actoviqGui.js +5319 -0
- package/dist/src/gui/actoviqGui.js.map +1 -0
- package/dist/src/gui/electronMain.d.ts +3 -0
- package/dist/src/gui/electronMain.d.ts.map +1 -0
- package/dist/src/gui/electronMain.js +176 -0
- package/dist/src/gui/electronMain.js.map +1 -0
- package/dist/src/gui/guiMarkdown.d.ts +3 -0
- package/dist/src/gui/guiMarkdown.d.ts.map +1 -0
- package/dist/src/gui/guiMarkdown.js +188 -0
- package/dist/src/gui/guiMarkdown.js.map +1 -0
- package/dist/src/hooks/userHooks.d.ts +39 -0
- package/dist/src/hooks/userHooks.d.ts.map +1 -0
- package/dist/src/hooks/userHooks.js +169 -0
- package/dist/src/hooks/userHooks.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/mcpServerConfig.d.ts +19 -0
- package/dist/src/mcp/mcpServerConfig.d.ts.map +1 -0
- package/dist/src/mcp/mcpServerConfig.js +51 -0
- package/dist/src/mcp/mcpServerConfig.js.map +1 -0
- package/dist/src/memory/projectContext.d.ts +32 -0
- package/dist/src/memory/projectContext.d.ts.map +1 -0
- package/dist/src/memory/projectContext.js +175 -0
- package/dist/src/memory/projectContext.js.map +1 -0
- package/dist/src/memory/sessionHistory.d.ts +21 -0
- package/dist/src/memory/sessionHistory.d.ts.map +1 -0
- package/dist/src/memory/sessionHistory.js +83 -0
- package/dist/src/memory/sessionHistory.js.map +1 -0
- package/dist/src/parity/actoviqBridgeSdk.d.ts +3 -0
- package/dist/src/parity/actoviqBridgeSdk.d.ts.map +1 -1
- package/dist/src/parity/actoviqBridgeSdk.js +93 -92
- package/dist/src/parity/actoviqBridgeSdk.js.map +1 -1
- package/dist/src/parity/bridgeConfigs.d.ts +49 -0
- package/dist/src/parity/bridgeConfigs.d.ts.map +1 -0
- package/dist/src/parity/bridgeConfigs.js +134 -0
- package/dist/src/parity/bridgeConfigs.js.map +1 -0
- package/dist/src/parity/bridgeEventAdapter.d.ts +20 -0
- package/dist/src/parity/bridgeEventAdapter.d.ts.map +1 -0
- package/dist/src/parity/bridgeEventAdapter.js +121 -0
- package/dist/src/parity/bridgeEventAdapter.js.map +1 -0
- package/dist/src/parity/bridgeExecResolver.d.ts +27 -0
- package/dist/src/parity/bridgeExecResolver.d.ts.map +1 -0
- package/dist/src/parity/bridgeExecResolver.js +95 -0
- package/dist/src/parity/bridgeExecResolver.js.map +1 -0
- package/dist/src/parity/bridgeProviders.d.ts +132 -0
- package/dist/src/parity/bridgeProviders.d.ts.map +1 -0
- package/dist/src/parity/bridgeProviders.js +651 -0
- package/dist/src/parity/bridgeProviders.js.map +1 -0
- package/dist/src/prompts/outputStyles.d.ts +20 -0
- package/dist/src/prompts/outputStyles.d.ts.map +1 -0
- package/dist/src/prompts/outputStyles.js +35 -0
- package/dist/src/prompts/outputStyles.js.map +1 -0
- package/dist/src/prompts/systemPrompt.d.ts.map +1 -1
- package/dist/src/prompts/systemPrompt.js +9 -1
- package/dist/src/prompts/systemPrompt.js.map +1 -1
- package/dist/src/router/modelRouter.d.ts +7 -1
- package/dist/src/router/modelRouter.d.ts.map +1 -1
- package/dist/src/router/modelRouter.js +50 -15
- package/dist/src/router/modelRouter.js.map +1 -1
- package/dist/src/runtime/agentSession.d.ts +6 -0
- package/dist/src/runtime/agentSession.d.ts.map +1 -1
- package/dist/src/runtime/agentSession.js +15 -0
- package/dist/src/runtime/agentSession.js.map +1 -1
- package/dist/src/runtime/bashClassification.d.ts +18 -0
- package/dist/src/runtime/bashClassification.d.ts.map +1 -0
- package/dist/src/runtime/bashClassification.js +83 -0
- package/dist/src/runtime/bashClassification.js.map +1 -0
- package/dist/src/team/modelTeam.d.ts +3 -5
- package/dist/src/team/modelTeam.d.ts.map +1 -1
- package/dist/src/team/modelTeam.js +98 -162
- package/dist/src/team/modelTeam.js.map +1 -1
- package/dist/src/team/teamRuntime.d.ts +60 -0
- package/dist/src/team/teamRuntime.d.ts.map +1 -0
- package/dist/src/team/teamRuntime.js +139 -0
- package/dist/src/team/teamRuntime.js.map +1 -0
- package/dist/src/tools/actoviqCoreTools.d.ts +1 -0
- package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqCoreTools.js +5 -0
- package/dist/src/tools/actoviqCoreTools.js.map +1 -1
- package/dist/src/tools/planMode/PlanModeTools.d.ts +18 -0
- package/dist/src/tools/planMode/PlanModeTools.d.ts.map +1 -0
- package/dist/src/tools/planMode/PlanModeTools.js +71 -0
- package/dist/src/tools/planMode/PlanModeTools.js.map +1 -0
- package/dist/src/tui/actoviqTui.d.ts +3 -0
- package/dist/src/tui/actoviqTui.d.ts.map +1 -1
- package/dist/src/tui/actoviqTui.js +1474 -91
- package/dist/src/tui/actoviqTui.js.map +1 -1
- package/dist/src/tui/transcript.d.ts +6 -0
- package/dist/src/tui/transcript.d.ts.map +1 -1
- package/dist/src/tui/transcript.js +27 -0
- package/dist/src/tui/transcript.js.map +1 -1
- package/dist/src/types.d.ts +128 -9
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/ui/commandSurface.d.ts +13 -0
- package/dist/src/ui/commandSurface.d.ts.map +1 -0
- package/dist/src/ui/commandSurface.js +100 -0
- package/dist/src/ui/commandSurface.js.map +1 -0
- package/package.json +42 -3
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-provider abstraction for the bridge SDK's directCli mode.
|
|
3
|
+
*
|
|
4
|
+
* The vendored-bundle path and the original Claude Code `-p` stream-json path
|
|
5
|
+
* are both the `claude` provider. `pi` and `codex` reuse the same spawn +
|
|
6
|
+
* line-by-line JSONL pipeline but speak their own wire protocols, so each
|
|
7
|
+
* provider supplies four pieces the rest of `actoviqBridgeSdk.ts` stays
|
|
8
|
+
* protocol-agnostic around:
|
|
9
|
+
*
|
|
10
|
+
* 1. executable resolution (which binary on PATH, default error message)
|
|
11
|
+
* 2. argv construction (the provider's CLI flags + prompt placement)
|
|
12
|
+
* 3. child-process env injection (provider-specific credential variables)
|
|
13
|
+
* 4. event normalization (provider JSONL → the `system/assistant/result`
|
|
14
|
+
* trio `execute()` already switches on — plus claude content-block shape)
|
|
15
|
+
*
|
|
16
|
+
* Normalization is stateful (pi's session id arrives once in a header line;
|
|
17
|
+
* codex's model is never in the stream; assistant text accumulates across
|
|
18
|
+
* deltas), so each provider hands back a fresh normalizer per run.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { ActoviqBridgeProcessError } from '../errors.js';
|
|
22
|
+
import { mapActoviqEnvToAnthropicEnv } from '../config/anthropicEnvMapping.js';
|
|
23
|
+
import { getLoadedJsonConfig } from '../config/loadJsonConfigFile.js';
|
|
24
|
+
import { findExecutableOnPath, IS_WINDOWS, pathExists, } from './bridgeExecResolver.js';
|
|
25
|
+
function execFileAsync(file, args, options) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
execFile(file, args, options, (error, stdout, stderr) => {
|
|
28
|
+
if (error)
|
|
29
|
+
reject(error);
|
|
30
|
+
else
|
|
31
|
+
resolve({ stdout: stdout, stderr: stderr });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Shared base for executable resolution (PATH lookup + explicit-path check). */
|
|
36
|
+
export class BaseRuntimeProvider {
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the executable for a run. Precedence (all in-memory; mirrors
|
|
39
|
+
* `buildChildEnvironment` in actoviqBridgeSdk.ts — no file I/O here):
|
|
40
|
+
* 1. `explicitPath` arg (caller-supplied `{ executable }`)
|
|
41
|
+
* 2. `ACTOVIQ_<ID>_PATH` env var (top-level or `env:` block — both are
|
|
42
|
+
* captured by `extractEnv` in loadJsonConfigFile.ts)
|
|
43
|
+
* 3. `raw.bridge.providers[id].path` from the loaded settings store
|
|
44
|
+
* 4. `findExecutableOnPath(this.pathBinary)` — the binary on PATH
|
|
45
|
+
*
|
|
46
|
+
* Mirrors the `ACTOVIQ_BASH_PATH` precedent in src/tools/bash/BashTool.ts.
|
|
47
|
+
*/
|
|
48
|
+
async resolveExecutable(explicitPath) {
|
|
49
|
+
// For user-specified paths (explicit, env, settings block), just check
|
|
50
|
+
// existence — the user knows what they're pointing at, and scripts (.mjs,
|
|
51
|
+
// .js, …) often lack +x on Linux. Only PATH lookups need the stricter
|
|
52
|
+
// isExecutable check so we don't pick up non-runnable files.
|
|
53
|
+
if (explicitPath) {
|
|
54
|
+
if (!(await pathExists(explicitPath))) {
|
|
55
|
+
throw new ActoviqBridgeProcessError(`The configured executable was not found: ${explicitPath}`);
|
|
56
|
+
}
|
|
57
|
+
return explicitPath;
|
|
58
|
+
}
|
|
59
|
+
const envVar = `ACTOVIQ_${this.id.toUpperCase()}_PATH`;
|
|
60
|
+
const loaded = getLoadedJsonConfig();
|
|
61
|
+
const settingsEnvPath = typeof loaded?.env?.[envVar] === 'string' ? loaded.env[envVar] : undefined;
|
|
62
|
+
const processEnvPath = process.env[envVar];
|
|
63
|
+
const envPath = settingsEnvPath ?? processEnvPath;
|
|
64
|
+
if (envPath) {
|
|
65
|
+
if (!(await pathExists(envPath))) {
|
|
66
|
+
throw new ActoviqBridgeProcessError(`${envVar} (${envPath}) was not found.`);
|
|
67
|
+
}
|
|
68
|
+
return envPath;
|
|
69
|
+
}
|
|
70
|
+
const settingsBlockPath = readSettingsBlockPath(loaded?.raw, this.id);
|
|
71
|
+
if (settingsBlockPath) {
|
|
72
|
+
if (!(await pathExists(settingsBlockPath))) {
|
|
73
|
+
throw new ActoviqBridgeProcessError(`Configured ${this.id} bridge path (${settingsBlockPath}) was not found.`);
|
|
74
|
+
}
|
|
75
|
+
return settingsBlockPath;
|
|
76
|
+
}
|
|
77
|
+
const pathCandidate = await findExecutableOnPath(this.pathBinary);
|
|
78
|
+
if (pathCandidate) {
|
|
79
|
+
return pathCandidate;
|
|
80
|
+
}
|
|
81
|
+
throw new ActoviqBridgeProcessError(`No "${this.pathBinary}" executable was found on PATH. Install ${this.displayName}, set ${envVar}, or run \`/bridge\` to configure it.`);
|
|
82
|
+
}
|
|
83
|
+
async probeVersion(executablePath) {
|
|
84
|
+
// Windows npm shims are `.cmd`/`.bat` and need a shell to run. Mirror the
|
|
85
|
+
// spawn shape used by actoviqBridgeSdk.ts:1536.
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await execFileAsync(executablePath, ['--version'], {
|
|
88
|
+
shell: IS_WINDOWS && /\.(?:cmd|bat)$/i.test(executablePath),
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
timeout: 10_000,
|
|
91
|
+
maxBuffer: 1024 * 1024,
|
|
92
|
+
});
|
|
93
|
+
const trimmed = (stdout ?? '').trim();
|
|
94
|
+
return trimmed || undefined;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build a single normalized event with common optional fields pre-stamped.
|
|
103
|
+
* Normalizers route every native event through this so the downstream
|
|
104
|
+
* `execute()` switch (system/assistant/result) and `runtimeInfoFromInitEvent`
|
|
105
|
+
* keep working untouched.
|
|
106
|
+
*/
|
|
107
|
+
export function bridgeEvent(type, fields = {}) {
|
|
108
|
+
return { type, ...fields };
|
|
109
|
+
}
|
|
110
|
+
let currentProvider;
|
|
111
|
+
/**
|
|
112
|
+
* Read the per-provider path override from the `bridge.providers[id].path`
|
|
113
|
+
* settings block (in-memory only — the caller persists via the settings store).
|
|
114
|
+
*/
|
|
115
|
+
function readSettingsBlockPath(raw, id) {
|
|
116
|
+
if (!raw)
|
|
117
|
+
return undefined;
|
|
118
|
+
const bridge = raw.bridge;
|
|
119
|
+
if (!bridge || typeof bridge !== 'object')
|
|
120
|
+
return undefined;
|
|
121
|
+
const providers = bridge.providers;
|
|
122
|
+
if (!providers || typeof providers !== 'object')
|
|
123
|
+
return undefined;
|
|
124
|
+
const entry = providers[id];
|
|
125
|
+
if (!entry || typeof entry !== 'object')
|
|
126
|
+
return undefined;
|
|
127
|
+
const p = entry.path;
|
|
128
|
+
return typeof p === 'string' && p ? p : undefined;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The configured default provider, read from `bridge.defaultProvider` in the
|
|
132
|
+
* loaded settings store. Falls back to `'claude'`. Explicit
|
|
133
|
+
* `directCliProvider` (passed to `resolveProvider`) always wins over this.
|
|
134
|
+
*/
|
|
135
|
+
export function getDefaultProviderId() {
|
|
136
|
+
const raw = getLoadedJsonConfig()?.raw;
|
|
137
|
+
if (raw && typeof raw === 'object') {
|
|
138
|
+
const bridge = raw.bridge;
|
|
139
|
+
if (bridge && typeof bridge === 'object') {
|
|
140
|
+
const dp = bridge.defaultProvider;
|
|
141
|
+
if (dp === 'claude' || dp === 'pi' || dp === 'codex' || dp === 'codewhale' || dp === 'reasonix' || dp === 'crush')
|
|
142
|
+
return dp;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return 'claude';
|
|
146
|
+
}
|
|
147
|
+
export function resolveProvider(id) {
|
|
148
|
+
const resolved = id ?? getDefaultProviderId();
|
|
149
|
+
if (resolved === 'claude')
|
|
150
|
+
return claudeProvider;
|
|
151
|
+
if (resolved === 'pi')
|
|
152
|
+
return piProvider;
|
|
153
|
+
if (resolved === 'codex')
|
|
154
|
+
return codexProvider;
|
|
155
|
+
if (resolved === 'codewhale')
|
|
156
|
+
return codewhaleProvider;
|
|
157
|
+
if (resolved === 'reasonix')
|
|
158
|
+
return reasonixProvider;
|
|
159
|
+
if (resolved === 'crush')
|
|
160
|
+
return crushProvider;
|
|
161
|
+
throw new ActoviqBridgeProcessError(`Unknown bridge provider: ${String(resolved)}`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Probe the locally installed agent CLIs. Resolves each provider via the
|
|
165
|
+
* env/settings/PATH chain (so env overrides are honored) and best-effort
|
|
166
|
+
* `--version`. Never throws — a missing provider is reported as
|
|
167
|
+
* `available: false` with `path: undefined`.
|
|
168
|
+
*/
|
|
169
|
+
export async function detectBridgeProviders() {
|
|
170
|
+
const results = [];
|
|
171
|
+
for (const provider of [claudeProvider, piProvider, codexProvider, codewhaleProvider, reasonixProvider, crushProvider]) {
|
|
172
|
+
let path;
|
|
173
|
+
let available = false;
|
|
174
|
+
let version;
|
|
175
|
+
try {
|
|
176
|
+
path = await provider.resolveExecutable();
|
|
177
|
+
available = Boolean(path);
|
|
178
|
+
if (path) {
|
|
179
|
+
version = await provider.probeVersion(path);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Not installed / not configured — report unavailable.
|
|
184
|
+
}
|
|
185
|
+
results.push({
|
|
186
|
+
id: provider.id,
|
|
187
|
+
displayName: provider.displayName,
|
|
188
|
+
path,
|
|
189
|
+
available,
|
|
190
|
+
version,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
/** Package-private seam for tests that need the ambient provider. */
|
|
196
|
+
export function _setCurrentProvider(provider) {
|
|
197
|
+
currentProvider = provider;
|
|
198
|
+
}
|
|
199
|
+
export function _getCurrentProvider() {
|
|
200
|
+
return currentProvider;
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// claude provider (stream-json — the original protocol)
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
class ClaudeProvider extends BaseRuntimeProvider {
|
|
206
|
+
id = 'claude';
|
|
207
|
+
pathBinary = 'claude';
|
|
208
|
+
displayName = 'Claude Code (@anthropic-ai/claude-code)';
|
|
209
|
+
buildArgs(prompt, _options) {
|
|
210
|
+
// The full flag set lives in actoviqBridgeSdk.buildCliArgs(); claude is the
|
|
211
|
+
// default and keeps using that builder. Other providers override buildArgs.
|
|
212
|
+
// (buildCliArgs is invoked directly from actoviqBridgeSdk.ts for the claude
|
|
213
|
+
// path; this indirection exists so the provider list is exhaustive.)
|
|
214
|
+
return ['-p', prompt];
|
|
215
|
+
}
|
|
216
|
+
buildChildEnv(baseEnv, settingsEnv, overrides) {
|
|
217
|
+
// Actoviq settings are the single source of model/credential config: derive
|
|
218
|
+
// ANTHROPIC_* equivalents so the Claude Code-based child process does not
|
|
219
|
+
// silently fall back to ~/.claude/settings.json or keychain credentials.
|
|
220
|
+
// Derived values override inherited process.env ANTHROPIC_* entries, while
|
|
221
|
+
// explicit ANTHROPIC_* keys in the settings env block and caller overrides win.
|
|
222
|
+
return {
|
|
223
|
+
...baseEnv,
|
|
224
|
+
...mapActoviqEnvToAnthropicEnv(settingsEnv),
|
|
225
|
+
...settingsEnv,
|
|
226
|
+
...(overrides ?? {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
createNormalizer() {
|
|
230
|
+
// claude's stream-json is already the canonical system/assistant/result shape.
|
|
231
|
+
return { translate: raw => [raw] };
|
|
232
|
+
}
|
|
233
|
+
suggestedModels() {
|
|
234
|
+
return ['claude-sonnet-4-6', 'claude-opus-4-8', 'claude-haiku-4-5', 'claude-fable-5'];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// pi provider (pi -p --mode json)
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
class PiProvider extends BaseRuntimeProvider {
|
|
241
|
+
id = 'pi';
|
|
242
|
+
pathBinary = 'pi';
|
|
243
|
+
displayName = 'pi CLI (@earendil-works/pi-coding-agent)';
|
|
244
|
+
buildArgs(prompt, options) {
|
|
245
|
+
const args = ['-p', '--mode', 'json'];
|
|
246
|
+
// Non-interactive, no trust prompt — matches a headless bridge run.
|
|
247
|
+
// (Previously --no-session made every turn ephemeral; we now persist so
|
|
248
|
+
// multi-turn works: first turn --session-id <uuid> creates, later turns
|
|
249
|
+
// --session <uuid> resumes. The PiNormalizer surfaces the same id back.)
|
|
250
|
+
args.push('--no-approve');
|
|
251
|
+
if (options.model) {
|
|
252
|
+
// pi accepts "provider/id"; pass through unchanged.
|
|
253
|
+
args.push('--model', options.model);
|
|
254
|
+
}
|
|
255
|
+
if (options.appendSystemPrompt) {
|
|
256
|
+
args.push('--append-system-prompt', options.appendSystemPrompt);
|
|
257
|
+
}
|
|
258
|
+
else if (options.systemPrompt) {
|
|
259
|
+
args.push('--system-prompt', options.systemPrompt);
|
|
260
|
+
}
|
|
261
|
+
// Multi-turn session threading. ActoviqBridgeSession sets sessionId on
|
|
262
|
+
// the first turn and resume: this.id afterwards.
|
|
263
|
+
if (typeof options.resume === 'string') {
|
|
264
|
+
args.push('--session', options.resume);
|
|
265
|
+
}
|
|
266
|
+
else if (options.resume === true) {
|
|
267
|
+
args.push('--resume');
|
|
268
|
+
}
|
|
269
|
+
else if (options.sessionId) {
|
|
270
|
+
args.push('--session-id', options.sessionId);
|
|
271
|
+
}
|
|
272
|
+
else if (options.continueMostRecent) {
|
|
273
|
+
args.push('--continue');
|
|
274
|
+
}
|
|
275
|
+
// pi takes the prompt as the final positional argument.
|
|
276
|
+
args.push(prompt);
|
|
277
|
+
return args;
|
|
278
|
+
}
|
|
279
|
+
buildChildEnv(baseEnv, settingsEnv, overrides) {
|
|
280
|
+
// pi reads *_API_KEY by provider (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.).
|
|
281
|
+
// We pass the Actoviq settings env through unchanged; callers set the
|
|
282
|
+
// provider-specific key directly. No ANTHROPIC_* remapping for non-claude.
|
|
283
|
+
return { ...baseEnv, ...settingsEnv, ...(overrides ?? {}) };
|
|
284
|
+
}
|
|
285
|
+
createNormalizer() {
|
|
286
|
+
return new PiNormalizer();
|
|
287
|
+
}
|
|
288
|
+
suggestedModels() {
|
|
289
|
+
return ['gpt-5', 'gpt-5-mini', 'claude-sonnet-4-6', 'deepseek-v4-pro', 'gemini-2.5-pro'];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
class PiNormalizer {
|
|
293
|
+
sessionId;
|
|
294
|
+
cwd;
|
|
295
|
+
model;
|
|
296
|
+
initEmitted = false;
|
|
297
|
+
pendingAssistantText = '';
|
|
298
|
+
translate(raw) {
|
|
299
|
+
const type = typeof raw.type === 'string' ? raw.type : '';
|
|
300
|
+
if (type === 'session') {
|
|
301
|
+
this.sessionId = typeof raw.id === 'string' ? raw.id : this.sessionId;
|
|
302
|
+
this.cwd = typeof raw.cwd === 'string' ? raw.cwd : this.cwd;
|
|
303
|
+
// No init emission yet — wait for agent_start so we mirror claude's
|
|
304
|
+
// "runtime ready" semantics. The session header carries no model.
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
if (type === 'agent_start' && !this.initEmitted) {
|
|
308
|
+
this.initEmitted = true;
|
|
309
|
+
const init = bridgeEvent('system', {
|
|
310
|
+
subtype: 'init',
|
|
311
|
+
session_id: this.sessionId ?? '',
|
|
312
|
+
cwd: this.cwd,
|
|
313
|
+
// pi exposes no tool/skill/agent catalog in its stream; introspection
|
|
314
|
+
// methods will return empty/limited data for this provider.
|
|
315
|
+
tools: [],
|
|
316
|
+
mcp_servers: [],
|
|
317
|
+
slash_commands: [],
|
|
318
|
+
agents: [],
|
|
319
|
+
skills: [],
|
|
320
|
+
plugins: [],
|
|
321
|
+
model: this.model,
|
|
322
|
+
});
|
|
323
|
+
return [init];
|
|
324
|
+
}
|
|
325
|
+
if (type === 'message_update') {
|
|
326
|
+
// Assistant streaming deltas. Accumulate text; emit assistant text deltas
|
|
327
|
+
// in the claude stream_event shape so existing delta consumers work.
|
|
328
|
+
const ame = raw.assistantMessageEvent;
|
|
329
|
+
if (isRecord(ame) && ame.type === 'text_delta' && typeof ame.delta === 'string') {
|
|
330
|
+
this.pendingAssistantText += ame.delta;
|
|
331
|
+
return [bridgeEvent('stream_event', {
|
|
332
|
+
session_id: this.sessionId ?? '',
|
|
333
|
+
event: {
|
|
334
|
+
type: 'content_block_delta',
|
|
335
|
+
index: 0,
|
|
336
|
+
delta: { type: 'text_delta', text: ame.delta },
|
|
337
|
+
},
|
|
338
|
+
})];
|
|
339
|
+
}
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
if (type === 'message_end') {
|
|
343
|
+
const message = raw.message;
|
|
344
|
+
if (isRecord(message) && message.role === 'assistant') {
|
|
345
|
+
// Capture model from the finalized assistant message (only place it appears).
|
|
346
|
+
if (typeof message.model === 'string') {
|
|
347
|
+
this.model = message.model;
|
|
348
|
+
}
|
|
349
|
+
const text = extractPiAssistantText(message);
|
|
350
|
+
return [bridgeEvent('assistant', {
|
|
351
|
+
session_id: this.sessionId ?? '',
|
|
352
|
+
message: {
|
|
353
|
+
role: 'assistant',
|
|
354
|
+
content: text ? [{ type: 'text', text }] : [],
|
|
355
|
+
},
|
|
356
|
+
})];
|
|
357
|
+
}
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
if (type === 'agent_end') {
|
|
361
|
+
return [bridgeEvent('result', {
|
|
362
|
+
subtype: 'success',
|
|
363
|
+
session_id: this.sessionId ?? '',
|
|
364
|
+
is_error: false,
|
|
365
|
+
result: this.pendingAssistantText,
|
|
366
|
+
stop_reason: 'end_turn',
|
|
367
|
+
num_turns: 1,
|
|
368
|
+
})];
|
|
369
|
+
}
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function extractPiAssistantText(message) {
|
|
374
|
+
const content = message.content;
|
|
375
|
+
if (!Array.isArray(content)) {
|
|
376
|
+
return typeof content === 'string' ? content : '';
|
|
377
|
+
}
|
|
378
|
+
return content
|
|
379
|
+
.map(block => {
|
|
380
|
+
if (!isRecord(block))
|
|
381
|
+
return '';
|
|
382
|
+
return block.type === 'text' && typeof block.text === 'string' ? block.text : '';
|
|
383
|
+
})
|
|
384
|
+
.join('');
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// codex provider (codex exec --json)
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
class CodexProvider extends BaseRuntimeProvider {
|
|
390
|
+
id = 'codex';
|
|
391
|
+
pathBinary = 'codex';
|
|
392
|
+
displayName = 'Codex CLI (@openai/codex)';
|
|
393
|
+
buildArgs(prompt, options) {
|
|
394
|
+
const args = ['exec', '--json', '--skip-git-repo-check', '--color', 'never', '--ephemeral'];
|
|
395
|
+
// Autonomous: no approval prompt can block a non-TTY run.
|
|
396
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
397
|
+
if (options.model) {
|
|
398
|
+
args.push('-m', options.model);
|
|
399
|
+
}
|
|
400
|
+
if (options.systemPrompt) {
|
|
401
|
+
args.push('-c', `system_prompt="${options.systemPrompt.replace(/"/g, '\\"')}"`);
|
|
402
|
+
}
|
|
403
|
+
if (typeof options.maxTurns === 'number') {
|
|
404
|
+
args.push('-c', `max_turns=${options.maxTurns}`);
|
|
405
|
+
}
|
|
406
|
+
args.push(prompt);
|
|
407
|
+
return args;
|
|
408
|
+
}
|
|
409
|
+
buildChildEnv(baseEnv, settingsEnv, overrides) {
|
|
410
|
+
// codex reads OPENAI_API_KEY / config.toml. Pass Actoviq env through.
|
|
411
|
+
return { ...baseEnv, ...settingsEnv, ...(overrides ?? {}) };
|
|
412
|
+
}
|
|
413
|
+
createNormalizer() {
|
|
414
|
+
return new CodexNormalizer();
|
|
415
|
+
}
|
|
416
|
+
suggestedModels() {
|
|
417
|
+
return ['gpt-5', 'gpt-5-mini', 'o3', 'o4-mini'];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
class CodexNormalizer {
|
|
421
|
+
threadId;
|
|
422
|
+
initEmitted = false;
|
|
423
|
+
translate(raw) {
|
|
424
|
+
const type = typeof raw.type === 'string' ? raw.type : '';
|
|
425
|
+
if (type === 'thread.started') {
|
|
426
|
+
this.threadId = typeof raw.thread_id === 'string' ? raw.thread_id : this.threadId;
|
|
427
|
+
if (!this.initEmitted) {
|
|
428
|
+
this.initEmitted = true;
|
|
429
|
+
return [bridgeEvent('system', {
|
|
430
|
+
subtype: 'init',
|
|
431
|
+
session_id: this.threadId ?? '',
|
|
432
|
+
// codex exec JSONL carries no model/tools catalog.
|
|
433
|
+
tools: [],
|
|
434
|
+
mcp_servers: [],
|
|
435
|
+
slash_commands: [],
|
|
436
|
+
agents: [],
|
|
437
|
+
skills: [],
|
|
438
|
+
plugins: [],
|
|
439
|
+
model: undefined,
|
|
440
|
+
})];
|
|
441
|
+
}
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
if (type === 'item.completed') {
|
|
445
|
+
const item = raw.item;
|
|
446
|
+
if (isRecord(item) && item.type === 'agent_message' && typeof item.text === 'string') {
|
|
447
|
+
return [bridgeEvent('assistant', {
|
|
448
|
+
session_id: this.threadId ?? '',
|
|
449
|
+
message: {
|
|
450
|
+
role: 'assistant',
|
|
451
|
+
content: [{ type: 'text', text: item.text }],
|
|
452
|
+
},
|
|
453
|
+
})];
|
|
454
|
+
}
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
if (type === 'turn.completed') {
|
|
458
|
+
return [bridgeEvent('result', {
|
|
459
|
+
subtype: 'success',
|
|
460
|
+
session_id: this.threadId ?? '',
|
|
461
|
+
is_error: false,
|
|
462
|
+
stop_reason: 'end_turn',
|
|
463
|
+
num_turns: 1,
|
|
464
|
+
})];
|
|
465
|
+
}
|
|
466
|
+
if (type === 'turn.failed' || type === 'error') {
|
|
467
|
+
const message = typeof raw.message === 'string'
|
|
468
|
+
? raw.message
|
|
469
|
+
: (isRecord(raw.error) && typeof raw.error.message === 'string' ? raw.error.message : 'codex run failed');
|
|
470
|
+
return [bridgeEvent('result', {
|
|
471
|
+
subtype: 'error',
|
|
472
|
+
session_id: this.threadId ?? '',
|
|
473
|
+
is_error: true,
|
|
474
|
+
result: message,
|
|
475
|
+
stop_reason: 'error',
|
|
476
|
+
num_turns: 1,
|
|
477
|
+
})];
|
|
478
|
+
}
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
// Plain-text normalizer — shared by providers whose headless mode emits
|
|
484
|
+
// plain text (no JSONL / stream-json). Captures the full stdout as the
|
|
485
|
+
// assistant text and closes with a result event. No streaming deltas, no
|
|
486
|
+
// tool cards — simple and adequate for reasonix / crush.
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
class PlainTextNormalizer {
|
|
489
|
+
rawText = true;
|
|
490
|
+
sessionId;
|
|
491
|
+
text = '';
|
|
492
|
+
translate(raw) {
|
|
493
|
+
// raw-text mode: `parseStdoutEvents` wraps each line as `{_raw: line}`.
|
|
494
|
+
const line = typeof raw._raw === 'string' ? raw._raw : '';
|
|
495
|
+
this.text += (this.text ? '\n' : '') + line;
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
flush() {
|
|
499
|
+
const sid = this.sessionId ?? '';
|
|
500
|
+
return [
|
|
501
|
+
bridgeEvent('system', {
|
|
502
|
+
subtype: 'init',
|
|
503
|
+
session_id: sid,
|
|
504
|
+
tools: [],
|
|
505
|
+
mcp_servers: [],
|
|
506
|
+
slash_commands: [],
|
|
507
|
+
agents: [],
|
|
508
|
+
skills: [],
|
|
509
|
+
plugins: [],
|
|
510
|
+
}),
|
|
511
|
+
bridgeEvent('assistant', {
|
|
512
|
+
session_id: sid,
|
|
513
|
+
message: {
|
|
514
|
+
role: 'assistant',
|
|
515
|
+
content: this.text ? [{ type: 'text', text: this.text }] : [],
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
bridgeEvent('result', {
|
|
519
|
+
subtype: 'success',
|
|
520
|
+
session_id: sid,
|
|
521
|
+
is_error: false,
|
|
522
|
+
result: this.text,
|
|
523
|
+
stop_reason: 'end_turn',
|
|
524
|
+
num_turns: 1,
|
|
525
|
+
}),
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// codewhale provider (stream-json — compatible with Claude Code)
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
class CodewhaleProvider extends BaseRuntimeProvider {
|
|
533
|
+
id = 'codewhale';
|
|
534
|
+
pathBinary = 'codewhale';
|
|
535
|
+
displayName = 'CodeWhale CLI (codewhale)';
|
|
536
|
+
buildArgs(prompt, options) {
|
|
537
|
+
const args = ['exec', '--auto', '--output-format', 'stream-json'];
|
|
538
|
+
// codewhale persists sessions per workspace. Its --session-id RESUMES by
|
|
539
|
+
// id rather than creating, and we can't reliably feed its native id back
|
|
540
|
+
// through ActoviqBridgeSession's uuid, so use --continue (most-recent) for
|
|
541
|
+
// resumed turns — correct for sequential same-provider turns and survives
|
|
542
|
+
// switching away and back.
|
|
543
|
+
if (options.resume || options.continueMostRecent) {
|
|
544
|
+
args.push('--continue');
|
|
545
|
+
}
|
|
546
|
+
args.push(prompt);
|
|
547
|
+
return args;
|
|
548
|
+
}
|
|
549
|
+
buildChildEnv(baseEnv, settingsEnv, overrides) {
|
|
550
|
+
return { ...baseEnv, ...settingsEnv, ...(overrides ?? {}) };
|
|
551
|
+
}
|
|
552
|
+
createNormalizer() {
|
|
553
|
+
return { translate: raw => [raw] };
|
|
554
|
+
}
|
|
555
|
+
suggestedModels() { return []; }
|
|
556
|
+
}
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// reasonix provider (plain-text, DeepSeek-native)
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
class ReasonixProvider extends BaseRuntimeProvider {
|
|
561
|
+
id = 'reasonix';
|
|
562
|
+
pathBinary = 'reasonix';
|
|
563
|
+
displayName = 'Reasonix CLI (reasonix)';
|
|
564
|
+
buildArgs(prompt, options) {
|
|
565
|
+
const args = ['run'];
|
|
566
|
+
if (options.model) {
|
|
567
|
+
args.push('-m', options.model);
|
|
568
|
+
}
|
|
569
|
+
if (options.systemPrompt) {
|
|
570
|
+
args.push('-s', options.systemPrompt);
|
|
571
|
+
}
|
|
572
|
+
if (options.effort) {
|
|
573
|
+
args.push('--effort', options.effort);
|
|
574
|
+
}
|
|
575
|
+
args.push(prompt);
|
|
576
|
+
return args;
|
|
577
|
+
}
|
|
578
|
+
buildChildEnv(baseEnv, settingsEnv, overrides) {
|
|
579
|
+
return { ...baseEnv, ...settingsEnv, ...(overrides ?? {}) };
|
|
580
|
+
}
|
|
581
|
+
createNormalizer() {
|
|
582
|
+
return new PlainTextNormalizer();
|
|
583
|
+
}
|
|
584
|
+
suggestedModels() {
|
|
585
|
+
return ['deepseek-v4-pro', 'deepseek-v4-flash'];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
class CrushProvider extends BaseRuntimeProvider {
|
|
589
|
+
id = 'crush';
|
|
590
|
+
pathBinary = 'crush';
|
|
591
|
+
displayName = 'Crush CLI (crush)';
|
|
592
|
+
buildArgs(prompt, options) {
|
|
593
|
+
const args = ['run'];
|
|
594
|
+
if (options.model) {
|
|
595
|
+
args.push('-m', options.model);
|
|
596
|
+
}
|
|
597
|
+
// crush persists sessions per workspace and emits plain text (no session
|
|
598
|
+
// id to capture), so resumed turns use --continue (most-recent). Correct
|
|
599
|
+
// for sequential turns and survives switching away and back.
|
|
600
|
+
if (options.resume || options.continueMostRecent) {
|
|
601
|
+
args.push('--continue');
|
|
602
|
+
}
|
|
603
|
+
args.push(prompt);
|
|
604
|
+
return args;
|
|
605
|
+
}
|
|
606
|
+
buildChildEnv(baseEnv, settingsEnv, overrides) {
|
|
607
|
+
return { ...baseEnv, ...settingsEnv, ...(overrides ?? {}) };
|
|
608
|
+
}
|
|
609
|
+
createNormalizer() {
|
|
610
|
+
return new PlainTextNormalizer();
|
|
611
|
+
}
|
|
612
|
+
suggestedModels() {
|
|
613
|
+
return ['gpt-5', 'claude-sonnet-4-6', 'gemini-2.5-pro'];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function isRecord(value) {
|
|
617
|
+
return typeof value === 'object' && value !== null;
|
|
618
|
+
}
|
|
619
|
+
export const claudeProvider = new ClaudeProvider();
|
|
620
|
+
export const piProvider = new PiProvider();
|
|
621
|
+
export const codexProvider = new CodexProvider();
|
|
622
|
+
export const codewhaleProvider = new CodewhaleProvider();
|
|
623
|
+
export const reasonixProvider = new ReasonixProvider();
|
|
624
|
+
export const crushProvider = new CrushProvider();
|
|
625
|
+
export const BRIDGE_PROVIDERS = {
|
|
626
|
+
claude: claudeProvider,
|
|
627
|
+
pi: piProvider,
|
|
628
|
+
codex: codexProvider,
|
|
629
|
+
codewhale: codewhaleProvider,
|
|
630
|
+
reasonix: reasonixProvider,
|
|
631
|
+
crush: crushProvider,
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* Best-effort credential env-var names each provider's CLI reads. The TUI
|
|
635
|
+
* treats "any one of these set" (across the settings env block ∪ process.env)
|
|
636
|
+
* as "credentials likely configured". Empty arrays mean the provider's
|
|
637
|
+
* credential var is not known from its public surface (codewhale/crush are
|
|
638
|
+
* multi-backend); the UI shows an honest "(unknown)" rather than a wrong key.
|
|
639
|
+
* This is advisory display data only — it never gates a run.
|
|
640
|
+
*/
|
|
641
|
+
export const BRIDGE_PROVIDER_CREDENTIALS = {
|
|
642
|
+
// claude maps ACTOVIQ_* → ANTHROPIC_* (see anthropicEnvMapping.ts); either form works.
|
|
643
|
+
claude: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ACTOVIQ_API_KEY', 'ACTOVIQ_AUTH_TOKEN'],
|
|
644
|
+
// pi supports OpenAI- and Anthropic-compatible backends.
|
|
645
|
+
pi: ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY'],
|
|
646
|
+
codex: ['OPENAI_API_KEY'],
|
|
647
|
+
reasonix: ['DEEPSEEK_API_KEY'],
|
|
648
|
+
codewhale: [],
|
|
649
|
+
crush: [],
|
|
650
|
+
};
|
|
651
|
+
//# sourceMappingURL=bridgeProviders.js.map
|