@xingjisoft/neutrino 0.6.2 → 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.
Files changed (166) hide show
  1. package/README.md +6 -1
  2. package/assets/fast-planner.txt +57 -0
  3. package/assets/fast-replanner.txt +28 -0
  4. package/dist/agents.d.ts +25 -0
  5. package/dist/app.d.ts +206 -0
  6. package/dist/app.js +15 -0
  7. package/dist/background.d.ts +96 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +40 -3
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/clipboard.d.ts +16 -0
  12. package/dist/colors.d.ts +29 -0
  13. package/dist/compact.d.ts +52 -0
  14. package/dist/config.d.ts +221 -0
  15. package/dist/config.js +19 -7
  16. package/dist/cwd.d.ts +19 -0
  17. package/dist/fast/artifacts.d.ts +26 -0
  18. package/dist/fast/artifacts.js +109 -0
  19. package/dist/fast/engine.d.ts +78 -0
  20. package/dist/fast/engine.js +657 -0
  21. package/dist/fast/evidence.d.ts +21 -0
  22. package/dist/fast/evidence.js +68 -0
  23. package/dist/fast/planner.d.ts +10 -0
  24. package/dist/fast/planner.js +51 -0
  25. package/dist/fast/pool.d.ts +17 -0
  26. package/dist/fast/pool.js +78 -0
  27. package/dist/fast/scheduler.d.ts +54 -0
  28. package/dist/fast/scheduler.js +383 -0
  29. package/dist/fast/types.d.ts +100 -0
  30. package/dist/fast/types.js +21 -0
  31. package/dist/fast/worker.d.ts +23 -0
  32. package/dist/fast/worker.js +185 -0
  33. package/dist/header.d.ts +3 -0
  34. package/dist/index.d.ts +5 -0
  35. package/dist/llm/approval.d.ts +204 -0
  36. package/dist/llm/approval.js +14 -3
  37. package/dist/llm/client.d.ts +118 -0
  38. package/dist/llm/client.js +57 -8
  39. package/dist/llm/codeExecutor.d.ts +14 -0
  40. package/dist/llm/codemode.d.ts +15 -0
  41. package/dist/llm/context.d.ts +29 -0
  42. package/dist/llm/diagnostics.d.ts +91 -0
  43. package/dist/llm/diagnostics.js +242 -0
  44. package/dist/llm/plan.d.ts +148 -0
  45. package/dist/llm/retry.d.ts +80 -0
  46. package/dist/llm/risk.d.ts +21 -0
  47. package/dist/llm/risk.js +7 -0
  48. package/dist/llm/runtime.d.ts +189 -0
  49. package/dist/llm/runtime.js +77 -4
  50. package/dist/llm/search.d.ts +2 -0
  51. package/dist/llm/searchNetwork.d.ts +8 -0
  52. package/dist/llm/subagent.d.ts +112 -0
  53. package/dist/llm/subagent.js +4 -2
  54. package/dist/llm/tools.d.ts +20 -0
  55. package/dist/llm/tools.js +4 -1
  56. package/dist/llm/workspace.d.ts +6 -0
  57. package/dist/llm/workspace.js +271 -0
  58. package/dist/markdown.d.ts +58 -0
  59. package/dist/migration.d.ts +8 -0
  60. package/dist/persistence.d.ts +14 -0
  61. package/dist/plugins/catalog.d.ts +20 -0
  62. package/dist/plugins/catalog.js +45 -0
  63. package/dist/plugins/cli.d.ts +1 -0
  64. package/dist/plugins/cli.js +106 -0
  65. package/dist/plugins/host.d.ts +121 -0
  66. package/dist/plugins/host.js +450 -0
  67. package/dist/plugins/manager.d.ts +24 -0
  68. package/dist/plugins/manager.js +288 -0
  69. package/dist/plugins/package.d.ts +5 -0
  70. package/dist/plugins/package.js +53 -0
  71. package/dist/plugins/runner.d.ts +1 -0
  72. package/dist/plugins/runner.js +227 -0
  73. package/dist/plugins/session.d.ts +12 -0
  74. package/dist/plugins/session.js +97 -0
  75. package/dist/plugins/terminal.d.ts +22 -0
  76. package/dist/plugins/terminal.js +33 -0
  77. package/dist/plugins/types.d.ts +178 -0
  78. package/dist/plugins/types.js +6 -0
  79. package/dist/plugins/validation.d.ts +14 -0
  80. package/dist/plugins/validation.js +174 -0
  81. package/dist/plugins/web-sdk.d.ts +58 -0
  82. package/dist/plugins/web-sdk.js +1 -0
  83. package/dist/pro/agents.d.ts +34 -0
  84. package/dist/pro/config.d.ts +75 -0
  85. package/dist/pro/config.js +3 -2
  86. package/dist/pro/preflight.d.ts +21 -0
  87. package/dist/sanitize.d.ts +34 -0
  88. package/dist/screens/agentConversation.d.ts +28 -0
  89. package/dist/screens/agentHistory.d.ts +25 -0
  90. package/dist/screens/agentSelect.d.ts +16 -0
  91. package/dist/screens/chat.d.ts +292 -0
  92. package/dist/screens/chat.js +139 -21
  93. package/dist/screens/history.d.ts +23 -0
  94. package/dist/screens/modelSelect.d.ts +29 -0
  95. package/dist/screens/plugins.d.ts +29 -0
  96. package/dist/screens/plugins.js +196 -0
  97. package/dist/screens/settings.d.ts +42 -0
  98. package/dist/screens/start.d.ts +43 -0
  99. package/dist/screens/start.js +25 -17
  100. package/dist/screens/updatePrompt.d.ts +41 -0
  101. package/dist/selection.d.ts +75 -0
  102. package/dist/state.d.ts +26 -0
  103. package/dist/terminal/buffer.d.ts +47 -0
  104. package/dist/terminal/input.d.ts +70 -0
  105. package/dist/terminal/renderer.d.ts +43 -0
  106. package/dist/terminal/restore.d.ts +41 -0
  107. package/dist/terminal/screen.d.ts +57 -0
  108. package/dist/tray.d.ts +14 -0
  109. package/dist/ui.d.ts +123 -0
  110. package/dist/update.d.ts +71 -0
  111. package/dist/updateWatch.d.ts +43 -0
  112. package/dist/web/approvalQueue.d.ts +8 -0
  113. package/dist/web/approvalQueue.js +59 -0
  114. package/dist/web/auth.d.ts +14 -0
  115. package/dist/web/clientState.d.ts +16 -0
  116. package/dist/web/folderPicker.d.ts +34 -0
  117. package/dist/web/folderPicker.js +544 -0
  118. package/dist/web/hub.d.ts +213 -0
  119. package/dist/web/hub.js +14 -2
  120. package/dist/web/localApi.d.ts +11 -0
  121. package/dist/web/localApi.js +260 -0
  122. package/dist/web/plugins.d.ts +8 -0
  123. package/dist/web/plugins.js +64 -0
  124. package/dist/web/server.d.ts +81 -0
  125. package/dist/web/server.js +127 -19
  126. package/dist/web/service.d.ts +7 -0
  127. package/dist/web/service.js +1 -0
  128. package/dist/web/session.d.ts +176 -0
  129. package/dist/web/session.js +153 -19
  130. package/dist/web/settings.d.ts +65 -0
  131. package/dist/web/terminalBridge.d.ts +15 -0
  132. package/dist/web/terminalBridge.js +8 -2
  133. package/dist/width.d.ts +6 -0
  134. package/docs/plugins/README.md +126 -0
  135. package/docs/plugins/agent-sdk.md +92 -0
  136. package/docs/plugins/architecture.md +64 -0
  137. package/docs/plugins/compatibility.md +34 -0
  138. package/docs/plugins/development.md +55 -0
  139. package/docs/plugins/manifest.md +59 -0
  140. package/docs/plugins/manifest.schema.json +286 -0
  141. package/docs/plugins/security.md +53 -0
  142. package/docs/plugins/skills-prompts.md +69 -0
  143. package/docs/plugins/ui-sdk.md +109 -0
  144. package/examples/local-api.py +105 -0
  145. package/examples/plugins/cards/card.html +6 -0
  146. package/examples/plugins/cards/neutrino.plugin.json +42 -0
  147. package/examples/plugins/composer/editor.html +10 -0
  148. package/examples/plugins/composer/neutrino.plugin.json +10 -0
  149. package/examples/plugins/function-tools/agent.js +32 -0
  150. package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
  151. package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
  152. package/examples/plugins/menus-tabs/panel.html +6 -0
  153. package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
  154. package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
  155. package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
  156. package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
  157. package/examples/plugins/theme/neutrino.plugin.json +12 -0
  158. package/package.json +66 -42
  159. package/web/LOCAL-API.md +65 -0
  160. package/web/README.md +27 -6
  161. package/web/app.js +201 -32
  162. package/web/i18n.js +15 -0
  163. package/web/index.html +3 -1
  164. package/web/plugins.css +10 -0
  165. package/web/plugins.js +230 -0
  166. package/web/style.css +11 -2
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Sub-agents.
3
+ *
4
+ * A sub-agent is an agent loop inside an agent loop: the parent hands one
5
+ * self-contained task to a fresh conversation, the child works with the same
6
+ * tools in the same directory, and only its final report travels back. Its
7
+ * intermediate messages are dropped on purpose — that isolation is the point: a
8
+ * survey that reads thirty files must not fill the parent's context with thirty
9
+ * files.
10
+ *
11
+ * That dropped transcript is why the loop keeps its own evidence. The last
12
+ * round is sent without tools, so a child that has spent its budget can only
13
+ * write; and a child that still runs out hands back what it actually ran and
14
+ * read, instead of the half-sentence it happened to say last.
15
+ *
16
+ * This module owns the child loop and nothing else. Everything that touches the
17
+ * app — which model, how a call is approved, where progress is shown, how tokens
18
+ * are counted — is passed in, so the terminal session and the browser session
19
+ * run the exact same sub-agent.
20
+ */
21
+ import type { LLMClient, UsageInfo } from "./client.js";
22
+ import type { RetryNotice } from "./retry.js";
23
+ import type { ApprovalDecision } from "./approval.js";
24
+ import type { ToolDefinition } from "./tools.js";
25
+ import { TaskRuntime, type AgentResult, type TaskScope } from "./runtime.js";
26
+ /** One message of the child's private transcript. */
27
+ export interface SubAgentMessage {
28
+ role: "system" | "user" | "assistant" | "tool";
29
+ content?: string;
30
+ /** Thinking text echoed back on an assistant tool-call message. */
31
+ reasoning_content?: string;
32
+ tool_calls?: Array<{
33
+ id: string;
34
+ type: "function";
35
+ function: {
36
+ name: string;
37
+ arguments: string;
38
+ };
39
+ }>;
40
+ tool_call_id?: string;
41
+ anthropic_content?: unknown[];
42
+ }
43
+ export interface SubAgentOptions {
44
+ runtime?: TaskRuntime;
45
+ scope?: TaskScope;
46
+ readOnly?: boolean;
47
+ writePaths?: readonly string[];
48
+ signal?: AbortSignal;
49
+ /** A fresh client per round; the loop owns usage and retries. */
50
+ createClient(): LLMClient;
51
+ /** The sub-agent's system prompt (see `loadSubAgentPrompt`). */
52
+ system: string;
53
+ /** The delegated task, already flattened into a standalone brief. */
54
+ task: string;
55
+ tools: ToolDefinition[];
56
+ /** The same approval gate the parent turn uses. */
57
+ approve(name: string, args: Record<string, unknown>): Promise<ApprovalDecision>;
58
+ /** Run one approved call. */
59
+ execute(name: string, args: Record<string, unknown>): Promise<string>;
60
+ /** What the child is doing right now, for the parent's status line. */
61
+ onProgress?(label: string): void;
62
+ /** Tokens spent by the child, added to the parent's conversation counters. */
63
+ onUsage?(usage: UsageInfo): void;
64
+ /** A dropped request is being retried, so the parent can explain the silence. */
65
+ onRetry?(notice: RetryNotice): void;
66
+ /** True once the parent turn was paused, superseded or cleared. */
67
+ cancelled?(): boolean;
68
+ maxRounds?: number;
69
+ /** Pro role capacity; ordinary sub-agents retain the historical behaviour. */
70
+ contextWindow?: number;
71
+ }
72
+ export interface SubAgentRun extends AgentResult {
73
+ /** The child's final message: the only thing the parent receives. */
74
+ report: string;
75
+ /** The parent turn ended before the child finished. */
76
+ aborted: boolean;
77
+ /** The child used up its rounds without finishing. */
78
+ exhausted: boolean;
79
+ rounds: number;
80
+ calls: number;
81
+ }
82
+ /**
83
+ * Rounds one sub-agent may take, the last of which has no tools. Enough for real
84
+ * work, few enough that a runaway child cannot cost a fortune.
85
+ */
86
+ export declare const SUBAGENT_MAX_ROUNDS = 16;
87
+ /** A wrap-up round is only worth anything with at least one round of work before it. */
88
+ export declare const SUBAGENT_MIN_ROUNDS = 2;
89
+ /** A report is evidence for the parent, not a transcript of the child. */
90
+ export declare const SUBAGENT_MAX_REPORT = 8000;
91
+ /** The configured round budget, reconciled with what the loop can honour. */
92
+ export declare function subAgentRounds(configured: unknown): number;
93
+ /** The task brief handed to the child: where it works, what it has, what to do. */
94
+ export declare function buildSubAgentTask(input: {
95
+ task: string;
96
+ context?: string;
97
+ cwd?: string;
98
+ rounds?: number;
99
+ }): string;
100
+ /**
101
+ * Run one sub-agent to its report.
102
+ *
103
+ * The child sees only its own task, its own tool results and the parent's
104
+ * approval decisions. Nothing it says reaches the parent except the last
105
+ * message, which is why the sub-agent prompt insists that the report be
106
+ * complete on its own.
107
+ */
108
+ export declare const FINISH_TASK_TOOL = "finish_task";
109
+ export declare const FINISH_TASK_DEFINITION: ToolDefinition;
110
+ export declare function runSubAgent(options: SubAgentOptions): Promise<SubAgentRun>;
111
+ /** The tool result the parent sees: the report, or why there is none. */
112
+ export declare function formatSubAgentResult(run: SubAgentRun): string;
@@ -1,3 +1,4 @@
1
+ import { pluginPrompt } from "../plugins/session.js";
1
2
  import { describeError } from "./retry.js";
