@nexrall/code-core 1.4.58 → 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 +123 -17
- 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 +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/tools/executor.d.ts +4 -1
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +223 -9
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './types';
|
|
2
|
+
export * from './util/safeSlice';
|
|
2
3
|
export * from './auth/index';
|
|
3
4
|
export * from './api/client';
|
|
4
5
|
export * from './tools/executor';
|
|
@@ -29,4 +30,10 @@ export * from './plugins/index';
|
|
|
29
30
|
export * from './plugins/installer';
|
|
30
31
|
export * from './plugins/sources';
|
|
31
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';
|
|
32
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,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
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./util/safeSlice"), exports);
|
|
18
19
|
__exportStar(require("./auth/index"), exports);
|
|
19
20
|
__exportStar(require("./api/client"), exports);
|
|
20
21
|
__exportStar(require("./tools/executor"), exports);
|
|
@@ -45,4 +46,10 @@ __exportStar(require("./plugins/index"), exports);
|
|
|
45
46
|
__exportStar(require("./plugins/installer"), exports);
|
|
46
47
|
__exportStar(require("./plugins/sources"), exports);
|
|
47
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);
|
|
48
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 };
|
|
@@ -740,9 +751,21 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
|
|
|
740
751
|
if (shell.output.length > MAX_OUTPUT_CHARS) {
|
|
741
752
|
// Drop from the front but never behind the read cursor's logical position;
|
|
742
753
|
// readCursor is an absolute offset so we track how much we've discarded.
|
|
754
|
+
//
|
|
755
|
+
// sliceSafeStart (not a raw .slice()) — a raw start-slice landing on the
|
|
756
|
+
// low half of a surrogate pair leaves an orphaned low surrogate at the
|
|
757
|
+
// front of shell.output. That corrupts the NEXT bash_output/tool_result
|
|
758
|
+
// JSON.stringify onto the wire and Anthropic rejects the whole request
|
|
759
|
+
// with a deterministic, un-retryable 400 "no low surrogate in string".
|
|
760
|
+
// sliceSafeStart may drop one extra char to avoid the split, so recompute
|
|
761
|
+
// the actual amount discarded from the real length delta rather than
|
|
762
|
+
// trusting `overflow` — that keeps readCursor's absolute-offset bookkeeping
|
|
763
|
+
// correct in that edge case too.
|
|
743
764
|
const overflow = shell.output.length - MAX_OUTPUT_CHARS;
|
|
744
|
-
|
|
745
|
-
shell.
|
|
765
|
+
const beforeLen = shell.output.length;
|
|
766
|
+
shell.output = (0, safeSlice_1.sliceSafeStart)(shell.output, overflow);
|
|
767
|
+
const actualDropped = beforeLen - shell.output.length;
|
|
768
|
+
shell.readCursor = Math.max(0, shell.readCursor - actualDropped);
|
|
746
769
|
shell.truncated = true;
|
|
747
770
|
}
|
|
748
771
|
};
|
|
@@ -976,15 +999,27 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
|
|
|
976
999
|
}
|
|
977
1000
|
if (head.length < HEAD_CHARS) {
|
|
978
1001
|
const room = HEAD_CHARS - head.length;
|
|
979
|
-
|
|
980
|
-
|
|
1002
|
+
// sliceSafeEnd, not a raw slice(0, room) — a cut landing between the
|
|
1003
|
+
// high/low half of an emoji or CJK-extension glyph leaves a lone
|
|
1004
|
+
// surrogate at the end of `head`. That produces a deterministic,
|
|
1005
|
+
// un-retryable 400 "no low surrogate in string" once this bash result
|
|
1006
|
+
// is JSON.stringify'd into the next request. sliceSafeEnd may take one
|
|
1007
|
+
// fewer char to avoid the split; take the ACTUAL prefix length so the
|
|
1008
|
+
// remainder handed to `chunk` starts exactly where head left off
|
|
1009
|
+
// (never re-splitting the same pair from the other side).
|
|
1010
|
+
const taken = (0, safeSlice_1.sliceSafeEnd)(chunk, room);
|
|
1011
|
+
head += taken;
|
|
1012
|
+
chunk = chunk.slice(taken.length);
|
|
981
1013
|
if (!chunk)
|
|
982
1014
|
return;
|
|
983
1015
|
}
|
|
984
1016
|
// Remaining goes into a rolling tail buffer.
|
|
985
1017
|
tail += chunk;
|
|
986
1018
|
if (tail.length > TAIL_CHARS) {
|
|
987
|
-
|
|
1019
|
+
// sliceSafeStart — the tail is a rolling window kept from the END of the
|
|
1020
|
+
// stream; a raw start-slice can orphan a low surrogate at its front for
|
|
1021
|
+
// the same reason as above.
|
|
1022
|
+
tail = (0, safeSlice_1.sliceSafeStart)(tail, tail.length - TAIL_CHARS);
|
|
988
1023
|
truncated = true;
|
|
989
1024
|
}
|
|
990
1025
|
};
|
|
@@ -1604,7 +1639,15 @@ async function editFile(input, workDir) {
|
|
|
1604
1639
|
// noise that has nothing to do with the change being made, and the fastest way
|
|
1605
1640
|
// to teach everyone to ignore the warning.
|
|
1606
1641
|
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
|
|
1607
|
-
|
|
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
|
+
};
|
|
1608
1651
|
}
|
|
1609
1652
|
catch (err) {
|
|
1610
1653
|
return { error: err.message };
|
|
@@ -2006,6 +2049,8 @@ async function multiEdit(input, workDir) {
|
|
|
2006
2049
|
// Inserted text from every edit, collected for the security lint below so it
|
|
2007
2050
|
// scans only what this call ADDED rather than the whole file.
|
|
2008
2051
|
const insertedText = [];
|
|
2052
|
+
let linesAdded = 0;
|
|
2053
|
+
let linesRemoved = 0;
|
|
2009
2054
|
for (let i = 0; i < edits.length; i++) {
|
|
2010
2055
|
const edit = edits[i];
|
|
2011
2056
|
// Normalize incoming old/new strings too
|
|
@@ -2026,6 +2071,8 @@ async function multiEdit(input, workDir) {
|
|
|
2026
2071
|
// the point this edit was applied (not the final state after all edits).
|
|
2027
2072
|
diffs.push(buildDiff(filePath, oldStr, newStr, content));
|
|
2028
2073
|
insertedText.push(newStr);
|
|
2074
|
+
linesAdded += newStr.split('\n').length;
|
|
2075
|
+
linesRemoved += oldStr.split('\n').length;
|
|
2029
2076
|
content = literalReplace(content, oldStr, newStr);
|
|
2030
2077
|
}
|
|
2031
2078
|
// Restore original line endings before writing (atomic temp+rename),
|
|
@@ -2038,6 +2085,8 @@ async function multiEdit(input, workDir) {
|
|
|
2038
2085
|
return {
|
|
2039
2086
|
output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
|
|
2040
2087
|
diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') + xfile + sec,
|
|
2088
|
+
linesAdded,
|
|
2089
|
+
linesRemoved,
|
|
2041
2090
|
};
|
|
2042
2091
|
}
|
|
2043
2092
|
catch (err) {
|
|
@@ -2125,6 +2174,147 @@ function formatTodos(scope) {
|
|
|
2125
2174
|
const lines = scope.items.map(t => `${icons[t.status] ?? '☐'} [${t.id}] ${t.content} (${t.status})`);
|
|
2126
2175
|
return `Tasks (${scope.items.filter(t => t.status === 'completed').length}/${scope.items.length} done):\n${lines.join('\n')}`;
|
|
2127
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
|
+
}
|
|
2128
2318
|
function nbJoin(src) {
|
|
2129
2319
|
return Array.isArray(src) ? src.join('') : (src ?? '');
|
|
2130
2320
|
}
|
|
@@ -2667,6 +2857,15 @@ const TOOL_MAP = {
|
|
|
2667
2857
|
go_to_definition: goToDefinition,
|
|
2668
2858
|
find_references: findReferences,
|
|
2669
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,
|
|
2670
2869
|
};
|
|
2671
2870
|
// Tools that accept a workDir for correct relative-path resolution.
|
|
2672
2871
|
// TOOL_MAP stores the base signatures; executeTool injects workDir at call time.
|
|
@@ -2681,8 +2880,15 @@ const WORKDIR_TOOLS = new Set([
|
|
|
2681
2880
|
'get_symbols', 'get_workspace_symbols',
|
|
2682
2881
|
'go_to_definition', 'find_references', 'get_hover',
|
|
2683
2882
|
'memory_write', 'memory_read', 'use_skill',
|
|
2883
|
+
'list_shared_tasks', 'create_shared_task', 'claim_shared_task', 'update_shared_task_status',
|
|
2684
2884
|
]);
|
|
2685
|
-
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) {
|
|
2686
2892
|
if (!TOOL_MAP[name])
|
|
2687
2893
|
return { error: `Unknown tool: ${name}` };
|
|
2688
2894
|
try {
|
|
@@ -2697,6 +2903,14 @@ async function executeTool(name, input, abortSignal, sandbox, workDir, agentScop
|
|
|
2697
2903
|
return await todoWrite(input, agentScope);
|
|
2698
2904
|
if (name === 'todo_read')
|
|
2699
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);
|
|
2700
2914
|
// Network-bound tools get abortSignal explicitly (not through the generic
|
|
2701
2915
|
// WORKDIR_TOOLS branch below, whose signature is (input, workDir) only) —
|
|
2702
2916
|
// these are the tools that previously had NO way to stop early on Ctrl+C,
|