@cydm/magic-shell-agent-node 0.1.18 → 0.1.19

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/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { AgentNode, type NodeOptions, main as runAgentNode } from "./node.js";
2
2
  export { getDefaultPluginDir } from "./plugin-loader.js";
3
+ export { getMagicShellCliPath, getPackagedPluginDir, getPrimaryPieExtensionDistDir, getWorkbenchRoot } from "./runtime-assets.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { AgentNode, main as runAgentNode } from "./node.js";
2
2
  export { getDefaultPluginDir } from "./plugin-loader.js";
3
+ export { getMagicShellCliPath, getPackagedPluginDir, getPrimaryPieExtensionDistDir, getWorkbenchRoot } from "./runtime-assets.js";
@@ -1,8 +1,8 @@
1
1
  import { createServer } from "node:http";
2
2
  import { existsSync, createReadStream } from "node:fs";
3
3
  import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  import { WebSocketServer, WebSocket } from "ws";
5
+ import { getWorkbenchRoot } from "./runtime-assets.js";
6
6
  function createConnectionId() {
7
7
  return `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8
8
  }
@@ -21,25 +21,6 @@ function guessContentType(filePath) {
21
21
  return "image/png";
22
22
  return "text/plain; charset=utf-8";
23
23
  }
24
- function findWorkbenchRoot() {
25
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
26
- const envOverride = process.env.MAGIC_SHELL_WORKBENCH_ROOT;
27
- const candidates = [
28
- envOverride ? path.resolve(envOverride) : null,
29
- path.resolve(currentDir, "./workbench"),
30
- path.resolve(currentDir, "../dist/workbench"),
31
- path.resolve(currentDir, "../workbench"),
32
- path.resolve(currentDir, "../../../apps/web/src"),
33
- path.resolve(currentDir, "../../../../apps/web/src"),
34
- path.resolve(process.cwd(), "apps/web/src"),
35
- ].filter((value) => Boolean(value));
36
- for (const candidate of candidates) {
37
- if (existsSync(path.join(candidate, "index.html"))) {
38
- return candidate;
39
- }
40
- }
41
- return null;
42
- }
43
24
  export class LocalDirectServer {
44
25
  options;
45
26
  delegate;
@@ -47,7 +28,7 @@ export class LocalDirectServer {
47
28
  wsServer = new WebSocketServer({ noServer: true });
48
29
  connections = new Map();
49
30
  sessionToConnections = new Map();
50
- workbenchRoot = findWorkbenchRoot();
31
+ workbenchRoot = getWorkbenchRoot();
51
32
  constructor(options, delegate) {
52
33
  this.options = options;
53
34
  this.delegate = delegate;
package/dist/node.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { WebSocketClient } from "./ws-client.js";
2
3
  import { SessionManager } from "./session-manager.js";
3
4
  import { LocalDirectServer } from "./local-direct-server.js";
@@ -16,9 +17,10 @@ import { runClaudeExec } from "./claude-exec.js";
16
17
  import { codexNeedsTrustConfirmation, getLastCodexWorkerMessage, isCodexReadyForTask, isCodexWorker, summarizeCodexWorkerOutput, } from "./codex-worker.js";
17
18
  import { runCodexExec } from "./codex-exec.js";
18
19
  import { extractTerminalMetadata, normalizeWorkerTitleCandidate, } from "./terminal-metadata.js";
19
- import { existsSync } from "node:fs";
20
+ import { chmodSync, existsSync, readdirSync } from "node:fs";
20
21
  import os from "node:os";
21
22
  import path from "node:path";
23
+ const require = createRequire(import.meta.url);
22
24
  function generateSessionId() {
23
25
  return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
24
26
  }
@@ -84,6 +86,7 @@ export class AgentNode {
84
86
  console.log(`[AgentNode] Starting...`);
85
87
  console.log(`[AgentNode] Node ID: ${this.options.nodeId}`);
86
88
  console.log(`[AgentNode] Relay: ${this.options.relayUrl}`);
89
+ ensureSpawnHelperExecutable();
87
90
  // 1. 加载插件
88
91
  this.plugins = loadPlugins({ dir: this.options.pluginDir });
89
92
  if (this.plugins.size === 0) {
@@ -1933,6 +1936,34 @@ Environment Variables:
1933
1936
  MAGIC_SHELL_DISABLE_LOCAL_DIRECT Set to 1 to disable the local direct server
1934
1937
  `);
1935
1938
  }
1939
+ function ensureSpawnHelperExecutable() {
1940
+ try {
1941
+ const pkgPath = require.resolve("node-pty/package.json");
1942
+ const prebuildRoot = path.join(path.dirname(pkgPath), "prebuilds");
1943
+ if (!existsSync(prebuildRoot))
1944
+ return;
1945
+ for (const entry of walk(prebuildRoot)) {
1946
+ if (path.basename(entry) === "spawn-helper") {
1947
+ chmodSync(entry, 0o755);
1948
+ }
1949
+ }
1950
+ }
1951
+ catch {
1952
+ // Ignore best-effort fixups during startup.
1953
+ }
1954
+ }
1955
+ function walk(dir) {
1956
+ const files = [];
1957
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1958
+ const fullPath = path.join(dir, entry.name);
1959
+ if (entry.isDirectory()) {
1960
+ files.push(...walk(fullPath));
1961
+ continue;
1962
+ }
1963
+ files.push(fullPath);
1964
+ }
1965
+ return files;
1966
+ }
1936
1967
  // 主函数
1937
1968
  async function main() {
1938
1969
  const options = parseArgs();
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, readdirSync, existsSync } from "fs";
2
2
  import { join, extname, dirname, isAbsolute, resolve } from "path";
3
- import { fileURLToPath } from "url";
4
3
  import { createRequire } from "module";
4
+ import { getPackagedPluginDir } from "./runtime-assets.js";
5
5
  const require = createRequire(import.meta.url);
6
6
  /**
7
7
  * 加载单个插件配置
@@ -39,9 +39,6 @@ export function loadPlugin(path) {
39
39
  });
40
40
  }
41
41
  function normalizePluginConfigForRuntime(config) {
42
- if (process.platform !== "win32") {
43
- return config;
44
- }
45
42
  if (config.name === "pie") {
46
43
  const pieEntry = resolvePackageBinEntry("@cydm/pie", "pie");
47
44
  if (pieEntry) {
@@ -52,6 +49,9 @@ function normalizePluginConfigForRuntime(config) {
52
49
  };
53
50
  }
54
51
  }
52
+ if (process.platform !== "win32") {
53
+ return config;
54
+ }
55
55
  if ((config.name === "claude-code" || config.name === "codex") && isShellCommand(config.command)) {
56
56
  return {
57
57
  ...config,
@@ -174,18 +174,9 @@ export function loadPlugins(options) {
174
174
  * 获取默认插件目录
175
175
  */