2
3
  import { prepareContext, outputAllowance } from "./context.js";
3
4
  import { currentTask, TaskRuntime, TaskControlError, linkSignals } from "./runtime.js";
@@ -156,10 +157,11 @@ export async function runSubAgent(options) {
156
157
  runId = currentTask()?.runId ?? runId;
157
158
  scopeSignal = currentTask()?.signal;
158
159
  const messages = [
159
- { role: "system", content: `${options.system}\n\n${runtime.instructions()}` },
160
+ { role: "system", content: `${pluginPrompt(options.system, currentTask()?.role ?? "subagent")}\n\n${runtime.instructions()}` },
160
161
  { role: "user", content: options.task },
161
162
  ];
162
- const tools = runtime.availableTools([...options.tools.filter(tool => tool.function.name !== FINISH_TASK_TOOL), FINISH_TASK_DEFINITION]);
163
+ const tools = runtime.availableTools([...new Map([...options.tools.filter(tool => tool.function.name !== FINISH_TASK_TOOL),
164
+ ...(runtime.plugins?.definitions(currentTask()?.role) ?? []), FINISH_TASK_DEFINITION].map(tool => [tool.function.name, tool])).values()]);
163
165
  const names = new Set(tools.map(tool => tool.function.name));
164
166
  let lastPromptTokens = 0;
165
167
  let emptyReplies = 0;
@@ -0,0 +1,20 @@
1
+ export interface ToolDefinition {
2
+ type: "function";
3
+ function: {
4
+ name: string;
5
+ description: string;
6
+ parameters: Record<string, unknown>;
7
+ };
8
+ }
9
+ /** The tool a parent agent calls to hand work to a sub-agent. */
10
+ export declare const DELEGATE_TOOL = "delegate";
11
+ /**
12
+ * The toolset a sub-agent runs with: the same tools as its parent, minus
13
+ * `delegate` - one level of delegation, never a tree of agents that nobody can
14
+ * follow (or stop). The sub-agent prompt says so as well; removing the tool
15
+ * makes it true.
16
+ */
17
+ export declare function getSubAgentToolDefinitions(): ToolDefinition[];
18
+ /** Every tool a top-level agent can call: the base set plus delegation. */
19
+ export declare function getToolDefinitions(): ToolDefinition[];
20
+ export declare function executeTool(name: string, args: Record<string, unknown>, cwd?: string, signal?: AbortSignal): Promise<string>;
package/dist/llm/tools.js CHANGED
@@ -142,6 +142,7 @@ export function getSubAgentToolDefinitions() {
142
142
  },
143
143
  },
