@nexrall/code-core 1.4.63 → 1.4.65

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 (45) hide show
  1. package/dist/agent/agentRegistry.d.ts +14 -3
  2. package/dist/agent/agentRegistry.d.ts.map +1 -1
  3. package/dist/agent/agentRegistry.js +136 -6
  4. package/dist/agent/agentTypes.d.ts +51 -1
  5. package/dist/agent/agentTypes.d.ts.map +1 -1
  6. package/dist/agent/agentTypes.js +194 -11
  7. package/dist/agent/backgroundAgents.d.ts +66 -0
  8. package/dist/agent/backgroundAgents.d.ts.map +1 -0
  9. package/dist/agent/backgroundAgents.js +145 -0
  10. package/dist/agent/loop.d.ts +43 -6
  11. package/dist/agent/loop.d.ts.map +1 -1
  12. package/dist/agent/loop.js +942 -259
  13. package/dist/agent/modelCatalogue.d.ts +15 -0
  14. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  15. package/dist/agent/modelCatalogue.js +46 -0
  16. package/dist/agent/readDedupe.d.ts +15 -0
  17. package/dist/agent/readDedupe.d.ts.map +1 -0
  18. package/dist/agent/readDedupe.js +146 -0
  19. package/dist/agent/toolPrefetch.d.ts +44 -0
  20. package/dist/agent/toolPrefetch.d.ts.map +1 -0
  21. package/dist/agent/toolPrefetch.js +101 -0
  22. package/dist/agent/trust.d.ts +0 -5
  23. package/dist/agent/trust.d.ts.map +1 -1
  24. package/dist/agent/trust.js +41 -0
  25. package/dist/api/client.d.ts +14 -0
  26. package/dist/api/client.d.ts.map +1 -1
  27. package/dist/api/client.js +88 -8
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/permissions/modePolicy.d.ts +10 -0
  32. package/dist/permissions/modePolicy.d.ts.map +1 -1
  33. package/dist/permissions/modePolicy.js +11 -0
  34. package/dist/tools/executor.d.ts +36 -0
  35. package/dist/tools/executor.d.ts.map +1 -1
  36. package/dist/tools/executor.js +408 -85
  37. package/dist/tools/tsLangService.d.ts.map +1 -1
  38. package/dist/tools/tsLangService.js +107 -21
  39. package/dist/types.d.ts +99 -5
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js +12 -1
  42. package/dist/util/miniYaml.d.ts +10 -0
  43. package/dist/util/miniYaml.d.ts.map +1 -0
  44. package/dist/util/miniYaml.js +149 -0
  45. package/package.json +8 -17
@@ -44,9 +44,17 @@ export interface StoredAgent {
44
44
  seq: number;
45
45
  /** Rough JSON size, cached so the byte cap doesn't re-serialise on every write. */
46
46
  bytes: number;
47
+ /**
48
+ * The conversation (AgentLoopOptions.sessionId) that created it. Every main agent's
49
+ * scope is 'root', so without this any chat in a shared process (VS Code panels,
50
+ * desktop tasks) could resume another chat's sub-agents.
51
+ */
52
+ sessionKey?: string;
47
53
  }
54
+ export declare function enableAgentTranscriptPersistence(dir?: string): void;
55
+ export declare function disableAgentTranscriptPersistence(): void;
48
56
  /** Store a finished sub-agent's transcript. Returns its resumable id. */
49
- export declare function rememberAgent(agentName: string | null, description: string, messages: Message[], ownerScope?: string): string;
57
+ export declare function rememberAgent(agentName: string | null, description: string, messages: Message[], ownerScope?: string, sessionKey?: string): string;
50
58
  /**
51
59
  * May `scope` resume/modify the agent stored under `id`?
52
60
  *
@@ -55,10 +63,13 @@ export declare function rememberAgent(agentName: string | null, description: str
55
63
  * and inventing a prefix convention to fake it would be a second guessable capability.
56
64
  * The main agent is included because it owns the session and already sees every result.
57
65
  */
58
- export declare function canResume(stored: StoredAgent, scope: string): boolean;
66
+ export declare function canResume(stored: StoredAgent, scope: string, sessionKey?: string): boolean;
59
67
  /** Append a resumed run's new messages to an existing agent. */
60
- export declare function updateAgent(id: string, messages: Message[], scope?: string): void;
68
+ export declare function updateAgent(id: string, messages: Message[], scope?: string, sessionKey?: string): void;
61
69
  export declare function getAgent(id: string): StoredAgent | undefined;
