@nexrall/code-core 1.4.59 → 1.4.60

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.
@@ -0,0 +1,203 @@
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._worktreeEnforcementInternal = void 0;
37
+ exports.checkWorktreeIsolation = checkWorktreeIsolation;
38
+ exports.worktreeInstructions = worktreeInstructions;
39
+ // ─── Worktree isolation enforcement ───────────────────────────────────────────
40
+ //
41
+ // A session-wide lock, structurally identical to planMode.ts (same file, read
42
+ // its header first) but protecting a DIFFERENT invariant: not "nothing may be
43
+ // mutated", but "nothing outside THIS worktree may be mutated". Whichever
44
+ // session created the isolation is confined to its own directory; the main
45
+ // checkout — the thing another session (or the user themselves) may be
46
+ // working in RIGHT NOW — must be provably untouched.
47
+ //
48
+ // FAIL CLOSED, same rule as plan mode: a command whose target this module
49
+ // cannot verify stays inside the worktree is REFUSED, not permitted. Claude
50
+ // Code's own docs describe the identical trade-off for their worktree
51
+ // isolation checks ("Command shape... You can't turn this check off").
52
+ //
53
+ // Four checks, mirroring Claude Code's four:
54
+ // 1. File edits (write_file/edit_file/multi_edit/notebook_edit/delete_file/
55
+ // move_file/copy_file) — refuse a path that resolves into the main
56
+ // checkout.
57
+ // 2. Command working directory — refuse a `bash` call whose input.workDir
58
+ // (rare — most calls inherit the session's) resolves into the main
59
+ // checkout. (executeTool always receives the SESSION's workDir as a
60
+ // parameter, never as part of `input`, so in practice this check exists
61
+ // for defence in depth against a future tool that lets a call declare
62
+ // its own cwd.)
63
+ // 3. Git redirects — refuse a bash command that points git at the main
64
+ // checkout via `git -C <path>`, `--git-dir=`, `GIT_DIR=`/`GIT_WORK_TREE=`,
65
+ // or a `cd <main-checkout-path>` earlier in the same command line.
66
+ // 4. Command shape Claude Code cannot verify — a command name computed at
67
+ // runtime, or a construct this module's necessarily-conservative parser
68
+ // cannot fully account for, is refused rather than guessed at.
69
+ const path = __importStar(require("path"));
70
+ const worktree_1 = require("./worktree");
71
+ const EDIT_TOOLS = new Set([
72
+ 'write_file', 'write_docx', 'write_xlsx', 'write_pptx',
73
+ 'edit_file', 'multi_edit', 'delete_file', 'notebook_edit',
74
+ ]);
75
+ // move_file/copy_file carry two paths (source/destination — see loop.ts's
76
+ // lockPathsFor, which handles the identical shape for the same reason).
77
+ const DUAL_PATH_TOOLS = new Set(['move_file', 'copy_file']);
78
+ function pathsFromInput(tool, input) {
79
+ if (DUAL_PATH_TOOLS.has(tool)) {
80
+ return [input.source, input.destination, input.dest]
81
+ .filter((p) => typeof p === 'string' && p.length > 0);
82
+ }
83
+ if (typeof input.path === 'string' && input.path)
84
+ return [input.path];
85
+ return [];
86
+ }
87
+ const REFUSAL_FOOTER = 'Do NOT ask the user to approve it and do NOT try a variation to get around this — this session ' +
88
+ 'is isolated to its own worktree so it cannot collide with other work on the same repository. ' +
89
+ 'Operate only inside the worktree directory.';
90
+ /**
91
+ * Check ONE tool call against active worktree isolation. Returns null when
92
+ * the call is fine (either no isolation is active, or the call stays inside
93
+ * the worktree). Call this the same way checkPlanMode is called — BEFORE
94
+ * requestPermission, since no user answer at that prompt can lawfully unlock
95
+ * a session-wide isolation boundary.
96
+ */
97
+ function checkWorktreeIsolation(tool, input, state, workDir) {
98
+ if (!state)
99
+ return null; // no isolation active — nothing to enforce
100
+ if (tool === 'bash') {
101
+ const command = typeof input.command === 'string' ? input.command : '';
102
+ return checkBashAgainstWorktree(command, state);
103
+ }
104
+ if (EDIT_TOOLS.has(tool) || DUAL_PATH_TOOLS.has(tool)) {
105
+ for (const raw of pathsFromInput(tool, input)) {
106
+ const abs = path.isAbsolute(raw) ? raw : path.resolve(workDir ?? state.worktreePath, raw);
107
+ if ((0, worktree_1.pathEscapesWorktree)(abs, state)) {
108
+ return {
109
+ reason: 'edit-outside-worktree',
110
+ message: `\`${tool}\` targets "${raw}", which resolves outside this session's worktree ` +
111
+ `(${state.worktreePath}) and into the main checkout (${state.mainCheckout}). ${REFUSAL_FOOTER}`,
112
+ };
113
+ }
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ // ── bash analysis ──────────────────────────────────────────────────────────
119
+ //
120
+ // Deliberately conservative: this is NOT a shell parser. It looks for the
121
+ // SPECIFIC constructs that can redirect a command at the main checkout, and
122
+ // refuses anything it can't positively clear — the same "command shape it
123
+ // can't verify" refusal Claude Code's own docs describe, worded so the model
124
+ // is told exactly how to recover (split into plain commands) rather than
125
+ // left to guess.
126
+ /** `cd <path>` anywhere in the command, expanded to that path if it looks like the main checkout. */
127
+ const CD_RE = /(?:^|[;&|]|&&|\|\|)\s*cd\s+(\S+)/g;
128
+ /** `git -C <path>` — redirects git's working tree without an actual cd. */
129
+ const GIT_DASH_C_RE = /\bgit\s+-C\s+(\S+)/g;
130
+ /** `git --git-dir=<path>` or `git --git-dir <path>`. */
131
+ const GIT_DIR_FLAG_RE = /\bgit\s+(?:--git-dir=(\S+)|--git-dir\s+(\S+))/g;
132
+ /** `GIT_DIR=<path>` / `GIT_WORK_TREE=<path>` env-var prefix. */
133
+ const GIT_ENV_RE = /\b(?:GIT_DIR|GIT_WORK_TREE)=(\S+)/g;
134
+ /** Constructs this module refuses to reason about at all — runtime-computed command names, unparseable expansions. */
135
+ const UNVERIFIABLE_RE = /\$\(|`|\$\{!|\beval\b|\bexec\b\s*\$/;
136
+ function stripQuotes(s) {
137
+ return s.replace(/^["']|["']$/g, '');
138
+ }
139
+ function resolvesIntoMainCheckout(candidate, state) {
140
+ const stripped = stripQuotes(candidate);
141
+ const abs = path.isAbsolute(stripped) ? stripped : path.resolve(state.worktreePath, stripped);
142
+ return (0, worktree_1.pathEscapesWorktree)(abs, state);
143
+ }
144
+ function checkBashAgainstWorktree(command, state) {
145
+ if (!command)
146
+ return null;
147
+ // 4. Shape Claude Code (and we) cannot verify at all — refuse outright.
148
+ // A command substitution or eval could compute a path/verb this scan would
149
+ // never see, so there is nothing safe to check.
150
+ if (UNVERIFIABLE_RE.test(command)) {
151
+ return {
152
+ reason: 'bash-shape-unverifiable',
153
+ message: `This command uses command substitution, backticks, or \`eval\`, so this session's worktree ` +
154
+ `isolation cannot verify where it points before running it. Split it into plain, separate ` +
155
+ `commands with no substitution so each part can be checked. ${REFUSAL_FOOTER}`,
156
+ };
157
+ }
158
+ // 2/3. cd / git -C / --git-dir / GIT_DIR / GIT_WORK_TREE all name a path
159
+ // directly — check every one found anywhere in the command line, not just
160
+ // the first, since `cd repo && git -C ../other status` chains them.
161
+ const candidates = [];
162
+ for (const re of [CD_RE, GIT_DASH_C_RE, GIT_DIR_FLAG_RE, GIT_ENV_RE]) {
163
+ re.lastIndex = 0;
164
+ let m;
165
+ while ((m = re.exec(command))) {
166
+ const val = m[1] ?? m[2];
167
+ if (val)
168
+ candidates.push(val);
169
+ }
170
+ }
171
+ for (const candidate of candidates) {
172
+ if (resolvesIntoMainCheckout(candidate, state)) {
173
+ return {
174
+ reason: 'git-redirect-outside-worktree',
175
+ message: `This command points at "${candidate}", which resolves into the main checkout ` +
176
+ `(${state.mainCheckout}) rather than this session's worktree (${state.worktreePath}). ${REFUSAL_FOOTER}`,
177
+ };
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ /**
183
+ * Prepended to nexrall.md when worktree isolation is active — same rationale
184
+ * as PLAN_MODE_INSTRUCTIONS in planMode.ts: telling the model up front is not
185
+ * the enforcement (checkWorktreeIsolation at the call site is), it exists so
186
+ * the model plans its work AROUND the boundary instead of discovering it one
187
+ * refused tool call at a time.
188
+ */
189
+ function worktreeInstructions(state) {
190
+ return (`## Worktree isolation is ACTIVE for this session\n\n` +
191
+ `Every file write and every \`bash\` call in this session is confined to:\n` +
192
+ ` ${state.worktreePath}\n\n` +
193
+ `This exists so this session can never overwrite files another session (or the user) ` +
194
+ `is working on in the main checkout:\n` +
195
+ ` ${state.mainCheckout}\n\n` +
196
+ `Any write, edit, or command that resolves outside the worktree above is refused ` +
197
+ `automatically, before it runs — this is enforced, not a convention to remember. ` +
198
+ `Work entirely within the worktree; when the work is ready to share with the main ` +
199
+ `checkout, that merge is done via git (or whatever the project's normal review process ` +
200
+ `is) — not by writing outside the worktree from inside this session.`);
201
+ }
202
+ exports._worktreeEnforcementInternal = { checkBashAgainstWorktree, resolvesIntoMainCheckout };
203
+ //# sourceMappingURL=worktreeEnforcement.js.map
@@ -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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"}
@@ -1242,6 +1242,11 @@ async function streamChat(messages, options, onEvent) {
1242
1242
  // instead of re-running the model. Nothing new was charged, so a cost
1243
1243
  // display must not add them again.
1244
1244
  ...(evt.replayed === true ? { replayed: true } : {}),
1245
+ // Real USD cost, computed server-side by the same function that decides
1246
+ // the actual wallet debit. Absent on an older backend — omit the key
1247
+ // entirely rather than coercing to 0, so a cost display can tell
1248
+ // "unknown" apart from "this turn genuinely cost nothing".
1249
+ ...(typeof evt.cost === 'number' ? { cost: evt.cost } : {}),
1245
1250
  usage: {
1246
1251
  input_tokens: evt.input_tokens,
1247
1252
  output_tokens: evt.output_tokens,
package/dist/index.d.ts CHANGED
@@ -30,4 +30,10 @@ export * from './plugins/index';
30
30
  export * from './plugins/installer';
31
31
  export * from './plugins/sources';
32
32
  export * from './agent/trust';
33
+ export * from './agent/crossProcessLock';
34
+ export * from './agent/worktree';
35
+ export * from './agent/worktreeEnforcement';
36
+ export * from './agent/sharedTasks';
37
+ export * from './agent/peerRegistry';
38
+ export * from './agent/peerTransport';
33
39
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
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"}
package/dist/index.js CHANGED
@@ -46,4 +46,10 @@ __exportStar(require("./plugins/index"), exports);
46
46
  __exportStar(require("./plugins/installer"), exports);
47
47
  __exportStar(require("./plugins/sources"), exports);
48
48
  __exportStar(require("./agent/trust"), exports);
49
+ __exportStar(require("./agent/crossProcessLock"), exports);
50
+ __exportStar(require("./agent/worktree"), exports);
51
+ __exportStar(require("./agent/worktreeEnforcement"), exports);
52
+ __exportStar(require("./agent/sharedTasks"), exports);
53
+ __exportStar(require("./agent/peerRegistry"), exports);
54
+ __exportStar(require("./agent/peerTransport"), exports);
49
55
  //# sourceMappingURL=index.js.map
@@ -2,5 +2,8 @@ import type { ToolResult } from '../types';
2
2
  import { type SandboxConfig } from './sandbox';
3
3
  export declare function executeTool(name: string, input: Record<string, unknown>, abortSignal?: {
4
4
  aborted: boolean;
5
- }, sandbox?: SandboxConfig, workDir?: string, agentScope?: string, onStream?: (chunk: string) => void): Promise<ToolResult>;
5
+ }, sandbox?: SandboxConfig, workDir?: string, agentScope?: string, onStream?: (chunk: string) => void, selfPeer?: {
6
+ id: string;
7
+ name: string;
8
+ }): Promise<ToolResult>;
6
9
  //# sourceMappingURL=executor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAsmFtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CA6BrB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAixFtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EAMlC,QAAQ,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,UAAU,CAAC,CAkCrB"}
@@ -53,6 +53,9 @@ const client_1 = require("../api/client");
53
53
  const symbols_1 = require("./symbols");
54
54
  const tsLangService_1 = require("./tsLangService");
55
55
  const safeSlice_1 = require("../util/safeSlice");
56
+ const sharedTasks_1 = require("../agent/sharedTasks");
57
+ const peerRegistry_1 = require("../agent/peerRegistry");
58
+ const peerTransport_1 = require("../agent/peerTransport");
56
59
  // ─── Constants ────────────────────────────────────────────────────────────────
57
60
  const DEFAULT_TIMEOUT_MS = 60000;
58
61
  const MAX_FETCH_BYTES = 200 * 1024; // 200 KB
@@ -424,7 +427,8 @@ async function writeFile(input, workDir) {
424
427
  // otherwise. This warns; it never blocks (see securityLint.ts for why).
425
428
  const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
426
429
  if (isNew) {
427
- return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}` };
430
+ // A brand-new file has no "removed" side — every line is an addition.
431
+ return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}`, linesAdded: lines, linesRemoved: 0 };
428
432
  }
429
433
  const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
430
434
  // Reward-hacking guard: a write_file that OVERWRITES an existing test file can
@@ -436,7 +440,14 @@ async function writeFile(input, workDir) {
436
440
  const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
437
441
  if (ti.suspicious)
438
442
  tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
439
- return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}` };
443
+ // An overwrite has no line-by-line diff computed here (the whole file is
444
+ // replaced wholesale) — report the FULL new content as added and the full
445
+ // prior content as removed, which is what "overwrite" actually means.
446
+ return {
447
+ output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}`,
448
+ linesAdded: lines,
449
+ linesRemoved: priorContent ? normalizeLF(priorContent).split('\n').length : 0,
450
+ };
440
451
  }
441
452
  catch (err) {
442
453
  return { error: err.message };
@@ -1628,7 +1639,15 @@ async function editFile(input, workDir) {
1628
1639
  // noise that has nothing to do with the change being made, and the fastest way
1629
1640
  // to teach everyone to ignore the warning.
1630
1641
  const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
1631
- return { output: `Edited ${resolved} (${linesBefore} → ${linesAfter} lines)\n\n${diff}${xfile}${sec}` };
1642
+ // Lines touched by THIS edit (old_string vs new_string), not the whole file —
1643
+ // matches what a caller displaying a per-edit diff (e.g. the VS Code permission
1644
+ // bubble) actually shows. A caller wanting "total file size change" already has
1645
+ // linesBefore/linesAfter above via the `output` text.
1646
+ return {
1647
+ output: `Edited ${resolved} (${linesBefore} → ${linesAfter} lines)\n\n${diff}${xfile}${sec}`,
1648
+ linesAdded: newNorm.split('\n').length,
1649
+ linesRemoved: oldNorm.split('\n').length,
1650
+ };
1632
1651
  }
1633
1652
  catch (err) {
1634
1653
  return { error: err.message };
@@ -2030,6 +2049,8 @@ async function multiEdit(input, workDir) {
2030
2049
  // Inserted text from every edit, collected for the security lint below so it
2031
2050
  // scans only what this call ADDED rather than the whole file.
2032
2051
  const insertedText = [];
2052
+ let linesAdded = 0;
2053
+ let linesRemoved = 0;
2033
2054
  for (let i = 0; i < edits.length; i++) {
2034
2055
  const edit = edits[i];
2035
2056
  // Normalize incoming old/new strings too
@@ -2050,6 +2071,8 @@ async function multiEdit(input, workDir) {
2050
2071
  // the point this edit was applied (not the final state after all edits).
2051
2072
  diffs.push(buildDiff(filePath, oldStr, newStr, content));
2052
2073
  insertedText.push(newStr);
2074
+ linesAdded += newStr.split('\n').length;
2075
+ linesRemoved += oldStr.split('\n').length;
2053
2076
  content = literalReplace(content, oldStr, newStr);
2054
2077
  }
2055
2078
  // Restore original line endings before writing (atomic temp+rename),
@@ -2062,6 +2085,8 @@ async function multiEdit(input, workDir) {
2062
2085
  return {
2063
2086
  output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
2064
2087
  diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') + xfile + sec,
2088
+ linesAdded,
2089
+ linesRemoved,
2065
2090
  };
2066
2091
  }
2067
2092
  catch (err) {
@@ -2149,6 +2174,147 @@ function formatTodos(scope) {
2149
2174
  const lines = scope.items.map(t => `${icons[t.status] ?? '☐'} [${t.id}] ${t.content} (${t.status})`);
2150
2175
  return `Tasks (${scope.items.filter(t => t.status === 'completed').length}/${scope.items.length} done):\n${lines.join('\n')}`;
2151
2176
  }
2177
+ // ─── Shared task list (cross-SESSION coordination — see agent/sharedTasks.ts) ──
2178
+ //
2179
+ // Deliberately separate from todo_write/todo_read above: those are a
2180
+ // PER-AGENT-PROCESS scratchpad (an in-memory Map, scoped by agentScope) for
2181
+ // tracking one session's own progress. These three read/write REAL files
2182
+ // under ~/.nexrall/peers/tasks/, visible to every OTHER session working the
2183
+ // same project — the coordination layer described in sharedTasks.ts's header
2184
+ // (mirrors Claude Code's agent-teams task list). A session's own peer name
2185
+ // (once peer messaging lands) is the natural `claimedBy` identity; until
2186
+ // then, callers pass whatever string they like to identify themselves.
2187
+ function formatSharedTask(t) {
2188
+ const icons = { pending: '☐', in_progress: '▶', completed: '✓', blocked: '⛔' };
2189
+ const claim = t.claimedBy ? ` — claimed by ${t.claimedBy}` : '';
2190
+ return `${icons[t.status] ?? '☐'} [${t.id}] ${t.content} (${t.status}${claim})`;
2191
+ }
2192
+ async function listSharedTasksTool(_input, workDir) {
2193
+ if (!workDir)
2194
+ return { error: 'workDir is required' };
2195
+ const tasks = (0, sharedTasks_1.listSharedTasks)(workDir);
2196
+ if (!tasks.length)
2197
+ return { output: 'No shared tasks yet for this project. Use create_shared_task to add one.' };
2198
+ return { output: tasks.map(formatSharedTask).join('\n') };
2199
+ }
2200
+ async function createSharedTaskTool(input, workDir) {
2201
+ if (!workDir)
2202
+ return { error: 'workDir is required' };
2203
+ const content = typeof input.content === 'string' ? input.content.trim() : '';
2204
+ if (!content)
2205
+ return { error: 'content must be a non-empty string' };
2206
+ const blockedBy = Array.isArray(input.blocked_by)
2207
+ ? input.blocked_by.filter((x) => typeof x === 'string')
2208
+ : undefined;
2209
+ const task = (0, sharedTasks_1.createSharedTask)(workDir, content, { blockedBy });
2210
+ return { output: `Created shared task [${task.id}]: ${task.content}` };
2211
+ }
2212
+ async function claimSharedTaskTool(input, workDir) {
2213
+ if (!workDir)
2214
+ return { error: 'workDir is required' };
2215
+ const taskId = typeof input.task_id === 'string' ? input.task_id : '';
2216
+ const claimedBy = typeof input.claimed_by === 'string' && input.claimed_by.trim() ? input.claimed_by.trim() : 'this session';
2217
+ if (!taskId)
2218
+ return { error: 'task_id is required' };
2219
+ const res = await (0, sharedTasks_1.claimTask)(workDir, taskId, claimedBy);
2220
+ if (!res.ok) {
2221
+ const reasons = {
2222
+ 'not-found': `No shared task with id "${taskId}" exists for this project.`,
2223
+ 'already-claimed': `Task "${taskId}" is already claimed by another session. Pick a different task, or check back later.`,
2224
+ 'blocked': `Task "${taskId}" is blocked — one or more tasks it depends on are not completed yet.`,
2225
+ 'not-pending': `Task "${taskId}" is already completed.`,
2226
+ };
2227
+ return { error: reasons[res.reason] ?? `Could not claim task "${taskId}" (${res.reason}).` };
2228
+ }
2229
+ return { output: `Claimed [${res.task.id}] ${res.task.content} as "${claimedBy}".` };
2230
+ }
2231
+ async function updateSharedTaskStatusTool(input, workDir) {
2232
+ if (!workDir)
2233
+ return { error: 'workDir is required' };
2234
+ const taskId = typeof input.task_id === 'string' ? input.task_id : '';
2235
+ const status = input.status;
2236
+ const claimedBy = typeof input.claimed_by === 'string' ? input.claimed_by : undefined;
2237
+ if (!taskId)
2238
+ return { error: 'task_id is required' };
2239
+ if (status !== 'pending' && status !== 'in_progress' && status !== 'completed' && status !== 'blocked') {
2240
+ return { error: 'status must be one of: pending, in_progress, completed, blocked' };
2241
+ }
2242
+ const res = await (0, sharedTasks_1.updateSharedTaskStatus)(workDir, taskId, status, { claimedBy });
2243
+ if (!res.ok) {
2244
+ if (res.reason === 'not-found')
2245
+ return { error: `No shared task with id "${taskId}" exists for this project.` };
2246
+ if (res.reason === 'already-claimed')
2247
+ return { error: `Task "${taskId}" is claimed by a different session — only the session that claimed it may change its status.` };
2248
+ return { error: `Could not update task "${taskId}" (${res.reason}).` };
2249
+ }
2250
+ return { output: `Updated [${res.task.id}] ${res.task.content} → ${res.task.status}.` };
2251
+ }
2252
+ // ─── Cross-session peer messaging (see agent/peerRegistry.ts + peerTransport.ts) ──
2253
+ //
2254
+ // Deliberately separate function signature (takes `selfId` explicitly rather
2255
+ // than reading it from workDir/agentScope) because peer identity is NOT
2256
+ // project-scoped like shared tasks — a session's peer id/name is a property
2257
+ // of THIS PROCESS, set once at registration (see registerPeer in
2258
+ // peerRegistry.ts, wired in by each client host), and has nothing to do with
2259
+ // which project directory the tool call happens to be running against.
2260
+ function formatPeerForList(p, selfId) {
2261
+ const marker = p.id === selfId ? ' (you)' : '';
2262
+ const status = p.status === 'running' ? 'busy' : 'idle';
2263
+ return `${p.name}${marker} — ${p.clientType}, ${status}, ${p.workDir}`;
2264
+ }
2265
+ async function listPeerSessionsTool(_input, selfId) {
2266
+ const peers = (0, peerRegistry_1.listPeers)(selfId);
2267
+ if (!peers.length)
2268
+ return { output: 'No other Nexrall Code sessions are currently discoverable on this machine.' };
2269
+ return { output: peers.map((p) => formatPeerForList(p, selfId)).join('\n') };
2270
+ }
2271
+ async function messagePeerSessionTool(input, selfPeer) {
2272
+ const targetName = typeof input.name === 'string' ? input.name.trim() : '';
2273
+ const text = typeof input.message === 'string' ? input.message.trim() : '';
2274
+ if (!targetName)
2275
+ return { error: 'name is required — see list_peer_sessions for available session names.' };
2276
+ if (!text)
2277
+ return { error: 'message must be a non-empty string.' };
2278
+ if (!selfPeer)
2279
+ return { error: 'This session is not registered as a discoverable peer, so it cannot send messages.' };
2280
+ const target = (0, peerRegistry_1.findPeerByName)(targetName);
2281
+ if (!target)
2282
+ return { error: `No live session named "${targetName}" was found. Use list_peer_sessions to see who is currently reachable.` };
2283
+ if (target.id === selfPeer.id)
2284
+ return { error: 'Cannot message yourself.' };
2285
+ if (!target.socketPath)
2286
+ return { error: `Session "${targetName}" is not currently reachable for messaging.` };
2287
+ if (target.inbound === 'refuse')
2288
+ return { error: `Session "${targetName}" is not accepting messages right now.` };
2289
+ const result = await (0, peerTransport_1.sendPeerMessage)(target.socketPath, { from: selfPeer.name, fromId: selfPeer.id, text });
2290
+ if (!result.ok)
2291
+ return { error: `Could not deliver message to "${targetName}": ${result.error ?? 'unknown error'}` };
2292
+ return { output: `Message sent to "${targetName}".` };
2293
+ }
2294
+ async function broadcastPeerMessageTool(input, selfPeer) {
2295
+ const text = typeof input.message === 'string' ? input.message.trim() : '';
2296
+ if (!text)
2297
+ return { error: 'message must be a non-empty string.' };
2298
+ if (!selfPeer)
2299
+ return { error: 'This session is not registered as a discoverable peer, so it cannot send messages.' };
2300
+ // Same exclusions as message_peer_session would apply per-target: never
2301
+ // include self, and never target a peer whose inbound policy is 'refuse'
2302
+ // — broadcasting is fan-out over the identical single-message rules, not
2303
+ // an escape hatch around them.
2304
+ const targets = (0, peerRegistry_1.listPeers)(selfPeer.id)
2305
+ .filter((p) => p.id !== selfPeer.id && p.socketPath && p.inbound !== 'refuse')
2306
+ .map((p) => ({ name: p.name, socketPath: p.socketPath }));
2307
+ if (!targets.length)
2308
+ return { output: 'No other reachable Nexrall Code sessions to broadcast to.' };
2309
+ const results = await (0, peerTransport_1.broadcastPeerMessage)(targets, { from: selfPeer.name, fromId: selfPeer.id, text });
2310
+ const succeeded = results.filter((r) => r.ok);
2311
+ const failed = results.filter((r) => !r.ok);
2312
+ const lines = [`Broadcast to ${results.length} session(s): ${succeeded.length} delivered.`];
2313
+ if (failed.length) {
2314
+ lines.push(...failed.map((r) => ` ✗ ${r.name}: ${r.error ?? 'unknown error'}`));
2315
+ }
2316
+ return { output: lines.join('\n') };
2317
+ }
2152
2318
  function nbJoin(src) {
2153
2319
  return Array.isArray(src) ? src.join('') : (src ?? '');
2154
2320
  }
@@ -2691,6 +2857,15 @@ const TOOL_MAP = {
2691
2857
  go_to_definition: goToDefinition,
2692
2858
  find_references: findReferences,
2693
2859
  get_hover: getHover,
2860
+ // Cross-session task coordination — see agent/sharedTasks.ts.
2861
+ list_shared_tasks: listSharedTasksTool,
2862
+ create_shared_task: createSharedTaskTool,
2863
+ claim_shared_task: claimSharedTaskTool,
2864
+ update_shared_task_status: updateSharedTaskStatusTool,
2865
+ // Cross-session messaging — see agent/peerRegistry.ts + peerTransport.ts.
2866
+ list_peer_sessions: listPeerSessionsTool,
2867
+ message_peer_session: messagePeerSessionTool,
2868
+ broadcast_peer_message: broadcastPeerMessageTool,
2694
2869
  };
2695
2870
  // Tools that accept a workDir for correct relative-path resolution.
2696
2871
  // TOOL_MAP stores the base signatures; executeTool injects workDir at call time.
@@ -2705,8 +2880,15 @@ const WORKDIR_TOOLS = new Set([
2705
2880
  'get_symbols', 'get_workspace_symbols',
2706
2881
  'go_to_definition', 'find_references', 'get_hover',
2707
2882
  'memory_write', 'memory_read', 'use_skill',
2883
+ 'list_shared_tasks', 'create_shared_task', 'claim_shared_task', 'update_shared_task_status',
2708
2884
  ]);
2709
- async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope, onStream) {
2885
+ async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope, onStream,
2886
+ // This session's own peer identity (see agent/peerRegistry.ts), if it is
2887
+ // registered as a discoverable peer at all — undefined for a session that
2888
+ // isn't (the default; see Priority 4d wiring in each client host). Threaded
2889
+ // as its own trailing parameter, not through workDir/agentScope, because
2890
+ // peer identity is a property of the PROCESS, not the project.
2891
+ selfPeer) {
2710
2892
  if (!TOOL_MAP[name])
2711
2893
  return { error: `Unknown tool: ${name}` };
2712
2894
  try {
@@ -2721,6 +2903,14 @@ async function executeTool(name, input, abortSignal, sandbox, workDir, agentScop
2721
2903
  return await todoWrite(input, agentScope);
2722
2904
  if (name === 'todo_read')
2723
2905
  return await todoRead(input, agentScope);
2906
+ // Peer messaging — identity-scoped, not workDir-scoped (see the doc
2907
+ // comment right above these two functions' definitions).
2908
+ if (name === 'list_peer_sessions')
2909
+ return await listPeerSessionsTool(input, selfPeer?.id);
2910
+ if (name === 'message_peer_session')
2911
+ return await messagePeerSessionTool(input, selfPeer);
2912
+ if (name === 'broadcast_peer_message')
2913
+ return await broadcastPeerMessageTool(input, selfPeer);
2724
2914
  // Network-bound tools get abortSignal explicitly (not through the generic
2725
2915
  // WORKDIR_TOOLS branch below, whose signature is (input, workDir) only) —
2726
2916
  // these are the tools that previously had NO way to stop early on Ctrl+C,