@nexrall/code-core 1.4.63 → 1.4.66

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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/agent/agentRegistry.d.ts +14 -3
  3. package/dist/agent/agentRegistry.d.ts.map +1 -1
  4. package/dist/agent/agentRegistry.js +136 -6
  5. package/dist/agent/agentTypes.d.ts +53 -3
  6. package/dist/agent/agentTypes.d.ts.map +1 -1
  7. package/dist/agent/agentTypes.js +200 -153
  8. package/dist/agent/backgroundAgents.d.ts +66 -0
  9. package/dist/agent/backgroundAgents.d.ts.map +1 -0
  10. package/dist/agent/backgroundAgents.js +145 -0
  11. package/dist/agent/loop.d.ts +44 -7
  12. package/dist/agent/loop.d.ts.map +1 -1
  13. package/dist/agent/loop.js +946 -263
  14. package/dist/agent/memory.d.ts +11 -0
  15. package/dist/agent/memory.d.ts.map +1 -1
  16. package/dist/agent/memory.js +23 -3
  17. package/dist/agent/modelCatalogue.d.ts +15 -0
  18. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  19. package/dist/agent/modelCatalogue.js +46 -0
  20. package/dist/agent/planMode.d.ts +14 -0
  21. package/dist/agent/planMode.d.ts.map +1 -1
  22. package/dist/agent/planMode.js +145 -17
  23. package/dist/agent/readDedupe.d.ts +15 -0
  24. package/dist/agent/readDedupe.d.ts.map +1 -0
  25. package/dist/agent/readDedupe.js +146 -0
  26. package/dist/agent/securityLint.js +2 -2
  27. package/dist/agent/toolPrefetch.d.ts +44 -0
  28. package/dist/agent/toolPrefetch.d.ts.map +1 -0
  29. package/dist/agent/toolPrefetch.js +101 -0
  30. package/dist/agent/trust.d.ts +0 -5
  31. package/dist/agent/trust.d.ts.map +1 -1
  32. package/dist/agent/trust.js +41 -0
  33. package/dist/api/client.d.ts +15 -1
  34. package/dist/api/client.d.ts.map +1 -1
  35. package/dist/api/client.js +88 -8
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/permissions/destructive.d.ts +9 -7
  40. package/dist/permissions/destructive.d.ts.map +1 -1
  41. package/dist/permissions/destructive.js +55 -8
  42. package/dist/permissions/destructiveTokens.d.ts +29 -0
  43. package/dist/permissions/destructiveTokens.d.ts.map +1 -0
  44. package/dist/permissions/destructiveTokens.js +469 -0
  45. package/dist/permissions/modePolicy.d.ts +22 -8
  46. package/dist/permissions/modePolicy.d.ts.map +1 -1
  47. package/dist/permissions/modePolicy.js +25 -10
  48. package/dist/tools/executor.d.ts +36 -0
  49. package/dist/tools/executor.d.ts.map +1 -1
  50. package/dist/tools/executor.js +408 -85
  51. package/dist/tools/tsLangService.d.ts.map +1 -1
  52. package/dist/tools/tsLangService.js +107 -21
  53. package/dist/types.d.ts +101 -7
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +12 -1
  56. package/dist/util/miniYaml.d.ts +10 -0
  57. package/dist/util/miniYaml.d.ts.map +1 -0
  58. package/dist/util/miniYaml.js +149 -0
  59. package/package.json +2 -10
