@xingjisoft/neutrino 0.6.0 → 0.7.0
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/README.md +6 -1
- package/assets/fast-planner.txt +57 -0
- package/assets/fast-replanner.txt +28 -0
- package/dist/agents.d.ts +25 -0
- package/dist/app.d.ts +206 -0
- package/dist/app.js +15 -0
- package/dist/background.d.ts +96 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +40 -3
- package/dist/browser.d.ts +18 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/colors.d.ts +29 -0
- package/dist/compact.d.ts +52 -0
- package/dist/config.d.ts +221 -0
- package/dist/config.js +22 -7
- package/dist/cwd.d.ts +19 -0
- package/dist/fast/artifacts.d.ts +26 -0
- package/dist/fast/artifacts.js +109 -0
- package/dist/fast/engine.d.ts +78 -0
- package/dist/fast/engine.js +657 -0
- package/dist/fast/evidence.d.ts +21 -0
- package/dist/fast/evidence.js +68 -0
- package/dist/fast/planner.d.ts +10 -0
- package/dist/fast/planner.js +51 -0
- package/dist/fast/pool.d.ts +17 -0
- package/dist/fast/pool.js +78 -0
- package/dist/fast/scheduler.d.ts +54 -0
- package/dist/fast/scheduler.js +383 -0
- package/dist/fast/types.d.ts +100 -0
- package/dist/fast/types.js +21 -0
- package/dist/fast/worker.d.ts +23 -0
- package/dist/fast/worker.js +185 -0
- package/dist/header.d.ts +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/llm/approval.d.ts +204 -0
- package/dist/llm/approval.js +14 -3
- package/dist/llm/client.d.ts +118 -0
- package/dist/llm/client.js +57 -8
- package/dist/llm/codeExecutor.d.ts +14 -0
- package/dist/llm/codemode.d.ts +15 -0
- package/dist/llm/context.d.ts +29 -0
- package/dist/llm/context.js +17 -4
- package/dist/llm/diagnostics.d.ts +91 -0
- package/dist/llm/diagnostics.js +242 -0
- package/dist/llm/plan.d.ts +148 -0
- package/dist/llm/retry.d.ts +80 -0
- package/dist/llm/risk.d.ts +21 -0
- package/dist/llm/risk.js +7 -0
- package/dist/llm/runtime.d.ts +189 -0
- package/dist/llm/runtime.js +178 -24
- package/dist/llm/search.d.ts +2 -0
- package/dist/llm/searchNetwork.d.ts +8 -0
- package/dist/llm/subagent.d.ts +112 -0
- package/dist/llm/subagent.js +25 -12
- package/dist/llm/tools.d.ts +20 -0
- package/dist/llm/tools.js +4 -1
- package/dist/llm/workspace.d.ts +6 -0
- package/dist/llm/workspace.js +271 -0
- package/dist/markdown.d.ts +58 -0
- package/dist/migration.d.ts +8 -0
- package/dist/persistence.d.ts +14 -0
- package/dist/plugins/catalog.d.ts +20 -0
- package/dist/plugins/catalog.js +45 -0
- package/dist/plugins/cli.d.ts +1 -0
- package/dist/plugins/cli.js +106 -0
- package/dist/plugins/host.d.ts +121 -0
- package/dist/plugins/host.js +450 -0
- package/dist/plugins/manager.d.ts +24 -0
- package/dist/plugins/manager.js +288 -0
- package/dist/plugins/package.d.ts +5 -0
- package/dist/plugins/package.js +53 -0
- package/dist/plugins/runner.d.ts +1 -0
- package/dist/plugins/runner.js +227 -0
- package/dist/plugins/session.d.ts +12 -0
- package/dist/plugins/session.js +97 -0
- package/dist/plugins/terminal.d.ts +22 -0
- package/dist/plugins/terminal.js +33 -0
- package/dist/plugins/types.d.ts +178 -0
- package/dist/plugins/types.js +6 -0
- package/dist/plugins/validation.d.ts +14 -0
- package/dist/plugins/validation.js +174 -0
- package/dist/plugins/web-sdk.d.ts +58 -0
- package/dist/plugins/web-sdk.js +1 -0
- package/dist/pro/agents.d.ts +34 -0
- package/dist/pro/agents.js +1 -1
- package/dist/pro/config.d.ts +75 -0
- package/dist/pro/config.js +3 -2
- package/dist/pro/preflight.d.ts +21 -0
- package/dist/sanitize.d.ts +34 -0
- package/dist/screens/agentConversation.d.ts +28 -0
- package/dist/screens/agentHistory.d.ts +25 -0
- package/dist/screens/agentSelect.d.ts +16 -0
- package/dist/screens/chat.d.ts +292 -0
- package/dist/screens/chat.js +179 -65
- package/dist/screens/history.d.ts +23 -0
- package/dist/screens/modelSelect.d.ts +29 -0
- package/dist/screens/plugins.d.ts +29 -0
- package/dist/screens/plugins.js +196 -0
- package/dist/screens/settings.d.ts +42 -0
- package/dist/screens/start.d.ts +43 -0
- package/dist/screens/start.js +25 -17
- package/dist/screens/updatePrompt.d.ts +41 -0
- package/dist/selection.d.ts +75 -0
- package/dist/state.d.ts +26 -0
- package/dist/terminal/buffer.d.ts +47 -0
- package/dist/terminal/input.d.ts +70 -0
- package/dist/terminal/renderer.d.ts +43 -0
- package/dist/terminal/restore.d.ts +41 -0
- package/dist/terminal/screen.d.ts +57 -0
- package/dist/tray.d.ts +14 -0
- package/dist/ui.d.ts +123 -0
- package/dist/update.d.ts +71 -0
- package/dist/updateWatch.d.ts +43 -0
- package/dist/web/approvalQueue.d.ts +8 -0
- package/dist/web/approvalQueue.js +59 -0
- package/dist/web/auth.d.ts +14 -0
- package/dist/web/clientState.d.ts +16 -0
- package/dist/web/folderPicker.d.ts +34 -0
- package/dist/web/folderPicker.js +544 -0
- package/dist/web/hub.d.ts +213 -0
- package/dist/web/hub.js +14 -2
- package/dist/web/localApi.d.ts +11 -0
- package/dist/web/localApi.js +260 -0
- package/dist/web/plugins.d.ts +8 -0
- package/dist/web/plugins.js +64 -0
- package/dist/web/server.d.ts +81 -0
- package/dist/web/server.js +127 -19
- package/dist/web/service.d.ts +7 -0
- package/dist/web/service.js +1 -0
- package/dist/web/session.d.ts +176 -0
- package/dist/web/session.js +170 -21
- package/dist/web/settings.d.ts +65 -0
- package/dist/web/terminalBridge.d.ts +15 -0
- package/dist/web/terminalBridge.js +8 -2
- package/dist/width.d.ts +6 -0
- package/docs/plugins/README.md +126 -0
- package/docs/plugins/agent-sdk.md +92 -0
- package/docs/plugins/architecture.md +64 -0
- package/docs/plugins/compatibility.md +34 -0
- package/docs/plugins/development.md +55 -0
- package/docs/plugins/manifest.md +59 -0
- package/docs/plugins/manifest.schema.json +286 -0
- package/docs/plugins/security.md +53 -0
- package/docs/plugins/skills-prompts.md +69 -0
- package/docs/plugins/ui-sdk.md +109 -0
- package/examples/local-api.py +105 -0
- package/examples/plugins/cards/card.html +6 -0
- package/examples/plugins/cards/neutrino.plugin.json +42 -0
- package/examples/plugins/composer/editor.html +10 -0
- package/examples/plugins/composer/neutrino.plugin.json +10 -0
- package/examples/plugins/function-tools/agent.js +32 -0
- package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/panel.html +6 -0
- package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
- package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
- package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
- package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
- package/examples/plugins/theme/neutrino.plugin.json +12 -0
- package/package.json +66 -42
- package/web/LOCAL-API.md +65 -0
- package/web/README.md +30 -8
- package/web/app.js +256 -67
- package/web/i18n.js +18 -3
- package/web/index.html +3 -1
- package/web/plugins.css +10 -0
- package/web/plugins.js +230 -0
- package/web/style.css +12 -3
- package/web/turns.js +7 -8
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
/** Recognize an explicit confinement instruction, never permission inferred from tool output. */
|
|
6
|
+
export function requestsWorkspaceOnly(request) {
|
|
7
|
+
const english = /\b(?:work|operate)\s+only\s+(?:inside|within)\s+(?:(?:this|the|current|temporary|project|working)\s+)*(?:fixture|workspace|directory)\b/ig;
|
|
8
|
+
for (const match of request.matchAll(english)) {
|
|
9
|
+
if (!/(?:do\s+not|don't|never)\s*$/i.test(request.slice(Math.max(0, match.index - 12), match.index)))
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return /(?:仅|只)(?:允许|能|可以)?在(?:当前|本|这个)?(?:项目|工作|临时)?(?:目录|工作区|文件夹)(?:内|中)(?:工作|操作|读写|执行|创建|修改)/.test(request);
|
|
13
|
+
}
|
|
14
|
+
function canonical(input) {
|
|
15
|
+
let cursor = path.resolve(input);
|
|
16
|
+
const tail = [];
|
|
17
|
+
// lstat sees a dangling symlink too; realpath will reject it instead of treating it as a new directory.
|
|
18
|
+
while (!fs.existsSync(cursor)) {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.lstatSync(cursor).isSymbolicLink())
|
|
21
|
+
throw new Error("Unresolved symbolic link in workspace path");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code !== "ENOENT")
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(cursor);
|
|
28
|
+
if (parent === cursor)
|
|
29
|
+
break;
|
|
30
|
+
tail.unshift(path.basename(cursor));
|
|
31
|
+
cursor = parent;
|
|
32
|
+
}
|
|
33
|
+
const resolved = path.join(fs.realpathSync(cursor), ...tail);
|
|
34
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
35
|
+
}
|
|
36
|
+
function confined(target, cwd) {
|
|
37
|
+
if (process.platform !== "win32" && /^[A-Za-z]:[\\/]/.test(target))
|
|
38
|
+
return false;
|
|
39
|
+
const expanded = target === "~" || /^~[\\/]/.test(target) ? path.join(os.homedir(), target.slice(1)) : target;
|
|
40
|
+
const relative = path.relative(canonical(cwd), canonical(path.resolve(cwd, expanded)));
|
|
41
|
+
return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
|
|
42
|
+
}
|
|
43
|
+
const pathLike = (value) => /^(?:\/(?!\/|\*)|[A-Za-z]:[\\/]|\.\.(?:[\\/]|$)|\.[\\/]|~(?:[\\/]|$))/.test(value) ||
|
|
44
|
+
(!/[\s:]/.test(value) && /^[\w.@+-]+(?:[\\/][\w.@+-]+)*$/.test(value));
|
|
45
|
+
const FILE_CALLS = new Set(["readFile", "readFileSync", "writeFile", "writeFileSync", "appendFile", "appendFileSync",
|
|
46
|
+
"open", "openSync", "createReadStream", "createWriteStream", "access", "accessSync", "stat", "statSync", "lstat", "lstatSync",
|
|
47
|
+
"realpath", "realpathSync", "readdir", "readdirSync", "mkdir", "mkdirSync", "mkdtemp", "mkdtempSync", "existsSync",
|
|
48
|
+
"unlink", "unlinkSync", "rm", "rmSync", "rmdir", "rmdirSync", "rename", "renameSync", "copyFile", "copyFileSync", "cp", "cpSync"]);
|
|
49
|
+
function programPaths(program, cwd, filename) {
|
|
50
|
+
const paths = [];
|
|
51
|
+
const initial = { values: new Map([["__dirname", filename ? path.dirname(filename) : cwd], ["__filename", filename ?? path.join(cwd, "[eval]")]]), arrays: new Map(), functions: new Map(), aliases: new Map() };
|
|
52
|
+
const fork = (env) => ({ values: new Map(env.values), arrays: new Map(env.arrays), functions: new Map(env.functions), aliases: new Map(env.aliases) });
|
|
53
|
+
const name = (node) => ts.isIdentifier(node) ? node.text : ts.isPropertyAccessExpression(node) ? node.name.text : undefined;
|
|
54
|
+
const value = (node, env) => {
|
|
55
|
+
if (!node)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
58
|
+
return node.text;
|
|
59
|
+
if (ts.isIdentifier(node))
|
|
60
|
+
return env.values.get(node.text);
|
|
61
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node))
|
|
62
|
+
return value(node.expression, env);
|
|
63
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
64
|
+
const left = value(node.left, env), right = value(node.right, env);
|
|
65
|
+
return left !== undefined && right !== undefined ? left + right : undefined;
|
|
66
|
+
}
|
|
67
|
+
if (ts.isTemplateExpression(node)) {
|
|
68
|
+
let text = node.head.text;
|
|
69
|
+
for (const span of node.templateSpans) {
|
|
70
|
+
const part = value(span.expression, env);
|
|
71
|
+
if (part === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
text += part + span.literal.text;
|
|
74
|
+
}
|
|
75
|
+
return text;
|
|
76
|
+
}
|
|
77
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
78
|
+
const receiver = node.expression.expression.getText(), method = node.expression.name.text;
|
|
79
|
+
if (receiver === "process" && method === "cwd")
|
|
80
|
+
return cwd;
|
|
81
|
+
if (receiver === "os" && method === "tmpdir")
|
|
82
|
+
return os.tmpdir();
|
|
83
|
+
if (receiver === "os" && method === "homedir")
|
|
84
|
+
return os.homedir();
|
|
85
|
+
if (receiver === "path" && ["join", "resolve", "normalize", "dirname", "basename"].includes(method)) {
|
|
86
|
+
const parts = node.arguments.map(arg => value(arg, env));
|
|
87
|
+
if (parts.some(part => part === undefined))
|
|
88
|
+
return undefined;
|
|
89
|
+
const strings = parts;
|
|
90
|
+
if (method === "join")
|
|
91
|
+
return path.join(...strings);
|
|
92
|
+
if (method === "resolve")
|
|
93
|
+
return path.resolve(cwd, ...strings);
|
|
94
|
+
if (method === "normalize")
|
|
95
|
+
return path.normalize(strings[0] ?? "");
|
|
96
|
+
if (method === "dirname")
|
|
97
|
+
return path.dirname(strings[0] ?? "");
|
|
98
|
+
return path.basename(strings[0] ?? "");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
};
|
|
103
|
+
const array = (node, env) => {
|
|
104
|
+
if (!node)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (ts.isIdentifier(node))
|
|
107
|
+
return env.arrays.get(node.text);
|
|
108
|
+
if (ts.isArrayLiteralExpression(node) && node.elements.length <= 32) {
|
|
109
|
+
const entries = node.elements.map(entry => value(entry, env));
|
|
110
|
+
if (entries.every((entry) => entry !== undefined))
|
|
111
|
+
return entries;
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
};
|
|
115
|
+
const add = (target) => { if (target !== undefined && target !== "/dev/null")
|
|
116
|
+
paths.push(target); };
|
|
117
|
+
const active = new Set();
|
|
118
|
+
const visit = (node, env, depth = 0) => {
|
|
119
|
+
if (depth > 12)
|
|
120
|
+
return;
|
|
121
|
+
if (ts.isBlock(node)) {
|
|
122
|
+
const local = fork(env);
|
|
123
|
+
for (const statement of node.statements)
|
|
124
|
+
visit(statement, local, depth + 1);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (ts.isForOfStatement(node) && ts.isVariableDeclarationList(node.initializer)) {
|
|
128
|
+
const binding = node.initializer.declarations[0]?.name, entries = array(node.expression, env);
|
|
129
|
+
if (binding && ts.isIdentifier(binding) && entries) {
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const local = fork(env);
|
|
132
|
+
local.values.set(binding.text, entry);
|
|
133
|
+
visit(node.statement, local, depth + 1);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
139
|
+
env.functions.set(node.name.text, node);
|
|
140
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
141
|
+
const bound = value(node.initializer, env);
|
|
142
|
+
if (bound !== undefined)
|
|
143
|
+
env.values.set(node.name.text, bound);
|
|
144
|
+
else
|
|
145
|
+
env.values.delete(node.name.text);
|
|
146
|
+
const entries = array(node.initializer, env);
|
|
147
|
+
if (entries)
|
|
148
|
+
env.arrays.set(node.name.text, entries);
|
|
149
|
+
else
|
|
150
|
+
env.arrays.delete(node.name.text);
|
|
151
|
+
if (node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)))
|
|
152
|
+
env.functions.set(node.name.text, node.initializer);
|
|
153
|
+
else if (node.initializer && ts.isPropertyAccessExpression(node.initializer) && FILE_CALLS.has(node.initializer.name.text))
|
|
154
|
+
env.aliases.set(node.name.text, node.initializer.name.text);
|
|
155
|
+
}
|
|
156
|
+
if (ts.isCallExpression(node)) {
|
|
157
|
+
const called = name(node.expression), method = called ? env.aliases.get(called) ?? called : undefined;
|
|
158
|
+
if (method && FILE_CALLS.has(method)) {
|
|
159
|
+
add(value(node.arguments[0], env));
|
|
160
|
+
if (/^(?:rename|copyFile|cp)(?:Sync)?$/.test(method))
|
|
161
|
+
add(value(node.arguments[1], env));
|
|
162
|
+
}
|
|
163
|
+
if (ts.isPropertyAccessExpression(node.expression) && ["forEach", "map", "reduce", "reduceRight"].includes(node.expression.name.text)) {
|
|
164
|
+
const entries = array(node.expression.expression, env), callback = node.arguments[0];
|
|
165
|
+
if (entries && callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
166
|
+
const parameter = callback.parameters[/^reduce/.test(node.expression.name.text) ? 1 : 0]?.name;
|
|
167
|
+
if (parameter && ts.isIdentifier(parameter))
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const local = fork(env);
|
|
170
|
+
local.values.set(parameter.text, entry);
|
|
171
|
+
visit(callback.body, local, depth + 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const helper = called && ts.isIdentifier(node.expression) ? env.functions.get(called) : undefined;
|
|
176
|
+
if (helper?.body && !active.has(helper)) {
|
|
177
|
+
active.add(helper);
|
|
178
|
+
const local = fork(env);
|
|
179
|
+
helper.parameters.forEach((parameter, i) => {
|
|
180
|
+
if (ts.isIdentifier(parameter.name)) {
|
|
181
|
+
const bound = value(node.arguments[i], env);
|
|
182
|
+
if (bound !== undefined)
|
|
183
|
+
local.values.set(parameter.name.text, bound);
|
|
184
|
+
else
|
|
185
|
+
local.values.delete(parameter.name.text);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
visit(helper.body, local, depth + 1);
|
|
189
|
+
active.delete(helper);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (ts.isFunctionLike(node)) {
|
|
193
|
+
const local = fork(env);
|
|
194
|
+
for (const parameter of node.parameters)
|
|
195
|
+
if (ts.isIdentifier(parameter.name))
|
|
196
|
+
local.values.delete(parameter.name.text);
|
|
197
|
+
ts.forEachChild(node, child => visit(child, local, depth + 1));
|
|
198
|
+
}
|
|
199
|
+
else
|
|
200
|
+
ts.forEachChild(node, child => visit(child, env, depth));
|
|
201
|
+
};
|
|
202
|
+
visit(ts.createSourceFile(filename ?? "workspace-command.js", program, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS), initial);
|
|
203
|
+
return paths;
|
|
204
|
+
}
|
|
205
|
+
/** Inspect literal shell/program paths, including quoted inline programs and heredoc bodies. */
|
|
206
|
+
export function commandWorkspacePaths(command, cwd = process.cwd(), depth = 0) {
|
|
207
|
+
const candidates = [];
|
|
208
|
+
command = command.replace(/([^\n]*<<-?\s*['"]?(\w+)['"]?[^\n]*)\n([\s\S]*?)\n\2(?=\n|;|$)/g, (full, header, _end, body) => {
|
|
209
|
+
if (!/\bnode\b|\bcat\b[^\n]*\.(?:c?js|mjs)\b/.test(header))
|
|
210
|
+
return full;
|
|
211
|
+
candidates.push(...programPaths(body, cwd));
|
|
212
|
+
return header;
|
|
213
|
+
});
|
|
214
|
+
const tokens = command.matchAll(/"(?:\\[\s\S]|[^"\\])*"|'[^']*'|`(?:\\[\s\S]|[^`\\])*`|[^\s"'`;&|<>(){},\[\]]+/g);
|
|
215
|
+
for (const match of tokens) {
|
|
216
|
+
const token = match[0];
|
|
217
|
+
const quoted = /^["'`]/.test(token);
|
|
218
|
+
const value = quoted ? token.slice(1, -1).replace(/\\(["'`\\])/g, "$1") : token;
|
|
219
|
+
const segment = command.slice(0, match.index).split(/[;&|\n]/).at(-1) ?? "";
|
|
220
|
+
const presentation = /^\s*(?:echo|printf)\b/.test(segment) && !/[<>]/.test(segment);
|
|
221
|
+
const parts = quoted ? [value] : [value, ...(value.includes("=") ? [value.slice(value.indexOf("=") + 1)] : [])];
|
|
222
|
+
for (const part of parts) {
|
|
223
|
+
// /dev/null is the standard empty input/output sink, not project data.
|
|
224
|
+
if (part === "/dev/null")
|
|
225
|
+
continue;
|
|
226
|
+
if (!presentation && pathLike(part))
|
|
227
|
+
candidates.push(part);
|
|
228
|
+
}
|
|
229
|
+
if (quoted && /\bnode\b[^\n;&|]*\s--?(?:e|p|eval|print)\s*$/.test(command.slice(0, match.index)))
|
|
230
|
+
candidates.push(...programPaths(value, cwd));
|
|
231
|
+
else if (quoted && depth < 3 && (!presentation || /\$\(|`/.test(value)) && /[\n;(){}<>|&]|(?:read|write|require|import)/.test(value))
|
|
232
|
+
candidates.push(...commandWorkspacePaths(value, cwd, depth + 1));
|
|
233
|
+
}
|
|
234
|
+
return [...new Set(candidates)];
|
|
235
|
+
}
|
|
236
|
+
/** Preflight for declared workspace confinement. Arbitrary programs still require the normal approval gate. */
|
|
237
|
+
export function workspaceViolation(name, args, cwd) {
|
|
238
|
+
const paths = ["write_file", "read_file", "list_files"].includes(name)
|
|
239
|
+
? [typeof args.path === "string" && args.path.trim() ? args.path : "."]
|
|
240
|
+
: name === "run_command" ? commandWorkspacePaths(String(args.command ?? ""), cwd) : [];
|
|
241
|
+
if (name === "run_command") {
|
|
242
|
+
// A local Node helper can contain the same literal paths as an inline program.
|
|
243
|
+
const command = String(args.command ?? "");
|
|
244
|
+
for (const match of command.matchAll(/(?:^|[\s;&|])node\s+(?:--[\w=-]+\s+)*("[^"]+"|'[^']+'|[^\s;&|]+)/g)) {
|
|
245
|
+
const script = match[1].replace(/^["']|["']$/g, "");
|
|
246
|
+
if (script.startsWith("-") || !/\.(?:c?js|mjs)$/.test(script))
|
|
247
|
+
continue;
|
|
248
|
+
try {
|
|
249
|
+
if (!confined(script, cwd)) {
|
|
250
|
+
paths.push(script);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const file = path.resolve(cwd, script);
|
|
254
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile())
|
|
255
|
+
paths.push(...programPaths(fs.readFileSync(file, "utf8"), cwd, file));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
paths.push(script);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const target of paths) {
|
|
263
|
+
try {
|
|
264
|
+
if (confined(target, cwd))
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
catch { /* Unresolved paths fail closed. */ }
|
|
268
|
+
return `Path ${JSON.stringify(target)} is outside the explicitly confined workspace ${JSON.stringify(cwd)}. Nothing ran. Use workspace-relative paths, including helper scripts, and correct only this call; do not replay completed operations.`;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Buffer } from "./terminal/buffer.js";
|
|
2
|
+
import { type Style } from "./colors.js";
|
|
3
|
+
export interface Span {
|
|
4
|
+
text: string;
|
|
5
|
+
style: Style;
|
|
6
|
+
}
|
|
7
|
+
export interface Line {
|
|
8
|
+
spans: Span[];
|
|
9
|
+
}
|
|
10
|
+
export type TableAlign = "left" | "center" | "right";
|
|
11
|
+
export type Block = {
|
|
12
|
+
type: "paragraph";
|
|
13
|
+
spans: Span[];
|
|
14
|
+
} | {
|
|
15
|
+
type: "heading";
|
|
16
|
+
level: number;
|
|
17
|
+
spans: Span[];
|
|
18
|
+
} | {
|
|
19
|
+
type: "listItem";
|
|
20
|
+
ordered: boolean;
|
|
21
|
+
number: number;
|
|
22
|
+
level: number;
|
|
23
|
+
spans: Span[];
|
|
24
|
+
} | {
|
|
25
|
+
type: "code";
|
|
26
|
+
lang: string;
|
|
27
|
+
lines: string[];
|
|
28
|
+
} | {
|
|
29
|
+
type: "quote";
|
|
30
|
+
spans: Span[];
|
|
31
|
+
} | {
|
|
32
|
+
type: "rule";
|
|
33
|
+
} | {
|
|
34
|
+
type: "table";
|
|
35
|
+
header: Span[][];
|
|
36
|
+
rows: Span[][][];
|
|
37
|
+
align: TableAlign[];
|
|
38
|
+
} | {
|
|
39
|
+
type: "blank";
|
|
40
|
+
};
|
|
41
|
+
export declare function parseInline(text: string, base?: Style): Span[];
|
|
42
|
+
export declare function parseMarkdown(text: string, base?: Style): Block[];
|
|
43
|
+
/**
|
|
44
|
+
* Wrap spans into lines, optionally prefixing the first line and indenting
|
|
45
|
+
* continuation lines. Used for headings, list items and blockquotes.
|
|
46
|
+
*/
|
|
47
|
+
export declare function wrapSpans(spans: Span[], width: number, opts?: {
|
|
48
|
+
prefix?: Span[];
|
|
49
|
+
indent?: number;
|
|
50
|
+
}): Line[];
|
|
51
|
+
export declare function renderBlocks(blocks: Block[], width: number): Line[];
|
|
52
|
+
/** Parse and render a markdown string into styled lines. */
|
|
53
|
+
export declare function renderMarkdownText(text: string, width: number, base?: Style): Line[];
|
|
54
|
+
/**
|
|
55
|
+
* Draw a sequence of styled spans onto the buffer, starting at (x, y),
|
|
56
|
+
* clipped to `maxWidth` columns.
|
|
57
|
+
*/
|
|
58
|
+
export declare function drawSpans(buf: Buffer, x: number, y: number, spans: Span[], maxWidth: number): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AgentId } from "./agents.js";
|
|
2
|
+
import { type Mode, type ModelSelection, type SessionData } from "./config.js";
|
|
3
|
+
export declare class SessionMigrationError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
constructor(status: number, message: string);
|
|
6
|
+
}
|
|
7
|
+
/** Copy the readable conversation into a fresh native transcript, ready to resume. */
|
|
8
|
+
export declare function migrateAgentSession(agent: AgentId, sourceId: string, cwd: string, selection: ModelSelection, mode?: Mode, proPreset?: string): SessionData;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Bound how much streamed output a hard kill can lose, without writing per token. */
|
|
2
|
+
export declare const CHECKPOINT_MS = 1000;
|
|
3
|
+
export declare class SessionCheckpoint {
|
|
4
|
+
private readonly write;
|
|
5
|
+
private timer;
|
|
6
|
+
constructor(write: () => boolean);
|
|
7
|
+
register(): void;
|
|
8
|
+
/** A trailing checkpoint still runs if the provider stalls after one chunk. */
|
|
9
|
+
schedule(): void;
|
|
10
|
+
flush(): boolean;
|
|
11
|
+
dispose(): boolean;
|
|
12
|
+
}
|
|
13
|
+
/** No promises: exit handlers cannot wait for asynchronous disk writes. */
|
|
14
|
+
export declare function flushSessionCheckpoints(): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OwnedUI } from "./host.js";
|
|
2
|
+
import type { Profile } from "./types.js";
|
|
3
|
+
/** Reading the UI catalogue must never activate backend code or start a worker. */
|
|
4
|
+
export declare function pluginCatalog(profile?: Profile, cwd?: string): {
|
|
5
|
+
ui: OwnedUI[];
|
|
6
|
+
theme: Record<string, string>;
|
|
7
|
+
skills: {
|
|
8
|
+
id: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}[];
|
|
11
|
+
prompts: {
|
|
12
|
+
id: string;
|
|
13
|
+
replace: boolean;
|
|
14
|
+
}[];
|
|
15
|
+
permissions: Record<string, string[]>;
|
|
16
|
+
entries: Record<string, string | undefined>;
|
|
17
|
+
providers: Record<string, string>;
|
|
18
|
+
errors: string[];
|
|
19
|
+
safeMode: boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { pluginRecords, providerSelections, safeMode } from "./manager.js";
|
|
3
|
+
import { safeFile } from "./validation.js";
|
|
4
|
+
/** Reading the UI catalogue must never activate backend code or start a worker. */
|
|
5
|
+
export function pluginCatalog(profile = "standard", cwd) {
|
|
6
|
+
const ui = [], theme = {}, skills = [], prompts = [];
|
|
7
|
+
const errors = [];
|
|
8
|
+
const permissions = {}, entries = {};
|
|
9
|
+
try {
|
|
10
|
+
for (const record of pluginRecords(profile, cwd)) {
|
|
11
|
+
const m = record.manifest;
|
|
12
|
+
if (record.error) {
|
|
13
|
+
errors.push(`${m.id}: ${record.error}`);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
permissions[m.id] = m.permissions ?? [];
|
|
17
|
+
entries[m.id] = m.entries?.web;
|
|
18
|
+
if (m.permissions?.includes("ui.contribute")) {
|
|
19
|
+
for (const item of m.contributes?.ui ?? [])
|
|
20
|
+
ui.push({ ...item, key: `${m.id}/${item.id}`, pluginId: m.id, entry: m.entries?.web, permissions: m.permissions ?? [] });
|
|
21
|
+
Object.assign(theme, m.contributes?.theme ?? {});
|
|
22
|
+
}
|
|
23
|
+
if (m.permissions?.includes("prompts.contribute")) {
|
|
24
|
+
for (const p of m.contributes?.prompts ?? [])
|
|
25
|
+
prompts.push({ id: `${m.id}/${p.id}`, replace: p.replace === true });
|
|
26
|
+
for (const file of m.contributes?.skills ?? []) {
|
|
27
|
+
const text = fs.readFileSync(safeFile(record.directory, file), "utf8").slice(0, 64_000);
|
|
28
|
+
const name = /^name:\s*["']?([^\r\n"']+)/m.exec(text)?.[1]?.trim() ?? file.split("/").slice(-2, -1)[0];
|
|
29
|
+
skills.push({ id: `${m.id}/${name}`, description: /^description:\s*["']?([^\r\n"']+)/m.exec(text)?.[1]?.trim() ?? String(name) });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
errors.push(e.message);
|
|
36
|
+
}
|
|
37
|
+
let providers = {};
|
|
38
|
+
try {
|
|
39
|
+
providers = providerSelections(profile);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
errors.push(e.message);
|
|
43
|
+
}
|
|
44
|
+
return { ui, theme, skills, prompts, permissions, entries, providers, errors, safeMode: safeMode() };
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runPluginCLI(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { configurePlugin, installPlugin, listPlugins, removePlugin, rollbackPlugin, selectProvider } from "./manager.js";
|
|
5
|
+
import { PluginHost } from "./host.js";
|
|
6
|
+
import { packPlugin } from "./package.js";
|
|
7
|
+
export async function runPluginCLI(args) {
|
|
8
|
+
let profile = "standard", cwd, trustNative = false;
|
|
9
|
+
const values = [];
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
if (args[i] === "--profile") {
|
|
12
|
+
const p = args[++i];
|
|
13
|
+
if (p !== "standard" && p !== "pro")
|
|
14
|
+
throw new Error("Profile must be standard or pro");
|
|
15
|
+
profile = p;
|
|
16
|
+
}
|
|
17
|
+
else if (args[i] === "--workspace") {
|
|
18
|
+
const value = args[++i];
|
|
19
|
+
if (!value || value.startsWith("--"))
|
|
20
|
+
throw new Error("--workspace requires a directory");
|
|
21
|
+
cwd = path.resolve(value);
|
|
22
|
+
}
|
|
23
|
+
else if (args[i] === "--trust-native")
|
|
24
|
+
trustNative = true;
|
|
25
|
+
else
|
|
26
|
+
values.push(args[i]);
|
|
27
|
+
}
|
|
28
|
+
const [action, id, extra] = values;
|
|
29
|
+
const print = (v) => process.stdout.write((typeof v === "string" ? v : JSON.stringify(v, null, 2)) + "\n");
|
|
30
|
+
if (!action || action === "help" || action === "--help") {
|
|
31
|
+
print(`Plugin API v1\n\nneutrino plugin list [--profile standard|pro] [--workspace <path>]\nneutrino plugin inspect <id>\nneutrino plugin install <local-directory-or-bundle>\nneutrino plugin enable <id> [--trust-native]\nneutrino plugin disable <id>\nneutrino plugin config <id> '<JSON object>'\nneutrino plugin update <id>\nneutrino plugin rollback <id>\nneutrino plugin remove <id>\nneutrino plugin provider <slot> <plugin-id/contribution-id|default>\nneutrino plugin doctor\nneutrino plugin create <directory>\nneutrino plugin pack <directory> <output.neutrino-plugin>\nneutrino --safe-mode --no-web\n\nInstall is separate from enable. Native plugins execute unrestricted local code.\n`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (action === "list") {
|
|
35
|
+
print(listPlugins(profile, cwd));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (action === "pack") {
|
|
39
|
+
if (!id || !extra)
|
|
40
|
+
throw new Error("Usage: neutrino plugin pack <directory> <output.neutrino-plugin>");
|
|
41
|
+
print(packPlugin(path.resolve(id), path.resolve(extra)));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (action === "install") {
|
|
45
|
+
if (!id)
|
|
46
|
+
throw new Error("Missing source directory");
|
|
47
|
+
print(installPlugin(path.resolve(id)));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (action === "enable" || action === "disable") {
|
|
51
|
+
configurePlugin(id, { enabled: action === "enable", trustNative }, profile, cwd);
|
|
52
|
+
print(`${id}: ${action}d`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (action === "config") {
|
|
56
|
+
configurePlugin(id, { config: JSON.parse(extra ?? "{}") }, profile, cwd);
|
|
57
|
+
print("Configuration saved");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (action === "remove") {
|
|
61
|
+
removePlugin(id);
|
|
62
|
+
print("Plugin removed; immutable files retained for running tasks");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (action === "rollback") {
|
|
66
|
+
rollbackPlugin(id);
|
|
67
|
+
print("Previous package revision restored");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (action === "provider") {
|
|
71
|
+
selectProvider(id, extra === "default" ? "" : extra, profile);
|
|
72
|
+
print("Provider selected");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (action === "update" || action === "inspect") {
|
|
76
|
+
const p = listPlugins(profile, cwd).plugins.find(p => p.manifest.id === id);
|
|
77
|
+
if (!p)
|
|
78
|
+
throw new Error("Unknown plugin");
|
|
79
|
+
print(action === "update" ? installPlugin(p.source) : p);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (action === "doctor") {
|
|
83
|
+
const host = await PluginHost.create({ scope: { profile, cwd: cwd ?? process.cwd(), role: "main" } });
|
|
84
|
+
try {
|
|
85
|
+
print({ plugins: host.pins, tools: host.definitions(), commands: [...host.commands.keys()], errors: host.diagnostics });
|
|
86
|
+
if (host.diagnostics.length)
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await host.dispose();
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (action === "create") {
|
|
95
|
+
if (!id)
|
|
96
|
+
throw new Error("Missing target directory");
|
|
97
|
+
const dir = path.resolve(id);
|
|
98
|
+
if (fs.existsSync(dir))
|
|
99
|
+
throw new Error("Target directory already exists");
|
|
100
|
+
const source = fileURLToPath(new URL("../../examples/plugins/function-tools/", import.meta.url));
|
|
101
|
+
fs.cpSync(source, dir, { recursive: true });
|
|
102
|
+
print(`Created ${dir}. Edit neutrino.plugin.json and agent.js, then install the directory.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Unknown plugin command ${action}`);
|
|
106
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../llm/tools.js";
|
|
2
|
+
import type { Effect, EventName, InvocationContext, JsonObject, PluginPin, PluginRecord, PluginScope, Profile, PromptContribution, UIContribution } from "./types.js";
|
|
3
|
+
interface Registered {
|
|
4
|
+
kind: string;
|
|
5
|
+
handler: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface Owned {
|
|
9
|
+
record: PluginRecord;
|
|
10
|
+
worker?: PluginWorker;
|
|
11
|
+
}
|
|
12
|
+
interface Tool extends Owned {
|
|
13
|
+
definition: ToolDefinition;
|
|
14
|
+
handler: string;
|
|
15
|
+
effects: Effect[];
|
|
16
|
+
roles?: string[];
|
|
17
|
+
}
|
|
18
|
+
interface Command extends Owned {
|
|
19
|
+
handler: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
parameters: JsonObject;
|
|
23
|
+
effects: Effect[];
|
|
24
|
+
}
|
|
25
|
+
export interface OwnedUI extends UIContribution {
|
|
26
|
+
pluginId: string;
|
|
27
|
+
key: string;
|
|
28
|
+
entry?: string;
|
|
29
|
+
permissions: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface Skill {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
text: string;
|
|
36
|
+
pluginId: string;
|
|
37
|
+
}
|
|
38
|
+
interface HostOptions {
|
|
39
|
+
scope: PluginScope;
|
|
40
|
+
context?(): InvocationContext;
|
|
41
|
+
broker?(name: string, args: JsonObject, signal?: AbortSignal): Promise<unknown>;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
allowNative?(): boolean;
|
|
45
|
+
}
|
|
46
|
+
declare class PluginWorker {
|
|
47
|
+
private record;
|
|
48
|
+
private timeout;
|
|
49
|
+
private log;
|
|
50
|
+
private child;
|
|
51
|
+
private readyResolve;
|
|
52
|
+
private readyReject;
|
|
53
|
+
readonly ready: Promise<Registered[]>;
|
|
54
|
+
private current?;
|
|
55
|
+
private sequence;
|
|
56
|
+
private queue;
|
|
57
|
+
private operationQueue;
|
|
58
|
+
private closed;
|
|
59
|
+
private initTimer;
|
|
60
|
+
constructor(record: PluginRecord, scope: PluginScope, timeout: number, log: (message: string) => void);
|
|
61
|
+
private resetTimer;
|
|
62
|
+
private receive;
|
|
63
|
+
private reply;
|
|
64
|
+
private fail;
|
|
65
|
+
invoke(handler: string, value: unknown, context: InvocationContext, operation: (op: string, args: JsonObject, signal: AbortSignal) => Promise<unknown>): Promise<unknown>;
|
|
66
|
+
dispose(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/** One frozen plugin composition per turn, shared by both frontends and all child scopes. */
|
|
69
|
+
export declare class PluginHost {
|
|
70
|
+
private options;
|
|
71
|
+
readonly providers: Record<string, string>;
|
|
72
|
+
readonly tools: Map<string, Tool>;
|
|
73
|
+
readonly commands: Map<string, Command>;
|
|
74
|
+
readonly ui: OwnedUI[];
|
|
75
|
+
readonly skills: Skill[];
|
|
76
|
+
readonly pins: PluginPin[];
|
|
77
|
+
readonly diagnostics: string[];
|
|
78
|
+
readonly prompts: Array<PromptContribution & {
|
|
79
|
+
pluginId: string;
|
|
80
|
+
}>;
|
|
81
|
+
readonly theme: Record<string, string>;
|
|
82
|
+
private workers;
|
|
83
|
+
private hooks;
|
|
84
|
+
private events;
|
|
85
|
+
private disposed;
|
|
86
|
+
get profile(): Profile;
|
|
87
|
+
private constructor();
|
|
88
|
+
static create(options: HostOptions): Promise<PluginHost>;
|
|
89
|
+
private report;
|
|
90
|
+
private permission;
|
|
91
|
+
private declarations;
|
|
92
|
+
private addSkill;
|
|
93
|
+
private resource;
|
|
94
|
+
private addPrompt;
|
|
95
|
+
private addUI;
|
|
96
|
+
private register;
|
|
97
|
+
private remove;
|
|
98
|
+
definitions(role?: string): ToolDefinition[];
|
|
99
|
+
toolPolicy(name: string): {
|
|
100
|
+
effects: Effect[];
|
|
101
|
+
native: boolean;
|
|
102
|
+
roles?: string[];
|
|
103
|
+
} | undefined;
|
|
104
|
+
private context;
|
|
105
|
+
private invoke;
|
|
106
|
+
execute(name: string, args: JsonObject): Promise<string>;
|
|
107
|
+
runCommand(name: string, args?: JsonObject, stack?: string[]): Promise<unknown>;
|
|
108
|
+
guardTool(name: string, args: JsonObject): Promise<void>;
|
|
109
|
+
requestSuffix(value: JsonObject): Promise<string>;
|
|
110
|
+
emit(name: EventName, value: JsonObject): Promise<void>;
|
|
111
|
+
composePrompt(base: string, role: string): {
|
|
112
|
+
text: string;
|
|
113
|
+
sections: Array<{
|
|
114
|
+
id: string;
|
|
115
|
+
text: string;
|
|
116
|
+
}>;
|
|
117
|
+
};
|
|
118
|
+
effectiveUI(platform: "web" | "tui"): OwnedUI[];
|
|
119
|
+
dispose(): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
export {};
|