@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/agent/agentTypes.d.ts +17 -0
  4. package/dist/agent/agentTypes.d.ts.map +1 -0
  5. package/dist/agent/agentTypes.js +156 -0
  6. package/dist/agent/loop.d.ts +3 -0
  7. package/dist/agent/loop.d.ts.map +1 -0
  8. package/dist/agent/loop.js +775 -0
  9. package/dist/api/client.d.ts +27 -0
  10. package/dist/api/client.d.ts.map +1 -0
  11. package/dist/api/client.js +414 -0
  12. package/dist/auth/index.d.ts +7 -0
  13. package/dist/auth/index.d.ts.map +1 -0
  14. package/dist/auth/index.js +95 -0
  15. package/dist/checkpoint/manager.d.ts +86 -0
  16. package/dist/checkpoint/manager.d.ts.map +1 -0
  17. package/dist/checkpoint/manager.js +409 -0
  18. package/dist/commands/loader.d.ts +18 -0
  19. package/dist/commands/loader.d.ts.map +1 -0
  20. package/dist/commands/loader.js +183 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +30 -0
  24. package/dist/mcp/client.d.ts +46 -0
  25. package/dist/mcp/client.d.ts.map +1 -0
  26. package/dist/mcp/client.js +128 -0
  27. package/dist/mcp/httpClient.d.ts +25 -0
  28. package/dist/mcp/httpClient.d.ts.map +1 -0
  29. package/dist/mcp/httpClient.js +143 -0
  30. package/dist/mcp/manager.d.ts +56 -0
  31. package/dist/mcp/manager.d.ts.map +1 -0
  32. package/dist/mcp/manager.js +234 -0
  33. package/dist/mcp/sseClient.d.ts +30 -0
  34. package/dist/mcp/sseClient.d.ts.map +1 -0
  35. package/dist/mcp/sseClient.js +224 -0
  36. package/dist/permissions/rules.d.ts +20 -0
  37. package/dist/permissions/rules.d.ts.map +1 -0
  38. package/dist/permissions/rules.js +218 -0
  39. package/dist/plugins/index.d.ts +18 -0
  40. package/dist/plugins/index.d.ts.map +1 -0
  41. package/dist/plugins/index.js +139 -0
  42. package/dist/tools/executor.d.ts +6 -0
  43. package/dist/tools/executor.d.ts.map +1 -0
  44. package/dist/tools/executor.js +1458 -0
  45. package/dist/tools/sandbox.d.ts +13 -0
  46. package/dist/tools/sandbox.d.ts.map +1 -0
  47. package/dist/tools/sandbox.js +140 -0
  48. package/dist/tools/symbols.d.ts +13 -0
  49. package/dist/tools/symbols.d.ts.map +1 -0
  50. package/dist/tools/symbols.js +279 -0
  51. package/dist/types.d.ts +186 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +4 -0
  54. 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"}