@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.
- package/dist/agent/crossProcessLock.d.ts +45 -0
- package/dist/agent/crossProcessLock.d.ts.map +1 -0
- package/dist/agent/crossProcessLock.js +270 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +114 -16
- package/dist/agent/peerRegistry.d.ts +64 -0
- package/dist/agent/peerRegistry.d.ts.map +1 -0
- package/dist/agent/peerRegistry.js +238 -0
- package/dist/agent/peerTransport.d.ts +122 -0
- package/dist/agent/peerTransport.d.ts.map +1 -0
- package/dist/agent/peerTransport.js +348 -0
- package/dist/agent/sharedTasks.d.ts +63 -0
- package/dist/agent/sharedTasks.d.ts.map +1 -0
- package/dist/agent/sharedTasks.js +284 -0
- package/dist/agent/worktree.d.ts +111 -0
- package/dist/agent/worktree.d.ts.map +1 -0
- package/dist/agent/worktree.js +502 -0
- package/dist/agent/worktreeEnforcement.d.ts +29 -0
- package/dist/agent/worktreeEnforcement.d.ts.map +1 -0
- package/dist/agent/worktreeEnforcement.js +203 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +5 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/tools/executor.d.ts +4 -1
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +194 -4
- package/dist/types.d.ts +83 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAiGD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAmLD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
|
package/dist/api/client.js
CHANGED
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
package/dist/tools/executor.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|