176
176
  export function getDefaultPluginDir() {
177
- // 优先使用环境变量
178
- if (process.env.MAGIC_SHELL_PLUGINS_DIR) {
179
- return process.env.MAGIC_SHELL_PLUGINS_DIR;
180
- }
181
- const packagePluginDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "plugins");
182
- if (existsSync(packagePluginDir)) {
183
- return packagePluginDir;
184
- }
185
- const distPluginDir = resolve(dirname(fileURLToPath(import.meta.url)), "plugins");
186
- if (existsSync(distPluginDir)) {
187
- return distPluginDir;
177
+ const pluginDir = getPackagedPluginDir();
178
+ if (pluginDir) {
179
+ return pluginDir;
188
180
  }
189
- // 回退到当前工作目录的 plugins,兼容仓库内开发
190
- return join(process.cwd(), "plugins");
181
+ throw new Error("Magic Shell plugin assets are unavailable. Set MAGIC_SHELL_PLUGINS_DIR or run from a built source checkout.");
191
182
  }
@@ -55,7 +55,6 @@ export interface PrimarySessionSnapshot {
55
55
  lastMessageRole?: string;
56
56
  lastAssistantHasToolCall?: boolean;
57
57
  }
58
- export declare function getPrimaryPieExtensionDistDir(): string;
59
58
  export declare function withPrimaryPieExtensionPath(plugin: PluginConfig): PluginConfig;
60
59
  export declare function buildPrimaryPrompt(text: string, context: PrimaryAgentPromptContext): string;
61
60
  export declare function buildPrimarySessionPrompt(text: string, context: PrimarySessionPromptContext): string;
