zelari-code 0.1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/zelari-code.js +64 -0
- package/dist/agents/advancedTools.js +115 -0
- package/dist/agents/advancedTools.js.map +1 -0
- package/dist/agents/councilApi.js +375 -0
- package/dist/agents/councilApi.js.map +1 -0
- package/dist/agents/councilDirectives.js +100 -0
- package/dist/agents/councilDirectives.js.map +1 -0
- package/dist/agents/promoteMember.js +210 -0
- package/dist/agents/promoteMember.js.map +1 -0
- package/dist/agents/promptModules.js +139 -0
- package/dist/agents/promptModules.js.map +1 -0
- package/dist/agents/roles.js +281 -0
- package/dist/agents/roles.js.map +1 -0
- package/dist/agents/skills/builtin/debugging.js +248 -0
- package/dist/agents/skills/builtin/debugging.js.map +1 -0
- package/dist/agents/skills/builtin/docs.js +301 -0
- package/dist/agents/skills/builtin/docs.js.map +1 -0
- package/dist/agents/skills/builtin/git-ops.js +301 -0
- package/dist/agents/skills/builtin/git-ops.js.map +1 -0
- package/dist/agents/skills/builtin/planning.js +335 -0
- package/dist/agents/skills/builtin/planning.js.map +1 -0
- package/dist/agents/skills/builtin/refactoring.js +261 -0
- package/dist/agents/skills/builtin/refactoring.js.map +1 -0
- package/dist/agents/skills/builtin/review.js +302 -0
- package/dist/agents/skills/builtin/review.js.map +1 -0
- package/dist/agents/skills/builtin/testing.js +245 -0
- package/dist/agents/skills/builtin/testing.js.map +1 -0
- package/dist/agents/skills.js +272 -0
- package/dist/agents/skills.js.map +1 -0
- package/dist/agents/systemPromptBuilder.js +151 -0
- package/dist/agents/systemPromptBuilder.js.map +1 -0
- package/dist/agents/toolSchemas.js +249 -0
- package/dist/agents/toolSchemas.js.map +1 -0
- package/dist/agents/tools.js +196 -0
- package/dist/agents/tools.js.map +1 -0
- package/dist/agents/vaultTools.js +134 -0
- package/dist/agents/vaultTools.js.map +1 -0
- package/dist/cli/app.js +1823 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/branchManager.js +280 -0
- package/dist/cli/branchManager.js.map +1 -0
- package/dist/cli/compaction.js +63 -0
- package/dist/cli/compaction.js.map +1 -0
- package/dist/cli/components/ChatStream.js +23 -0
- package/dist/cli/components/ChatStream.js.map +1 -0
- package/dist/cli/components/CollapsibleToolOutput.js +38 -0
- package/dist/cli/components/CollapsibleToolOutput.js.map +1 -0
- package/dist/cli/components/Header.js +31 -0
- package/dist/cli/components/Header.js.map +1 -0
- package/dist/cli/components/InputBar.js +9 -0
- package/dist/cli/components/InputBar.js.map +1 -0
- package/dist/cli/components/Sidebar.js +17 -0
- package/dist/cli/components/Sidebar.js.map +1 -0
- package/dist/cli/councilCost.js +81 -0
- package/dist/cli/councilCost.js.map +1 -0
- package/dist/cli/councilDispatcher.js +56 -0
- package/dist/cli/councilDispatcher.js.map +1 -0
- package/dist/cli/councilFeedback.js +164 -0
- package/dist/cli/councilFeedback.js.map +1 -0
- package/dist/cli/crossProviderFailover.js +83 -0
- package/dist/cli/crossProviderFailover.js.map +1 -0
- package/dist/cli/gitOps.js +96 -0
- package/dist/cli/gitOps.js.map +1 -0
- package/dist/cli/grokOAuth.js +375 -0
- package/dist/cli/grokOAuth.js.map +1 -0
- package/dist/cli/keyStore.js +263 -0
- package/dist/cli/keyStore.js.map +1 -0
- package/dist/cli/keyValidator.js +83 -0
- package/dist/cli/keyValidator.js.map +1 -0
- package/dist/cli/main.bundled.js +21428 -0
- package/dist/cli/main.bundled.js.map +7 -0
- package/dist/cli/main.js +72 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/metrics.js +131 -0
- package/dist/cli/metrics.js.map +1 -0
- package/dist/cli/modelDiscovery.js +234 -0
- package/dist/cli/modelDiscovery.js.map +1 -0
- package/dist/cli/modelPricing.js +112 -0
- package/dist/cli/modelPricing.js.map +1 -0
- package/dist/cli/oauthCallbackServer.js +207 -0
- package/dist/cli/oauthCallbackServer.js.map +1 -0
- package/dist/cli/provider/openai-compatible.js +236 -0
- package/dist/cli/provider/openai-compatible.js.map +1 -0
- package/dist/cli/providerConfig.js +210 -0
- package/dist/cli/providerConfig.js.map +1 -0
- package/dist/cli/providerFailover.js +90 -0
- package/dist/cli/providerFailover.js.map +1 -0
- package/dist/cli/refreshRegistry.js +77 -0
- package/dist/cli/refreshRegistry.js.map +1 -0
- package/dist/cli/safety/auditLogger.js +111 -0
- package/dist/cli/safety/auditLogger.js.map +1 -0
- package/dist/cli/safety/sandboxPath.js +62 -0
- package/dist/cli/safety/sandboxPath.js.map +1 -0
- package/dist/cli/safety/shellBlocklist.js +71 -0
- package/dist/cli/safety/shellBlocklist.js.map +1 -0
- package/dist/cli/sessionManager.js +149 -0
- package/dist/cli/sessionManager.js.map +1 -0
- package/dist/cli/skillCache.js +205 -0
- package/dist/cli/skillCache.js.map +1 -0
- package/dist/cli/skillHistory.js +178 -0
- package/dist/cli/skillHistory.js.map +1 -0
- package/dist/cli/skillSuggest.js +132 -0
- package/dist/cli/skillSuggest.js.map +1 -0
- package/dist/cli/slashCommands.js +431 -0
- package/dist/cli/slashCommands.js.map +1 -0
- package/dist/cli/toolRegistry.js +187 -0
- package/dist/cli/toolRegistry.js.map +1 -0
- package/dist/cli/updater.js +157 -0
- package/dist/cli/updater.js.map +1 -0
- package/dist/main/core/AgentHarness.js +345 -0
- package/dist/main/core/AgentHarness.js.map +1 -0
- package/dist/main/core/providerStream.js +99 -0
- package/dist/main/core/providerStream.js.map +1 -0
- package/dist/main/core/sessionJsonl.js +99 -0
- package/dist/main/core/sessionJsonl.js.map +1 -0
- package/dist/main/core/tools/auditLog.js +26 -0
- package/dist/main/core/tools/auditLog.js.map +1 -0
- package/dist/main/core/tools/builtin/filesystem.js +110 -0
- package/dist/main/core/tools/builtin/filesystem.js.map +1 -0
- package/dist/main/core/tools/builtin/search.js +56 -0
- package/dist/main/core/tools/builtin/search.js.map +1 -0
- package/dist/main/core/tools/builtin/shell.js +51 -0
- package/dist/main/core/tools/builtin/shell.js.map +1 -0
- package/dist/main/core/tools/registry.js +71 -0
- package/dist/main/core/tools/registry.js.map +1 -0
- package/dist/main/core/tools/toolTypes.js +8 -0
- package/dist/main/core/tools/toolTypes.js.map +1 -0
- package/dist/main/core/tools/zodBridge.js +48 -0
- package/dist/main/core/tools/zodBridge.js.map +1 -0
- package/dist/shared/eventBus.js +104 -0
- package/dist/shared/eventBus.js.map +1 -0
- package/dist/shared/events.js +73 -0
- package/dist/shared/events.js.map +1 -0
- package/dist/shared/index.js +8 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/types/context.js +3 -0
- package/dist/types/context.js.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/knowledge.js +2 -0
- package/dist/types/knowledge.js.map +1 -0
- package/dist/types/systemTypes.js +10 -0
- package/dist/types/systemTypes.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oauthCallbackServer — minimal HTTP server for OAuth browser-flow callbacks.
|
|
3
|
+
*
|
|
4
|
+
* The CLI uses this to receive the redirect from the OAuth provider after
|
|
5
|
+
* the user authenticates in their browser. The server:
|
|
6
|
+
* - listens on a local port (default 14523) on 127.0.0.1
|
|
7
|
+
* - captures `?code=XXX` from the first request to `expectedPath`
|
|
8
|
+
* - returns a friendly 200 HTML response so the user can close the tab
|
|
9
|
+
* - resolves a single `waitForCode()` Promise with the code, or rejects on timeout
|
|
10
|
+
* - shuts itself down after capture (or via explicit close())
|
|
11
|
+
*
|
|
12
|
+
* No deps — pure node:http. Browser-importable for jsdom tests.
|
|
13
|
+
*
|
|
14
|
+
* @see docs/plans/2026-06-29-anathema-coder-v2.md (Task 16.1)
|
|
15
|
+
*/
|
|
16
|
+
import { createServer } from 'node:http';
|
|
17
|
+
const DEFAULT_SUCCESS_HTML = `
|
|
18
|
+
<!DOCTYPE html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="UTF-8">
|
|
22
|
+
<title>zelari-code OAuth</title>
|
|
23
|
+
</head>
|
|
24
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 40px; text-align: center; color: #1a1a1a;">
|
|
25
|
+
<h1 style="color: #00aa33;">✓ Authentication complete</h1>
|
|
26
|
+
<p>You can close this tab and return to zelari-code.</p>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
29
|
+
`.trim();
|
|
30
|
+
export class OAuthCallbackTimeoutError extends Error {
|
|
31
|
+
constructor(timeoutMs) {
|
|
32
|
+
super(`OAuth callback not received within ${timeoutMs}ms`);
|
|
33
|
+
this.name = 'OAuthCallbackTimeoutError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class OAuthCallbackClosedError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super('OAuth callback server closed before receiving a callback');
|
|
39
|
+
this.name = 'OAuthCallbackClosedError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class OAuthCallbackError extends Error {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'OAuthCallbackError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Start the OAuth callback server. Returns a handle with port + waitForCode + close.
|
|
50
|
+
*
|
|
51
|
+
* The first request to `expectedPath` resolves waitForCode with the `code`
|
|
52
|
+
* query parameter. Any other path returns a 404.
|
|
53
|
+
*/
|
|
54
|
+
export function startOAuthCallbackServer(options = {}) {
|
|
55
|
+
const host = options.host ?? '127.0.0.1';
|
|
56
|
+
const preferredPort = options.port ?? 14523;
|
|
57
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
58
|
+
const expectedPath = options.expectedPath ?? '/oauth/callback';
|
|
59
|
+
const successHtml = options.successHtml ?? DEFAULT_SUCCESS_HTML;
|
|
60
|
+
return new Promise((resolveStart, rejectStart) => {
|
|
61
|
+
let resolved = false;
|
|
62
|
+
let resolveCode = null;
|
|
63
|
+
let rejectCode = null;
|
|
64
|
+
let timeoutHandle = null;
|
|
65
|
+
const server = createServer((req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
if (!req.url) {
|
|
68
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
69
|
+
res.end('Bad request');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Parse path + query.
|
|
73
|
+
const url = new URL(req.url, `http://${host}:${preferredPort}`);
|
|
74
|
+
if (url.pathname !== expectedPath) {
|
|
75
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
76
|
+
res.end('Not found');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const code = url.searchParams.get('code');
|
|
80
|
+
const state = url.searchParams.get('state');
|
|
81
|
+
const errorParam = url.searchParams.get('error');
|
|
82
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
83
|
+
// Capture any extra params for diagnostics / future use.
|
|
84
|
+
const extras = {};
|
|
85
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
86
|
+
if (key !== 'code' && key !== 'state' && key !== 'error' && key !== 'error_description') {
|
|
87
|
+
extras[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const params = {
|
|
91
|
+
...(code ? { code } : {}),
|
|
92
|
+
...(state ? { state } : {}),
|
|
93
|
+
...(errorParam ? { error: errorParam } : {}),
|
|
94
|
+
...(errorDescription ? { errorDescription } : {}),
|
|
95
|
+
extras,
|
|
96
|
+
};
|
|
97
|
+
if (errorParam) {
|
|
98
|
+
// OAuth provider returned an error (e.g. user denied).
|
|
99
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
100
|
+
res.end(`<h1>OAuth error: ${errorParam}</h1>`);
|
|
101
|
+
if (rejectCode && !resolved) {
|
|
102
|
+
resolved = true;
|
|
103
|
+
if (timeoutHandle)
|
|
104
|
+
clearTimeout(timeoutHandle);
|
|
105
|
+
rejectCode(new OAuthCallbackError(`OAuth provider returned error: ${errorParam}`));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!code) {
|
|
110
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
111
|
+
res.end('<h1>Missing <code>code</code> query parameter</h1>');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Success.
|
|
115
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
116
|
+
res.end(successHtml);
|
|
117
|
+
if (resolveCode && !resolved) {
|
|
118
|
+
resolved = true;
|
|
119
|
+
if (timeoutHandle)
|
|
120
|
+
clearTimeout(timeoutHandle);
|
|
121
|
+
resolveCode(params);
|
|
122
|
+
}
|
|
123
|
+
// Give the response a moment to flush before closing.
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
try {
|
|
126
|
+
server.close();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// already closed
|
|
130
|
+
}
|
|
131
|
+
}, 100);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// Defensive: never crash the server on a single bad request.
|
|
135
|
+
try {
|
|
136
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
137
|
+
res.end('Internal error');
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
if (!resolved && rejectCode) {
|
|
143
|
+
resolved = true;
|
|
144
|
+
if (timeoutHandle)
|
|
145
|
+
clearTimeout(timeoutHandle);
|
|
146
|
+
rejectCode(err instanceof Error ? err : new Error(String(err)));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const onError = (err) => {
|
|
151
|
+
if (!resolved) {
|
|
152
|
+
resolved = true;
|
|
153
|
+
if (timeoutHandle)
|
|
154
|
+
clearTimeout(timeoutHandle);
|
|
155
|
+
rejectStart(err);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
server.once('error', onError);
|
|
159
|
+
server.listen(preferredPort, host, () => {
|
|
160
|
+
// Resolve with the actual bound port (important when port=0).
|
|
161
|
+
const addr = server.address();
|
|
162
|
+
const actualPort = addr ? addr.port : preferredPort;
|
|
163
|
+
const waitForCode = () => {
|
|
164
|
+
return new Promise((res, rej) => {
|
|
165
|
+
if (resolved) {
|
|
166
|
+
// Server already resolved (timeout fired, etc.) — reject.
|
|
167
|
+
rej(new OAuthCallbackClosedError());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolveCode = res;
|
|
171
|
+
rejectCode = rej;
|
|
172
|
+
timeoutHandle = setTimeout(() => {
|
|
173
|
+
if (!resolved) {
|
|
174
|
+
resolved = true;
|
|
175
|
+
try {
|
|
176
|
+
server.close();
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// ignore
|
|
180
|
+
}
|
|
181
|
+
rej(new OAuthCallbackTimeoutError(timeoutMs));
|
|
182
|
+
}
|
|
183
|
+
}, timeoutMs);
|
|
184
|
+
// Don't keep the process alive just for this timer.
|
|
185
|
+
if (typeof timeoutHandle?.unref === 'function')
|
|
186
|
+
timeoutHandle.unref();
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
const close = () => {
|
|
190
|
+
if (timeoutHandle)
|
|
191
|
+
clearTimeout(timeoutHandle);
|
|
192
|
+
try {
|
|
193
|
+
server.close();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// ignore
|
|
197
|
+
}
|
|
198
|
+
if (!resolved && rejectCode) {
|
|
199
|
+
resolved = true;
|
|
200
|
+
rejectCode(new OAuthCallbackClosedError());
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
resolveStart({ port: actualPort, waitForCode, close });
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=oauthCallbackServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthCallbackServer.js","sourceRoot":"","sources":["../../src/cli/oauthCallbackServer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible HTTP provider.
|
|
3
|
+
* Works against any chat-completions endpoint that follows OpenAI's
|
|
4
|
+
* streaming protocol: minimax, glm, grok, custom.
|
|
5
|
+
*
|
|
6
|
+
* Reads API key from the active provider (env first, then keyStore).
|
|
7
|
+
* Resolves baseUrl via `PROVIDER_ENDPOINTS` (providerId → baseUrl).
|
|
8
|
+
* Resolves model via `providerConfig.getModelForProvider(id)`.
|
|
9
|
+
*
|
|
10
|
+
* Active provider resolution: `ANATHEMA_ACTIVE_PROVIDER` env first, then
|
|
11
|
+
* the on-disk providerConfig.json (Phase 15), default `openai-compatible`.
|
|
12
|
+
*
|
|
13
|
+
* Implements ProviderStreamFn (AsyncIterable<ProviderDelta>).
|
|
14
|
+
*/
|
|
15
|
+
import { resolveApiKeyWithMeta } from '../keyStore.js';
|
|
16
|
+
import { getProviderConfig, getModelForProvider, getCustomEndpoint } from '../providerConfig.js';
|
|
17
|
+
/**
|
|
18
|
+
* Default endpoints for each provider. `openai-compatible` defaults to
|
|
19
|
+
* api.x.ai (backward compatibility); `custom` is empty and must be set
|
|
20
|
+
* via OPENAI_BASE_URL.
|
|
21
|
+
*/
|
|
22
|
+
export const PROVIDER_ENDPOINTS = {
|
|
23
|
+
'openai-compatible': 'https://api.x.ai/v1',
|
|
24
|
+
'minimax': 'https://api.MiniMax.chat/v1',
|
|
25
|
+
'glm': 'https://api.z.ai/v1',
|
|
26
|
+
'grok': 'https://api.x.ai/v1',
|
|
27
|
+
'custom': '',
|
|
28
|
+
};
|
|
29
|
+
/** Resolve the active provider id (env override > on-disk config > default). */
|
|
30
|
+
export function resolveActiveProvider() {
|
|
31
|
+
return getProviderConfig().activeProviderId;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve baseUrl for a given provider. Priority:
|
|
35
|
+
* 1. Custom endpoint persisted via `/provider custom <url>` (wins always)
|
|
36
|
+
* 2. OPENAI_BASE_URL env override (for openai-compatible / custom)
|
|
37
|
+
* 3. PROVIDER_ENDPOINTS default for that provider
|
|
38
|
+
*/
|
|
39
|
+
export function resolveBaseUrl(providerId) {
|
|
40
|
+
const custom = getCustomEndpoint(providerId);
|
|
41
|
+
if (custom)
|
|
42
|
+
return custom;
|
|
43
|
+
if (providerId === 'openai-compatible' || providerId === 'custom') {
|
|
44
|
+
return process.env.OPENAI_BASE_URL ?? PROVIDER_ENDPOINTS[providerId];
|
|
45
|
+
}
|
|
46
|
+
return PROVIDER_ENDPOINTS[providerId];
|
|
47
|
+
}
|
|
48
|
+
export function openaiCompatibleProvider(config) {
|
|
49
|
+
return async function* (params) {
|
|
50
|
+
const body = {
|
|
51
|
+
model: config.model,
|
|
52
|
+
messages: params.messages
|
|
53
|
+
.filter((m) => m.role !== 'tool') // skip tool messages for simplicity
|
|
54
|
+
.map((m) => ({ role: m.role, content: m.content })),
|
|
55
|
+
stream: true,
|
|
56
|
+
temperature: 0.7,
|
|
57
|
+
// Task G.4.2 — request the provider to send real token usage in
|
|
58
|
+
// the final chunk (gated by `stream_options.include_usage` on the
|
|
59
|
+
// OpenAI-compatible API). Providers that don't honor this (some
|
|
60
|
+
// self-hosted gateways) will simply not send a `usage` chunk, and
|
|
61
|
+
// the harness will fall back to the ~4-char/token approximation.
|
|
62
|
+
stream_options: { include_usage: true },
|
|
63
|
+
};
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
signal: config.signal,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
yield { kind: 'error', message: `Network error: ${err instanceof Error ? err.message : String(err)}` };
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!response.ok || !response.body) {
|
|
81
|
+
const errText = await response.text().catch(() => '');
|
|
82
|
+
yield { kind: 'error', message: `HTTP ${response.status}: ${errText.slice(0, 200)}` };
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const reader = response.body.getReader();
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
let buffer = '';
|
|
88
|
+
// Accumulator for OpenAI tool_calls streaming (Task A1).
|
|
89
|
+
// Args JSON is delivered incrementally; we emit a `tool_call` delta
|
|
90
|
+
// when the accumulated args for a given index parse to a complete
|
|
91
|
+
// JSON object.
|
|
92
|
+
const toolCallAccumulator = new Map();
|
|
93
|
+
try {
|
|
94
|
+
while (true) {
|
|
95
|
+
const { value, done } = await reader.read();
|
|
96
|
+
if (done)
|
|
97
|
+
break;
|
|
98
|
+
buffer += decoder.decode(value, { stream: true });
|
|
99
|
+
// SSE format: lines starting with "data: "
|
|
100
|
+
const lines = buffer.split('\n');
|
|
101
|
+
buffer = lines.pop() ?? ''; // keep the incomplete line in buffer
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed.startsWith('data:'))
|
|
105
|
+
continue;
|
|
106
|
+
const data = trimmed.slice(5).trim();
|
|
107
|
+
if (data === '[DONE]') {
|
|
108
|
+
yield { kind: 'finish', reason: 'stop' };
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(data);
|
|
113
|
+
const choice = parsed.choices?.[0];
|
|
114
|
+
const delta = choice?.delta;
|
|
115
|
+
// Task G.4.2 — emit a `usage` delta whenever the provider
|
|
116
|
+
// includes a usage payload on the chunk. We don't gate on
|
|
117
|
+
// `choice.finish_reason` because some providers (xAI) send
|
|
118
|
+
// the usage on a chunk whose choices array is empty.
|
|
119
|
+
if (parsed.usage && typeof parsed.usage === 'object') {
|
|
120
|
+
const promptTokens = typeof parsed.usage.prompt_tokens === 'number'
|
|
121
|
+
? parsed.usage.prompt_tokens
|
|
122
|
+
: 0;
|
|
123
|
+
const completionTokens = typeof parsed.usage.completion_tokens === 'number'
|
|
124
|
+
? parsed.usage.completion_tokens
|
|
125
|
+
: 0;
|
|
126
|
+
const totalTokens = typeof parsed.usage.total_tokens === 'number'
|
|
127
|
+
? parsed.usage.total_tokens
|
|
128
|
+
: promptTokens + completionTokens;
|
|
129
|
+
yield {
|
|
130
|
+
kind: 'usage',
|
|
131
|
+
usage: { promptTokens, completionTokens, totalTokens },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (typeof delta?.content === 'string' && delta.content.length > 0) {
|
|
135
|
+
yield { kind: 'text', delta: delta.content };
|
|
136
|
+
}
|
|
137
|
+
// OpenAI tool_calls are streamed incrementally — accumulate args
|
|
138
|
+
// per index and emit a tool_call delta when the args JSON closes.
|
|
139
|
+
if (Array.isArray(delta?.tool_calls)) {
|
|
140
|
+
for (const tc of delta.tool_calls) {
|
|
141
|
+
const idx = tc.index ?? 0;
|
|
142
|
+
const existing = toolCallAccumulator.get(idx) ?? {
|
|
143
|
+
id: tc.id ?? '',
|
|
144
|
+
name: tc.function?.name ?? '',
|
|
145
|
+
argsJson: '',
|
|
146
|
+
};
|
|
147
|
+
if (tc.id)
|
|
148
|
+
existing.id = tc.id;
|
|
149
|
+
if (tc.function?.name)
|
|
150
|
+
existing.name = tc.function.name;
|
|
151
|
+
if (tc.function?.arguments)
|
|
152
|
+
existing.argsJson += tc.function.arguments;
|
|
153
|
+
toolCallAccumulator.set(idx, existing);
|
|
154
|
+
// Heuristic: when argsJson parses as a complete JSON object, emit.
|
|
155
|
+
if (existing.argsJson.trim().endsWith('}')) {
|
|
156
|
+
try {
|
|
157
|
+
const parsedArgs = JSON.parse(existing.argsJson);
|
|
158
|
+
toolCallAccumulator.delete(idx);
|
|
159
|
+
yield {
|
|
160
|
+
kind: 'tool_call',
|
|
161
|
+
toolCallId: existing.id || `tc-${idx}`,
|
|
162
|
+
toolName: existing.name,
|
|
163
|
+
args: parsedArgs,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// JSON not closed yet — keep accumulating.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Skip malformed lines
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
reader.releaseLock();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Helper: read env + keyStore + providerConfig and return a configured
|
|
186
|
+
* provider for the currently-active provider. Returns null if the API key
|
|
187
|
+
* is missing (CLI will show a friendly error).
|
|
188
|
+
*
|
|
189
|
+
* Resolution order:
|
|
190
|
+
* 1. ANATHEMA_ACTIVE_PROVIDER env (overrides everything)
|
|
191
|
+
* 2. providerConfig.activeProviderId (Phase 15 persistent state)
|
|
192
|
+
* 3. Default: 'openai-compatible'
|
|
193
|
+
*
|
|
194
|
+
* API key resolution (per providerId):
|
|
195
|
+
* 1. {PROVIDER_UPPER}_API_KEY env (e.g. GROK_API_KEY)
|
|
196
|
+
* 2. keyStore stored key (OAuth-aware via resolveApiKeyWithMeta)
|
|
197
|
+
* — this triggers auto-refresh when the token is near expiry (Task D.3.2)
|
|
198
|
+
*
|
|
199
|
+
* Base URL resolution (per providerId):
|
|
200
|
+
* - 'openai-compatible' / 'custom': OPENAI_BASE_URL env or default to api.x.ai
|
|
201
|
+
* - others: hardcoded PROVIDER_ENDPOINTS entry
|
|
202
|
+
*
|
|
203
|
+
* Model resolution:
|
|
204
|
+
* - OPENAI_MODEL env override (always wins)
|
|
205
|
+
* - providerConfig.modelByProvider[providerId]
|
|
206
|
+
*/
|
|
207
|
+
export async function providerFromEnv() {
|
|
208
|
+
const providerId = resolveActiveProvider();
|
|
209
|
+
const apiKey = await resolveApiKeyWithMeta(providerId);
|
|
210
|
+
if (!apiKey)
|
|
211
|
+
return null;
|
|
212
|
+
return {
|
|
213
|
+
apiKey: apiKey.apiKey,
|
|
214
|
+
baseUrl: resolveBaseUrl(providerId),
|
|
215
|
+
model: getModelForProvider(providerId),
|
|
216
|
+
providerId,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolve a provider config explicitly for a given provider id. Useful
|
|
221
|
+
* when the caller needs to switch providers at runtime (e.g. `/provider <name>`).
|
|
222
|
+
*
|
|
223
|
+
* Returns null if the API key for that provider is missing.
|
|
224
|
+
*/
|
|
225
|
+
export async function providerConfigFor(providerId) {
|
|
226
|
+
const apiKey = await resolveApiKeyWithMeta(providerId);
|
|
227
|
+
if (!apiKey)
|
|
228
|
+
return null;
|
|
229
|
+
return {
|
|
230
|
+
apiKey: apiKey.apiKey,
|
|
231
|
+
baseUrl: resolveBaseUrl(providerId),
|
|
232
|
+
model: getModelForProvider(providerId),
|
|
233
|
+
providerId,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=openai-compatible.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-compatible.js","sourceRoot":"","sources":["../../../src/cli/provider/openai-compatible.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providerConfig — CLI provider runtime configuration.
|
|
3
|
+
*
|
|
4
|
+
* Persists the active provider id + per-provider default models to a JSON
|
|
5
|
+
* file in the user's home directory. Complements `keyStore` (which stores
|
|
6
|
+
* API keys) with the *routing* state: which provider the CLI should use
|
|
7
|
+
* next, and which model to use for each one.
|
|
8
|
+
*
|
|
9
|
+
* Storage layout (alongside keyStore):
|
|
10
|
+
* ~/.tmp/anathema-coder/
|
|
11
|
+
* keys.json ← keyStore (Task 14.9)
|
|
12
|
+
* provider.json ← this file (Task 15.1)
|
|
13
|
+
* sessions/<id>.jsonl
|
|
14
|
+
* branches/<name>/
|
|
15
|
+
*
|
|
16
|
+
* Pure node:fs — no Electron deps, browser-importable for jsdom tests.
|
|
17
|
+
* Env override: `ANATHEMA_PROVIDER_CONFIG_FILE` (useful for tests + CI).
|
|
18
|
+
*
|
|
19
|
+
* @see docs/plans/2026-06-29-anathema-coder-v2.md (Task 15.1)
|
|
20
|
+
*/
|
|
21
|
+
import { promises as fs, existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import { PROVIDERS } from './keyStore.js';
|
|
25
|
+
const DEFAULTS = {
|
|
26
|
+
activeProviderId: 'openai-compatible',
|
|
27
|
+
modelByProvider: {
|
|
28
|
+
'openai-compatible': 'grok-4',
|
|
29
|
+
'minimax': 'MiniMax-M2.5',
|
|
30
|
+
'glm': 'glm-4.6',
|
|
31
|
+
'grok': 'grok-4',
|
|
32
|
+
'custom': '',
|
|
33
|
+
},
|
|
34
|
+
customEndpoints: {},
|
|
35
|
+
};
|
|
36
|
+
export function getProviderConfigPath() {
|
|
37
|
+
return process.env.ANATHEMA_PROVIDER_CONFIG_FILE
|
|
38
|
+
?? path.join(os.homedir(), '.tmp', 'zelari-code', 'provider.json');
|
|
39
|
+
}
|
|
40
|
+
/** Return the resolved ProviderConfig (env override > on-disk > defaults). */
|
|
41
|
+
export function getProviderConfig() {
|
|
42
|
+
// Env override for active provider (used by tests + CI).
|
|
43
|
+
const envActive = process.env.ANATHEMA_ACTIVE_PROVIDER;
|
|
44
|
+
const envModel = process.env.OPENAI_MODEL;
|
|
45
|
+
const file = getProviderConfigPath();
|
|
46
|
+
let stored = null;
|
|
47
|
+
if (existsSync(file)) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = readFileSync(file, 'utf-8');
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.activeProviderId === 'string'
|
|
52
|
+
&& parsed.modelByProvider && typeof parsed.modelByProvider === 'object') {
|
|
53
|
+
stored = {
|
|
54
|
+
activeProviderId: parsed.activeProviderId,
|
|
55
|
+
modelByProvider: { ...DEFAULTS.modelByProvider, ...parsed.modelByProvider },
|
|
56
|
+
customEndpoints: mergeCustomEndpoints(parsed.customEndpoints),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Corrupt file — fall through to defaults.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const base = stored ?? {
|
|
65
|
+
...DEFAULTS,
|
|
66
|
+
modelByProvider: { ...DEFAULTS.modelByProvider },
|
|
67
|
+
customEndpoints: { ...DEFAULTS.customEndpoints },
|
|
68
|
+
};
|
|
69
|
+
// Apply env overrides last so they always win.
|
|
70
|
+
if (envActive && PROVIDERS.some((p) => p.id === envActive)) {
|
|
71
|
+
base.activeProviderId = envActive;
|
|
72
|
+
}
|
|
73
|
+
if (envModel && envModel.trim().length > 0) {
|
|
74
|
+
base.modelByProvider[base.activeProviderId] = envModel;
|
|
75
|
+
}
|
|
76
|
+
return base;
|
|
77
|
+
}
|
|
78
|
+
function writeProviderConfig(config) {
|
|
79
|
+
const file = getProviderConfigPath();
|
|
80
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
writeFileSync(file, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sanitize a parsed `customEndpoints` blob into a clean
|
|
85
|
+
* `Partial<Record<ProviderName, string>>`. Drops non-string values,
|
|
86
|
+
* non-ProviderName keys, and empty / whitespace-only URLs.
|
|
87
|
+
*/
|
|
88
|
+
function mergeCustomEndpoints(raw) {
|
|
89
|
+
if (!raw || typeof raw !== 'object')
|
|
90
|
+
return {};
|
|
91
|
+
const result = {};
|
|
92
|
+
const validIds = new Set(PROVIDERS.map((p) => p.id));
|
|
93
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
94
|
+
if (!validIds.has(key))
|
|
95
|
+
continue;
|
|
96
|
+
if (typeof value !== 'string')
|
|
97
|
+
continue;
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
if (trimmed.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
result[key] = trimmed;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/** Return the resolved custom endpoint for a provider, or undefined. */
|
|
106
|
+
export function getCustomEndpoint(id) {
|
|
107
|
+
return getProviderConfig().customEndpoints[id];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Save a custom base URL for a provider. Validates the URL is parseable.
|
|
111
|
+
* Empty string is rejected — use `clearCustomEndpoint()` to remove.
|
|
112
|
+
*/
|
|
113
|
+
export function setCustomEndpoint(id, url) {
|
|
114
|
+
const spec = PROVIDERS.find((p) => p.id === id);
|
|
115
|
+
if (!spec) {
|
|
116
|
+
throw new Error(`Unknown provider id: "${id}". Available: ${PROVIDERS.map((p) => p.id).join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
const trimmed = url.trim();
|
|
119
|
+
if (trimmed.length === 0) {
|
|
120
|
+
throw new Error('Custom endpoint URL cannot be empty. Use clearCustomEndpoint() to remove.');
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
// eslint-disable-next-line no-new
|
|
124
|
+
new URL(trimmed);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error(`Invalid custom endpoint URL: "${url}"`);
|
|
128
|
+
}
|
|
129
|
+
const config = getProviderConfig();
|
|
130
|
+
config.customEndpoints[id] = trimmed;
|
|
131
|
+
writeProviderConfig(config);
|
|
132
|
+
}
|
|
133
|
+
/** Remove the custom base URL override for a provider (falls back to default). */
|
|
134
|
+
export function clearCustomEndpoint(id) {
|
|
135
|
+
const spec = PROVIDERS.find((p) => p.id === id);
|
|
136
|
+
if (!spec) {
|
|
137
|
+
throw new Error(`Unknown provider id: "${id}". Available: ${PROVIDERS.map((p) => p.id).join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
const config = getProviderConfig();
|
|
140
|
+
if (!(id in config.customEndpoints))
|
|
141
|
+
return; // no-op
|
|
142
|
+
delete config.customEndpoints[id];
|
|
143
|
+
writeProviderConfig(config);
|
|
144
|
+
}
|
|
145
|
+
export function setActiveProviderId(id) {
|
|
146
|
+
const spec = PROVIDERS.find((p) => p.id === id);
|
|
147
|
+
if (!spec) {
|
|
148
|
+
throw new Error(`Unknown provider id: "${id}". Available: ${PROVIDERS.map((p) => p.id).join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
const config = getProviderConfig();
|
|
151
|
+
config.activeProviderId = id;
|
|
152
|
+
// Ensure the model entry exists for the new provider.
|
|
153
|
+
if (!config.modelByProvider[id]) {
|
|
154
|
+
config.modelByProvider[id] = DEFAULTS.modelByProvider[id];
|
|
155
|
+
}
|
|
156
|
+
writeProviderConfig(config);
|
|
157
|
+
}
|
|
158
|
+
export function setModelForProvider(id, model) {
|
|
159
|
+
const spec = PROVIDERS.find((p) => p.id === id);
|
|
160
|
+
if (!spec) {
|
|
161
|
+
throw new Error(`Unknown provider id: "${id}". Available: ${PROVIDERS.map((p) => p.id).join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
if (!model || model.trim().length === 0) {
|
|
164
|
+
throw new Error('Model name cannot be empty.');
|
|
165
|
+
}
|
|
166
|
+
const config = getProviderConfig();
|
|
167
|
+
config.modelByProvider[id] = model.trim();
|
|
168
|
+
writeProviderConfig(config);
|
|
169
|
+
}
|
|
170
|
+
export function getModelForProvider(id) {
|
|
171
|
+
const config = getProviderConfig();
|
|
172
|
+
return config.modelByProvider[id] ?? DEFAULTS.modelByProvider[id] ?? '';
|
|
173
|
+
}
|
|
174
|
+
export function getActiveProvider() {
|
|
175
|
+
const config = getProviderConfig();
|
|
176
|
+
const spec = PROVIDERS.find((p) => p.id === config.activeProviderId);
|
|
177
|
+
// activeProviderId is validated at write time, so spec is always defined.
|
|
178
|
+
if (!spec)
|
|
179
|
+
throw new Error(`Invalid active provider id in config: ${config.activeProviderId}`);
|
|
180
|
+
return spec;
|
|
181
|
+
}
|
|
182
|
+
export function getActiveModel() {
|
|
183
|
+
const config = getProviderConfig();
|
|
184
|
+
return config.modelByProvider[config.activeProviderId] ?? DEFAULTS.modelByProvider[config.activeProviderId] ?? '';
|
|
185
|
+
}
|
|
186
|
+
/** Async variant of getProviderConfig (used by tests + CLI startup). */
|
|
187
|
+
export async function loadProviderConfig() {
|
|
188
|
+
const file = getProviderConfigPath();
|
|
189
|
+
try {
|
|
190
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
191
|
+
const parsed = JSON.parse(raw);
|
|
192
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.activeProviderId === 'string'
|
|
193
|
+
&& parsed.modelByProvider && typeof parsed.modelByProvider === 'object') {
|
|
194
|
+
return {
|
|
195
|
+
activeProviderId: parsed.activeProviderId,
|
|
196
|
+
modelByProvider: { ...DEFAULTS.modelByProvider, ...parsed.modelByProvider },
|
|
197
|
+
customEndpoints: mergeCustomEndpoints(parsed.customEndpoints),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// ENOENT or JSON parse failure — fall back.
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
...DEFAULTS,
|
|
206
|
+
modelByProvider: { ...DEFAULTS.modelByProvider },
|
|
207
|
+
customEndpoints: { ...DEFAULTS.customEndpoints },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=providerConfig.js.map
|