144
144
  },
145
+ ...(currentTask()?.runtime.plugins?.definitions() ?? []),
145
146
  ];
146
147
  }
147
148
  /** Every tool a top-level agent can call: the base set plus delegation. */
@@ -180,7 +181,9 @@ export async function executeTool(name, args, cwd = process.cwd(), signal) {
180
181
  result = listFiles(String(args.path ?? ""), base);
181
182
  break;
182
183
  default:
183
- result = `[Error: unknown tool '${name}']`;
184
+ result = currentTask()?.runtime.plugins?.tools.has(name)
185
+ ? await currentTask().runtime.plugins.execute(name, args)
186
+ : `[Error: unknown tool '${name}']`;
184
187
  }
185
188
  }
186
189
  catch (e) {
@@ -0,0 +1,6 @@
1
+ /** Recognize an explicit confinement instruction, never permission inferred from tool output. */
2
+ export declare function requestsWorkspaceOnly(request: string): boolean;
3
+ /** Inspect literal shell/program paths, including quoted inline programs and heredoc bodies. */
4
+ export declare function commandWorkspacePaths(command: string, cwd?: string, depth?: number): string[];
5
+ /** Preflight for declared workspace confinement. Arbitrary programs still require the normal approval gate. */
6
+ export declare function workspaceViolation(name: string, args: Record<string, unknown>, cwd: string): string | undefined;
@@ -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>;