nolo-cli 0.1.23 → 0.1.26

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 (76) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/hybridRecordStore.ts +30 -9
  3. package/agent-runtime/localLoop.ts +12 -4
  4. package/agent-runtime/localToolPolicy.ts +22 -10
  5. package/agent-runtime/localWorkspaceTools.ts +140 -374
  6. package/agent-runtime/runtimeToolPolicy.ts +43 -2
  7. package/agent-runtime/types.ts +5 -0
  8. package/agentAliases.ts +1 -1
  9. package/agentCommandSupport.ts +77 -0
  10. package/agentInternalCommandEntries.ts +34 -0
  11. package/agentListCommands.ts +123 -0
  12. package/agentListHelpers.ts +187 -0
  13. package/agentMachineCommands.ts +402 -0
  14. package/agentRecordCommands.ts +141 -0
  15. package/agentRecordHelpers.ts +395 -0
  16. package/agentRunCommand.ts +1 -2
  17. package/agentRuntimeCommands.ts +7 -701
  18. package/ai/agent/_executeModel.ts +29 -3
  19. package/ai/agent/buildSystemPrompt.ts +17 -0
  20. package/ai/agent/cliChatClient.ts +38 -4
  21. package/ai/agent/cliExecutor.ts +304 -25
  22. package/ai/agent/createAgentSchema.ts +2 -2
  23. package/ai/agent/guidedCreation/assistant.ts +82 -0
  24. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  25. package/ai/agent/guidedCreation/draft.ts +62 -0
  26. package/ai/agent/guidedCreation/types.ts +38 -0
  27. package/ai/agent/machineRunPermissions.ts +21 -3
  28. package/ai/agent/runtimeGuidance.ts +1 -1
  29. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  30. package/ai/agent/streamAgentChatTurn.ts +492 -19
  31. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  32. package/ai/ai.locale.ts +14 -2
  33. package/ai/chat/sendOpenAICompletionsRequest.ts +28 -3
  34. package/ai/chat/sendOpenAIResponseRequest.ts +3 -2
  35. package/ai/llm/getPricing.ts +14 -0
  36. package/ai/llm/kimi.ts +3 -0
  37. package/ai/llm/modelAvatar.ts +1 -0
  38. package/ai/llm/providers.ts +1 -1
  39. package/ai/taskRun/taskRunProtocol.ts +24 -10
  40. package/ai/token/modelUsageQuery.ts +17 -0
  41. package/ai/tools/agent/agentTools.ts +21 -0
  42. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  43. package/ai/tools/checkEnvTool.ts +52 -20
  44. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  45. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  46. package/ai/tools/index.ts +24 -14
  47. package/ai/tools/marxistsOfflineBook.ts +253 -0
  48. package/ai/tools/modelUsageTools.ts +4 -2
  49. package/ai/tools/readTool.ts +31 -0
  50. package/ai/tools/toolApiClient.ts +35 -1
  51. package/ai/tools/toolNameAliases.ts +13 -0
  52. package/cliCommandAdapters.ts +28 -0
  53. package/cliCommandDispatch.ts +66 -0
  54. package/cliCommandFactories.ts +34 -0
  55. package/cliCommandTypes.ts +49 -0
  56. package/cliEnvHelpers.ts +44 -0
  57. package/client/agentRun.test.ts +123 -19
  58. package/client/agentRun.ts +40 -1
  59. package/client/hybridRecordStore.test.ts +42 -0
  60. package/client/hybridRecordStore.ts +11 -0
  61. package/client/localRuntimeAdapter.test.ts +189 -21
  62. package/client/localRuntimeAdapter.ts +84 -8
  63. package/client/localRuntimeDryRun.test.ts +5 -2
  64. package/client/taskRunPrompt.ts +1 -0
  65. package/commandRegistry.ts +11 -61
  66. package/connector-experimental/capabilities.ts +1 -0
  67. package/database/server/coreDataProxy.ts +43 -26
  68. package/index.ts +54 -191
  69. package/internalCommandEntries.ts +12 -0
  70. package/machineCommands.ts +301 -32
  71. package/offlineMarxistsAgentCommand.ts +314 -0
  72. package/package.json +20 -2
  73. package/runtimeDoctorCommands.ts +118 -0
  74. package/scriptCommandEntries.ts +71 -0
  75. package/systemInternalCommandEntries.ts +71 -0
  76. package/workflowInternalCommandEntries.ts +11 -0