@@ -0,0 +1,44 @@
1
+ import type { ToolResult } from '../types';
2
+ /**
3
+ * Streaming tool prefetch — start read-only tools while the model is still
4
+ * generating the rest of its message.
5
+ *
6
+ * Before: every tool waited for the WHOLE assistant message (all later tool calls,
7
+ * any trailing text, and the backend's post-message billing write) before the first
8
+ * one started. The backend now announces each tool_use block the moment it is
9
+ * complete (SSE `tool_use_ready`); for tools that only READ the filesystem we start
10
+ * executing immediately and hold the result.
11
+ *
12
+ * Safety model — the prefetched result is only a cache, never a bypass:
13
+ * • Dispatch still happens from the completed message and still goes through every
14
+ * gate (plan mode, worktree isolation, permission, PreToolUse hooks, read dedupe).
15
+ * A refused call simply drops its prefetched result; reading had no side effect.
16
+ * • Reused only for the identical call (same id, same name, same input).
17
+ * • Only pure reads are prefetched, and only reused when nothing else in the same
18
+ * message can change the filesystem (see isPrefetchSafeMessage) — otherwise the
19
+ * early read could observe state from BEFORE a write the model ordered first.
20
+ */
21
+ /** Built-in tools that only read the filesystem — safe to run before approval. */
22
+ export declare const PREFETCHABLE_TOOLS: ReadonlySet<string>;
23
+ /** True when no tool in this message can mutate the workspace. */
24
+ export declare function isPrefetchSafeMessage(toolNames: readonly string[]): boolean;
25
+ /** Kill switch: NEXRALL_TOOL_PREFETCH=0 restores the old wait-for-message timing. */
26
+ export declare function toolPrefetchEnabled(): boolean;
27
+ type Runner = (name: string, input: Record<string, unknown>) => Promise<ToolResult>;
28
+ export declare class ToolPrefetcher {
29
+ private readonly run;
30
+ private readonly entries;
31
+ constructor(run: Runner);
32
+ /** Start `name` early if it is a pure read. Idempotent per id. */
33
+ start(id: string, name: string, input: Record<string, unknown>): boolean;
34
+ /**
35
+ * The prefetched result for exactly this call, or null. Always removes the entry,
36
+ * so a result is handed out at most once.
37
+ */
38
+ take(id: string, name: string, input: Record<string, unknown>): Promise<ToolResult> | null;
39
+ /** Drop everything (stream restarted — the ids it announced will never be dispatched). */
40
+ clear(): void;
41
+ get size(): number;
42
+ }
43
+ export {};
44
+ //# sourceMappingURL=toolPrefetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolPrefetch.d.ts","sourceRoot":"","sources":["../../src/agent/toolPrefetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAKjD,CAAC;AAiBH,kEAAkE;AAClE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAE3E;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AASD,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEpF,qBAAa,cAAc;IAGb,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkF;gBAE7E,GAAG,EAAE,MAAM;IAExC,kEAAkE;IAClE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO;IAWxE;;;OAGG;IACH,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI;IAQ1F,0FAA0F;IAC1F,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ToolPrefetcher = exports.PREFETCHABLE_TOOLS = void 0;
4
+ exports.isPrefetchSafeMessage = isPrefetchSafeMessage;
5
+ exports.toolPrefetchEnabled = toolPrefetchEnabled;
6
+ /**
7
+ * Streaming tool prefetch — start read-only tools while the model is still
8
+ * generating the rest of its message.
9
+ *
10
+ * Before: every tool waited for the WHOLE assistant message (all later tool calls,
11
+ * any trailing text, and the backend's post-message billing write) before the first
12
+ * one started. The backend now announces each tool_use block the moment it is
13
+ * complete (SSE `tool_use_ready`); for tools that only READ the filesystem we start
14
+ * executing immediately and hold the result.
15
+ *
16
+ * Safety model — the prefetched result is only a cache, never a bypass:
17
+ * • Dispatch still happens from the completed message and still goes through every
18
+ * gate (plan mode, worktree isolation, permission, PreToolUse hooks, read dedupe).
19
+ * A refused call simply drops its prefetched result; reading had no side effect.
20
+ * • Reused only for the identical call (same id, same name, same input).
21
+ * • Only pure reads are prefetched, and only reused when nothing else in the same
22
+ * message can change the filesystem (see isPrefetchSafeMessage) — otherwise the
23
+ * early read could observe state from BEFORE a write the model ordered first.
24
+ */
25
+ /** Built-in tools that only read the filesystem — safe to run before approval. */
26
+ exports.PREFETCHABLE_TOOLS = new Set([
27
+ 'read_file',
28
+ 'list_directory',
29
+ 'glob',
30
+ 'search_files',
31
+ ]);
32
+ /**
33
+ * Tools that may share a message with a prefetched read without invalidating it:
34
+ * they cannot change files in the workspace.
35
+ */
36
+ const NON_MUTATING_COMPANIONS = new Set([
37
+ ...exports.PREFETCHABLE_TOOLS,
38
+ 'notebook_read',
39
+ 'todo_write', 'todo_read',
40
+ 'memory_read',
41
+ 'use_skill',
42
+ 'fetch_url', 'web_search',
43
+ 'get_diagnostics', 'go_to_definition', 'find_references',
44
+ 'get_symbols', 'get_workspace_symbols', 'get_hover',
45
+ ]);
46
+ /** True when no tool in this message can mutate the workspace. */
47
+ function isPrefetchSafeMessage(toolNames) {
48
+ return toolNames.every((n) => NON_MUTATING_COMPANIONS.has(n));
49
+ }
50
+ /** Kill switch: NEXRALL_TOOL_PREFETCH=0 restores the old wait-for-message timing. */
51
+ function toolPrefetchEnabled() {
52
+ return process.env.NEXRALL_TOOL_PREFETCH !== '0';
53
+ }
54
+ function stableKey(value) {
55
+ if (value === null || typeof value !== 'object')
56
+ return JSON.stringify(value) ?? 'undefined';
57
+ if (Array.isArray(value))
58
+ return `[${value.map(stableKey).join(',')}]`;
59
+ const obj = value;
60
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableKey(obj[k])}`).join(',')}}`;
61
+ }
62
+ class ToolPrefetcher {
63
+ constructor(run) {
64
+ this.run = run;
65
+ this.entries = new Map();
66
+ }
67
+ /** Start `name` early if it is a pure read. Idempotent per id. */
68
+ start(id, name, input) {
69
+ if (!id || !exports.PREFETCHABLE_TOOLS.has(name) || this.entries.has(id))
70
+ return false;
71
+ // Never rejects: an unobserved rejection (the call may be refused later and never
72
+ // awaited) would otherwise surface as an unhandled rejection.
73
+ const promise = Promise.resolve()
74
+ .then(() => this.run(name, input))
75
+ .catch((err) => ({ error: `Tool execution failed: ${err?.message ?? String(err)}` }));
76
+ this.entries.set(id, { name, key: stableKey(input), promise });
77
+ return true;
78
+ }
79
+ /**
80
+ * The prefetched result for exactly this call, or null. Always removes the entry,
81
+ * so a result is handed out at most once.
82
+ */
83
+ take(id, name, input) {
84
+ const e = this.entries.get(id);
85
+ if (!e)
86
+ return null;
87
+ this.entries.delete(id);
88
+ if (e.name !== name || e.key !== stableKey(input))
89
+ return null;
90
+ return e.promise;
91
+ }
92
+ /** Drop everything (stream restarted — the ids it announced will never be dispatched). */
93
+ clear() {
94
+ this.entries.clear();
95
+ }
96
+ get size() {
97
+ return this.entries.size;
98
+ }
99
+ }
100
+ exports.ToolPrefetcher = ToolPrefetcher;
101
+ //# sourceMappingURL=toolPrefetch.js.map
@@ -1,7 +1,2 @@
1
- /**
2
- * Which trust-sensitive files actually exist in `dir`, so a confirmation
3
- * prompt can tell the user what THIS folder will do rather than describing
4
- * risks in the abstract. Ordered most- to least-dangerous.
5
- */
6
1
  export declare function detectTrustSignals(dir: string): string[];
