@nexrall/code-core 1.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/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/agent/agentTypes.d.ts +17 -0
- package/dist/agent/agentTypes.d.ts.map +1 -0
- package/dist/agent/agentTypes.js +156 -0
- package/dist/agent/loop.d.ts +3 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +775 -0
- package/dist/api/client.d.ts +27 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +414 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +95 -0
- package/dist/checkpoint/manager.d.ts +86 -0
- package/dist/checkpoint/manager.d.ts.map +1 -0
- package/dist/checkpoint/manager.js +409 -0
- package/dist/commands/loader.d.ts +18 -0
- package/dist/commands/loader.d.ts.map +1 -0
- package/dist/commands/loader.js +183 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/mcp/client.d.ts +46 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +128 -0
- package/dist/mcp/httpClient.d.ts +25 -0
- package/dist/mcp/httpClient.d.ts.map +1 -0
- package/dist/mcp/httpClient.js +143 -0
- package/dist/mcp/manager.d.ts +56 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +234 -0
- package/dist/mcp/sseClient.d.ts +30 -0
- package/dist/mcp/sseClient.d.ts.map +1 -0
- package/dist/mcp/sseClient.js +224 -0
- package/dist/permissions/rules.d.ts +20 -0
- package/dist/permissions/rules.d.ts.map +1 -0
- package/dist/permissions/rules.js +218 -0
- package/dist/plugins/index.d.ts +18 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +139 -0
- package/dist/tools/executor.d.ts +6 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +1458 -0
- package/dist/tools/sandbox.d.ts +13 -0
- package/dist/tools/sandbox.d.ts.map +1 -0
- package/dist/tools/sandbox.js +140 -0
- package/dist/tools/symbols.d.ts +13 -0
- package/dist/tools/symbols.d.ts.map +1 -0
- package/dist/tools/symbols.js +279 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +92 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Message, SSEEvent, AuthConfig, EnvContext, EditorContext } from '../types';
|
|
2
|
+
export declare const API_BASE = "https://api.nexrall.com";
|
|
3
|
+
export interface StreamChatOptions {
|
|
4
|
+
model: string;
|
|
5
|
+
env?: EnvContext;
|
|
6
|
+
editorContext?: EditorContext | null;
|
|
7
|
+
nexrallMd?: string;
|
|
8
|
+
mode?: string;
|
|
9
|
+
effort?: string;
|
|
10
|
+
clientType?: string;
|
|
11
|
+
abortSignal?: {
|
|
12
|
+
aborted: boolean;
|
|
13
|
+
};
|
|
14
|
+
/** Extra tool definitions from MCP servers — backend merges into Anthropic call. */
|
|
15
|
+
extraTools?: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
input_schema: Record<string, unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
/** Catalogue of custom sub-agent types — injected into the system prompt. */
|
|
21
|
+
agents?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function streamChat(messages: Message[], options: StreamChatOptions, onEvent: (e: SSEEvent) => void): Promise<Message>;
|
|
24
|
+
export declare function getBalance(): Promise<number>;
|
|
25
|
+
export declare function exchangeVscodeCode(code: string): Promise<AuthConfig>;
|
|
26
|
+
export declare function login(email: string, password: string): Promise<AuthConfig>;
|
|
27
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CAkVlB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.API_BASE = void 0;
|
|
7
|
+
exports.streamChat = streamChat;
|
|
8
|
+
exports.getBalance = getBalance;
|
|
9
|
+
exports.exchangeVscodeCode = exchangeVscodeCode;
|
|
10
|
+
exports.login = login;
|
|
11
|
+
const eventsource_parser_1 = require("eventsource-parser");
|
|
12
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
13
|
+
const index_1 = require("../auth/index");
|
|
14
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
|
+
exports.API_BASE = 'https://api.nexrall.com';
|
|
16
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
17
|
+
function authHeaders() {
|
|
18
|
+
const token = (0, index_1.getToken)();
|
|
19
|
+
if (!token) {
|
|
20
|
+
throw new Error('Not authenticated. Run `nexrall-code login` first.');
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
Authorization: `Bearer ${token}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// ─── Retry helpers ────────────────────────────────────────────────────────────
|
|
28
|
+
const MAX_RETRIES = 3;
|
|
29
|
+
const RETRY_BASE_MS = 1000;
|
|
30
|
+
function sleep(ms) {
|
|
31
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
32
|
+
}
|
|
33
|
+
async function streamChat(messages, options, onEvent) {
|
|
34
|
+
const { model, env, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents } = options;
|
|
35
|
+
// Bridge custom abort signal → native AbortController so fetch is cancelled immediately
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
if (abortSignal?.aborted)
|
|
38
|
+
throw Object.assign(new Error('Aborted'), { name: 'AbortError' });
|
|
39
|
+
let abortPoll;
|
|
40
|
+
if (abortSignal) {
|
|
41
|
+
abortPoll = setInterval(() => { if (abortSignal.aborted)
|
|
42
|
+
controller.abort(); }, 50);
|
|
43
|
+
}
|
|
44
|
+
const fetchArgs = [
|
|
45
|
+
`${exports.API_BASE}/api/code/chat`,
|
|
46
|
+
{
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: authHeaders(),
|
|
49
|
+
body: JSON.stringify({ messages, model, env, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents }),
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
// A transient mid-stream failure (Anthropic "overloaded_error") arrives as an SSE error
|
|
54
|
+
// event over an ALREADY-200 response, so the connect-time HTTP-status retry below can't
|
|
55
|
+
// see it. We detect it in the parser and retry the WHOLE attempt with exponential backoff
|
|
56
|
+
// — but only while nothing has been emitted to the caller yet, so a retry can never
|
|
57
|
+
// duplicate already-rendered text or tool calls.
|
|
58
|
+
const isRetryableStreamMsg = (m) => /overloaded|rate.?limit|temporarily|unavailable|try again|internal server error/i.test(String(m ?? ''));
|
|
59
|
+
// One network attempt: connect (with connect-time 429/5xx retry) and consume the stream to
|
|
60
|
+
// completion. Returns the assembled assistant Message, or throws — tagging a transient
|
|
61
|
+
// pre-content failure with { retryable: true } for the outer loop below.
|
|
62
|
+
async function runAttempt() {
|
|
63
|
+
// Retry loop — handles connect-time 429 (rate limit) and 5xx (server errors, incl. 529)
|
|
64
|
+
let response;
|
|
65
|
+
let lastErr;
|
|
66
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
response = await (0, node_fetch_1.default)(...fetchArgs);
|
|
69
|
+
if (response.status === 429 && attempt < MAX_RETRIES) {
|
|
70
|
+
const retryAfter = parseInt(response.headers.get('retry-after') ?? '0', 10);
|
|
71
|
+
await sleep(retryAfter > 0 ? retryAfter * 1000 : RETRY_BASE_MS * Math.pow(2, attempt));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (response.status >= 500 && response.status < 600 && attempt < MAX_RETRIES) {
|
|
75
|
+
await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
break; // success or non-retryable error
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// Don't retry on abort
|
|
82
|
+
if (err.name === 'AbortError')
|
|
83
|
+
throw err;
|
|
84
|
+
lastErr = err;
|
|
85
|
+
if (attempt < MAX_RETRIES) {
|
|
86
|
+
await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!response)
|
|
92
|
+
throw lastErr ?? new Error('Request failed after max retries');
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errText = await response.text();
|
|
95
|
+
let errMsg = `API error ${response.status}`;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(errText);
|
|
98
|
+
if (parsed.error)
|
|
99
|
+
errMsg = parsed.error;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
errMsg = errText || errMsg;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(errMsg);
|
|
105
|
+
}
|
|
106
|
+
if (!response.body) {
|
|
107
|
+
throw new Error('Response body is null');
|
|
108
|
+
}
|
|
109
|
+
// Accumulated state for building the assistant message
|
|
110
|
+
const textParts = [];
|
|
111
|
+
const toolUseBlocks = [];
|
|
112
|
+
let completedMessage = null;
|
|
113
|
+
// Once anything has been shown to the caller (text / tool_use / thinking), a retry would
|
|
114
|
+
// duplicate it — so from that point on a stream error is surfaced, never retried.
|
|
115
|
+
let emittedToCaller = false;
|
|
116
|
+
// Partial input accumulator keyed by tool_use id
|
|
117
|
+
const partialInputs = {};
|
|
118
|
+
await new Promise((resolve, reject) => {
|
|
119
|
+
// Feed chunks from the response body into the SSE parser.
|
|
120
|
+
// Declared here so the heartbeat watchdog closure can reference it.
|
|
121
|
+
const stream = response.body;
|
|
122
|
+
// ── Heartbeat watchdog ────────────────────────────────────────────────────
|
|
123
|
+
// The backend sends ": ping" comments every 20 s. If we receive no data at
|
|
124
|
+
// all for 45 s the connection has silently dropped (proxy timeout, network
|
|
125
|
+
// blip). Reject so the caller sees an actionable error instead of hanging.
|
|
126
|
+
const HEARTBEAT_TIMEOUT_MS = 45000;
|
|
127
|
+
// Progress watchdog. The backend's ": ping" keepalive (every 20 s) resets lastDataAt,
|
|
128
|
+
// so the heartbeat above only catches a fully dead connection — NOT an upstream model
|
|
129
|
+
// stall, where Anthropic stops emitting tokens but the backend keeps pinging. That case
|
|
130
|
+
// would otherwise hang on "Thinking…" forever. lastProgressAt is bumped ONLY on real
|
|
131
|
+
// model events (see parser below), never on pings, so a genuine stall trips this and
|
|
132
|
+
// surfaces a retryable error.
|
|
133
|
+
// Two-phase stall limits:
|
|
134
|
+
// • BEFORE the first model event, the upstream is doing prompt processing (time-to-
|
|
135
|
+
// first-token). On big contexts (1M window, cold prompt cache, large tool schemas)
|
|
136
|
+
// that phase emits NOTHING until message_start and can legitimately exceed 150 s.
|
|
137
|
+
// Killing it there was the "timeout 150s → retry instantly works" bug: the first
|
|
138
|
+
// attempt warmed the prompt cache, so the retry's TTFT was short. Allow 300 s.
|
|
139
|
+
// • AFTER the first model event, tokens are flowing; a 150 s silent gap mid-stream
|
|
140
|
+
// is a genuine stall.
|
|
141
|
+
const FIRST_EVENT_TIMEOUT_MS = 300000;
|
|
142
|
+
const PROGRESS_TIMEOUT_MS = 150000;
|
|
143
|
+
let sawModelEvent = false;
|
|
144
|
+
let lastDataAt = Date.now();
|
|
145
|
+
let lastProgressAt = Date.now();
|
|
146
|
+
const heartbeatWatchdog = setInterval(() => {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (now - lastDataAt > HEARTBEAT_TIMEOUT_MS) {
|
|
149
|
+
clearInterval(heartbeatWatchdog);
|
|
150
|
+
stream.destroy?.();
|
|
151
|
+
reject(new Error('Connection lost — no data received for 45 s. Retry your message.'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const stallLimitMs = sawModelEvent ? PROGRESS_TIMEOUT_MS : FIRST_EVENT_TIMEOUT_MS;
|
|
155
|
+
if (now - lastProgressAt > stallLimitMs) {
|
|
156
|
+
clearInterval(heartbeatWatchdog);
|
|
157
|
+
stream.destroy?.();
|
|
158
|
+
// If nothing has been shown to the caller yet, a fresh attempt can't duplicate
|
|
159
|
+
// output — tag it retryable so the outer loop transparently retries the turn
|
|
160
|
+
// instead of killing it. Once output has been emitted, a retry would duplicate
|
|
161
|
+
// rendered text/tool calls, so surface the stall as a terminal error.
|
|
162
|
+
const stallErr = new Error(sawModelEvent
|
|
163
|
+
? `The model stopped responding mid-stream (no output for ${Math.round(stallLimitMs / 1000)} s). Retry your message.`
|
|
164
|
+
: `The model did not start responding within ${Math.round(stallLimitMs / 1000)} s (large context can take a while to process). Retry your message.`);
|
|
165
|
+
reject(emittedToCaller ? stallErr : Object.assign(stallErr, { retryable: true }));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}, 5000);
|
|
169
|
+
const parser = (0, eventsource_parser_1.createParser)((event) => {
|
|
170
|
+
lastDataAt = Date.now(); // any SSE data (including ": ping" comments) resets the watchdog
|
|
171
|
+
// Skip reconnect-interval events
|
|
172
|
+
if (event.type !== 'event') {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
{
|
|
176
|
+
const raw = event.data;
|
|
177
|
+
if (!raw || raw === '[DONE]') {
|
|
178
|
+
resolve();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(raw);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return; // Skip malformed lines
|
|
187
|
+
}
|
|
188
|
+
if (typeof parsed !== 'object' || parsed === null || !('type' in parsed)) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const evt = parsed;
|
|
192
|
+
// Reaching here means a well-formed model event was parsed (": ping" keepalive
|
|
193
|
+
// comments never produce a ParsedEvent with data) — count it as real progress.
|
|
194
|
+
lastProgressAt = Date.now();
|
|
195
|
+
sawModelEvent = true;
|
|
196
|
+
switch (evt.type) {
|
|
197
|
+
case 'text': {
|
|
198
|
+
const text = typeof evt.text === 'string' ? evt.text : '';
|
|
199
|
+
textParts.push(text);
|
|
200
|
+
emittedToCaller = true;
|
|
201
|
+
onEvent({ type: 'text', text });
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'tool_use': {
|
|
205
|
+
const block = {
|
|
206
|
+
type: 'tool_use',
|
|
207
|
+
id: typeof evt.id === 'string' ? evt.id : '',
|
|
208
|
+
name: typeof evt.name === 'string' ? evt.name : '',
|
|
209
|
+
input: evt.input ?? {},
|
|
210
|
+
};
|
|
211
|
+
// Handle streaming input deltas
|
|
212
|
+
if (typeof evt.input_json_delta === 'string') {
|
|
213
|
+
partialInputs[block.id] = (partialInputs[block.id] ?? '') + evt.input_json_delta;
|
|
214
|
+
try {
|
|
215
|
+
block.input = JSON.parse(partialInputs[block.id]);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Input not fully streamed yet
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
toolUseBlocks.push(block);
|
|
223
|
+
emittedToCaller = true;
|
|
224
|
+
onEvent({ type: 'tool_use', id: block.id, name: block.name, input: block.input });
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'message_complete': {
|
|
228
|
+
// Build the complete assistant message from accumulated parts
|
|
229
|
+
const contentBlocks = [];
|
|
230
|
+
const fullText = textParts.join('');
|
|
231
|
+
if (fullText) {
|
|
232
|
+
contentBlocks.push({ type: 'text', text: fullText });
|
|
233
|
+
}
|
|
234
|
+
contentBlocks.push(...toolUseBlocks);
|
|
235
|
+
completedMessage = {
|
|
236
|
+
role: 'assistant',
|
|
237
|
+
content: contentBlocks,
|
|
238
|
+
};
|
|
239
|
+
onEvent({ type: 'message_complete', message: completedMessage });
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case 'usage': {
|
|
243
|
+
if (typeof evt.input_tokens === 'number' && typeof evt.output_tokens === 'number') {
|
|
244
|
+
onEvent({
|
|
245
|
+
type: 'usage',
|
|
246
|
+
usage: {
|
|
247
|
+
input_tokens: evt.input_tokens,
|
|
248
|
+
output_tokens: evt.output_tokens,
|
|
249
|
+
cache_creation_input_tokens: typeof evt.cache_creation_input_tokens === 'number' ? evt.cache_creation_input_tokens : undefined,
|
|
250
|
+
cache_read_input_tokens: typeof evt.cache_read_input_tokens === 'number' ? evt.cache_read_input_tokens : undefined,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case 'context_warning': {
|
|
257
|
+
const message = typeof evt.message === 'string' ? evt.message : '';
|
|
258
|
+
if (message)
|
|
259
|
+
onEvent({ type: 'error', message: `⚠️ ${message}` });
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case 'thinking': {
|
|
263
|
+
const text = typeof evt.text === 'string' ? evt.text : '';
|
|
264
|
+
if (text) {
|
|
265
|
+
emittedToCaller = true;
|
|
266
|
+
onEvent({ type: 'thinking', text });
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case 'thinking_progress': {
|
|
271
|
+
// Without this case the live token counter never updated and long thinking
|
|
272
|
+
// phases (e.g. after reading a large file) looked frozen on "Thinking…".
|
|
273
|
+
const tokens = typeof evt.tokens === 'number' ? evt.tokens : 0;
|
|
274
|
+
onEvent({ type: 'thinking_progress', tokens });
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'thinking_delta': {
|
|
278
|
+
// Live thinking text streamed token-by-token so a long thinking phase shows
|
|
279
|
+
// content as it forms, not a single block dumped at message completion.
|
|
280
|
+
const text = typeof evt.text === 'string' ? evt.text : '';
|
|
281
|
+
if (text) {
|
|
282
|
+
emittedToCaller = true;
|
|
283
|
+
onEvent({ type: 'thinking_delta', text });
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case 'done': {
|
|
288
|
+
onEvent({ type: 'done' });
|
|
289
|
+
resolve();
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'error': {
|
|
293
|
+
const message = typeof evt.message === 'string' ? evt.message :
|
|
294
|
+
typeof evt.error === 'string' ? evt.error : 'Unknown SSE error';
|
|
295
|
+
// Transient upstream failure before any output → let the outer loop retry it
|
|
296
|
+
// transparently instead of killing the turn (this is the "Overloaded" case).
|
|
297
|
+
if (isRetryableStreamMsg(message) && !emittedToCaller) {
|
|
298
|
+
clearInterval(heartbeatWatchdog);
|
|
299
|
+
stream.destroy?.();
|
|
300
|
+
reject(Object.assign(new Error(message), { retryable: true }));
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
onEvent({ type: 'error', message });
|
|
304
|
+
reject(new Error(message));
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
stream.on('data', (chunk) => {
|
|
312
|
+
lastDataAt = Date.now();
|
|
313
|
+
if (abortSignal?.aborted) {
|
|
314
|
+
stream.destroy?.();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
parser.feed(chunk.toString('utf-8'));
|
|
318
|
+
});
|
|
319
|
+
stream.on('end', () => { clearInterval(heartbeatWatchdog); resolve(); });
|
|
320
|
+
// Treat stream destruction from abort as a clean exit, not an error
|
|
321
|
+
stream.on('error', (err) => {
|
|
322
|
+
clearInterval(heartbeatWatchdog);
|
|
323
|
+
if (abortSignal?.aborted || controller.signal.aborted) {
|
|
324
|
+
resolve();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
reject(err);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
if (completedMessage) {
|
|
331
|
+
return completedMessage;
|
|
332
|
+
}
|
|
333
|
+
// Fallback: build message from accumulated parts if message_complete wasn't sent
|
|
334
|
+
const contentBlocks = [];
|
|
335
|
+
const fullText = textParts.join('');
|
|
336
|
+
if (fullText) {
|
|
337
|
+
contentBlocks.push({ type: 'text', text: fullText });
|
|
338
|
+
}
|
|
339
|
+
contentBlocks.push(...toolUseBlocks);
|
|
340
|
+
return { role: 'assistant', content: contentBlocks };
|
|
341
|
+
}
|
|
342
|
+
// Outer retry loop — retries a WHOLE attempt on a transient pre-content failure
|
|
343
|
+
// (overloaded_error / stall / dropped connection), with exponential backoff. Because
|
|
344
|
+
// runAttempt only tags an error retryable when nothing was emitted, a retry can never
|
|
345
|
+
// duplicate rendered output.
|
|
346
|
+
try {
|
|
347
|
+
for (let sAttempt = 0; sAttempt <= MAX_RETRIES; sAttempt++) {
|
|
348
|
+
try {
|
|
349
|
+
return await runAttempt();
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
if (abortSignal?.aborted || controller.signal.aborted || err.name === 'AbortError')
|
|
353
|
+
throw err;
|
|
354
|
+
if (err.retryable && sAttempt < MAX_RETRIES) {
|
|
355
|
+
await sleep(RETRY_BASE_MS * Math.pow(2, sAttempt));
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
throw new Error('Request failed after max retries');
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (abortPoll)
|
|
365
|
+
clearInterval(abortPoll);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ─── Get Balance ──────────────────────────────────────────────────────────────
|
|
369
|
+
async function getBalance() {
|
|
370
|
+
const response = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/code/balance`, {
|
|
371
|
+
method: 'GET',
|
|
372
|
+
headers: authHeaders(),
|
|
373
|
+
});
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
const errText = await response.text();
|
|
376
|
+
throw new Error(`API error ${response.status}: ${errText}`);
|
|
377
|
+
}
|
|
378
|
+
const data = (await response.json());
|
|
379
|
+
return typeof data.balance === 'number' ? data.balance : 0;
|
|
380
|
+
}
|
|
381
|
+
// ─── VSCode OAuth ─────────────────────────────────────────────────────────────
|
|
382
|
+
async function exchangeVscodeCode(code) {
|
|
383
|
+
const response = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/vscode-exchange`, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
386
|
+
body: JSON.stringify({ code }),
|
|
387
|
+
});
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
const errText = await response.text();
|
|
390
|
+
throw new Error(`OAuth exchange failed (${response.status}): ${errText}`);
|
|
391
|
+
}
|
|
392
|
+
const data = (await response.json());
|
|
393
|
+
if (!data.token)
|
|
394
|
+
throw new Error('No token in exchange response');
|
|
395
|
+
return { token: data.token, email: data.user?.email ?? '' };
|
|
396
|
+
}
|
|
397
|
+
// ─── Login ────────────────────────────────────────────────────────────────────
|
|
398
|
+
async function login(email, password) {
|
|
399
|
+
const response = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/login/email`, {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
headers: { 'Content-Type': 'application/json' },
|
|
402
|
+
body: JSON.stringify({ email, password }),
|
|
403
|
+
});
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
const errText = await response.text();
|
|
406
|
+
throw new Error(`Login failed (${response.status}): ${errText}`);
|
|
407
|
+
}
|
|
408
|
+
const data = (await response.json());
|
|
409
|
+
if (!data.token) {
|
|
410
|
+
throw new Error('Login response missing token');
|
|
411
|
+
}
|
|
412
|
+
return { token: data.token, email: data.email ?? email };
|
|
413
|
+
}
|
|
414
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuthConfig } from '../types';
|
|
2
|
+
export declare function saveAuth(config: AuthConfig): void;
|
|
3
|
+
export declare function loadAuth(): AuthConfig | null;
|
|
4
|
+
export declare function clearAuth(): void;
|
|
5
|
+
export declare function isAuthenticated(): boolean;
|
|
6
|
+
export declare function getToken(): string | null;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAiB3C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGjD;AAED,wBAAgB,QAAQ,IAAI,UAAU,GAAG,IAAI,CAmB5C;AAED,wBAAgB,SAAS,IAAI,IAAI,CAQhC;AAED,wBAAgB,eAAe,IAAI,OAAO,CAGzC;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGxC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.saveAuth = saveAuth;
|
|
37
|
+
exports.loadAuth = loadAuth;
|
|
38
|
+
exports.clearAuth = clearAuth;
|
|
39
|
+
exports.isAuthenticated = isAuthenticated;
|
|
40
|
+
exports.getToken = getToken;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
// ─── Config Path ──────────────────────────────────────────────────────────────
|
|
45
|
+
const CONFIG_DIR = path.join(os.homedir(), '.nexrall');
|
|
46
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
47
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
48
|
+
function ensureConfigDir() {
|
|
49
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
50
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ─── Auth Operations ──────────────────────────────────────────────────────────
|
|
54
|
+
function saveAuth(config) {
|
|
55
|
+
ensureConfigDir();
|
|
56
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
57
|
+
}
|
|
58
|
+
function loadAuth() {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (typeof parsed === 'object' &&
|
|
66
|
+
parsed !== null &&
|
|
67
|
+
'token' in parsed &&
|
|
68
|
+
typeof parsed.token === 'string') {
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function clearAuth() {
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
80
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ignore errors on clear
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function isAuthenticated() {
|
|
88
|
+
const auth = loadAuth();
|
|
89
|
+
return auth !== null && auth.token.length > 0;
|
|
90
|
+
}
|
|
91
|
+
function getToken() {
|
|
92
|
+
const auth = loadAuth();
|
|
93
|
+
return auth?.token ?? null;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export declare function isMutatingTool(name: string): boolean;
|
|
2
|
+
export declare function isPotentiallyMutatingBash(command: string): boolean;
|
|
3
|
+
export interface FileSnapshot {
|
|
4
|
+
absPath: string;
|
|
5
|
+
/** Did the path exist (as a file) before the mutation? */
|
|
6
|
+
existed: boolean;
|
|
7
|
+
/** Pre-mutation content as a Buffer (binary-safe), or null if absent/too large/not a file. */
|
|
8
|
+
content: Buffer | null;
|
|
9
|
+
}
|
|
10
|
+
export interface CheckpointSummary {
|
|
11
|
+
id: number;
|
|
12
|
+
label: string;
|
|
13
|
+
/** Conversation length (message count) just before this turn ran. */
|
|
14
|
+
messageIndex: number;
|
|
15
|
+
fileCount: number;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
/** Number of potentially-mutating bash commands run in this turn (NOT rewindable). */
|
|
18
|
+
bashCount: number;
|
|
19
|
+
/** `git stash create` commit hash captured before the first mutating bash — manual recovery point. */
|
|
20
|
+
gitStashHash?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RestoreResult {
|
|
23
|
+
/** Absolute paths that were rolled back. */
|
|
24
|
+
restored: string[];
|
|
25
|
+
/** Truncate the conversation to this many messages to match the rewound state. */
|
|
26
|
+
messageIndex: number;
|
|
27
|
+
/** Total mutating bash commands across the rewound turns — their effects were NOT undone. */
|
|
28
|
+
bashCount: number;
|
|
29
|
+
/** git stash hashes captured across the rewound turns (newest first) for manual recovery. */
|
|
30
|
+
gitStashHashes: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare class CheckpointManager {
|
|
33
|
+
private readonly workDir;
|
|
34
|
+
private turns;
|
|
35
|
+
private current;
|
|
36
|
+
private nextId;
|
|
37
|
+
private readonly storeDir;
|
|
38
|
+
/**
|
|
39
|
+
* @param workDir project root the snapshots are relative to
|
|
40
|
+
* @param scope optional namespace (e.g. session id) so different sessions in
|
|
41
|
+
* the same directory keep independent rewind histories
|
|
42
|
+
*/
|
|
43
|
+
constructor(workDir: string, scope?: string);
|
|
44
|
+
private resolve;
|
|
45
|
+
/**
|
|
46
|
+
* Open a new turn. `label` is a short description (usually the user prompt),
|
|
47
|
+
* `messageIndex` is the conversation length the turn should rewind to.
|
|
48
|
+
*/
|
|
49
|
+
beginTurn(label: string, messageIndex: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Snapshot the pre-mutation state of the files a tool is about to touch.
|
|
52
|
+
* Must be called BEFORE the tool executes. No-op for non-mutating tools or
|
|
53
|
+
* when no turn is open. First snapshot of a path within a turn wins (so the
|
|
54
|
+
* earliest pre-state is preserved even if the file is edited repeatedly).
|
|
55
|
+
*/
|
|
56
|
+
recordBeforeMutation(name: string, input: Record<string, unknown>): void;
|
|
57
|
+
/**
|
|
58
|
+
* Record a bash command about to run in this turn. If it looks mutating,
|
|
59
|
+
* bump the turn's bash counter (so rewind can warn) and — on the FIRST
|
|
60
|
+
* mutating bash of the turn, if workDir is a dirty git repo — capture a
|
|
61
|
+
* `git stash create` hash as a manual recovery point. `git stash create`
|
|
62
|
+
* writes a dangling commit and does NOT touch the working tree or the stash
|
|
63
|
+
* list, so it is side-effect-free from the user's perspective.
|
|
64
|
+
*/
|
|
65
|
+
recordBashCommand(command: string): void;
|
|
66
|
+
private tryGitStashCreate;
|
|
67
|
+
/** Seal the open turn into history. Empty turns (no mutations, no bash) are discarded. */
|
|
68
|
+
commitTurn(): void;
|
|
69
|
+
list(): CheckpointSummary[];
|
|
70
|
+
hasCheckpoints(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Roll the working tree back to the state it had just before the checkpoint
|
|
73
|
+
* turn ran. Snapshots are applied newest → target so the earliest pre-state
|
|
74
|
+
* wins for any path touched in multiple turns. Returns the conversation
|
|
75
|
+
* truncation point, or null if the id is unknown.
|
|
76
|
+
*/
|
|
77
|
+
restore(checkpointId: number): RestoreResult | null;
|
|
78
|
+
private applySnapshot;
|
|
79
|
+
private turnFile;
|
|
80
|
+
private persistTurn;
|
|
81
|
+
private persistIndex;
|
|
82
|
+
private deleteTurnFile;
|
|
83
|
+
private prune;
|
|
84
|
+
private loadFromDisk;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/checkpoint/manager.ts"],"names":[],"mappings":"AA4CA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAQD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAMlE;AAyBD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,8FAA8F;IAC9F,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,sGAAsG;IACtG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAMD,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AA6BD,qBAAa,iBAAiB;IAWhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAVpC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC;;;;OAIG;gBAC0B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;IAa5D,OAAO,CAAC,OAAO;IAIf;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAcpD;;;;;OAKG;IACH,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IA8BxE;;;;;;;OAOG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQxC,OAAO,CAAC,iBAAiB;IAiBzB,0FAA0F;IAC1F,UAAU,IAAI,IAAI;IAWlB,IAAI,IAAI,iBAAiB,EAAE;IAY3B,cAAc,IAAI,OAAO;IAIzB;;;;;OAKG;IACH,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IA0BnD,OAAO,CAAC,aAAa;IAsBrB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,WAAW;IAuBnB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,YAAY;CAkCrB"}
|