@@ -2,20 +2,50 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- export function getPrimaryPieExtensionDistDir() {
7
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
8
- const repoRoot = path.resolve(currentDir, "..", "..", "..");
9
- return path.join(repoRoot, "packages", "primary-pie-extension", "dist");
5
+ import { getMagicShellCliPath, getPrimaryPieExtensionDistDir } from "./runtime-assets.js";
6
+ function getPrimaryPieExtensionPackageDir(extensionRoot) {
7
+ const packagedDir = path.join(extensionRoot, "magic-shell-agent");
8
+ if (fs.existsSync(path.join(packagedDir, "package.json"))) {
9
+ return packagedDir;
10
+ }
11
+ if (fs.existsSync(path.join(extensionRoot, "package.json"))) {
12
+ return extensionRoot;
13
+ }
14
+ return null;
15
+ }
16
+ function ensurePieExtensionDiscovery(extensionRoot) {
17
+ const extensionPackageDir = getPrimaryPieExtensionPackageDir(extensionRoot);
18
+ if (!extensionPackageDir) {
19
+ throw new Error(`Primary PIE extension package is missing under ${extensionRoot}`);
20
+ }
21
+ const targetDir = path.join(os.homedir(), ".pie", "extensions", path.basename(extensionPackageDir));
22
+ fs.rmSync(targetDir, { recursive: true, force: true });
23
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
24
+ fs.cpSync(extensionPackageDir, targetDir, { recursive: true });
25
+ return targetDir;
10
26
  }
11
27
  export function withPrimaryPieExtensionPath(plugin) {
12
28
  if (plugin.name !== "pie") {
13
29
  return plugin;
14
30
  }
15
31
  const extensionDir = getPrimaryPieExtensionDistDir();
32
+ if (!extensionDir) {
33
+ throw new Error("Primary PIE extension assets are unavailable. Use the packaged @cydm/magic-shell runtime or build packages/primary-pie-extension first.");
34
+ }
35
+ const cliPath = getMagicShellCliPath();
36
+ if (!cliPath) {
37
+ throw new Error("Magic Shell CLI runtime is unavailable for the primary PIE extension. Start the node via `magic-shell node start` or set MAGIC_SHELL_CLI_PATH.");
38
+ }
39
+ const installedExtensionDir = ensurePieExtensionDiscovery(extensionDir);
16
40
  return {
17
41
  ...plugin,
18
42
  args: [...(plugin.args || []), "--extension-path", extensionDir],
43
+ env: {
44
+ ...(plugin.env || {}),
45
+ MAGIC_SHELL_CLI_PATH: cliPath,
46
+ MAGIC_SHELL_PRIMARY_EXTENSION_DIR: extensionDir,
47
+ MAGIC_SHELL_PRIMARY_EXTENSION_PACKAGE_DIR: installedExtensionDir,
48
+ },
19
49
  };
20
50
  }
21
51
  export function buildPrimaryPrompt(text, context) {
@@ -0,0 +1,1019 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const pieRequireBase = (() => {
11
+ try {
12
+ if (process.argv[1]) {
13
+ return fs.realpathSync(process.argv[1]);
14
+ }
15
+ } catch {
16
+ // fall through
17
+ }
18
+ return import.meta.url;
19
+ })();
20
+ const require = createRequire(pieRequireBase);
21
+ const { Type } = require("@sinclair/typebox");
22
+ const sessionsDir = path.join(os.homedir(), ".pie", "sessions");
23
+ const inboxPollers = new Map();
24
+ const activeRequests = new Map();
25
+ const delegatedTurnState = new Map();
26
+
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
31
+ function appendDebugLog(message) {
32
+ try {
33
+ fs.appendFileSync("/tmp/magic-shell-agent-extension.log", `${new Date().toISOString()} ${message}\n`);
34
+ } catch {
35
+ // ignore debug logging failures
36
+ }
37
+ }
38
+
39
+ function safeCwd(ctx) {
40
+ try {
41
+ return ctx?.cwd || process.cwd();
42
+ } catch {
43
+ return process.cwd();
44
+ }
45
+ }
46
+
47
+ function tryDirectory(candidate) {
48
+ try {
49
+ if (candidate && fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
50
+ return candidate;
51
+ }
52
+ } catch {
53
+ // ignore bad candidate
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function looksLikeStandaloneRepo(directory) {
59
+ try {
60
+ if (!directory) return false;
61
+ return fs.existsSync(path.join(directory, ".git")) || fs.existsSync(path.join(directory, "package.json"));
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function findNamedDirectoryNear(directoryName, startCwd) {
68
+ if (!directoryName || !startCwd) return null;
69
+ let current = startCwd;
70
+ while (current && current !== path.dirname(current)) {
71
+ const directChild = tryDirectory(path.join(current, directoryName));
72
+ const siblingUnderParent = tryDirectory(path.join(path.dirname(current), directoryName));
73
+ if (directChild && siblingUnderParent) {
74
+ if (looksLikeStandaloneRepo(siblingUnderParent) && !looksLikeStandaloneRepo(directChild)) {
75
+ return siblingUnderParent;
76
+ }
77
+ return directChild;
78
+ }
79
+ if (siblingUnderParent) return siblingUnderParent;
80
+ if (directChild) return directChild;
81
+
82
+ current = path.dirname(current);
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function findMagicShellRoot(startCwd) {
88
+ let current = startCwd;
89
+ while (current && current !== path.dirname(current)) {
90
+ const packageJsonPath = path.join(current, "package.json");
91
+ const cliPath = path.join(current, "packages", "cli", "dist", "cli.js");
92
+ if (fs.existsSync(packageJsonPath) && fs.existsSync(cliPath)) {
93
+ return current;
94
+ }
95
+ current = path.dirname(current);
96
+ }
97
+ return startCwd;
98
+ }
99
+
100
+ function getExtensionRuntimeDir() {
101
+ try {
102
+ return path.dirname(fileURLToPath(import.meta.url));
103
+ } catch {
104
+ return process.cwd();
105
+ }
106
+ }
107
+
108
+ function findExistingFile(candidates) {
109
+ for (const candidate of candidates) {
110
+ if (!candidate) continue;
111
+ try {
112
+ if (fs.existsSync(candidate)) {
113
+ return candidate;
114
+ }
115
+ } catch {
116
+ // ignore invalid candidate
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+
122
+ function getCliPath(cwd) {
123
+ const runtimeDir = getExtensionRuntimeDir();
124
+ const siblingPackagedCli = path.resolve(runtimeDir, "../../../../../magic-shell/dist/cli.js");
125
+ const nestedPackagedCli = path.resolve(runtimeDir, "../../../../../../dist/cli.js");
126
+ const devCli = path.resolve(runtimeDir, "../../../../cli/dist/cli.js");
127
+ const envCli = process.env.MAGIC_SHELL_CLI_PATH
128
+ ? path.resolve(process.env.MAGIC_SHELL_CLI_PATH)
129
+ : null;
130
+ const discovered = findExistingFile([
131
+ envCli,
132
+ siblingPackagedCli,
133
+ nestedPackagedCli,
134
+ devCli,
135
+ ]);
136
+ if (discovered) {
137
+ return discovered;
138
+ }
139
+
140
+ const root = findMagicShellRoot(cwd);
141
+ return path.join(root, "packages", "cli", "dist", "cli.js");
142
+ }
143
+
144
+ function getCliWorkingDirectory(cwd, cliPath) {
145
+ if (!cliPath) {
146
+ return cwd || process.cwd();
147
+ }
148
+ if (process.env.MAGIC_SHELL_CLI_PATH && fs.existsSync(process.env.MAGIC_SHELL_CLI_PATH)) {
149
+ return cwd || process.cwd();
150
+ }
151
+ if (cliPath.includes(`${path.sep}dist${path.sep}cli.js`)) {
152
+ const packageRoot = path.resolve(path.dirname(cliPath), "..");
153
+ if (fs.existsSync(path.join(packageRoot, "package.json"))) {
154
+ return packageRoot;
155
+ }
156
+ }
157
+ return findMagicShellRoot(cwd);
158
+ }
159
+
160
+ function inferTargetDirectory(task, cwd) {
161
+ const text = String(task || "").trim();
162
+ if (!text) return null;
163
+ const lowered = text.toLowerCase();
164
+
165
+ const homePathMatches = text.match(/~\/[^\s"'`,。;、]+/g) || [];
166
+ for (const candidate of homePathMatches) {
167
+ const expanded = path.join(os.homedir(), candidate.slice(2));
168
+ try {
169
+ if (fs.existsSync(expanded) && fs.statSync(expanded).isDirectory()) {
170
+ return expanded;
171
+ }
172
+ } catch {
173
+ // ignore bad candidate
174
+ }
175
+ }
176
+
177
+ if (
178
+ text === "~"
179
+ || lowered.includes("home directory")
180
+ || lowered.includes("home dir")
181
+ || text.includes("家目录")
182
+ || text.includes("主目录")
183
+ ) {
184
+ return os.homedir();
185
+ }
186
+
187
+ const absolutePathMatches = text.match(/\/[^\s"'`,。;、]+/g) || [];
188
+ for (const candidate of absolutePathMatches) {
189
+ try {
190
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
191
+ return candidate;
192
+ }
193
+ } catch {
194
+ // ignore bad candidate
195
+ }
196
+ }
197
+
198
+ const locatedUnderMatch = text.match(/([A-Za-z0-9._-]+)\s*目录在\s*([A-Za-z0-9._-]+)\s*下面/u);
199
+ if (locatedUnderMatch) {
200
+ const childName = locatedUnderMatch[1];
201
+ const parentName = locatedUnderMatch[2];
202
+ let current = cwd;
203
+ while (current && current !== path.dirname(current)) {
204
+ if (path.basename(current) === parentName) {
205
+ const candidate = tryDirectory(path.join(current, childName));
206
+ if (candidate) return candidate;
207
+ }
208
+ const parentSibling = tryDirectory(path.join(path.dirname(current), parentName, childName));
209
+ if (parentSibling) return parentSibling;
210
+ current = path.dirname(current);
211
+ }
212
+ }
213
+
214
+ const namedDirectoryMatch = text.match(/(?:去|到|在|进|进入|spawn.*?到|在)\s*([A-Za-z0-9._-]+)\s*目录/u)
215
+ || text.match(/\b([A-Za-z0-9._-]+)\s+directory\b/i)
216
+ || text.match(/\b([A-Za-z0-9._-]+)\b(?=.*\b(?:repo|repository|目录|folder|directory)\b)/i);
217
+ if (namedDirectoryMatch?.[1]) {
218
+ const nearby = findNamedDirectoryNear(namedDirectoryMatch[1], cwd);
219
+ if (nearby) {
220
+ return nearby;
221
+ }
222
+ }
223
+
224
+ return null;
225
+ }
226
+
227
+ function normalizeDelegatedCwd(rawCwd, baseCwd) {
228
+ const value = String(rawCwd || "").trim();
229
+ if (!value) return null;
230
+
231
+ if (value === "~") {
232
+ return os.homedir();
233
+ }
234
+ if (value.startsWith("~/")) {
235
+ return tryDirectory(path.join(os.homedir(), value.slice(2)));
236
+ }
237
+ if (path.isAbsolute(value)) {
238
+ const absolute = tryDirectory(value);
239
+ if (!absolute) {
240
+ return null;
241
+ }
242
+ if (!looksLikeStandaloneRepo(absolute)) {
243
+ let current = baseCwd || process.cwd();
244
+ while (current && current !== path.dirname(current)) {
245
+ const sibling = tryDirectory(path.join(path.dirname(current), path.basename(absolute)));
246
+ if (sibling && sibling !== absolute && looksLikeStandaloneRepo(sibling)) {
247
+ return sibling;
248
+ }
249
+ current = path.dirname(current);
250
+ }
251
+ }
252
+ return absolute;
253
+ }
254
+ const resolved = path.resolve(baseCwd || process.cwd(), value);
255
+ if (tryDirectory(resolved)) {
256
+ if (!looksLikeStandaloneRepo(resolved)) {
257
+ let current = baseCwd || process.cwd();
258
+ while (current && current !== path.dirname(current)) {
259
+ const sibling = tryDirectory(path.join(path.dirname(current), path.basename(resolved)));
260
+ if (sibling && sibling !== resolved && looksLikeStandaloneRepo(sibling)) {
261
+ return sibling;
262
+ }
263
+ current = path.dirname(current);
264
+ }
265
+ }
266
+ return resolved;
267
+ }
268
+ const basename = path.basename(value);
269
+ if (basename) {
270
+ const nearby = findNamedDirectoryNear(basename, baseCwd || process.cwd());
271
+ if (nearby) {
272
+ return nearby;
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+
278
+ function inferWorkerName(task) {
279
+ const text = String(task || "").trim();
280
+ if (!text) return null;
281
+ const match = text.match(/(?:叫|名叫|叫做|名字是|named|call(?:ed)?)(?:\s*[::]?\s*)([A-Za-z0-9_\-\u4e00-\u9fa5]{2,20})/iu);
282
+ if (!match?.[1]) return null;
283
+ return match[1].trim();
284
+ }
285
+
286
+ function inferMentionedWorkerName(task, snapshot) {
287
+ const text = String(task || "").trim();
288
+ if (!text) return null;
289
+ const workers = Array.isArray(snapshot?.workers) ? snapshot.workers : [];
290
+ const liveNames = workers
291
+ .filter((worker) => worker?.status !== "stopped" && worker?.status !== "failed" && worker?.displayName)
292
+ .map((worker) => String(worker.displayName).trim())
293
+ .filter(Boolean)
294
+ .sort((a, b) => b.length - a.length);
295
+ for (const name of liveNames) {
296
+ if (text.includes(name)) {
297
+ return name;
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+
303
+ function buildDelegationCacheKey({ sessionId, workerName, cwd, mode, task }) {
304
+ return JSON.stringify({
305
+ sessionId: sessionId || null,
306
+ workerName: workerName || null,
307
+ cwd: cwd || null,
308
+ mode: mode || "steer",
309
+ task: normalizeDelegatedText(task || ""),
310
+ });
311
+ }
312
+
313
+ function getCurrentSessionId() {
314
+ const args = process.argv.slice(2);
315
+ const flagIndex = args.indexOf("--session-id");
316
+ return flagIndex >= 0 && args[flagIndex + 1] ? args[flagIndex + 1] : null;
317
+ }
318
+
319
+ function getInboxPath(sessionId) {
320
+ return path.join(sessionsDir, `${sessionId}.magic-shell-inbox.json`);
321
+ }
322
+
323
+ function getStatePath(sessionId) {
324
+ return path.join(sessionsDir, `${sessionId}.magic-shell-state.json`);
325
+ }
326
+
327
+ function writeSessionControlState(sessionId, patch) {
328
+ fs.mkdirSync(sessionsDir, { recursive: true });
329
+ const statePath = getStatePath(sessionId);
330
+ let current = {};
331
+ if (fs.existsSync(statePath)) {
332
+ try {
333
+ current = JSON.parse(fs.readFileSync(statePath, "utf8"));
334
+ } catch {
335
+ current = {};
336
+ }
337
+ }
338
+ const next = {
339
+ sessionId,
340
+ updatedAt: Date.now(),
341
+ ...current,
342
+ ...patch,
343
+ };
344
+ fs.writeFileSync(statePath, JSON.stringify(next, null, 2));
345
+ }
346
+
347
+ function extractAssistantText(message) {
348
+ if (!message || !Array.isArray(message.content)) {
349
+ return "";
350
+ }
351
+ return message.content
352
+ .filter((item) => item?.type === "text" && typeof item.text === "string")
353
+ .map((item) => item.text.trim())
354
+ .filter(Boolean)
355
+ .join("\n\n")
356
+ .trim();
357
+ }
358
+
359
+ function startInboxPoller(ctx, sessionId) {
360
+ if (!sessionId || inboxPollers.has(sessionId)) {
361
+ return;
362
+ }
363
+ fs.mkdirSync(sessionsDir, { recursive: true });
364
+ const tick = () => {
365
+ if (!ctx.isIdle()) {
366
+ return;
367
+ }
368
+ const inboxPath = getInboxPath(sessionId);
369
+ if (!fs.existsSync(inboxPath)) {
370
+ return;
371
+ }
372
+
373
+ try {
374
+ const command = JSON.parse(fs.readFileSync(inboxPath, "utf8"));
375
+ if (!command?.requestId || !command?.message) {
376
+ return;
377
+ }
378
+ const current = activeRequests.get(sessionId);
379
+ if (current?.requestId === command.requestId) {
380
+ return;
381
+ }
382
+ activeRequests.set(sessionId, command);
383
+ writeSessionControlState(sessionId, {
384
+ activeRequestId: command.requestId,
385
+ status: "busy",
386
+ lastError: null,
387
+ });
388
+ fs.unlinkSync(inboxPath);
389
+ ctx.sendUserMessage(command.message);
390
+ } catch (error) {
391
+ writeSessionControlState(sessionId, {
392
+ lastError: error instanceof Error ? error.message : String(error),
393
+ });
394
+ }
395
+ };
396
+ const interval = setInterval(tick, 250);
397
+ inboxPollers.set(sessionId, interval);
398
+ writeSessionControlState(sessionId, {
399
+ activeRequestId: null,
400
+ status: "idle",
401
+ });
402
+ }
403
+
404
+ async function runMagicShellCli(cwd, args) {
405
+ const cliPath = getCliPath(cwd);
406
+ if (!fs.existsSync(cliPath)) {
407
+ throw new Error(`Magic Shell CLI is unavailable: ${cliPath}. Start via magic-shell or set MAGIC_SHELL_CLI_PATH.`);
408
+ }
409
+
410
+ const { stdout, stderr } = await execFileAsync(process.execPath, [cliPath, ...args], {
411
+ cwd: getCliWorkingDirectory(cwd, cliPath),
412
+ env: {
413
+ ...process.env,
414
+ MAGIC_SHELL_CLI_PATH: cliPath,
415
+ },
416
+ maxBuffer: 1024 * 1024 * 4,
417
+ });
418
+ const output = String(stdout || "").trim();
419
+ if (!output) {
420
+ if (stderr?.trim()) {
421
+ throw new Error(stderr.trim());
422
+ }
423
+ throw new Error("Magic Shell CLI returned empty output");
424
+ }
425
+
426
+ try {
427
+ return JSON.parse(output);
428
+ } catch (error) {
429
+ throw new Error(`Magic Shell CLI returned invalid JSON: ${output.slice(0, 240)}`);
430
+ }
431
+ }
432
+
433
+ function pickWorker(snapshot, preferredSessionId, pluginName, desiredCwd, workerName, { allowExactReuseAcrossCwd = false } = {}) {
434
+ const workers = Array.isArray(snapshot?.workers) ? snapshot.workers : [];
435
+ const pluginMatches = (worker) => !pluginName || !worker?.agentType || worker.agentType === pluginName;
436
+ const cwdMatches = (worker) => !desiredCwd || !worker?.cwd || path.resolve(worker.cwd) === path.resolve(desiredCwd);
437
+ if (preferredSessionId) {
438
+ const exact = workers.find((worker) =>
439
+ worker.sessionId === preferredSessionId
440
+ && pluginMatches(worker)
441
+ && worker.status !== "stopped"
442
+ && worker.status !== "failed",
443
+ );
444
+ if (exact) {
445
+ if (allowExactReuseAcrossCwd || cwdMatches(exact)) {
446
+ return exact;
447
+ }
448
+ return null;
449
+ }
450
+ }
451
+ if (workerName) {
452
+ const named = workers.find((worker) =>
453
+ worker.displayName === workerName
454
+ && pluginMatches(worker)
455
+ && worker.status !== "stopped"
456
+ && worker.status !== "failed",
457
+ );
458
+ if (named) {
459
+ if (allowExactReuseAcrossCwd || cwdMatches(named)) {
460
+ return named;
461
+ }
462
+ return null;
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+
468
+ function shouldPreferExistingWorker(task, preferredSessionId, workerName) {
469
+ if (preferredSessionId || workerName) {
470
+ return true;
471
+ }
472
+ const text = String(task || "");
473
+ return text.includes("继续")
474
+ || text.includes("接着")
475
+ || text.includes("再")
476
+ || text.includes("让他")
477
+ || text.includes("让她")
478
+ || text.toLowerCase().includes("follow up");
479
+ }
480
+
481
+ function shouldTreatAsSpawnRequest(task) {
482
+ const text = String(task || "").trim();
483
+ if (!text) return false;
484
+ const lowered = text.toLowerCase();
485
+ return text.includes("spawn")
486
+ || text.includes("启动")
487
+ || text.includes("创建")
488
+ || text.includes("新建")
489
+ || lowered.includes("create worker")
490
+ || lowered.includes("start worker");
491
+ }
492
+
493
+ function resolveDelegatedCwd(rawCwd, task, baseCwd, existingWorker = null) {
494
+ const normalized = normalizeDelegatedCwd(rawCwd, baseCwd);
495
+ if (normalized) {
496
+ return normalized;
497
+ }
498
+ if (existingWorker?.cwd && shouldPreferExistingWorker(task, existingWorker.sessionId, existingWorker.displayName)) {
499
+ return existingWorker.cwd;
500
+ }
501
+ return inferTargetDirectory(task, baseCwd);
502
+ }
503
+
504
+ function buildFirstTurnMessage(task) {
505
+ return `${task}
506
+
507
+ Work in your own session. Complete the task and return the actual findings in this same turn. If you inspect files or run commands, report the concrete result directly instead of restating the task. If the task cannot be completed, explain the real failure briefly.`;
508
+ }
509
+
510
+ function normalizeDelegatedText(value) {
511
+ return String(value || "").replace(/\s+/g, " ").trim();
512
+ }
513
+
514
+ function looksLikeDelegatedNoise(value) {
515
+ const normalized = normalizeDelegatedText(value).toLowerCase();
516
+ if (!normalized) return true;
517
+ return normalized.includes("queued message for")
518
+ || normalized.includes("message_update")
519
+ || normalized.includes("looks ready for input")
520
+ || normalized.includes("work in your own session")
521
+ || normalized.includes("reply to the primary agent now")
522
+ || normalized.startsWith("the user asked me")
523
+ || normalized.startsWith("用户要求我");
524
+ }
525
+
526
+ function hasUsableDelegatedText(value) {
527
+ const normalized = normalizeDelegatedText(value);
528
+ return !!normalized && !looksLikeDelegatedNoise(normalized);
529
+ }
530
+
531
+ function isUsableDelegatedTurn(result) {
532
+ return hasUsableDelegatedText(result?.message) || hasUsableDelegatedText(result?.summary);
533
+ }
534
+
535
+ async function readDelegatedSessionResult(cwd, sessionId) {
536
+ const [messageResult, summaryResult] = await Promise.all([
537
+ runMagicShellCli(cwd, ["session-message", "--session", sessionId, "--json"]),
538
+ runMagicShellCli(cwd, ["session-summary", "--session", sessionId, "--json"]),
539
+ ]);
540
+ return {
541
+ sessionId,
542
+ message: messageResult?.message || null,
543
+ summary: summaryResult?.summary || null,
544
+ };
545
+ }
546
+
547
+ async function waitForDelegatedSessionResult(cwd, sessionId, timeoutMs = 20000) {
548
+ const deadline = Date.now() + timeoutMs;
549
+ while (Date.now() <= deadline) {
550
+ const snapshot = await readDelegatedSessionResult(cwd, sessionId);
551
+ if (isUsableDelegatedTurn(snapshot)) {
552
+ return snapshot;
553
+ }
554
+ await sleep(750);
555
+ }
556
+ return null;
557
+ }
558
+
559
+ async function waitForWorkerSession(cwd, sessionId, timeoutMs = 12000) {
560
+ const deadline = Date.now() + timeoutMs;
561
+ while (Date.now() <= deadline) {
562
+ try {
563
+ const workersResult = await runMagicShellCli(cwd, ["workers", "--json"]);
564
+ const workers = Array.isArray(workersResult) ? workersResult : [];
565
+ const worker = workers.find((entry) => entry?.sessionId === sessionId);
566
+ if (worker && worker.status !== "stopped" && worker.status !== "failed") {
567
+ return worker;
568
+ }
569
+ } catch {
570
+ // ignore transient lookup failures and keep waiting
571
+ }
572
+ await sleep(500);
573
+ }
574
+ return null;
575
+ }
576
+
577
+ function formatDelegatedResult(result) {
578
+ const parts = [];
579
+ if (hasUsableDelegatedText(result.message)) {
580
+ parts.push(`Worker reply:\n${result.message}`);
581
+ }
582
+ if (hasUsableDelegatedText(result.summary) && result.summary !== result.message) {
583
+ parts.push(`Worker summary:\n${result.summary}`);
584
+ }
585
+ if (!parts.length) {
586
+ parts.push("The worker completed a turn, but it did not produce a visible assistant reply.");
587
+ }
588
+ return parts.join("\n\n");
589
+ }
590
+
591
+ function taskNeedsExplicitPathResolution(task) {
592
+ const text = String(task || "").trim();
593
+ if (!text) return false;
594
+ const lowered = text.toLowerCase();
595
+ return text.includes("目录")
596
+ || text.includes("路径")
597
+ || text.includes("文件夹")
598
+ || text.includes("在")
599
+ || text.includes("~/")
600
+ || lowered.includes("directory")
601
+ || lowered.includes("folder")
602
+ || lowered.includes("path");
603
+ }
604
+
605
+ function isSpawnOnlyDelegation(task) {
606
+ const text = String(task || "").trim();
607
+ if (!text) return false;
608
+ const lowered = text.toLowerCase();
609
+ const asksToSpawn = text.includes("spawn")
610
+ || text.includes("启动")
611
+ || text.includes("创建")
612
+ || text.includes("新建")
613
+ || lowered.includes("create worker")
614
+ || lowered.includes("start worker");
615
+ const alsoRequestsWork = text.includes("看看")
616
+ || text.includes("查看")
617
+ || text.includes("检查")
618
+ || text.includes("读")
619
+ || text.includes("统计")
620
+ || text.includes("运行")
621
+ || text.includes("执行")
622
+ || lowered.includes("inspect")
623
+ || lowered.includes("check")
624
+ || lowered.includes("read")
625
+ || lowered.includes("list")
626
+ || lowered.includes("summarize")
627
+ || lowered.includes("run ");
628
+ return asksToSpawn && !alsoRequestsWork;
629
+ }
630
+
631
+ function buildDelegatedFailureText({ phase, recoverable, reason, message, cwd, task }) {
632
+ const lines = [
633
+ `Delegated worker turn failed.`,
634
+ `phase: ${phase}`,
635
+ `recoverable: ${recoverable ? "yes" : "no"}`,
636
+ `reason: ${reason}`,
637
+ message,
638
+ ];
639
+ if (cwd) {
640
+ lines.push(`cwd: ${cwd}`);
641
+ }
642
+ if (task) {
643
+ lines.push(`task: ${task}`);
644
+ }
645
+ lines.push("You should continue reasoning from this tool result instead of stopping. If recoverable is yes, either refine the target path or try a new worker turn with corrected arguments.");
646
+ return lines.filter(Boolean).join("\n");
647
+ }
648
+
649
+ function buildDelegatedFailureResult({ phase, recoverable, reason, message, cwd, task, sessionId = null, spawned = false, pluginName = "pie", mode = "steer" }) {
650
+ return {
651
+ content: [{
652
+ type: "text",
653
+ text: buildDelegatedFailureText({ phase, recoverable, reason, message, cwd, task }),
654
+ }],
655
+ details: {
656
+ ok: false,
657
+ phase,
658
+ recoverable,
659
+ reason,
660
+ message,
661
+ cwd: cwd || null,
662
+ task: task || null,
663
+ sessionId,
664
+ spawned,
665
+ pluginName,
666
+ mode,
667
+ },
668
+ isError: false,
669
+ };
670
+ }
671
+
672
+ function clearDelegatedTurnProgress(sessionId) {
673
+ if (!sessionId) return;
674
+ const current = delegatedTurnState.get(sessionId) || { cachedResults: {} };
675
+ delegatedTurnState.set(sessionId, {
676
+ inProgress: false,
677
+ cachedResults: current.cachedResults || {},
678
+ });
679
+ }
680
+
681
+ export default function magicShellAgentExtension(ctx) {
682
+ ctx.log("Magic Shell delegated-turn extension loaded");
683
+ const currentSessionId = getCurrentSessionId();
684
+ const isPrimarySession = !!currentSessionId && currentSessionId.startsWith("primary-");
685
+ appendDebugLog(`load session=${currentSessionId || "unknown"} cwd=${safeCwd(ctx)}`);
686
+
687
+ ctx.on("tool:call", async (event) => {
688
+ if (!isPrimarySession) {
689
+ return undefined;
690
+ }
691
+ if (event.toolName === "magic_shell_delegate_worker_turn") {
692
+ return undefined;
693
+ }
694
+ appendDebugLog(`blocked tool=${event.toolName} session=${currentSessionId || "unknown"}`);
695
+ return {
696
+ block: true,
697
+ reason: "The protected primary agent must use magic_shell_delegate_worker_turn for execution tasks instead of calling other tools directly.",
698
+ };
699
+ });
700
+
701
+ if (currentSessionId) {
702
+ startInboxPoller(ctx, currentSessionId);
703
+ if (isPrimarySession) {
704
+ ctx.on("agent:start", async () => {
705
+ appendDebugLog(`agent:start session=${currentSessionId} setActiveTools=magic_shell_delegate_worker_turn`);
706
+ ctx.setActiveTools(["magic_shell_delegate_worker_turn"]);
707
+ });
708
+ }
709
+ ctx.on("turn:start", async () => {
710
+ const active = activeRequests.get(currentSessionId);
711
+ delegatedTurnState.set(currentSessionId, {
712
+ inProgress: false,
713
+ cachedResults: {},
714
+ });
715
+ writeSessionControlState(currentSessionId, {
716
+ activeRequestId: active?.requestId || null,
717
+ status: "busy",
718
+ });
719
+ });
720
+ ctx.on("turn:end", async (event) => {
721
+ const active = activeRequests.get(currentSessionId);
722
+ writeSessionControlState(currentSessionId, {
723
+ activeRequestId: null,
724
+ lastCompletedRequestId: active?.requestId || null,
725
+ lastTurnIndex: typeof event?.turnIndex === "number" ? event.turnIndex : null,
726
+ lastAssistantText: extractAssistantText(event?.message) || null,
727
+ lastError: null,
728
+ status: "idle",
729
+ });
730
+ delegatedTurnState.delete(currentSessionId);
731
+ activeRequests.delete(currentSessionId);
732
+ });
733
+ ctx.on("agent:end", async () => {
734
+ delegatedTurnState.delete(currentSessionId);
735
+ writeSessionControlState(currentSessionId, {
736
+ status: "idle",
737
+ });
738
+ });
739
+ }
740
+
741
+ if (!isPrimarySession) {
742
+ return;
743
+ }
744
+
745
+ ctx.registerTool({
746
+ name: "magic_shell_delegate_worker_turn",
747
+ description: `Delegate one worker turn through Magic Shell and return the result in the same conversation turn.
748
+
749
+ Use this when the user wants the primary agent to inspect files, run commands, continue work, or ask another session to do something concrete.`,
750
+ parameters: Type.Object({
751
+ task: Type.String({
752
+ description: "The concrete task or instruction for the worker session.",
753
+ }),
754
+ sessionId: Type.Optional(Type.String({
755
+ description: "Preferred worker session ID to reuse when continuing an existing task.",
756
+ })),
757
+ cwd: Type.Optional(Type.String({
758
+ description: "Optional working directory for a new worker session.",
759
+ })),
760
+ workerName: Type.Optional(Type.String({
761
+ description: "Optional display name for the worker session.",
762
+ })),
763
+ pluginName: Type.Optional(Type.String({
764
+ description: "Worker plugin name to use when spawning. Defaults to pie.",
765
+ })),
766
+ mode: Type.Optional(Type.Union([
767
+ Type.Literal("steer"),
768
+ Type.Literal("follow_up"),
769
+ ], {
770
+ description: "Delegation mode. Use steer for a fresh task and follow_up for a continuation.",
771
+ })),
772
+ }, {
773
+ additionalProperties: false,
774
+ }),
775
+ category: "agent",
776
+ async execute(args, context) {
777
+ const turnState = currentSessionId
778
+ ? (delegatedTurnState.get(currentSessionId) || { inProgress: false, cachedResults: {} })
779
+ : { inProgress: false, cachedResults: {} };
780
+ const pluginName = args.pluginName || "pie";
781
+ const mode = args.mode === "follow_up" ? "follow_up" : "steer";
782
+ let runtimeSnapshot;
783
+ try {
784
+ runtimeSnapshot = await runMagicShellCli(context.cwd, ["status", "--json"]);
785
+ } catch (error) {
786
+ clearDelegatedTurnProgress(currentSessionId);
787
+ return buildDelegatedFailureResult({
788
+ phase: "runtime_snapshot",
789
+ recoverable: true,
790
+ reason: "runtime_unavailable",
791
+ message: error instanceof Error ? error.message : String(error),
792
+ cwd: null,
793
+ task: args.task,
794
+ pluginName,
795
+ mode,
796
+ });
797
+ }
798
+ const workerName = args.workerName
799
+ || inferWorkerName(args.task)
800
+ || inferMentionedWorkerName(args.task, runtimeSnapshot);
801
+ let worker = pickWorker(runtimeSnapshot, args.sessionId, pluginName, null, workerName, {
802
+ allowExactReuseAcrossCwd: true,
803
+ });
804
+ const delegatedCwd = resolveDelegatedCwd(args.cwd, args.task, context.cwd, worker);
805
+ const cacheKey = buildDelegationCacheKey({
806
+ sessionId: args.sessionId,
807
+ workerName,
808
+ cwd: delegatedCwd,
809
+ mode,
810
+ task: args.task,
811
+ });
812
+ const cachedResult = currentSessionId ? turnState.cachedResults?.[cacheKey] : null;
813
+ if (currentSessionId && cachedResult) {
814
+ appendDebugLog(`tool reuse cached session=${currentSessionId} key=${cacheKey}`);
815
+ return {
816
+ ...cachedResult,
817
+ details: {
818
+ ...(cachedResult.details || {}),
819
+ reusedWithinTurn: true,
820
+ },
821
+ };
822
+ }
823
+
824
+ appendDebugLog(`tool execute session=${currentSessionId || "unknown"} task=${String(args.task || "").slice(0, 120)}`);
825
+ appendDebugLog(`tool plan session=${currentSessionId || "unknown"} cwd=${delegatedCwd || "(default)"} mode=${mode}`);
826
+ if (currentSessionId) {
827
+ delegatedTurnState.set(currentSessionId, {
828
+ ...turnState,
829
+ inProgress: true,
830
+ });
831
+ }
832
+ context.reportProgress({ message: "Checking runtime" });
833
+
834
+ if (!delegatedCwd && taskNeedsExplicitPathResolution(args.task)) {
835
+ clearDelegatedTurnProgress(currentSessionId);
836
+ return buildDelegatedFailureResult({
837
+ phase: "resolve_path",
838
+ recoverable: true,
839
+ reason: "target_path_unresolved",
840
+ message: "Could not resolve the target directory from the task description. First determine the concrete path, then call the worker tool again.",
841
+ cwd: null,
842
+ task: args.task,
843
+ pluginName,
844
+ mode,
845
+ });
846
+ }
847
+
848
+ worker = pickWorker(runtimeSnapshot, args.sessionId, pluginName, delegatedCwd, workerName, {
849
+ allowExactReuseAcrossCwd: !shouldTreatAsSpawnRequest(args.task),
850
+ }) || worker;
851
+ let spawned = false;
852
+
853
+ if (!worker) {
854
+ context.reportProgress({ message: "Spawning worker" });
855
+ let spawnResult;
856
+ try {
857
+ spawnResult = await runMagicShellCli(context.cwd, [
858
+ "spawn",
859
+ "--plugin", pluginName,
860
+ "--task", args.task,
861
+ ...(workerName ? ["--name", workerName] : []),
862
+ ...(delegatedCwd ? ["--cwd", delegatedCwd] : []),
863
+ "--json",
864
+ ]);
865
+ } catch (error) {
866
+ clearDelegatedTurnProgress(currentSessionId);
867
+ return buildDelegatedFailureResult({
868
+ phase: "spawn_worker",
869
+ recoverable: true,
870
+ reason: "spawn_failed",
871
+ message: error instanceof Error ? error.message : String(error),
872
+ cwd: delegatedCwd,
873
+ task: args.task,
874
+ pluginName,
875
+ mode,
876
+ spawned: false,
877
+ });
878
+ }
879
+ const sessionId = spawnResult?.sessionId;
880
+ if (!sessionId) {
881
+ clearDelegatedTurnProgress(currentSessionId);
882
+ return buildDelegatedFailureResult({
883
+ phase: "spawn_worker",
884
+ recoverable: true,
885
+ reason: "missing_session_id",
886
+ message: "Magic Shell did not return a worker session ID after spawn.",
887
+ cwd: delegatedCwd,
888
+ task: args.task,
889
+ pluginName,
890
+ mode,
891
+ spawned: false,
892
+ });
893
+ }
894
+ spawned = true;
895
+ worker = { sessionId, agentType: pluginName };
896
+ const readyWorker = await waitForWorkerSession(context.cwd, sessionId, 12000);
897
+ if (readyWorker) {
898
+ worker = readyWorker;
899
+ } else {
900
+ await sleep(1200);
901
+ }
902
+ }
903
+
904
+ const sessionId = worker.sessionId;
905
+ if (isSpawnOnlyDelegation(args.task)) {
906
+ const response = {
907
+ content: [{
908
+ type: "text",
909
+ text: [
910
+ `Spawned worker session ${sessionId}.`,
911
+ workerName ? `Worker name: ${workerName}` : null,
912
+ delegatedCwd ? `Working directory: ${delegatedCwd}` : null,
913
+ "Worker is ready for follow-up tasks.",
914
+ ].filter(Boolean).join("\n"),
915
+ }],
916
+ details: {
917
+ ok: true,
918
+ sessionId,
919
+ spawned,
920
+ pluginName,
921
+ displayName: workerName || null,
922
+ cwd: delegatedCwd || null,
923
+ mode,
924
+ spawnOnly: true,
925
+ },
926
+ isError: false,
927
+ };
928
+ if (currentSessionId) {
929
+ delegatedTurnState.set(currentSessionId, {
930
+ inProgress: false,
931
+ cachedResults: {
932
+ ...(turnState.cachedResults || {}),
933
+ [cacheKey]: response,
934
+ },
935
+ });
936
+ }
937
+ return response;
938
+ }
939
+ context.reportProgress({ message: `Delegating turn to ${sessionId.slice(0, 8)}...` });
940
+
941
+ const attemptArgs = [
942
+ "session-turn",
943
+ "--session", sessionId,
944
+ "--mode", mode,
945
+ "--data",
946
+ ];
947
+ let first;
948
+ try {
949
+ first = await runMagicShellCli(context.cwd, [
950
+ ...attemptArgs,
951
+ buildFirstTurnMessage(args.task),
952
+ "--timeout-ms", "35000",
953
+ "--poll-interval-ms", "250",
954
+ "--json",
955
+ ]);
956
+ } catch (error) {
957
+ clearDelegatedTurnProgress(currentSessionId);
958
+ return buildDelegatedFailureResult({
959
+ phase: "delegate_turn",
960
+ recoverable: true,
961
+ reason: "session_turn_failed",
962
+ message: error instanceof Error ? error.message : String(error),
963
+ cwd: delegatedCwd,
964
+ task: args.task,
965
+ pluginName,
966
+ mode,
967
+ sessionId,
968
+ spawned,
969
+ });
970
+ }
971
+
972
+ let finalTurn = first;
973
+ if (!isUsableDelegatedTurn(finalTurn)) {
974
+ context.reportProgress({ message: "Waiting for the worker to settle the delegated turn" });
975
+ const settled = await waitForDelegatedSessionResult(context.cwd, sessionId, 12000);
976
+ if (settled) {
977
+ finalTurn = {
978
+ ...finalTurn,
979
+ ...settled,
980
+ changed: true,
981
+ timedOut: false,
982
+ };
983
+ }
984
+ }
985
+
986
+ const text = [
987
+ `Delegated to worker session ${sessionId}.`,
988
+ formatDelegatedResult(finalTurn),
989
+ ].join("\n\n");
990
+
991
+ const response = {
992
+ content: [{ type: "text", text }],
993
+ details: {
994
+ ok: true,
995
+ sessionId,
996
+ spawned,
997
+ pluginName,
998
+ displayName: workerName || null,
999
+ cwd: delegatedCwd || null,
1000
+ mode,
1001
+ message: finalTurn?.message || null,
1002
+ summary: finalTurn?.summary || null,
1003
+ timedOut: !!finalTurn?.timedOut,
1004
+ },
1005
+ isError: !!finalTurn?.timedOut && !finalTurn?.message && !finalTurn?.summary,
1006
+ };
1007
+ if (currentSessionId) {
1008
+ delegatedTurnState.set(currentSessionId, {
1009
+ inProgress: false,
1010
+ cachedResults: {
1011
+ ...(turnState.cachedResults || {}),
1012
+ [cacheKey]: response,
1013
+ },
1014
+ });
1015
+ }
1016
+ return response;
1017
+ },
1018
+ });
1019
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@magicshell/primary-pie-extension",
3
+ "private": true,
4
+ "type": "module",
5
+ "pie": {
6
+ "name": "@magicshell/primary-pie-extension",
7
+ "main": "index.js"
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getPackagedPluginDir(): string | null;
2
+ export declare function getWorkbenchRoot(): string | null;
3
+ export declare function getPrimaryPieExtensionDistDir(): string | null;
4
+ export declare function getMagicShellCliPath(): string | null;
@@ -0,0 +1,83 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function currentDistDir() {
5
+ return path.dirname(fileURLToPath(import.meta.url));
6
+ }
7
+ function findExistingDirectory(candidates) {
8
+ for (const candidate of candidates) {
9
+ if (!candidate)
10
+ continue;
11
+ try {
12
+ if (existsSync(candidate)) {
13
+ return candidate;
14
+ }
15
+ }
16
+ catch {
17
+ // Ignore invalid paths while probing runtime assets.
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ function findExistingFile(candidates) {
23
+ for (const candidate of candidates) {
24
+ if (!candidate)
25
+ continue;
26
+ try {
27
+ if (existsSync(candidate)) {
28
+ return candidate;
29
+ }
30
+ }
31
+ catch {
32
+ // Ignore invalid paths while probing runtime assets.
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ export function getPackagedPluginDir() {
38
+ const currentDir = currentDistDir();
39
+ return findExistingDirectory([
40
+ process.env.MAGIC_SHELL_PLUGINS_DIR ? path.resolve(process.env.MAGIC_SHELL_PLUGINS_DIR) : null,
41
+ path.resolve(currentDir, "./plugins"),
42
+ // Development-only fallback for source checkouts.
43
+ path.resolve(currentDir, "../../../plugins"),
44
+ ]);
45
+ }
46
+ export function getWorkbenchRoot() {
47
+ const currentDir = currentDistDir();
48
+ const candidates = [
49
+ process.env.MAGIC_SHELL_WORKBENCH_ROOT ? path.resolve(process.env.MAGIC_SHELL_WORKBENCH_ROOT) : null,
50
+ path.resolve(currentDir, "./workbench"),
51
+ // Development-only fallback for source checkouts.
52
+ path.resolve(currentDir, "../../../apps/web/src"),
53
+ ];
54
+ for (const candidate of candidates) {
55
+ if (!candidate)
56
+ continue;
57
+ if (existsSync(path.join(candidate, "index.html"))) {
58
+ return candidate;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ export function getPrimaryPieExtensionDistDir() {
64
+ const currentDir = currentDistDir();
65
+ return findExistingDirectory([
66
+ process.env.MAGIC_SHELL_PRIMARY_EXTENSION_DIR ? path.resolve(process.env.MAGIC_SHELL_PRIMARY_EXTENSION_DIR) : null,
67
+ path.resolve(currentDir, "./primary-pie-extension"),
68
+ // Development-only fallback for source checkouts.
69
+ path.resolve(currentDir, "../../primary-pie-extension/dist"),
70
+ ]);
71
+ }
72
+ export function getMagicShellCliPath() {
73
+ const currentDir = currentDistDir();
74
+ return findExistingFile([
75
+ process.env.MAGIC_SHELL_CLI_PATH ? path.resolve(process.env.MAGIC_SHELL_CLI_PATH) : null,
76
+ // Installed alongside @cydm/magic-shell under the same @cydm scope.
77
+ path.resolve(currentDir, "../../magic-shell/dist/cli.js"),
78
+ // Installed under @cydm/magic-shell -> node_modules/@cydm/magic-shell-agent-node/dist
79
+ path.resolve(currentDir, "../../../../dist/cli.js"),
80
+ // Development-only fallback for source checkouts.
81
+ path.resolve(currentDir, "../../cli/dist/cli.js"),
82
+ ]);
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/magic-shell-agent-node",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Magic Shell Agent Node - Local agent connector",
5
5
  "homepage": "https://magicshell.ai",
6
6
  "keywords": [