7
2
  //# sourceMappingURL=trust.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../../src/agent/trust.ts"],"names":[],"mappings":"AAqDA;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAkExD"}
1
+ {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../../src/agent/trust.ts"],"names":[],"mappings":"AAiFA,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAyExD"}
@@ -90,6 +90,40 @@ function declaresPluginSources(dir) {
90
90
  * prompt can tell the user what THIS folder will do rather than describing
91
91
  * risks in the abstract. Ordered most- to least-dangerous.
92
92
  */
93
+ /** Agent definitions under .nexrall/agents that declare `hooks:` (they run shell commands). */
94
+ function agentFilesWithHooks(dir) {
95
+ const root = path.join(dir, '.nexrall', 'agents');
96
+ const found = [];
97
+ const walk = (d, depth) => {
98
+ if (depth > 4 || found.length > 20)
99
+ return;
100
+ let entries;
101
+ try {
102
+ entries = fs.readdirSync(d, { withFileTypes: true });
103
+ }
104
+ catch {
105
+ return;
106
+ }
107
+ for (const e of entries) {
108
+ const full = path.join(d, e.name);
109
+ if (e.isDirectory()) {
110
+ walk(full, depth + 1);
111
+ continue;
112
+ }
113
+ if (!e.name.endsWith('.md'))
114
+ continue;
115
+ try {
116
+ const head = fs.readFileSync(full, 'utf-8').slice(0, 64 * 1024);
117
+ const fm = head.replace(/^\uFEFF/, '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
118
+ if (fm && /^hooks\s*:/im.test(fm[1]))
119
+ found.push(path.relative(dir, full).replace(/\\/g, '/'));
120
+ }
121
+ catch { /* unreadable — not a signal */ }
122
+ }
123
+ };
124
+ walk(root, 0);
125
+ return found;
126
+ }
93
127
  function detectTrustSignals(dir) {
94
128
  const signals = [];
95
129
  const exists = (...p) => {
@@ -137,6 +171,13 @@ function detectTrustSignals(dir) {
137
171
  bits.push('runs hooks (shell commands) automatically around tool calls');
138
172
  signals.push(bits.join(', '));
139
173
  }
174
+ // Agent definitions can carry their own hooks (Claude Code's agent-scoped `hooks:`),
175
+ // which run shell commands around that agent's tool calls — same risk as settings hooks.
176
+ const hookedAgents = agentFilesWithHooks(dir);
177
+ if (hookedAgents.length) {
178
+ const shown = hookedAgents.slice(0, 3).join(', ') + (hookedAgents.length > 3 ? ` (+${hookedAgents.length - 3} more)` : '');
179
+ signals.push(`${shown} — agent definition(s) that run hooks (shell commands) around their tool calls`);
180
+ }
140
181
  // Then things that steer the agent's behaviour.
141
182
  if (exists('nexrall.md')) {
142
183
  signals.push('nexrall.md — project instructions added to the agent\u2019s prompt');
@@ -77,10 +77,16 @@ export interface StreamChatOptions {
77
77
  * Same rule as canWriteSharedMemory / canSpawnSubAgents, applied to the network:
78
78
  * READ_ONLY_TOOLS grants neither `fetch_url` nor `web_search`, yet the base prompt
79
79
  * told every run to search the web, so read-only sub-agents (explorer,
80
- * security-auditor, ...) kept calling a tool their own allowlist then refused.
80
+ * planner, ...) kept calling a tool their own allowlist then refused.
81
81
  * Undefined means "main agent" (may browse).
82
82
  */
83
83
  canBrowseWeb?: boolean;
84
+ /**
85
+ * When set, the backend sends ONLY these builtin tool schemas (sub-agents with an
86
+ * allowlist). Absent = every builtin. Older backends ignore it — the permission gate
87
+ * still enforces the allowlist either way; this only trims what is sent.
88
+ */
89
+ toolAllowlist?: string[];
84
90
  /**
85
91
  * Opt in to restarting a turn that died AFTER partial output was rendered.
86
92
  *
@@ -99,6 +105,14 @@ export interface StreamChatOptions {
99
105
  */
100
106
  allowRestartAfterRender?: boolean;
101
107
  }
108
+ /**
109
+ * Whether a tool_use round may return as soon as message_complete + usage arrive,
110
+ * instead of waiting for the trailing `done` (which the backend only sends after its
111
+ * billing DB write). NEXRALL_EARLY_TOOL_RESOLVE=0 restores the old wait.
112
+ */
113
+ export declare function earlyToolResolveEnabled(): boolean;
114
+ /** NEXRALL_GZIP_REQUESTS=0 sends chat bodies uncompressed. */
115
+ export declare function gzipRequestsEnabled(): boolean;
102
116
  export declare function streamChat(messages: Message[], options: StreamChatOptions, onEvent: (e: SSEEvent) => void): Promise<Message>;
103
117
  /**
104
118
  * Ask the server to stop generating a turn.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.API_BASE = void 0;
7
7
  exports.chooseFinalContent = chooseFinalContent;
8
+ exports.earlyToolResolveEnabled = earlyToolResolveEnabled;
9
+ exports.gzipRequestsEnabled = gzipRequestsEnabled;
8
10
  exports.streamChat = streamChat;
9
11
  exports.cancelTurn = cancelTurn;
10
12
  exports.revokeRefreshToken = revokeRefreshToken;
@@ -21,6 +23,7 @@ exports.getUserProfile = getUserProfile;
21
23
  const eventsource_parser_1 = require("eventsource-parser");
22
24
  const node_fetch_1 = __importDefault(require("node-fetch"));
23
25
  const crypto_1 = require("crypto");
26
+ const zlib_1 = require("zlib");
24
27
  const index_1 = require("../auth/index");
25
28
  // ─── Constants ────────────────────────────────────────────────────────────────
26
29
  const DEFAULT_API_BASE = 'https://api.nexrall.com';
@@ -216,6 +219,20 @@ function isExpiredTokenResponse(status, body) {
216
219
  const MAX_RETRIES = 5;
217
220
  const RETRY_BASE_MS = 1000;
218
221
  const RETRY_MAX_MS = 30000;
222
+ /**
223
+ * Whether a tool_use round may return as soon as message_complete + usage arrive,
224
+ * instead of waiting for the trailing `done` (which the backend only sends after its
225
+ * billing DB write). NEXRALL_EARLY_TOOL_RESOLVE=0 restores the old wait.
226
+ */
227
+ function earlyToolResolveEnabled() {
228
+ return process.env.NEXRALL_EARLY_TOOL_RESOLVE !== '0';
229
+ }
230
+ /** Chat bodies smaller than this are sent uncompressed. */
231
+ const GZIP_MIN_BYTES = 32 * 1024;
232
+ /** NEXRALL_GZIP_REQUESTS=0 sends chat bodies uncompressed. */
233
+ function gzipRequestsEnabled() {
234
+ return process.env.NEXRALL_GZIP_REQUESTS !== '0';
235
+ }
219
236
  /**
220
237
  * SSE frames that are backend HOUSEKEEPING, not model output.
221
238
  *
@@ -249,6 +266,7 @@ const RETRY_MAX_MS = 30000;
249
266
  */
250
267
  const HOUSEKEEPING_FRAMES = new Set([
251
268
  'resumable',
269
+ 'runtime_context',
252
270
  'context_warning',
253
271
  'tool_progress',
254
272
  ]);
@@ -373,7 +391,7 @@ function sleep(ms) {
373
391
  * the nested product (36) exists to bound pathological cases, not to be reached. */
374
392
  const MAX_TOTAL_ATTEMPTS = (MAX_RETRIES + 1) * (MAX_RETRIES + 1);
375
393
  async function streamChat(messages, options, onEvent) {
376
- const { model, env, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents, skills, allowRestartAfterRender, canWriteSharedMemory, hasOwnAgentStore, canSpawnSubAgents, canBrowseWeb } = options;
394
+ const { model, env, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents, skills, allowRestartAfterRender, canWriteSharedMemory, hasOwnAgentStore, canSpawnSubAgents, canBrowseWeb, toolAllowlist } = options;
377
395
  // IDEMPOTENCY KEY for this logical turn, generated ONCE and reused by every retry
378
396
  // below (that reuse is the entire point — a fresh id per attempt would be useless).
379
397
  //
@@ -449,6 +467,26 @@ async function streamChat(messages, options, onEvent) {
449
467
  // chained one so the header wait can be timed out independently. It stays because
450
468
  // this shape is also what an unwrapped fetch would need, and a request with no
451
469
  // signal at all would silently become uncancellable if that wrapper were removed.
470
+ // The chat body is serialised (and, when large, gzipped) ONCE per streamChat call and
471
+ // reused by every retry: the payload is identical across attempts, and re-stringifying
472
+ // a multi-MB history per retry is pure waste. Gzip is only applied above a threshold —
473
+ // below it, the CPU cost outweighs the uplink saving. Every Nexrall API host parses
474
+ // bodies with express.json(), which inflates `Content-Encoding: gzip` natively
475
+ // (verified against production). NEXRALL_GZIP_REQUESTS=0 disables it.
476
+ // Keyed by turnId: a restart mints a NEW turnId (prepareRestart), and the cached body
477
+ // must not carry the old one. The billing override is read per call for the same reason
478
+ // the body used to be rebuilt per attempt — it cannot change mid-turn in practice, and a
479
+ // restart (the only path long enough for it to change) rebuilds anyway.
480
+ let chatBody = null;
481
+ const getChatBody = () => {
482
+ if (chatBody && chatBody.turnId === turnId)
483
+ return chatBody;
484
+ const json = JSON.stringify({ messages, model, env, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents, skills, turnId, canWriteSharedMemory, hasOwnAgentStore, canSpawnSubAgents, canBrowseWeb, toolAllowlist, ...billingOverrideFields() });
485
+ chatBody = gzipRequestsEnabled() && json.length >= GZIP_MIN_BYTES
486
+ ? { data: (0, zlib_1.gzipSync)(json, { level: 5 }), gzip: true, turnId }
487
+ : { data: json, gzip: false, turnId };
488
+ return chatBody;
489
+ };
452
490
  const buildFetchArgs = () => {
453
491
  const signal = controller.signal;
454
492
  if (resuming) {
@@ -463,12 +501,15 @@ async function streamChat(messages, options, onEvent) {
463
501
  }
464
502
  return [
465
503
  `${exports.API_BASE}/api/code/chat`,
466
- {
467
- method: 'POST',
468
- headers: authHeaders(),
469
- body: JSON.stringify({ messages, model, env, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents, skills, turnId, canWriteSharedMemory, hasOwnAgentStore, canSpawnSubAgents, canBrowseWeb, ...billingOverrideFields() }),
470
- signal,
471
- },
504
+ (() => {
505
+ const body = getChatBody();
506
+ return {
507
+ method: 'POST',
508
+ headers: body.gzip ? { ...authHeaders(), 'Content-Encoding': 'gzip' } : authHeaders(),
509
+ body: body.data,
510
+ signal,
511
+ };
512
+ })(),
472
513
  ];
473
514
  };
474
515
  // A transient mid-stream failure (Anthropic "overloaded_error") arrives as an SSE error
@@ -1274,6 +1315,28 @@ async function streamChat(messages, options, onEvent) {
1274
1315
  cache_read_input_tokens: typeof evt.cache_read_input_tokens === 'number' ? evt.cache_read_input_tokens : undefined,
1275
1316
  },
1276
1317
  });
1318
+ // EARLY RESOLVE ON TOOL ROUNDS. The backend emits message_complete +
1319
+ // usage synchronously, then awaits its billing write (Postgres + Redis)
1320
+ // before sending `done`. For a tool_use turn nothing after this point is
1321
+ // needed to run the tools, so waiting for `done` put a DB round-trip on
1322
+ // the critical path of EVERY tool round. Return now; the socket keeps
1323
+ // draining in the background, so a late balance_status / done / error
1324
+ // still reaches onEvent. Only tool_use: a final (end_turn) answer keeps
1325
+ // waiting so the low-balance nudge lands before the turn is reported done.
1326
+ if (evt.partial !== true &&
1327
+ completedMessage?.stopReason === 'tool_use' &&
1328
+ earlyToolResolveEnabled()) {
1329
+ clearInterval(heartbeatWatchdog);
1330
+ // The watchdog is gone, so bound the background drain ourselves: a
1331
+ // backend that never sends `done` must not pin the socket forever.
1332
+ const drainGuard = setTimeout(() => {
1333
+ stream.destroy?.();
1334
+ }, HEARTBEAT_TIMEOUT_MS);
1335
+ drainGuard.unref?.();
1336
+ stream.once('end', () => clearTimeout(drainGuard));
1337
+ stream.once('close', () => clearTimeout(drainGuard));
1338
+ resolve();
1339
+ }
1277
1340
  }
1278
1341
  break;
1279
1342
  }
@@ -1326,6 +1389,23 @@ async function streamChat(messages, options, onEvent) {
1326
1389
  serverResumable = true;
1327
1390
  break;
1328
1391
  }
1392
+ case 'tool_use_ready': {
1393
+ // A complete tool_use block, sent before the message finishes (see
1394
+ // SSEToolUseReadyEvent). Forwarded as-is; the loop decides what to prefetch.
1395
+ if (typeof evt.id === 'string' && typeof evt.name === 'string') {
1396
+ const input = evt.input && typeof evt.input === 'object' ? evt.input : {};
1397
+ onEvent({ type: 'tool_use_ready', id: evt.id, name: evt.name, input });
1398
+ }
1399
+ break;
1400
+ }
1401
+ case 'runtime_context': {
1402
+ // The exact context block the backend appended to our final message. The
1403
+ // agent loop persists it so the next round carries it in the cached history.
1404
+ if (typeof evt.text === 'string' && evt.text) {
1405
+ onEvent({ type: 'runtime_context', text: evt.text });
1406
+ }
1407
+ break;
1408
+ }
1329
1409
  case 'balance_status': {
1330
1410
  const balance = typeof evt.balance === 'number' ? evt.balance : 0;
1331
1411
  const zero = !!evt.zero;
@@ -1780,7 +1860,7 @@ async function getCodeModels() {
1780
1860
  const data = (await response.json());
1781
1861
  return {
1782
1862
  models: Array.isArray(data.models) ? data.models : [],
1783
- defaultModel: typeof data.defaultModel === 'string' ? data.defaultModel : 'claude-sonnet-5',
1863
+ defaultModel: typeof data.defaultModel === 'string' ? data.defaultModel : 'deepseek-flash',
1784
1864
  };
1785
1865
  }
1786
1866
  /**
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export * from './commands/loader';
22
22
  export * from './agent/agentTypes';
23
23
  export * from './agent/planMode';
24
24
  export * from './agent/agentRegistry';
25
+ export * from './agent/backgroundAgents';
25
26
  export * from './permissions/rules';
26
27
  export * from './permissions/modePolicy';
27
28
  export * from './permissions/bashClassify';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ __exportStar(require("./commands/loader"), exports);
38
38
  __exportStar(require("./agent/agentTypes"), exports);
39
39
  __exportStar(require("./agent/planMode"), exports);
40
40
  __exportStar(require("./agent/agentRegistry"), exports);
41
+ __exportStar(require("./agent/backgroundAgents"), exports);
41
42
  __exportStar(require("./permissions/rules"), exports);
42
43
  __exportStar(require("./permissions/modePolicy"), exports);
43
44
  __exportStar(require("./permissions/bashClassify"), exports);
@@ -29,13 +29,15 @@ export interface DestructiveMatch {
29
29
  * Classify a bash command. Returns the FIRST matching destructive category, or
30
30
  * null when the command is not recognised as destructive.
31
31
  *
32
- * Two passes with different data-stripping:
33
- * - RAW_RULES run on the heredoc-stripped but quote-PRESERVING string, because
34
- * real SQL lives inside quotes (`psql -c "DROP TABLE"`); they are gated on a
35
- * DB-client token so a mention can't match.
36
- * - STRIPPED_RULES run on the fully stripped string (quotes + heredocs gone) so
37
- * mentions inside commit messages / echoes / grep patterns / heredoc'd docs
38
- * never trip the bare-command rules (git/infra/cloud/container/publish).
32
+ * Two layers:
33
+ * 1. Regex rules (above). RAW_RULES run on the heredoc-stripped but quote-PRESERVING
34
+ * string (real SQL lives inside quotes) and are gated on a DB-client token;
35
+ * STRIPPED_RULES run on the fully stripped string so mentions in commit messages /
36
+ * echoes / heredoc'd docs never trip them.
37
+ * 2. A token pass (destructiveTokens.ts) that splits the command like a shell, peels
38
+ * wrappers (sudo/env/xargs/bash -c/ssh/docker exec …) and judges the real command.
39
+ * It catches what a regex cannot: `git -C x reset --hard`, `git push -fu`,
40
+ * `git push origin +main`, `rm -rf src`, `find . -delete`, `> file`.
39
41
  */
40
42
  export declare function classifyDestructiveCommand(command: string): DestructiveMatch | null;
41
43
  /** True when the given tool + input represents a destructive bash command. */
@@ -1 +1 @@
1
- {"version":3,"file":"destructive.d.ts","sourceRoot":"","sources":["../../src/permissions/destructive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;CAChB;AAsMD;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAenF;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAQvG"}
1
+ {"version":3,"file":"destructive.d.ts","sourceRoot":"","sources":["../../src/permissions/destructive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;CAChB;AA8PD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAGnF;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAQvG"}
@@ -23,6 +23,7 @@
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.classifyDestructiveCommand = classifyDestructiveCommand;
25
25
  exports.isDestructiveBash = isDestructiveBash;
26
+ const destructiveTokens_1 = require("./destructiveTokens");
26
27
  // ── Data-section stripping (naive; feeds a heuristic, not an executor) ────────
27
28
  function stripHeredocs(command) {
28
29
  return command.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\2(?=\s|;|&|$)/g, ' ');
@@ -66,6 +67,16 @@ const RAW_RULES = [
66
67
  reason: 'drops a Postgres/MySQL role or user',
67
68
  re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bdrop\s+(?:role|user)\b`, 'i'),
68
69
  },
70
+ {
71
+ category: 'database',
72
+ reason: 'UPDATE without a WHERE clause (rewrites every row)',
73
+ re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bupdate\s+[\w.` + '`' + String.raw `"]+\s+set\b(?![\s\S]*\bwhere\b)`, 'i'),
74
+ },
75
+ {
76
+ category: 'database',
77
+ reason: 'DELETE with a WHERE clause that is always true (deletes every row)',
78
+ re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bdelete\s+from\b[\s\S]*?\bwhere\s+(?:1\s*=\s*1|true)\b`, 'i'),
79
+ },
69
80
  {
70
81
  category: 'database',
71
82
  reason: 'drops a MongoDB database or collection',
@@ -106,11 +117,6 @@ const STRIPPED_RULES = [
106
117
  reason: 'deletes untracked files (git clean)',
107
118
  re: /\bgit\s+clean\s+-[a-z]*f/i,
108
119
  },
109
- {
110
- category: 'git-history',
111
- reason: 'force-deletes a git branch',
112
- re: /\bgit\s+branch\s+-[a-z]*D/i,
113
- },
114
120
  {
115
121
  category: 'git-history',
116
122
  reason: 'rewrites git history (filter-branch/filter-repo)',
@@ -213,9 +219,7 @@ const STRIPPED_RULES = [
213
219
  * mentions inside commit messages / echoes / grep patterns / heredoc'd docs
214
220
  * never trip the bare-command rules (git/infra/cloud/container/publish).
215
221
  */
216
- function classifyDestructiveCommand(command) {
217
- if (!command || typeof command !== 'string')
218
- return null;
222
+ function classifyRegex(command) {
219
223
  const raw = stripHeredocs(command);
220
224
  for (const rule of RAW_RULES) {
221
225
  if (rule.re.test(raw)) {
@@ -230,6 +234,49 @@ function classifyDestructiveCommand(command) {
230
234
  }
231
235
  return null;
232
236
  }
237
+ /** Only the quote-preserving SQL rules — used for SQL a wrapper (ssh/docker exec/env) hid from CMD_POS. */
238
+ function classifyRawSql(command) {
239
+ const raw = stripHeredocs(command);
240
+ for (const rule of RAW_RULES) {
241
+ if (rule.re.test(raw))
242
+ return { category: rule.category, reason: rule.reason };
243
+ }
244
+ return null;
245
+ }
246
+ const MAX_DEPTH = 4;
247
+ function classify(command, depth) {
248
+ if (depth > MAX_DEPTH)
249
+ return null;
250
+ const viaRegex = classifyRegex(command);
251
+ if (viaRegex)
252
+ return viaRegex;
253
+ // Token pass: understands option order, bundled flags, wrappers and `bash -c "…"`.
254
+ for (const tokens of (0, destructiveTokens_1.tokenize)(stripHeredocs(command))) {
255
+ const m = (0, destructiveTokens_1.classifyTokens)(tokens, (inner) => classify(inner, depth + 1), classifyRawSql);
256
+ if (m)
257
+ return m;
258
+ }
259
+ return null;
260
+ }
261
+ /**
262
+ * Classify a bash command. Returns the FIRST matching destructive category, or
263
+ * null when the command is not recognised as destructive.
264
+ *
265
+ * Two layers:
266
+ * 1. Regex rules (above). RAW_RULES run on the heredoc-stripped but quote-PRESERVING
267
+ * string (real SQL lives inside quotes) and are gated on a DB-client token;
268
+ * STRIPPED_RULES run on the fully stripped string so mentions in commit messages /
269
+ * echoes / heredoc'd docs never trip them.
270
+ * 2. A token pass (destructiveTokens.ts) that splits the command like a shell, peels
271
+ * wrappers (sudo/env/xargs/bash -c/ssh/docker exec …) and judges the real command.
272
+ * It catches what a regex cannot: `git -C x reset --hard`, `git push -fu`,
273
+ * `git push origin +main`, `rm -rf src`, `find . -delete`, `> file`.
274
+ */
275
+ function classifyDestructiveCommand(command) {
276
+ if (!command || typeof command !== 'string')
277
+ return null;
278
+ return classify(command, 0);
279
+ }
233
280
  /** True when the given tool + input represents a destructive bash command. */
234
281
  function isDestructiveBash(tool, input) {
235
282
  if (tool !== 'bash' && tool !== 'run_command' && tool !== 'execute_command')
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Token-level half of the destructive-command classifier.
3
+ *
4
+ * destructive.ts started life as a pile of regexes run over the command STRING. That
5
+ * missed most real-world spellings, because a regex does not understand shell:
6
+ * - global options between the tool and its verb: git -C repo reset --hard
7
+ * - combined short flags: git push -fu origin main
8
+ * - refspec force syntax: git push origin +main
9
+ * - wrappers that hide the real command: sudo / env X=1 / xargs / nohup
10
+ * - interpreters that take the command as a string: bash -c "…", eval "…", ssh host "…"
11
+ * - containers that run it elsewhere: docker exec db psql … / kubectl exec -- …
12
+ * and it could not express "rm -rf node_modules is fine, rm -rf src is not".
13
+ *
14
+ * So this module splits the command into simple commands the way a shell would (honouring
15
+ * quotes, so a commit message that MENTIONS `git push --force` is one argument of
16
+ * `git commit`, never a command), peels wrappers off, and judges (binary, args).
17
+ *
18
+ * It is a heuristic, not a shell: no variable expansion, no aliases, no functions. When it
19
+ * cannot tell (an `rm -rf $DIR`), it errs toward asking — the cost of a false positive is
20
+ * one confirmation, the cost of a false negative is unrecoverable.
21
+ */
22
+ export interface TokenMatch {
23
+ category: string;
24
+ reason: string;
25
+ }
26
+ /** Split `src` into simple commands, each a list of unquoted words. */
27
+ export declare function tokenize(src: string): string[][];
28
+ export declare function classifyTokens(tokens: string[], recurse: (command: string) => TokenMatch | null, rawSql: (command: string) => TokenMatch | null): TokenMatch | null;
29
+ //# sourceMappingURL=destructiveTokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA2BhD;AAgQD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,EAC/C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAC7C,UAAU,GAAG,IAAI,CAyDnB"}