package/ai/tools/index.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  createWorkflowFunc,
23
23
  } from "./createWorkflowTool";
24
24
  import { delayFunctionSchema, delayFunc } from "./delayTool";
25
+ import { canonicalizeToolName } from "./toolNameAliases";
25
26
  import {
26
27
  completeDialogGoalFunc,
27
28
  completeDialogGoalFunctionSchema,
@@ -160,6 +161,10 @@ import {
160
161
  fetchWebpageFunctionSchema,
161
162
  fetchWebpageFunc,
162
163
  } from "./fetchWebpageTool";
164
+ import {
165
+ convertMarxistsBookToOfflineHtmlFunctionSchema,
166
+ convertMarxistsBookToOfflineHtmlFunc,
167
+ } from "./convertMarxistsBookTool";
163
168
  import {
164
169
  readXPostFunctionSchema,
165
170
  readXPostFunc,
@@ -281,7 +286,7 @@ import {
281
286
  rememberMemoryFunc,
282
287
  rememberMemoryFunctionSchema,
283
288
  } from "./rememberMemoryTool";
284
- import { execBashFunctionSchema, execBashFunc, execShellFunctionSchema, execShellFunc } from "./execBashTool";
289
+ import { execShellFunctionSchema, execShellFunc } from "./execShellTool";
285
290
  import { checkEnvFunctionSchema, checkEnvFunc } from "./checkEnvTool";
286
291
 
287
292
  import { agentToolDefinitions } from "./agent/agentTools";
@@ -1464,6 +1469,23 @@ const baseToolDefinitions: ToolDefinition[] = [
1464
1469
  },
1465
1470
  behavior: "data",
1466
1471
  },
1472
+ {
1473
+ id: "convertMarxistsBookToOfflineHtml",
1474
+ schema: convertMarxistsBookToOfflineHtmlFunctionSchema,
1475
+ executor: convertMarxistsBookToOfflineHtmlFunc,
1476
+ description: {
1477
+ name: "convertMarxistsBookToOfflineHtml",
1478
+ description:
1479
+ "将 Marxists.org 中文旧式书籍页面转换成保留原 CSS、背景和排版的离线单 HTML。",
1480
+ category: "网络与智能",
1481
+ },
1482
+ behavior: "data",
1483
+ capability: "web_access",
1484
+ riskLevel: "low",
1485
+ costLevel: "medium",
1486
+ defaultConsent: "auto",
1487
+ cancelable: true,
1488
+ },
1467
1489
  {
1468
1490
  id: "readXPost",
1469
1491
  schema: readXPostFunctionSchema,
@@ -1837,18 +1859,6 @@ const baseToolDefinitions: ToolDefinition[] = [
1837
1859
  behavior: "data",
1838
1860
  uiGroup: "general",
1839
1861
  },
1840
- {
1841
- id: "execBash",
1842
- schema: execBashFunctionSchema,
1843
- executor: execBashFunc,
1844
- description: {
1845
- name: "exec_bash",
1846
- description:
1847
- "通过后端 diff-server 的 /api/exec-bash 接口执行一条 bash 命令,仅用于本地开发调试。",
1848
- category: "网络与智能",
1849
- },
1850
- behavior: "action",
1851
- },
1852
1862
  {
1853
1863
  id: "execShell",
1854
1864
  schema: execShellFunctionSchema,
@@ -2070,7 +2080,7 @@ export const findToolExecutor = (
2070
2080
  executor: ToolDefinition["executor"];
2071
2081
  canonicalName: string;
2072
2082
  } => {
2073
- const normalizedRawName = normalizeToolName(rawName);
2083
+ const normalizedRawName = normalizeToolName(canonicalizeToolName(rawName));
2074
2084
  const canonicalName = Object.keys(toolExecutors).find(
2075
2085
  (key) => normalizeToolName(key) === normalizedRawName
2076
2086
  );
@@ -0,0 +1,253 @@
1
+ export type MarxistsOfflineBookPage = {
2
+ name: string;
3
+ url: string;
4
+ title: string;
5
+ };
6
+
7
+ export type MarxistsOfflineBookResult = {
8
+ title: string;
9
+ startUrl: string;
10
+ indexUrl: string;
11
+ stylesheetUrl: string | null;
12
+ pages: MarxistsOfflineBookPage[];
13
+ html: string;
14
+ validation: {
15
+ pageCount: number;
16
+ hasEmbeddedAssets: boolean;
17
+ hasNetworkUrls: boolean;
18
+ hasUtf8Meta: boolean;
19
+ byteLength: number;
20
+ };
21
+ };
22
+
23
+ export type ConvertMarxistsBookArgs = {
24
+ startUrl: string;
25
+ encoding?: string;
26
+ fetchImpl?: typeof fetch;
27
+ maxPages?: number;
28
+ };
29
+
30
+ const DEFAULT_ENCODING = "gb2312";
31
+ const USER_AGENT =
32
+ "Mozilla/5.0 (compatible; NoloMarxistsOfflineBook/1.0; +https://nolo.chat)";
33
+
34
+ const normalizeBookBaseUrl = (startUrl: string): URL => {
35
+ const parsed = new URL(startUrl);
36
+ if (!/^https?:$/i.test(parsed.protocol)) {
37
+ throw new Error("startUrl must be an http or https URL.");
38
+ }
39
+ return new URL("./", parsed);
40
+ };
41
+
42
+ const decodeBytes = (bytes: ArrayBuffer, encoding: string): string => {
43
+ try {
44
+ return new TextDecoder(encoding).decode(bytes);
45
+ } catch {
46
+ return new TextDecoder(DEFAULT_ENCODING).decode(bytes);
47
+ }
48
+ };
49
+
50
+ const fetchBytes = async (
51
+ url: string,
52
+ fetchImpl: typeof fetch,
53
+ ): Promise<ArrayBuffer> => {
54
+ const response = await fetchImpl(url, {
55
+ headers: {
56
+ "user-agent": USER_AGENT,
57
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
58
+ },
59
+ });
60
+ if (!response.ok) {
61
+ throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
62
+ }
63
+ return response.arrayBuffer();
64
+ };
65
+
66
+ const extractTitle = (html: string): string => {
67
+ return (
68
+ html.match(/<title>([\s\S]*?)<\/title>/i)?.[1]
69
+ ?.replace(/<[^>]+>/g, " ")
70
+ .replace(/\s+/g, " ")
71
+ .trim() || ""
72
+ );
73
+ };
74
+
75
+ const extractBody = (html: string): string => {
76
+ return html.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? html;
77
+ };
78
+
79
+ export const discoverMarxistsBookPageNames = (
80
+ indexHtml: string,
81
+ maxPages = 80,
82
+ ): string[] => {
83
+ const pageNames = [...indexHtml.matchAll(/href=["']?([^"' >#]+\.htm)(#[^"' >]*)?["']?/gi)]
84
+ .map((match) => match[1])
85
+ .filter((href) => !href.includes("/") && !href.startsWith(".."))
86
+ .map((href) => href.trim())
87
+ .filter(Boolean)
88
+ .sort();
89
+
90
+ const uniqueNames = Array.from(new Set(["index.htm", ...pageNames]));
91
+ if (uniqueNames.length <= 1) {
92
+ throw new Error("No same-directory .htm chapter links were found in index.htm.");
93
+ }
94
+ if (uniqueNames.length > maxPages) {
95
+ throw new Error(
96
+ `Refusing to download ${uniqueNames.length} pages; maxPages is ${maxPages}.`,
97
+ );
98
+ }
99
+ return uniqueNames;
100
+ };
101
+
102
+ const extractStylesheetHref = (html: string): string | null => {
103
+ return (
104
+ html.match(/<link[^>]+href=["']([^"']+\.css)["'][^>]*>/i)?.[1] ?? null
105
+ );
106
+ };
107
+
108
+ const guessMimeType = (url: string): string => {
109
+ const pathname = new URL(url).pathname.toLowerCase();
110
+ if (pathname.endsWith(".gif")) return "image/gif";
111
+ if (pathname.endsWith(".png")) return "image/png";
112
+ if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
113
+ if (pathname.endsWith(".webp")) return "image/webp";
114
+ if (pathname.endsWith(".css")) return "text/css";
115
+ return "application/octet-stream";
116
+ };
117
+
118
+ export const inlineCssUrlAssets = async (args: {
119
+ css: string;
120
+ cssUrl: string;
121
+ fetchImpl: typeof fetch;
122
+ }): Promise<string> => {
123
+ const replacements = new Map<string, string>();
124
+ const matches = [...args.css.matchAll(/url\((['"]?)([^)'"]+)\1\)/gi)];
125
+
126
+ for (const match of matches) {
127
+ const rawRef = match[2].trim();
128
+ if (!rawRef || /^(data:|https?:)/i.test(rawRef)) continue;
129
+ const assetUrl = new URL(rawRef, args.cssUrl).toString();
130
+ if (!replacements.has(rawRef)) {
131
+ const bytes = await fetchBytes(assetUrl, args.fetchImpl);
132
+ const base64 = Buffer.from(bytes).toString("base64");
133
+ replacements.set(
134
+ rawRef,
135
+ `data:${guessMimeType(assetUrl)};base64,${base64}`,
136
+ );
137
+ }
138
+ }
139
+
140
+ return args.css.replace(/url\((['"]?)([^)'"]+)\1\)/gi, (full, quote, rawRef) => {
141
+ const replacement = replacements.get(String(rawRef).trim());
142
+ return replacement ? `url('${replacement}')` : full;
143
+ });
144
+ };
145
+
146
+ const rewriteLocalChapterLinks = (body: string): string => {
147
+ return body.replace(
148
+ /href=["']([^"']+\.htm)(#[^"']*)?["']/gi,
149
+ (_full, href: string, hash: string | undefined) => {
150
+ const anchor = href.replace(/\.htm$/i, "");
151
+ return `href="#${anchor}${hash ? `-${hash.replace(/^#/, "")}` : ""}"`;
152
+ },
153
+ );
154
+ };
155
+
156
+ const buildSingleHtml = (args: {
157
+ title: string;
158
+ css: string;
159
+ pages: Array<MarxistsOfflineBookPage & { body: string }>;
160
+ }): string => {
161
+ const sections = args.pages.map((page) => {
162
+ const anchor = page.name.replace(/\.htm$/i, "");
163
+ return `<section class="chapter" id="${anchor}"><div class="chapter-source">${page.title}</div>${rewriteLocalChapterLinks(page.body)}</section>`;
164
+ });
165
+
166
+ return `<!doctype html>
167
+ <html lang="zh-CN">
168
+ <head>
169
+ <meta charset="utf-8">
170
+ <meta name="viewport" content="width=device-width, initial-scale=1">
171
+ <title>${args.title}</title>
172
+ <style>
173
+ ${args.css}
174
+ body { margin: 0 10px; }
175
+ .table1 { max-width: 960px; width: min(960px, calc(100vw - 20px)); }
176
+ .chapter { page-break-after: always; break-after: page; }
177
+ .chapter-source { display: none; }
178
+ @media screen and (max-width: 700px) {
179
+ html { margin-left: 8px !important; margin-right: 8px !important; }
180
+ body { margin-left: 8px !important; margin-right: 8px !important; }
181
+ .table1 { width: 100% !important; max-width: 100% !important; }
182
+ table { max-width: 100% !important; }
183
+ }
184
+ @media print {
185
+ .chapter { page-break-after: always; }
186
+ }
187
+ </style>
188
+ </head>
189
+ <body id="top">
190
+ ${sections.join("\n")}
191
+ </body>
192
+ </html>`;
193
+ };
194
+
195
+ export async function convertMarxistsBookToOfflineHtml(
196
+ args: ConvertMarxistsBookArgs,
197
+ ): Promise<MarxistsOfflineBookResult> {
198
+ const startUrl = args.startUrl?.trim();
199
+ if (!startUrl) throw new Error("startUrl is required.");
200
+
201
+ const fetchImpl = args.fetchImpl ?? fetch;
202
+ const encoding = args.encoding || DEFAULT_ENCODING;
203
+ const bookBaseUrl = normalizeBookBaseUrl(startUrl);
204
+ const indexUrl = new URL("index.htm", bookBaseUrl).toString();
205
+ const indexHtml = decodeBytes(await fetchBytes(indexUrl, fetchImpl), encoding);
206
+ const pageNames = discoverMarxistsBookPageNames(indexHtml, args.maxPages);
207
+
208
+ const pages: Array<MarxistsOfflineBookPage & { body: string }> = [];
209
+ for (const name of pageNames) {
210
+ const pageUrl = new URL(name, bookBaseUrl).toString();
211
+ const html = decodeBytes(await fetchBytes(pageUrl, fetchImpl), encoding);
212
+ pages.push({
213
+ name,
214
+ url: pageUrl,
215
+ title: extractTitle(html),
216
+ body: extractBody(html),
217
+ });
218
+ }
219
+
220
+ const stylesheetHref =
221
+ extractStylesheetHref(indexHtml) ||
222
+ pages.map((page) => extractStylesheetHref(page.body)).find(Boolean) ||
223
+ "../../MIA01.css";
224
+ const stylesheetUrl = stylesheetHref
225
+ ? new URL(stylesheetHref, indexUrl).toString()
226
+ : null;
227
+ const rawCss = stylesheetUrl
228
+ ? decodeBytes(await fetchBytes(stylesheetUrl, fetchImpl), encoding)
229
+ : "";
230
+ const css = stylesheetUrl
231
+ ? await inlineCssUrlAssets({ css: rawCss, cssUrl: stylesheetUrl, fetchImpl })
232
+ : rawCss;
233
+
234
+ const title = extractTitle(indexHtml) || pages[0]?.title || "Offline Book";
235
+ const html = buildSingleHtml({ title, css, pages });
236
+ const byteLength = new TextEncoder().encode(html).byteLength;
237
+
238
+ return {
239
+ title,
240
+ startUrl,
241
+ indexUrl,
242
+ stylesheetUrl,
243
+ pages: pages.map(({ body: _body, ...page }) => page),
244
+ html,
245
+ validation: {
246
+ pageCount: pages.length,
247
+ hasEmbeddedAssets: html.includes("data:image/"),
248
+ hasNetworkUrls: /https?:\/\//i.test(html),
249
+ hasUtf8Meta: html.includes('<meta charset="utf-8">'),
250
+ byteLength,
251
+ },
252
+ };
253
+ }
@@ -52,11 +52,13 @@ export const queryModelUsageFunctionSchema = {
52
52
  },
53
53
  thresholdCredits: {
54
54
  type: "number",
55
- description: "可选阈值,单位 credits;返回 threshold.exceeded。",
55
+ description:
56
+ "可选阈值,单位 credits;返回 threshold.exceeded 与 0-100 的 threshold.usedPercent。",
56
57
  },
57
58
  thresholdUsd: {
58
59
  type: "number",
59
- description: "可选阈值,单位 USD;会按 creditsPerUsd 换算。",
60
+ description:
61
+ "可选阈值,单位 USD;会按 creditsPerUsd 换算,并返回 0-100 的 threshold.usedPercent。",
60
62
  },
61
63
  },
62
64
  },
@@ -9,6 +9,7 @@ import { isTableMetaKey, rowKey, isAgentKey } from "database/keys";
9
9
  import { DataType } from "create/types";
10
10
  import { TableMeta } from "render/table/types";
11
11
  import { fetchAndSerializeTable } from "render/table/utils/tableSerialization";
12
+ import { readFileFunc } from "./readFileTool";
12
13
 
13
14
  // ---- Types ----
14
15
 
@@ -24,6 +25,13 @@ export type ReadArgs = {
24
25
  * - true:使用 readAndWaitAction,等待远程与本地决策后返回“权威”结果
25
26
  */
26
27
  waitRemote?: boolean;
28
+
29
+ /**
30
+ * Backward-compatible escape hatch for models that call read with a local
31
+ * file path even when the readFile tool is available.
32
+ */
33
+ filePath?: string;
34
+ path?: string;
27
35
  };
28
36
 
29
37
  // ---- 工具 Schema,供 LLM 调用 ----
@@ -68,6 +76,29 @@ export async function readFunc(
68
76
  context?: { parentMessageId?: string; signal?: AbortSignal }
69
77
  ): Promise<{ rawData: any; displayData?: string }> {
70
78
  const { dbKey, waitRemote = false } = args || {};
79
+ const localFilePath =
80
+ typeof args?.filePath === "string"
81
+ ? args.filePath
82
+ : typeof args?.path === "string"
83
+ ? args.path
84
+ : "";
85
+
86
+ if (!dbKey && localFilePath) {
87
+ const result = await readFileFunc(
88
+ {
89
+ filePath: localFilePath,
90
+ startLine: (args as any).startLine,
91
+ endLine: (args as any).endLine,
92
+ },
93
+ thunkApi,
94
+ context
95
+ );
96
+ return {
97
+ rawData: result,
98
+ displayData:
99
+ typeof result === "string" ? result : JSON.stringify(result, null, 2),
100
+ };
101
+ }
71
102
 
72
103
  if (!dbKey || typeof dbKey !== "string") {
73
104
  throw new Error("read 工具需要提供字符串类型的 id(dbKey)。");
@@ -14,6 +14,7 @@
14
14
 
15
15
  import type { RootState } from "app/store";
16
16
  import { selectCurrentServer } from "app/settings/settingSlice";
17
+ import { getIsDesktopApp } from "app/utils/env";
17
18
  import { selectCurrentToken } from "auth/authSlice";
18
19
  import { selectCurrentDialogKey } from "chat/dialog/dialogSlice";
19
20
  import { extractCustomId } from "core/prefix";
@@ -39,6 +40,34 @@ export const resolveToolBaseUrl = (currentServer?: string | null): string => {
39
40
  return currentServer.replace(/\/+$/, "");
40
41
  };
41
42
 
43
+ const DESKTOP_LOCAL_TOOL_PATHS = new Set([
44
+ "/api/exec-shell",
45
+ "/api/check-env",
46
+ "/api/read-file",
47
+ "/api/write-file",
48
+ "/api/apply-edit",
49
+ "/api/apply-line-edits",
50
+ "/api/code-search",
51
+ "/api/search-repo",
52
+ ]);
53
+
54
+ const getWindowOrigin = (): string | null => {
55
+ const _window = (globalThis as any).window;
56
+ const origin = _window?.location?.origin;
57
+ return typeof origin === "string" && origin ? origin.replace(/\/+$/, "") : null;
58
+ };
59
+
60
+ export const resolveToolApiBaseUrl = (
61
+ currentServer?: string | null,
62
+ path?: string
63
+ ): string => {
64
+ if (path && DESKTOP_LOCAL_TOOL_PATHS.has(path) && getIsDesktopApp()) {
65
+ const localOrigin = getWindowOrigin();
66
+ if (localOrigin) return localOrigin;
67
+ }
68
+ return resolveToolBaseUrl(currentServer);
69
+ };
70
+
42
71
  export const getRequestConfig = (thunkApi: any): RequestConfig => {
43
72
  const state = thunkApi.getState() as RootState;
44
73
  const currentServer = selectCurrentServer(state);
@@ -118,6 +147,9 @@ export const buildToolRequestHeaders = (
118
147
  if (agentKey && typeof agentKey === "string" && agentKey.trim()) {
119
148
  headers["X-Nolo-Agent-Key"] = agentKey.trim();
120
149
  }
150
+ if (typeof window !== "undefined" && (window as any).__NOLO_DESKTOP__) {
151
+ headers["X-Nolo-Desktop-Tool"] = "1";
152
+ }
121
153
  return headers;
122
154
  };
123
155
 
@@ -152,7 +184,9 @@ export async function callToolApi<T = any>(
152
184
  ): Promise<T> {
153
185
  const { currentServer } = getRequestConfig(thunkApi);
154
186
 
155
- const url = `${currentServer}${path}`;
187
+ const baseUrl = resolveToolApiBaseUrl(currentServer, path);
188
+ if (!baseUrl) throw new Error("无法获取工具服务器地址。");
189
+ const url = `${baseUrl}${path}`;
156
190
  const headers = buildToolRequestHeaders(thunkApi, options);
157
191
 
158
192
  const response = await fetch(url, {
@@ -15,6 +15,19 @@ const LEGACY_TOOL_NAME_ALIASES: Record<string, string> = {
15
15
  read_doc: "readDoc",
16
16
  createdoc: "createDoc",
17
17
  updateDocTool: "updateDoc",
18
+ readLocal: "readLocalFile",
19
+ read_local: "readLocalFile",
20
+ readLocalTool: "readLocalFile",
21
+ listLocal: "listLocalFiles",
22
+ list_local: "listLocalFiles",
23
+ terminalCommand: "execShell",
24
+ terminal_command: "execShell",
25
+ runCommand: "execShell",
26
+ run_command: "execShell",
27
+ runInBash: "execShell",
28
+ run_in_bash: "execShell",
29
+ executeCommand: "execShell",
30
+ execute_command: "execShell",
18
31
  };
19
32
 
20
33
  const normalizeToolName = (name: string): string =>
@@ -0,0 +1,28 @@
1
+ import type { CliCommandHandler, CliRuntimeContext } from "./cliCommandTypes";
2
+
3
+ export function withCliEnv<TDeps extends { env: NodeJS.ProcessEnv }>(
4
+ command: (args: string[], deps: TDeps) => Promise<number>
5
+ ): CliCommandHandler {
6
+ return (args: string[], ctx: CliRuntimeContext) =>
7
+ command(args, { env: ctx.env } as TDeps);
8
+ }
9
+
10
+ export function withCliEnvAndScriptDir<TDeps extends { env: NodeJS.ProcessEnv; scriptDir: string }>(
11
+ command: (args: string[], deps: TDeps) => Promise<number>
12
+ ): CliCommandHandler {
13
+ return (args: string[], ctx: CliRuntimeContext) =>
14
+ command(args, {
15
+ env: ctx.env,
16
+ scriptDir: ctx.scriptDir,
17
+ } as TDeps);
18
+ }
19
+
20
+ export function withCliEntrypointEnv<TDeps extends { env: NodeJS.ProcessEnv; cliEntrypointPath: string }>(
21
+ command: (args: string[], deps: TDeps) => Promise<number>
22
+ ): CliCommandHandler {
23
+ return (args: string[], ctx: CliRuntimeContext) =>
24
+ command(args, {
25
+ env: ctx.env,
26
+ cliEntrypointPath: ctx.entrypointPath,
27
+ } as TDeps);
28
+ }
@@ -0,0 +1,66 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import type { CliPackageInfo, CliRuntimeContext, CommandEntry } from "./cliCommandTypes";
3
+
4
+ export async function runResolvedCommand(
5
+ command: CommandEntry,
6
+ originalArgs: string[],
7
+ ctx: CliRuntimeContext,
8
+ deps: {
9
+ runScript: (script: string, forwardedArgs: string[], env: NodeJS.ProcessEnv) => Promise<number>;
10
+ }
11
+ ) {
12
+ if (command.kind === "internal") {
13
+ if (!command.handler) {
14
+ throw new Error(`Internal command ${command.path.join(" ")} is missing a handler.`);
15
+ }
16
+ return command.handler(originalArgs.slice(command.path.length), ctx);
17
+ }
18
+
19
+ if (!command.script) {
20
+ throw new Error(`Script command ${command.path.join(" ")} is missing a script.`);
21
+ }
22
+
23
+ const forwardedArgs = [...(command.fixedArgs ?? []), ...originalArgs.slice(command.path.length)];
24
+ return deps.runScript(command.script, forwardedArgs, ctx.env);
25
+ }
26
+
27
+ function readOption(args: string[], flag: string) {
28
+ const index = args.indexOf(flag);
29
+ return index >= 0 ? args[index + 1] : undefined;
30
+ }
31
+
32
+ function buildDaemonEnv(args: string[], runtimeEnv: NodeJS.ProcessEnv) {
33
+ const serverUrl = readOption(args, "--server-url") || readOption(args, "--server");
34
+ const apiKey = readOption(args, "--api-key") || readOption(args, "--token");
35
+ return {
36
+ ...runtimeEnv,
37
+ ...(serverUrl ? { NOLO_SERVER: serverUrl } : {}),
38
+ ...(apiKey ? { AUTH_TOKEN: apiKey } : {}),
39
+ };
40
+ }
41
+
42
+ export function looksLikeDaemonShortcut(args: string[]) {
43
+ return args.includes("--server-url") || args.includes("--api-key");
44
+ }
45
+
46
+ export function createCliRuntimeContext(args: {
47
+ env: NodeJS.ProcessEnv;
48
+ scriptDir: string;
49
+ entrypointPath?: string;
50
+ packageInfo: CliPackageInfo;
51
+ }): CliRuntimeContext {
52
+ return {
53
+ env: args.env,
54
+ scriptDir: args.scriptDir,
55
+ entrypointPath: args.entrypointPath ?? fileURLToPath(import.meta.url),
56
+ packageInfo: args.packageInfo,
57
+ };
58
+ }
59
+
60
+ export function withDaemonShortcutArgs(args: string[], runtimeEnv: NodeJS.ProcessEnv) {
61
+ if (!looksLikeDaemonShortcut(args)) return { commandArgs: args, envOverride: null as NodeJS.ProcessEnv | null };
62
+ return {
63
+ commandArgs: ["daemon", ...args],
64
+ envOverride: buildDaemonEnv(args, runtimeEnv),
65
+ };
66
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ withCliEntrypointEnv,
3
+ withCliEnv,
4
+ withCliEnvAndScriptDir,
5
+ } from "./cliCommandAdapters";
6
+ import {
7
+ createInternalCommand,
8
+ type CliCommandHandler,
9
+ type CommandEntry,
10
+ } from "./cliCommandTypes";
11
+
12
+ export function createEnvCommand<TDeps extends { env: NodeJS.ProcessEnv }>(
13
+ path: string[],
14
+ description: string,
15
+ command: (args: string[], deps: TDeps) => Promise<number>
16
+ ): CommandEntry {
17
+ return createInternalCommand(path, description, withCliEnv(command) as CliCommandHandler);
18
+ }
19
+
20
+ export function createEnvScriptDirCommand<TDeps extends { env: NodeJS.ProcessEnv; scriptDir: string }>(
21
+ path: string[],
22
+ description: string,
23
+ command: (args: string[], deps: TDeps) => Promise<number>
24
+ ): CommandEntry {
25
+ return createInternalCommand(path, description, withCliEnvAndScriptDir(command) as CliCommandHandler);
26
+ }
27
+
28
+ export function createEntrypointEnvCommand<TDeps extends { env: NodeJS.ProcessEnv; cliEntrypointPath: string }>(
29
+ path: string[],
30
+ description: string,
31
+ command: (args: string[], deps: TDeps) => Promise<number>
32
+ ): CommandEntry {
33
+ return createInternalCommand(path, description, withCliEntrypointEnv(command) as CliCommandHandler);
34
+ }
@@ -0,0 +1,49 @@
1
+ import type { readPackageInfo } from "./updateCommands";
2
+
3
+ export type CliPackageInfo = ReturnType<typeof readPackageInfo>;
4
+
5
+ export type CliRuntimeContext = {
6
+ env: NodeJS.ProcessEnv;
7
+ scriptDir: string;
8
+ entrypointPath: string;
9
+ packageInfo: CliPackageInfo;
10
+ };
11
+
12
+ export type CliCommandHandler = (args: string[], ctx: CliRuntimeContext) => Promise<number>;
13
+
14
+ export type CommandEntry = {
15
+ path: string[];
16
+ description: string;
17
+ kind: "internal" | "script";
18
+ script?: string;
19
+ fixedArgs?: string[];
20
+ handler?: CliCommandHandler;
21
+ };
22
+
23
+ export function createScriptCommand(
24
+ path: string[],
25
+ script: string,
26
+ description: string,
27
+ fixedArgs?: string[]
28
+ ): CommandEntry {
29
+ return {
30
+ path,
31
+ kind: "script",
32
+ script,
33
+ description,
34
+ ...(fixedArgs ? { fixedArgs } : {}),
35
+ };
36
+ }
37
+
38
+ export function createInternalCommand(
39
+ path: string[],
40
+ description: string,
41
+ handler: CliCommandHandler
42
+ ): CommandEntry {
43
+ return {
44
+ path,
45
+ kind: "internal",
46
+ description,
47
+ handler,
48
+ };
49
+ }