70
+ /** Mark an agent as being resumed. False if another run already holds it. */
71
+ export declare function claimAgentForResume(id: string): boolean;
72
+ export declare function releaseAgentForResume(id: string): void;
62
73
  /** Newest first — the order a "which agents can I resume?" list wants. */
63
74
  export declare function listAgents(): StoredAgent[];
64
75
  /** Test seam. Not exported through index.ts's public surface by intent. */
@@ -1 +1 @@
1
- {"version":3,"file":"agentRegistry.d.ts","sourceRoot":"","sources":["../../src/agent/agentRegistry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AA8CxC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;;;;;;;;;;OAcG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;CACf;AA4CD,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,SAAS,GAClB,MAAM,CAkBR;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,SAAS,GAAG,IAAI,CAgBjF;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAE5D;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,IAAI,WAAW,EAAE,CAE1C;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,eAAO,MAAM,OAAO;;;CAAkC,CAAC"}
1
+ {"version":3,"file":"agentRegistry.d.ts","sourceRoot":"","sources":["../../src/agent/agentRegistry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AA8CxC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;;;;;;;;;;OAcG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAsDD,wBAAgB,gCAAgC,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,iCAAiC,IAAI,IAAI,CAAwB;AAoCjF,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,SAAS,EACnB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAoBR;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAG1F;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,SAAS,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBtG;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAU5D;AAMD,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAIvD;AACD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAuB;AAE9E,0EAA0E;AAC1E,wBAAgB,UAAU,IAAI,WAAW,EAAE,CAE1C;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,eAAO,MAAM,OAAO;;;CAAkC,CAAC"}
@@ -1,13 +1,53 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports._limits = void 0;
37
+ exports.enableAgentTranscriptPersistence = enableAgentTranscriptPersistence;
38
+ exports.disableAgentTranscriptPersistence = disableAgentTranscriptPersistence;
4
39
  exports.rememberAgent = rememberAgent;
5
40
  exports.canResume = canResume;
6
41
  exports.updateAgent = updateAgent;
7
42
  exports.getAgent = getAgent;
43
+ exports.claimAgentForResume = claimAgentForResume;
44
+ exports.releaseAgentForResume = releaseAgentForResume;
8
45
  exports.listAgents = listAgents;
9
46
  exports._resetAgentRegistry = _resetAgentRegistry;
10
47
  const node_crypto_1 = require("node:crypto");
48
+ const fs = __importStar(require("node:fs"));
49
+ const os = __importStar(require("node:os"));
50
+ const path = __importStar(require("node:path"));
11
51
  // ─── Resumable sub-agents ────────────────────────────────────────────────────
12
52
  //
13
53
  // A sub-agent normally starts with a blank slate: fresh context, no memory of a
@@ -91,8 +131,72 @@ function sizeOf(messages) {
91
131
  return MAX_TOTAL_BYTES;
92
132
  }
93
133
  }
134
+ // ── Disk persistence (opt-in, per host) ─────────────────────────────────────────
135
+ //
136
+ // Claude Code keeps sub-agent transcripts on disk, so "continue that agent" survives a
137
+ // restart. Hosts opt in with enableAgentTranscriptPersistence() at startup; library
138
+ // callers and tests stay purely in-memory. Ids carry 32 random bits, so knowing one is
139
+ // the capability (the model only ever learns ids from its own tool results).
140
+ let _persistDir = null;
141
+ const PERSIST_MAX_FILES = 100;
142
+ const PERSIST_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
143
+ function enableAgentTranscriptPersistence(dir) {
144
+ _persistDir = dir ?? process.env.NEXRALL_AGENT_TRANSCRIPTS_DIR ?? path.join(os.homedir(), '.nexrall', 'agent-transcripts');
145
+ try {
146
+ fs.mkdirSync(_persistDir, { recursive: true, mode: 0o700 });
147
+ }
148
+ catch {
149
+ _persistDir = null;
150
+ return;
151
+ }
152
+ prunePersisted();
153
+ }
154
+ function disableAgentTranscriptPersistence() { _persistDir = null; }
155
+ function persistedPath(id) {
156
+ if (!_persistDir || !/^agent_\d+_[0-9a-f]{8}$/.test(id))
157
+ return null;
158
+ return path.join(_persistDir, `${id}.json`);
159
+ }
160
+ function persist(agent) {
161
+ const p = persistedPath(agent.id);
162
+ if (!p)
163
+ return;
164
+ try {
165
+ fs.writeFileSync(p, JSON.stringify(agent), { mode: 0o600 });
166
+ }
167
+ catch { /* best effort */ }
168
+ }
169
+ function loadPersisted(id) {
170
+ const p = persistedPath(id);
171
+ if (!p)
172
+ return undefined;
173
+ try {
174
+ const a = JSON.parse(fs.readFileSync(p, 'utf-8'));
175
+ if (a?.id !== id || !Array.isArray(a.messages))
176
+ return undefined;
177
+ // A restarted process has fresh scope counters, so only the main agent ('root')
178
+ // may resume a transcript from a previous process.
179
+ return { ...a, ownerScope: 'root', seq: ++_seq };
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ }
185
+ function prunePersisted() {
186
+ if (!_persistDir)
187
+ return;
188
+ try {
189
+ const files = fs.readdirSync(_persistDir).filter((f) => f.endsWith('.json'))
190
+ .map((f) => { const full = path.join(_persistDir, f); return { full, t: fs.statSync(full).mtimeMs }; })
191
+ .sort((a, b) => b.t - a.t);
192
+ const now = Date.now();
193
+ files.forEach((f, i) => { if (i >= PERSIST_MAX_FILES || now - f.t > PERSIST_MAX_AGE_MS)
194
+ fs.rmSync(f.full, { force: true }); });
195
+ }
196
+ catch { /* best effort */ }
197
+ }
94
198
  /** Store a finished sub-agent's transcript. Returns its resumable id. */
95
- function rememberAgent(agentName, description, messages, ownerScope = 'root') {
199
+ function rememberAgent(agentName, description, messages, ownerScope = 'root', sessionKey) {
96
200
  // Unguessable, so an id cannot be ENUMERATED even before the ownership check runs.
97
201
  // Defence in depth only — randomness is not authorisation, `canResume` is.
98
202
  const id = `agent_${++_counter}_${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
@@ -107,7 +211,9 @@ function rememberAgent(agentName, description, messages, ownerScope = 'root') {
107
211
  updatedAt: now,
108
212
  seq: ++_seq,
109
213
  bytes: sizeOf(messages),
214
+ ...(sessionKey ? { sessionKey } : {}),
110
215
  });
216
+ persist(_agents.get(id));
111
217
  evictUntilWithinLimits();
112
218
  return id;
113
219
  }
@@ -119,18 +225,20 @@ function rememberAgent(agentName, description, messages, ownerScope = 'root') {
119
225
  * and inventing a prefix convention to fake it would be a second guessable capability.
120
226
  * The main agent is included because it owns the session and already sees every result.
121
227
  */
122
- function canResume(stored, scope) {
228
+ function canResume(stored, scope, sessionKey) {
229
+ if (stored.sessionKey && sessionKey && stored.sessionKey !== sessionKey)
230
+ return false;
123
231
  return stored.ownerScope === scope || scope === 'root';
124
232
  }
125
233
  /** Append a resumed run's new messages to an existing agent. */
126
- function updateAgent(id, messages, scope = 'root') {
127
- const existing = _agents.get(id);
234
+ function updateAgent(id, messages, scope = 'root', sessionKey) {
235
+ const existing = getAgent(id);
128
236
  if (!existing)
129
237
  return;
130
238
  // Same authorisation as reading. Without it a low-privilege descendant could overwrite a
131
239
  // privileged agent's transcript while its `agentName` stayed unchanged — indirect prompt
132
240
  // injection into whatever frame resumes it next.
133
- if (!canResume(existing, scope))
241
+ if (!canResume(existing, scope, sessionKey))
134
242
  return;
135
243
  existing.messages = messages;
136
244
  existing.updatedAt = Date.now();
@@ -140,11 +248,33 @@ function updateAgent(id, messages, scope = 'root') {
140
248
  // actively-continued agent from being evicted by a burst of new one-shots.
141
249
  _agents.delete(id);
142
250
  _agents.set(id, existing);
251
+ persist(existing);
143
252
  evictUntilWithinLimits();
144
253
  }
145
254
  function getAgent(id) {
146
- return _agents.get(id);
255
+ const live = _agents.get(id);
256
+ if (live)
257
+ return live;
258
+ // Evicted from memory or from a previous process — fall back to disk.
259
+ const fromDisk = loadPersisted(id);
260
+ if (fromDisk) {
261
+ _agents.set(id, fromDisk);
262
+ evictUntilWithinLimits();
263
+ }
264
+ return fromDisk;
265
+ }
266
+ // Ids currently being resumed. Two `task` calls in one message resuming the SAME agent
267
+ // would both start from the same transcript and the last to finish would silently
268
+ // overwrite the other's work (updateAgent is last-write-wins).
269
+ const _inUse = new Set();
270
+ /** Mark an agent as being resumed. False if another run already holds it. */
271
+ function claimAgentForResume(id) {
272
+ if (_inUse.has(id))
273
+ return false;
274
+ _inUse.add(id);
275
+ return true;
147
276
  }
277
+ function releaseAgentForResume(id) { _inUse.delete(id); }
148
278
  /** Newest first — the order a "which agents can I resume?" list wants. */
149
279
  function listAgents() {
150
280
  return [..._agents.values()].sort((a, b) => b.seq - a.seq);
@@ -56,10 +56,50 @@ export interface AgentType {
56
56
  * — a reviewer or test-writer needs the project's rules, an explorer does not.
57
57
  */
58
58
  lightPrompt?: boolean;
59
+ /** Tools this agent may NOT use, on top of (or instead of) an allowlist. Claude Code's `disallowedTools`. */
60
+ disallowedTools?: string[];
61
+ /** Model round-trips before the run stops and reports back (Claude Code's `maxTurns`). */
62
+ maxTurns?: number;
63
+ /** Reasoning effort for this agent's requests (low | medium | high | xhigh | max). Default: the session's. */
64
+ effort?: string;
65
+ /** Always run this agent in the background (the `task` call returns immediately). */
66
+ background?: boolean;
67
+ /** `worktree` — run in a fresh isolated worktree; removed afterwards if it made no changes. */
68
+ isolation?: 'worktree';
69
+ /** `plan` — read-only lock for this agent regardless of the session mode. Only narrowing modes are honoured. */
70
+ permissionMode?: 'plan';
71
+ /** Display colour hint for UIs. */
72
+ color?: string;
73
+ /** Skills whose instructions are preloaded into this agent's prompt (Claude Code's `skills`). */
74
+ skills?: string[];
75
+ /** MCP servers this agent may use; others' tools are neither sent nor allowed (Claude Code's `mcpServers`). */
76
+ mcpServers?: string[];
77
+ /**
78
+ * Hooks active only while THIS agent runs (Claude Code's agent-scoped `hooks`).
79
+ * Same execution model and trust gate as settings.json hooks — see trust.ts, which
80
+ * lists an agent file declaring hooks as a trust-sensitive file.
81
+ */
82
+ hooks?: AgentHooks;
83
+ }
84
+ export interface AgentHookDef {
85
+ type: 'command';
86
+ command: string;
87
+ timeout_ms?: number;
88
+ }
89
+ export interface AgentHookEntry {
90
+ matcher?: string;
91
+ hooks: AgentHookDef[];
92
+ }
93
+ export interface AgentHooks {
94
+ PreToolUse?: AgentHookEntry[];
95
+ PostToolUse?: AgentHookEntry[];
96
+ /** When this agent finishes (Claude Code: `Stop` in an agent file == SubagentStop). */
97
+ Stop?: AgentHookDef[];
59
98
  }
60
99
  /** Where a sub-agent's own persistent notes live. */
61
100
  export type AgentMemoryScope = 'project' | 'user' | 'local';
62
- export declare const VALID_MODELS: readonly ["claude-sonnet-5", "claude-opus-5-5", "claude-fable-5-1", "gpt-5.4", "gpt-5.4-mini", "gpt-4.1", "turbo", "pro", "ultra", "fast"];
101
+ export declare function normaliseToolName(t: string): string;
102
+ export declare const VALID_MODELS: readonly ["claude-sonnet-5-5", "claude-opus-5-5", "claude-fable-5-1", "gpt-5.4", "gpt-5.4-mini", "gpt-4.1", "turbo", "pro", "ultra", "fast"];
63
103
  /** The four tier aliases specifically (a subset of VALID_MODELS) — the only values `AgentType.model`/`Skill.model` narrow to when recognised; a real model id passes through as a plain string instead. */
64
104
  export declare const MODEL_TIER_ALIASES: readonly ["turbo", "pro", "ultra", "fast"];
65
105
  export type ModelTier = (typeof MODEL_TIER_ALIASES)[number];
@@ -94,6 +134,16 @@ export interface AgentWarning {
94
134
  * happen to contain uppercase.
95
135
  */
96
136
  export declare function parseModel(v: string | undefined): string | undefined;
137
+ /**
138
+ * Read and validate an agent file's `hooks:` block. Accepts Claude Code's shape —
139
+ * `{ PreToolUse|PostToolUse: [{ matcher?, hooks: [{ type: command, command, timeout? }] }],
140
+ * Stop|SubagentStop: [{ hooks: [...] }] }` — with `timeout` in SECONDS (Claude Code)
141
+ * or `timeout_ms`. Anything else is an error, never a partial guess.
142
+ */
143
+ export declare function parseAgentHooks(raw: string): {
144
+ hooks?: AgentHooks;
145
+ error?: string;
146
+ };
97
147
  /**
98
148
  * Discover all agent types. Precedence: project > global > plugin > builtin.
99
149
  *
@@ -1 +1 @@
1
- {"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,cAAc,CAAC;IACrE;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qDAAqD;AACrD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAkX5D,eAAO,MAAM,YAAY,4IAKf,CAAC;AAEX,2MAA2M;AAC3M,eAAO,MAAM,kBAAkB,4CAA6C,CAAC;AAC7E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAOD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpE;AAoLD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAE3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,SAAS,EAAO,GACtB;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAuBlD;AAED,yFAAyF;AACzF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AASD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
1
+ {"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"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"}
@@ -34,13 +34,16 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MODEL_TIER_ALIASES = exports.VALID_MODELS = void 0;
37
+ exports.normaliseToolName = normaliseToolName;
37
38
  exports.parseModel = parseModel;
39
+ exports.parseAgentHooks = parseAgentHooks;
38
40
  exports.loadAgentTypes = loadAgentTypes;
39
41
  exports.loadAgentTypesWithWarnings = loadAgentTypesWithWarnings;
40
42
  exports.knownToolNames = knownToolNames;
41
43
  exports.builtinAgents = builtinAgents;
42
44
  exports.summariseAgents = summariseAgents;
43
45
  exports.findAgentType = findAgentType;
46
+ const miniYaml_1 = require("../util/miniYaml");
44
47
  const fs = __importStar(require("fs"));
45
48
  const path = __importStar(require("path"));
46
49
  const os = __importStar(require("os"));
@@ -79,7 +82,7 @@ const READ_ONLY_TOOLS = [
79
82
  // edit Z directly" does better work, and withholding it meant every sub-agent
80
83
  // rediscovered project conventions from scratch. Writing is a different thing
81
84
  // entirely — memory is durable, cross-session, SHARED state, while sub-agents run
82
- // up to 4-wide in parallel with their reasoning hidden from the user. A wrong fact
85
+ // up to 10-wide (configurable) in parallel with their reasoning hidden from the user. A wrong fact
83
86
  // written there is invisible and permanent, and it competes for a capped, shared
84
87
  // budget that gets periodically condensed.
85
88
  //
@@ -107,7 +110,9 @@ const READ_ONLY_TOOLS = [
107
110
  * without it can still search) and denies nothing. Naming the absence is the
108
111
  * only way to stop it being "helpfully" re-added.
109
112
  */
110
- const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'fetch_url'];
113
+ // web_search is a client-side tool now (executor.ts webSearch), so it must be granted
114
+ // explicitly — planner/devops-advisor were told to use it and then refused.
115
+ const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'fetch_url', 'web_search'];
111
116
  /** Read-only + the write tools, for agents that produce code. */
112
117
  const WRITE_TOOLS = [
113
118
  ...READ_ONLY_TOOLS,
@@ -231,10 +236,13 @@ const BUILTIN_AGENTS = [
231
236
  lightPrompt: true,
232
237
  description: 'Fast read-only codebase explorer — locates files, symbols, and call sites and reports concise findings. Use to keep bulk searching out of the main context. Cannot modify files.',
233
238
  tools: READ_ONLY_TOOLS,
234
- // No `model` override — inherits the session's. A previous version forced
235
- // 'turbo' (Claude Sonnet 5) on the theory that bulk search doesn't need a
236
- // frontier model, but that silently billed Anthropic regardless of which
237
- // provider the user actually selected for the conversation.
239
+ // The cheapest model of the SESSION'S OWN provider (Opus → Sonnet, GPT-6 Sol →
240
+ // Luna), like Claude Code's Explore on Haiku. A previous version forced 'turbo'
241
+ // (Claude Sonnet 5), which billed Anthropic even for DeepSeek/OpenAI sessions;
242
+ // 'cheapest' never leaves the provider and falls back to inheriting.
243
+ model: 'cheapest',
244
+ // Search-and-report needs breadth, not deep reasoning (Claude Code's Explore runs fast).
245
+ effort: 'low',
238
246
  source: 'builtin',
239
247
  prompt: [
240
248
  'You map code. You NEVER modify anything.',
@@ -383,7 +391,37 @@ const KNOWN_TOOL_NAMES = new Set([
383
391
  const KNOWN_META_KEYS = new Set([
384
392
  'name', 'description', 'tools', 'model', 'test_files_only', 'testfilesonly',
385
393
  'memory',
394
+ // Claude Code-compatible fields (keys arrive lowercased from parseFrontmatter).
395
+ 'disallowedtools', 'disallowed_tools', 'maxturns', 'max_turns', 'effort', 'background',
396
+ 'isolation', 'permissionmode', 'permission_mode', 'color',
397
+ 'skills', 'mcpservers', 'mcp_servers', 'hooks',
386
398
  ]);
399
+ /** Claude Code fields Nexrall recognises but does not implement — warned about by name. */
400
+ /** Claude Code fields Nexrall recognises but does not implement — warned about by name. */
401
+ const UNSUPPORTED_CC_KEYS = new Set([]);
402
+ const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
403
+ /**
404
+ * Claude Code tool names → Nexrall's, so an agent file written for Claude Code works
405
+ * when dropped into .nexrall/agents/ (`tools: Read, Grep, Glob` instead of a typo warning
406
+ * and an agent that silently cannot read). `mcp__server__tool` → `server__tool`.
407
+ */
408
+ const CLAUDE_CODE_TOOL_ALIASES = {
409
+ read: 'read_file', write: 'write_file', edit: 'edit_file', multiedit: 'multi_edit',
410
+ grep: 'search_files', glob: 'glob', ls: 'list_directory', bash: 'bash',
411
+ bashoutput: 'bash_output', killshell: 'kill_shell', killbash: 'kill_shell',
412
+ webfetch: 'fetch_url', websearch: 'web_search', todowrite: 'todo_write',
413
+ notebookread: 'notebook_read', notebookedit: 'notebook_edit', task: 'task', agent: 'task',
414
+ };
415
+ function normaliseToolName(t) {
416
+ if (t.startsWith('mcp__'))
417
+ return t.slice('mcp__'.length);
418
+ if (/[A-Z]/.test(t)) {
419
+ const alias = CLAUDE_CODE_TOOL_ALIASES[t.toLowerCase()];
420
+ if (alias)
421
+ return alias;
422
+ }
423
+ return t;
424
+ }
387
425
  // Models this build knows how to NAME. Used only for the frontmatter warning
388
426
  // below, never to reject a value — see parseModel for why an allowlist must not
389
427
  // gate what is sent.
@@ -399,7 +437,7 @@ const KNOWN_META_KEYS = new Set([
399
437
  // at all. Two independently-maintained model lists are exactly the kind of
400
438
  // drift the shared parseFrontmatter migration was meant to end.
401
439
  exports.VALID_MODELS = [
402
- 'claude-sonnet-5', 'claude-opus-5-5', 'claude-fable-5-1',
440
+ 'claude-sonnet-5-5', 'claude-opus-5-5', 'claude-fable-5-1',
403
441
  'gpt-5.4', 'gpt-5.4-mini', 'gpt-4.1',
404
442
  // Legacy tier aliases — still accepted, still resolved by the backend.
405
443
  'turbo', 'pro', 'ultra', 'fast',
@@ -433,6 +471,11 @@ function parseModel(v) {
433
471
  const lower = raw.toLowerCase();
434
472
  if (exports.MODEL_TIER_ALIASES.includes(lower))
435
473
  return lower;
474
+ // Resolved at dispatch against the parent's model (modelCatalogue.resolveSubAgentModel).
475
+ if (lower === 'inherit')
476
+ return undefined;
477
+ if (lower === 'cheapest')
478
+ return lower;
436
479
  return raw;
437
480
  }
438
481
  /** Parse a frontmatter boolean, accepting the usual truthy spellings. */
@@ -445,9 +488,19 @@ function parseToolList(v) {
445
488
  const tools = v
446
489
  .replace(/^\[|\]$/g, '') // tolerate [a, b] style
447
490
  .split(/[,\s]+/)
448
- .map((t) => t.trim())
491
+ .map((t) => normaliseToolName(t.trim()))
449
492
  .filter(Boolean);
450
- return tools.length ? tools : undefined;
493
+ return tools.length ? [...new Set(tools)] : undefined;
494
+ }
495
+ function parseNameList(v) {
496
+ if (!v)
497
+ return undefined;
498
+ const names = v.replace(/^\[|\]$/g, '').split(',').map((x) => x.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
499
+ return names.length ? [...new Set(names)] : undefined;
500
+ }
501
+ function parsePositiveInt(v) {
502
+ const n = Number((v ?? '').trim());
503
+ return Number.isInteger(n) && n > 0 ? n : undefined;
451
504
  }
452
505
  function loadDir(dir, source, into, warnings) {
453
506
  let entries;
@@ -487,6 +540,7 @@ function loadDir(dir, source, into, warnings) {
487
540
  continue;
488
541
  const tools = parseToolList(meta.tools);
489
542
  let effectiveTools = tools;
543
+ let agentHooks;
490
544
  // ── FAIL CLOSED on a definition we could not parse ────────────────────────
491
545
  //
492
546
  // A file with no `---` frontmatter block used to yield tools === undefined,
@@ -568,7 +622,44 @@ function loadDir(dir, source, into, warnings) {
568
622
  message: 'lists `task` as its ONLY tool, so it can delegate but cannot read, write or run anything itself — it has no way to do or verify work. Add the tools it needs, or drop the `tools:` line to inherit its parent\'s.',
569
623
  });
570
624
  }
571
- const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
625
+ const unsupported = Object.keys(meta).filter((k) => UNSUPPORTED_CC_KEYS.has(k));
626
+ if (unsupported.length) {
627
+ warnings.push({
628
+ file: full,
629
+ agent: name,
630
+ message: `uses Claude Code field(s) Nexrall does not support yet: ${unsupported.join(', ')} — ignored.`,
631
+ });
632
+ }
633
+ const pm = (meta.permissionmode ?? meta.permission_mode)?.trim();
634
+ if (pm && pm !== 'plan' && pm !== 'default') {
635
+ warnings.push({
636
+ file: full,
637
+ agent: name,
638
+ message: `has permissionMode: "${pm}" — only "plan" (read-only) is honoured; a definition file cannot widen the session's permissions. Ignored.`,
639
+ });
640
+ }
641
+ const eff = meta.effort?.trim().toLowerCase();
642
+ if (eff && !VALID_EFFORTS.includes(eff)) {
643
+ warnings.push({ file: full, agent: name, message: `has effort: "${meta.effort}" — expected one of ${VALID_EFFORTS.join(', ')}. Ignored.` });
644
+ }
645
+ const mt = meta.maxturns ?? meta.max_turns;
646
+ if (mt !== undefined && parsePositiveInt(mt) === undefined) {
647
+ warnings.push({ file: full, agent: name, message: `has maxTurns: "${mt}" — expected a positive integer. Ignored.` });
648
+ }
649
+ const iso = meta.isolation?.trim().toLowerCase();
650
+ if (iso && iso !== 'worktree') {
651
+ warnings.push({ file: full, agent: name, message: `has isolation: "${meta.isolation}" — only "worktree" is supported. Ignored.` });
652
+ }
653
+ if (meta.hooks !== undefined) {
654
+ const parsed = parseAgentHooks(raw);
655
+ if (parsed.error) {
656
+ warnings.push({ file: full, agent: name, message: `has an unusable hooks: block (${parsed.error}) — no hooks will run for this agent.` });
657
+ }
658
+ else {
659
+ agentHooks = parsed.hooks;
660
+ }
661
+ }
662
+ const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k) && !UNSUPPORTED_CC_KEYS.has(k));
572
663
  if (strayKeys.length) {
573
664
  warnings.push({
574
665
  file: full,
@@ -602,8 +693,100 @@ function loadDir(dir, source, into, warnings) {
602
693
  // definition never receives a writable store — should survive parseFrontmatter
603
694
  // being changed to salvage partial metadata, which is a plausible future edit.
604
695
  ...(ok && parseMemoryScope(meta.memory) ? { memory: parseMemoryScope(meta.memory) } : {}),
696
+ // Claude Code-compatible fields. Every one only NARROWS or tunes the agent — none
697
+ // can grant a tool or relax a permission, so an unreadable file (ok=false) simply
698
+ // has none of them.
699
+ ...(ok ? parseCcFields(meta) : {}),
700
+ ...(ok && agentHooks ? { hooks: agentHooks } : {}),
701
+ });
702
+ }
703
+ }
704
+ /**
705
+ * Read and validate an agent file's `hooks:` block. Accepts Claude Code's shape —
706
+ * `{ PreToolUse|PostToolUse: [{ matcher?, hooks: [{ type: command, command, timeout? }] }],
707
+ * Stop|SubagentStop: [{ hooks: [...] }] }` — with `timeout` in SECONDS (Claude Code)
708
+ * or `timeout_ms`. Anything else is an error, never a partial guess.
709
+ */
710
+ function parseAgentHooks(raw) {
711
+ let v;
712
+ try {
713
+ v = (0, miniYaml_1.frontmatterBlock)(raw, 'hooks');
714
+ }
715
+ catch (e) {
716
+ return { error: e.message };
717
+ }
718
+ if (v == null)
719
+ return {};
720
+ if (typeof v !== 'object' || Array.isArray(v))
721
+ return { error: 'expected a map of events' };
722
+ const defs = (list, where) => {
723
+ if (!Array.isArray(list))
724
+ throw new Error(`${where}: expected a list of hooks`);
725
+ return list.map((h, i) => {
726
+ const o = h;
727
+ if (!o || o.type !== 'command' || typeof o.command !== 'string' || !o.command.trim()) {
728
+ throw new Error(`${where}[${i}]: each hook needs type: command and a command`);
729
+ }
730
+ const ms = typeof o.timeout_ms === 'number' ? o.timeout_ms
731
+ : typeof o.timeout === 'number' ? o.timeout * 1000 : undefined;
732
+ return { type: 'command', command: o.command, ...(ms && ms > 0 ? { timeout_ms: ms } : {}) };
733
+ });
734
+ };
735
+ const entries = (list, ev) => {
736
+ if (!Array.isArray(list))
737
+ throw new Error(`${ev}: expected a list`);
738
+ return list.map((e, i) => {
739
+ const o = e;
740
+ if (!o || typeof o !== 'object')
741
+ throw new Error(`${ev}[${i}]: expected { matcher, hooks }`);
742
+ return {
743
+ ...(typeof o.matcher === 'string' && o.matcher ? { matcher: o.matcher } : {}),
744
+ hooks: defs(o.hooks, `${ev}[${i}].hooks`),
745
+ };
605
746
  });
747
+ };
748
+ try {
749
+ const out = {};
750
+ for (const [ev, val] of Object.entries(v)) {
751
+ if (ev === 'PreToolUse' || ev === 'PostToolUse')
752
+ out[ev] = entries(val, ev);
753
+ else if (ev === 'Stop' || ev === 'SubagentStop')
754
+ out.Stop = [...(out.Stop ?? []), ...entries(val, ev).flatMap((e) => e.hooks)];
755
+ else
756
+ throw new Error(`unsupported event "${ev}" (supported: PreToolUse, PostToolUse, Stop)`);
757
+ }
758
+ return { hooks: out };
606
759
  }
760
+ catch (e) {
761
+ return { error: e.message };
762
+ }
763
+ }
764
+ function parseCcFields(meta) {
765
+ const out = {};
766
+ const disallowed = parseToolList(meta.disallowedtools ?? meta.disallowed_tools);
767
+ if (disallowed)
768
+ out.disallowedTools = disallowed;
769
+ const mt = parsePositiveInt(meta.maxturns ?? meta.max_turns);
770
+ if (mt)
771
+ out.maxTurns = mt;
772
+ const eff = meta.effort?.trim().toLowerCase();
773
+ if (eff && VALID_EFFORTS.includes(eff))
774
+ out.effort = eff;
775
+ if (parseBool(meta.background))
776
+ out.background = true;
777
+ if (meta.isolation?.trim().toLowerCase() === 'worktree')
778
+ out.isolation = 'worktree';
779
+ if ((meta.permissionmode ?? meta.permission_mode)?.trim() === 'plan')
780
+ out.permissionMode = 'plan';
781
+ if (meta.color?.trim())
782
+ out.color = meta.color.trim();
783
+ const skills = parseNameList(meta.skills);
784
+ if (skills)
785
+ out.skills = skills;
786
+ const servers = parseNameList(meta.mcpservers ?? meta.mcp_servers);
787
+ if (servers)
788
+ out.mcpServers = servers;
789
+ return out;
607
790
  }
608
791
  /**
609
792
  * Discover all agent types. Precedence: project > global > plugin > builtin.
@@ -697,7 +880,7 @@ function summariseAgents(types) {
697
880
  const access = t.testFilesOnly
698
881
  ? 'writes TEST files only'
699
882
  : canWrite ? 'can modify files' : 'read-only';
700
- const model = t.model ? `, ${t.model} model` : '';
883
+ const model = t.model === 'cheapest' ? ', fast low-cost model' : t.model ? `, ${t.model} model` : '';
701
884
  return `- ${t.name} (${access}${model}): ${t.description}`;
702
885
  })
703
886
  .join('\n');