nolo-cli 0.1.38 → 0.1.40

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 (65) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/agentRecordConfig.ts +0 -14
  3. package/agent-runtime/dialogWritePlan.ts +39 -0
  4. package/agent-runtime/hostAdapter.ts +4 -1
  5. package/agent-runtime/hybridRecordStore.ts +1 -1
  6. package/agent-runtime/index.ts +39 -18
  7. package/agent-runtime/localDialogRead.ts +23 -0
  8. package/agent-runtime/localLoop.ts +87 -7
  9. package/agent-runtime/localToolPolicy.ts +1 -17
  10. package/agent-runtime/localWorkspaceTools.ts +51 -110
  11. package/agent-runtime/noloWorkspaceTools.ts +371 -0
  12. package/agent-runtime/runtimeToolPolicy.ts +3 -14
  13. package/agent-runtime/runtimeToolSurface.ts +198 -0
  14. package/agent-runtime/types.ts +2 -1
  15. package/agentAliases.ts +8 -1
  16. package/agentRunCommand.ts +65 -95
  17. package/agentRuntimeLocal.ts +1 -7
  18. package/ai/agent/cliExecutor.ts +249 -34
  19. package/chat/messages/fetchMessages.ts +84 -0
  20. package/chat/messages/types.ts +185 -0
  21. package/cli/agentAliases.ts +8 -1
  22. package/cli/agentRunCommand.ts +65 -95
  23. package/cli/agentRuntimeLocal.ts +1 -7
  24. package/cli/cliEnvHelpers.ts +11 -7
  25. package/cli/client/agentRun.ts +94 -93
  26. package/cli/client/localRuntimeAdapter.ts +250 -69
  27. package/cli/commandRegistry.ts +2 -2
  28. package/cli/connectorRunArtifact.ts +0 -1
  29. package/cli/dialogCommands.ts +572 -0
  30. package/cli/dialogInternalCommandEntries.ts +4 -0
  31. package/cli/machineWsRunDispatch.ts +76 -39
  32. package/cli/offlineMarxistsAgentCommand.ts +6 -4
  33. package/cli/scriptCommandEntries.ts +0 -2
  34. package/cli/tui/readlineWorkspace.ts +69 -0
  35. package/cli/tui/session.ts +176 -1
  36. package/cliEnvHelpers.ts +11 -7
  37. package/client/agentConfigResolver.test.ts +4 -5
  38. package/client/agentRun.test.ts +307 -57
  39. package/client/agentRun.ts +94 -93
  40. package/client/localRuntimeAdapter.test.ts +332 -53
  41. package/client/localRuntimeAdapter.ts +250 -69
  42. package/client/localRuntimeDryRun.test.ts +23 -9
  43. package/client/localToolPolicy.test.ts +4 -4
  44. package/commandRegistry.ts +2 -2
  45. package/connectorRunArtifact.ts +0 -1
  46. package/database/server/db.ts +99 -0
  47. package/database/server/ensureDbOpen.ts +12 -0
  48. package/database/server/levelAuthorityStore.ts +20 -1
  49. package/database/server/memoryAuthorityStore.ts +133 -0
  50. package/database/server/serverStoreFactory.ts +118 -0
  51. package/dialogCommands.ts +572 -0
  52. package/dialogInternalCommandEntries.ts +4 -0
  53. package/machineWsRunDispatch.ts +76 -39
  54. package/offlineMarxistsAgentCommand.ts +6 -4
  55. package/package.json +11 -9
  56. package/scriptCommandEntries.ts +0 -2
  57. package/tui/readlineWorkspace.ts +69 -0
  58. package/tui/session.ts +176 -1
  59. package/agent-runtime/taskWorkspace.ts +0 -193
  60. package/agent-runtime/workspaceSession.ts +0 -76
  61. package/cli/client/taskWorktree.ts +0 -8
  62. package/cli/client/workspaceSession.ts +0 -11
  63. package/client/taskWorktree.ts +0 -8
  64. package/client/workspaceSession.test.ts +0 -57
  65. package/client/workspaceSession.ts +0 -11
@@ -10,7 +10,6 @@ import {
10
10
  resolveConnectorRunCwd as defaultResolveConnectorRunCwd,
11
11
  } from "./connectorRunArtifact";
12
12
  import { createCliLocalRuntimeAdapter } from "./client/localRuntimeAdapter";
13
- import { prepareTaskWorktree } from "./client/taskWorktree";
14
13
  import {
15
14
  materializeLargeConnectorPrompt as defaultMaterializeLargeConnectorPrompt,
16
15
  readRuntimePromptPageMeta as defaultReadRuntimePromptPageMeta,
@@ -21,9 +20,52 @@ type EnvLike = Record<string, string | undefined>;
21
20
  export type LocalCliExecutor = (
22
21
  provider: string,
23
22
  prompt: string,
24
- options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean; env?: EnvLike }
23
+ options: {
24
+ model?: string;
25
+ timeout?: number;
26
+ cwd?: string;
27
+ yolo?: boolean;
28
+ env?: EnvLike;
29
+ imageInputs?: Array<{ source: string; materializedPath?: string }>;
30
+ }
25
31
  ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
26
32
 
33
+ /**
34
+ * 从 connector payload 的 userInput 中提取 multimodal 内容。
35
+ * 如果 userInput 是字符串,直接返回 { text, imageUrls: [] }。
36
+ * 如果 userInput 是 multimodal content array(`[{ type: "text" }, { type: "image_url" }]`),
37
+ * 提取 text 和 image_url parts。
38
+ */
39
+ function extractMultimodalUserInput(userInput: unknown): {
40
+ text: string;
41
+ imageUrls: string[];
42
+ } {
43
+ if (typeof userInput === "string") {
44
+ return { text: userInput, imageUrls: [] };
45
+ }
46
+ if (!Array.isArray(userInput)) {
47
+ return { text: String(userInput ?? ""), imageUrls: [] };
48
+ }
49
+
50
+ const textParts: string[] = [];
51
+ const imageUrls: string[] = [];
52
+
53
+ for (const part of userInput) {
54
+ if (!part || typeof part !== "object") continue;
55
+ if ((part as any).type === "text" && typeof (part as any).text === "string") {
56
+ textParts.push((part as any).text);
57
+ } else if (
58
+ (part as any).type === "image_url" &&
59
+ typeof (part as any).image_url?.url === "string" &&
60
+ (part as any).image_url.url.trim()
61
+ ) {
62
+ imageUrls.push((part as any).image_url.url.trim());
63
+ }
64
+ }
65
+
66
+ return { text: textParts.join("\n"), imageUrls };
67
+ }
68
+
27
69
  type ConnectorRuntimePolicy = {
28
70
  runtimeTools?: string[];
29
71
  agentTools?: string[];
@@ -65,7 +107,6 @@ type MachineWsRunDispatchDeps = {
65
107
  collectConnectorRunArtifact?: typeof defaultCollectConnectorRunArtifact;
66
108
  materializeLargeConnectorPrompt?: typeof defaultMaterializeLargeConnectorPrompt;
67
109
  readRuntimePromptPageMeta?: typeof defaultReadRuntimePromptPageMeta;
68
- prepareTaskWorktree?: typeof prepareTaskWorktree;
69
110
  buildConnectorCliPrompt?: (
70
111
  agentConfig: any,
71
112
  userInput: string,
@@ -97,6 +138,17 @@ function logConnectorRun(message: string, fields: Record<string, unknown> = {})
97
138
  console.error(`[nolo-connector] ${message} ${JSON.stringify(fields)}`);
98
139
  }
99
140
 
141
+ function summarizeRuntimePolicyForLogs(policy?: ConnectorRuntimePolicy) {
142
+ return {
143
+ hasPolicy: Boolean(policy),
144
+ runtimeTools: policy?.runtimeTools ?? [],
145
+ agentTools: policy?.agentTools ?? [],
146
+ workspaceMode: policy?.workspace?.mode ?? null,
147
+ workspaceCwd: policy?.workspace?.cwd ?? null,
148
+ shellEnabled: policy?.shell?.enabled ?? null,
149
+ };
150
+ }
151
+
100
152
  function summarizeEndpoint(value: unknown) {
101
153
  if (typeof value !== "string" || !value.trim()) return undefined;
102
154
  try {
@@ -139,14 +191,9 @@ function runtimePolicyFromConnectorPayload(parsed: any): ConnectorRuntimePolicy
139
191
  return policy ? policy as ConnectorRuntimePolicy : undefined;
140
192
  }
141
193
 
142
- function requestsTaskWorktree(policy?: ConnectorRuntimePolicy) {
143
- return policy?.workspace?.mode === "task-worktree" || policy?.workspace?.mode === "lease";
144
- }
145
-
146
194
  function requestsLocalWorkspaceRuntime(policy?: ConnectorRuntimePolicy) {
147
195
  return Boolean(
148
- Array.isArray(policy?.runtimeTools) && policy.runtimeTools.length > 0 ||
149
- requestsTaskWorktree(policy),
196
+ Array.isArray(policy?.runtimeTools) && policy.runtimeTools.length > 0,
150
197
  );
151
198
  }
152
199
 
@@ -183,13 +230,13 @@ function runtimeWorkspacePermissionPolicy(
183
230
  runtimeEnv: EnvLike,
184
231
  cwd: string
185
232
  ): PermissionPolicy | null {
186
- if (!requestsTaskWorktree(runtimePolicy)) return null;
233
+ void runtimeEnv;
234
+ if (!requestsLocalWorkspaceRuntime(runtimePolicy)) return null;
187
235
  return {
188
236
  mode: "full_access",
189
237
  allowFilesystemRead: true,
190
238
  allowFilesystemWrite: true,
191
239
  allowShell:
192
- ["worktree", "dangerous", "1", "true"].includes(runtimeEnv.NOLO_LOCAL_SHELL_MODE || "") &&
193
240
  Array.isArray(runtimePolicy?.runtimeTools) &&
194
241
  runtimePolicy.runtimeTools.includes("execShell"),
195
242
  writableRoots: [cwd],
@@ -201,7 +248,7 @@ function scopePermissionPolicyToRuntimeWorkspace(
201
248
  runtimePolicy: ConnectorRuntimePolicy | undefined,
202
249
  cwd: string
203
250
  ): PermissionPolicy {
204
- if (!requestsTaskWorktree(runtimePolicy)) return policy;
251
+ if (!requestsLocalWorkspaceRuntime(runtimePolicy)) return policy;
205
252
  return {
206
253
  ...policy,
207
254
  writableRoots: policy.allowFilesystemWrite ? [cwd] : [],
@@ -313,7 +360,6 @@ async function defaultRunConnectorLocalRuntimeAgent(args: {
313
360
  policy?.runtimeTools,
314
361
  ),
315
362
  ...(policy ? { runtimeToolPolicy: policy } : {}),
316
- ...(requestsTaskWorktree(policy) ? { localWorkspaceMode: "task-worktree" } : {}),
317
363
  };
318
364
  const store = new Map<string, any>([
319
365
  [agentKey, agentRecord],
@@ -324,19 +370,15 @@ async function defaultRunConnectorLocalRuntimeAgent(args: {
324
370
  withForwardedUserAuthToken(args.runtimeEnv, args.parsed),
325
371
  policy
326
372
  );
327
- const wantsTaskWorktree = requestsTaskWorktree(policy);
373
+ logConnectorRun("agent.run.local-runtime.env", {
374
+ requestId: args.parsed.requestId,
375
+ agentKey,
376
+ cwd: args.cwd,
377
+ ...summarizeRuntimePolicyForLogs(policy),
378
+ });
328
379
  const adapter = createCliLocalRuntimeAdapter({
329
380
  env,
330
381
  cwd: args.cwd,
331
- useCwdAsTaskWorkspaceBase: wantsTaskWorktree,
332
- ...(wantsTaskWorktree
333
- ? {
334
- prepareTaskWorktree: (input) => prepareTaskWorktree({
335
- ...input,
336
- cwd: args.cwd,
337
- }),
338
- }
339
- : {}),
340
382
  fetchImpl: args.fetchImpl ?? fetch,
341
383
  store: {
342
384
  read: async (key) => store.get(key) ?? null,
@@ -447,13 +489,14 @@ export async function handleConnectorRunMessage(
447
489
  deps.materializeLargeConnectorPrompt ?? defaultMaterializeLargeConnectorPrompt;
448
490
  const readRuntimePromptPageMeta =
449
491
  deps.readRuntimePromptPageMeta ?? defaultReadRuntimePromptPageMeta;
450
- const prepareTaskWorktreeImpl = deps.prepareTaskWorktree ?? prepareTaskWorktree;
451
492
  const runConnectorLocalRuntimeAgent =
452
493
  deps.runConnectorLocalRuntimeAgent ?? defaultRunConnectorLocalRuntimeAgent;
453
494
  try {
454
495
  const machinePermissionPolicy = resolveMachineRunPermissionPolicy(agentConfig);
455
496
  const runtimePolicy = runtimePolicyFromConnectorPayload(parsed);
456
- const userInput = String(parsed.payload?.userInput ?? "");
497
+ const extractedInput = extractMultimodalUserInput(parsed.payload?.userInput);
498
+ const userInput = extractedInput.text;
499
+ const connectorImageUrls = extractedInput.imageUrls;
457
500
  const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
458
501
  let cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy: machinePermissionPolicy });
459
502
  let runContent = "";
@@ -465,15 +508,6 @@ export async function handleConnectorRunMessage(
465
508
  if (!buildConnectorCliPrompt) {
466
509
  throw new Error("buildConnectorCliPrompt is required for CLI connector runs.");
467
510
  }
468
- if (requestsTaskWorktree(runtimePolicy)) {
469
- const prepared = await prepareTaskWorktreeImpl({
470
- agentKey: String(parsed.payload?.agentKey ?? "agent"),
471
- cwd,
472
- env: runtimeEnv,
473
- });
474
- cwd = prepared.path;
475
- artifactCwd = prepared.path;
476
- }
477
511
  const runtimePermissionPolicy = !hasExplicitMachinePermissions(agentConfig)
478
512
  ? runtimeWorkspacePermissionPolicy(runtimePolicy, runtimeEnv, cwd)
479
513
  : null;
@@ -507,7 +541,7 @@ export async function handleConnectorRunMessage(
507
541
  ...(runtimePromptPage?.dbKey ? { promptPageKey: runtimePromptPage.dbKey } : {}),
508
542
  ...(runtimePromptPage?.promptHash ? { promptPageHash: runtimePromptPage.promptHash } : {}),
509
543
  workspaceRoot: artifactCwd,
510
- workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
544
+ workspaceKind: "current",
511
545
  };
512
546
  logConnectorRun("agent.run.cli.start", {
513
547
  requestId: parsed.requestId,
@@ -561,6 +595,9 @@ export async function handleConnectorRunMessage(
561
595
  effectiveRuntimeEnv.AUTH_TOKEN ||
562
596
  effectiveRuntimeEnv.AUTH,
563
597
  },
598
+ ...(connectorImageUrls.length > 0
599
+ ? { imageInputs: connectorImageUrls.map((url) => ({ source: url })) }
600
+ : {}),
564
601
  }).finally(() => clearInterval(heartbeat));
565
602
  logConnectorRun("agent.run.cli.result", {
566
603
  requestId: parsed.requestId,
@@ -580,9 +617,9 @@ export async function handleConnectorRunMessage(
580
617
  artifacts,
581
618
  runtimePolicy,
582
619
  workspaceRoot: artifactCwd,
583
- workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
620
+ workspaceKind: "current",
584
621
  });
585
- if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
622
+ if (artifactProgress) sendProgress(artifactProgress);
586
623
  send(JSON.stringify({
587
624
  type: "agent.run.result",
588
625
  requestId: parsed.requestId,
@@ -628,9 +665,9 @@ export async function handleConnectorRunMessage(
628
665
  artifacts,
629
666
  runtimePolicy,
630
667
  workspaceRoot: artifactCwd,
631
- workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
668
+ workspaceKind: "current",
632
669
  });
633
- if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
670
+ if (artifactProgress) sendProgress(artifactProgress);
634
671
  send(JSON.stringify({
635
672
  type: "agent.run.result",
636
673
  requestId: parsed.requestId,
@@ -40,15 +40,17 @@ function readFlagValue(args: string[], flag: string) {
40
40
  }
41
41
 
42
42
  function parseUserIdFromToken(token: string) {
43
- const payload = token.split(".")[0];
44
- if (!payload) return "";
43
+ const payload = token.split(".")[1];
44
+ if (!payload) return undefined;
45
45
  try {
46
46
  const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
47
47
  const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
48
48
  const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
49
- return typeof parsed?.userId === "string" ? parsed.userId : "";
49
+ return typeof parsed?.userId === "string" && parsed.userId.trim()
50
+ ? parsed.userId.trim()
51
+ : undefined;
50
52
  } catch {
51
- return "";
53
+ return undefined;
52
54
  }
53
55
  }
54
56
 
@@ -16,8 +16,6 @@ function getDocScriptCommands(): CommandEntry[] {
16
16
 
17
17
  function getSpaceScriptCommands(): CommandEntry[] {
18
18
  return [
19
- createScriptCommand(["dialog", "read"], "readDialog.ts", "Read a dialog"),
20
- createScriptCommand(["dialog", "status"], "dialogStatus.ts", "Read compact dialog status"),
21
19
  createScriptCommand(["space", "list"], "listSpaces.ts", "List joined spaces"),
22
20
  createScriptCommand(["space", "read"], "readSpace.ts", "Read a space"),
23
21
  createScriptCommand(["space", "delete"], "deleteSpaces.ts", "Delete spaces by safe filters"),
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { stdin as defaultInput, stdout as defaultOutput } from "node:process";
3
+ import { join } from "node:path";
3
4
  import { runAgentTurn, type RunAgentTurnResult } from "../client/agentRun";
4
5
  import { compactDialog, type CompactDialogResult } from "../client/compactDialog";
5
6
  import { runSelfUpdate } from "../updateCommands";
@@ -21,7 +22,9 @@ type WorkspaceOptions = {
21
22
  env?: NodeJS.ProcessEnv;
22
23
  input?: NodeJS.ReadableStream;
23
24
  output?: NodeJS.WritableStream;
25
+ cliEntrypointPath?: string;
24
26
  agentRunner?: typeof runAgentTurn;
27
+ cliCommandRunner?: CliCommandRunner;
25
28
  compactRunner?: (options: {
26
29
  serverUrl: string;
27
30
  authToken: string;
@@ -30,6 +33,16 @@ type WorkspaceOptions = {
30
33
  selfUpdater?: SelfUpdater;
31
34
  };
32
35
 
36
+ type CliCommandRunner = (
37
+ args: string[],
38
+ context: {
39
+ env: NodeJS.ProcessEnv;
40
+ output: NodeJS.WritableStream;
41
+ scriptDir: string;
42
+ cliEntrypointPath: string;
43
+ }
44
+ ) => Promise<number>;
45
+
33
46
  async function runAgentChat(
34
47
  scriptDir: string,
35
48
  state: TuiState,
@@ -52,10 +65,48 @@ async function runAgentChat(
52
65
  return result;
53
66
  }
54
67
 
68
+ async function pipeReadableToOutput(
69
+ readable: ReadableStream<Uint8Array> | null,
70
+ output: NodeJS.WritableStream
71
+ ) {
72
+ if (!readable) return;
73
+ const text = await new Response(readable).text();
74
+ if (text) output.write(text);
75
+ }
76
+
77
+ function resolveDefaultCliEntrypoint(scriptDir: string) {
78
+ if (process.argv[1]) return process.argv[1];
79
+ return join(scriptDir, "..", "packages", "cli", "index.ts");
80
+ }
81
+
82
+ async function runCliCommandInChildProcess(
83
+ args: string[],
84
+ context: {
85
+ env: NodeJS.ProcessEnv;
86
+ output: NodeJS.WritableStream;
87
+ cliEntrypointPath: string;
88
+ }
89
+ ) {
90
+ const proc = Bun.spawn({
91
+ cmd: [process.execPath, context.cliEntrypointPath, ...args],
92
+ stdout: "pipe",
93
+ stderr: "pipe",
94
+ env: context.env,
95
+ });
96
+ await Promise.all([
97
+ pipeReadableToOutput(proc.stdout, context.output),
98
+ pipeReadableToOutput(proc.stderr, context.output),
99
+ ]);
100
+ return proc.exited;
101
+ }
102
+
55
103
  export async function startTuiWorkspace(options: WorkspaceOptions) {
56
104
  let state = createInitialTuiState(options.env ?? process.env);
57
105
  const input = options.input ?? defaultInput;
58
106
  const output = options.output ?? defaultOutput;
107
+ const cliEntrypointPath =
108
+ options.cliEntrypointPath ?? resolveDefaultCliEntrypoint(options.scriptDir);
109
+ const cliCommandRunner = options.cliCommandRunner ?? runCliCommandInChildProcess;
59
110
  const selfUpdater: SelfUpdater =
60
111
  options.selfUpdater ?? ((target) => runSelfUpdate({ output: target }));
61
112
  const rl = createInterface({ input, output });
@@ -113,6 +164,24 @@ export async function startTuiWorkspace(options: WorkspaceOptions) {
113
164
  }
114
165
  }
115
166
 
167
+ if (result.action?.type === "cli-command") {
168
+ try {
169
+ const exitCode = await cliCommandRunner(result.action.args, {
170
+ env: options.env ?? process.env,
171
+ output,
172
+ scriptDir: options.scriptDir,
173
+ cliEntrypointPath,
174
+ });
175
+ if (exitCode !== 0) {
176
+ output.write(`[nolo] CLI command exited with code ${exitCode}.\n`);
177
+ }
178
+ } catch (error) {
179
+ output.write(
180
+ `[nolo] CLI command failed: ${error instanceof Error ? error.message : String(error)}\n`
181
+ );
182
+ }
183
+ }
184
+
116
185
  if (result.action?.type === "chat") {
117
186
  const runResult = await runAgentChat(
118
187
  options.scriptDir,
@@ -63,6 +63,10 @@ export type TuiAction =
63
63
  | {
64
64
  type: "self-update";
65
65
  }
66
+ | {
67
+ type: "cli-command";
68
+ args: string[];
69
+ }
66
70
  | {
67
71
  type: "exit";
68
72
  };
@@ -152,7 +156,7 @@ export function renderTuiHelp() {
152
156
  " /version Show version/update hint",
153
157
  " /exit Leave the workspace",
154
158
  "",
155
- "You can also type normally. nolo will send it to the current agent.",
159
+ "You can also type normally. nolo routes simple read/status requests to CLI commands and sends the rest to the current agent.",
156
160
  ].join("\n");
157
161
  }
158
162
 
@@ -203,6 +207,165 @@ function resolveSwitchTarget(rawTarget: string) {
203
207
  return null;
204
208
  }
205
209
 
210
+ const DIALOG_ID_PATTERN = /[0-9A-HJKMNP-TV-Z]{26}/i;
211
+ const DIALOG_KEY_PATTERN = /dialog-[^\s"'<>]+-[0-9A-HJKMNP-TV-Z]{26}/i;
212
+ const DIALOG_URL_PATTERN = /https?:\/\/[^\s"'<>]+\/(?:space\/[^/\s"'<>]+\/)?dialog-[^\s"'<>]+-[0-9A-HJKMNP-TV-Z]{26}\/?/i;
213
+ const GENERIC_TOKEN_PATTERN = /(?:agent-pub-|agent-|space-|page-|meta-)?[A-Za-z0-9][A-Za-z0-9._:/-]{2,}/;
214
+
215
+ function parseChineseSmallNumber(value: string) {
216
+ const normalized = value.trim();
217
+ const direct: Record<string, number> = {
218
+ 一: 1,
219
+ 二: 2,
220
+ 两: 2,
221
+ 三: 3,
222
+ 四: 4,
223
+ 五: 5,
224
+ 六: 6,
225
+ 七: 7,
226
+ 八: 8,
227
+ 九: 9,
228
+ 十: 10,
229
+ };
230
+ if (direct[normalized]) return direct[normalized];
231
+ const teen = normalized.match(/^十([一二两三四五六七八九])$/);
232
+ if (teen) return 10 + direct[teen[1]];
233
+ const tens = normalized.match(/^([一二两三四五六七八九])十([一二两三四五六七八九])?$/);
234
+ if (tens) return direct[tens[1]] * 10 + (tens[2] ? direct[tens[2]] : 0);
235
+ return null;
236
+ }
237
+
238
+ function resolveDialogListLimit(input: string) {
239
+ const digitMatch =
240
+ input.match(/(?:最近|前|latest|last)?\s*(\d{1,3})\s*(?:个|条|篇)?\s*(?:对话|dialogs?)/i) ??
241
+ input.match(/(?:对话|dialogs?).*?(?:最近|前|latest|last)?\s*(\d{1,3})\s*(?:个|条|篇)?/i);
242
+ if (digitMatch) {
243
+ const parsed = Number(digitMatch[1]);
244
+ if (Number.isInteger(parsed) && parsed > 0) return String(parsed);
245
+ }
246
+
247
+ const chineseMatch =
248
+ input.match(/(?:最近|前)?\s*([一二两三四五六七八九十]{1,3})\s*(?:个|条|篇)?\s*对话/) ??
249
+ input.match(/对话.*?(?:最近|前)?\s*([一二两三四五六七八九十]{1,3})\s*(?:个|条|篇)?/);
250
+ if (chineseMatch) {
251
+ const parsed = parseChineseSmallNumber(chineseMatch[1]);
252
+ if (parsed && parsed > 0) return String(parsed);
253
+ }
254
+
255
+ return undefined;
256
+ }
257
+
258
+ function extractDialogRef(input: string) {
259
+ return (
260
+ input.match(DIALOG_URL_PATTERN)?.[0] ??
261
+ input.match(DIALOG_KEY_PATTERN)?.[0] ??
262
+ input.match(DIALOG_ID_PATTERN)?.[0] ??
263
+ null
264
+ );
265
+ }
266
+
267
+ function extractTokenAfterKeywords(input: string, keywords: string[]) {
268
+ for (const keyword of keywords) {
269
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
270
+ const match = input.match(new RegExp(`${escaped}\\s+(${GENERIC_TOKEN_PATTERN.source})`, "i"));
271
+ if (match?.[1]) return match[1];
272
+ }
273
+ return null;
274
+ }
275
+
276
+ function formatCliCommand(args: string[]) {
277
+ return `nolo ${args.join(" ")}`;
278
+ }
279
+
280
+ function wantsList(input: string) {
281
+ return /(列出|列表|有哪些|所有|我的|list|show all)/i.test(input);
282
+ }
283
+
284
+ function wantsRead(input: string) {
285
+ return /(读|读取|查看|打开|查询|read|show|open)/i.test(input);
286
+ }
287
+
288
+ function wantsAnalysisOrComposition(input: string) {
289
+ return /(总结|汇总|分析|归纳|对比|解释|整理|summary|summari[sz]e|analy[sz]e|compare)/i.test(input);
290
+ }
291
+
292
+ function resolveSystemCliCommand(input: string) {
293
+ if (/(我.*登录.*谁|当前.*登录|whoami|who am i)/i.test(input)) {
294
+ return ["whoami"];
295
+ }
296
+ if (/(版本|version)/i.test(input) && /(?:nolo|cli|版本|version)/i.test(input)) {
297
+ return ["version"];
298
+ }
299
+ if (/(诊断|doctor|健康检查|health)/i.test(input) && /(?:nolo|cli|诊断|doctor)/i.test(input)) {
300
+ return ["doctor"];
301
+ }
302
+ return null;
303
+ }
304
+
305
+ function resolveResourceCliCommand(input: string) {
306
+ if (/(agent|代理|智能体)/i.test(input)) {
307
+ if (wantsList(input)) return ["agent", "list"];
308
+ if (wantsRead(input)) {
309
+ const agentRef = extractTokenAfterKeywords(input, ["agent", "代理", "智能体"]);
310
+ if (agentRef) return ["agent", "read", agentRef];
311
+ }
312
+ }
313
+
314
+ if (/(space|空间)/i.test(input)) {
315
+ if (wantsList(input)) return ["space", "list"];
316
+ if (wantsRead(input)) {
317
+ const spaceRef = extractTokenAfterKeywords(input, ["space", "空间"]);
318
+ if (spaceRef) return ["space", "read", spaceRef];
319
+ }
320
+ }
321
+
322
+ if (/(skill-doc|skill doc|技能文档|技能)/i.test(input)) {
323
+ if (wantsRead(input)) {
324
+ const docRef = extractTokenAfterKeywords(input, ["skill-doc", "skill doc", "技能文档", "技能"]);
325
+ if (docRef) return ["skill-doc", "read", docRef];
326
+ }
327
+ }
328
+
329
+ if (/(doc|文档|page)/i.test(input)) {
330
+ if (wantsRead(input)) {
331
+ const docRef = extractTokenAfterKeywords(input, ["doc", "文档", "page"]);
332
+ if (docRef) return ["doc", "read", docRef];
333
+ }
334
+ }
335
+
336
+ if (/(table|表格|表|任务表)/i.test(input) && /(查询|查看|读取|query|read|show)/i.test(input)) {
337
+ const tableRef = extractTokenAfterKeywords(input, ["table", "表格", "表", "任务表"]);
338
+ if (tableRef) return ["table", "query", "--table", tableRef];
339
+ }
340
+
341
+ return null;
342
+ }
343
+
344
+ export function resolveNaturalLanguageCliCommand(input: string): string[] | null {
345
+ const normalized = input.trim();
346
+ if (!normalized) return null;
347
+
348
+ const systemCommand = resolveSystemCliCommand(normalized);
349
+ if (systemCommand) return systemCommand;
350
+
351
+ if (/(对话|dialog)/i.test(normalized)) {
352
+ const dialogRef = extractDialogRef(normalized);
353
+ if (dialogRef && wantsRead(normalized)) {
354
+ return ["dialog", "read", dialogRef];
355
+ }
356
+
357
+ const shouldList = /(查|查询|查看|列出|找|最近|list|show|latest|recent)/i.test(normalized);
358
+ if (shouldList && !dialogRef && !wantsAnalysisOrComposition(normalized)) {
359
+ const args = ["dialog", "list"];
360
+ const limit = resolveDialogListLimit(normalized);
361
+ if (limit) args.push("--limit", limit);
362
+ return args;
363
+ }
364
+ }
365
+
366
+ return resolveResourceCliCommand(normalized);
367
+ }
368
+
206
369
  export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
207
370
  const trimmed = input.trim();
208
371
  if (!trimmed) {
@@ -210,6 +373,18 @@ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
210
373
  }
211
374
 
212
375
  if (!trimmed.startsWith("/")) {
376
+ const cliArgs = resolveNaturalLanguageCliCommand(trimmed);
377
+ if (cliArgs) {
378
+ return {
379
+ nextState: state,
380
+ output: `[nolo] ${formatCliCommand(cliArgs)}`,
381
+ action: {
382
+ type: "cli-command",
383
+ args: cliArgs,
384
+ },
385
+ };
386
+ }
387
+
213
388
  return {
214
389
  nextState: state,
215
390
  output: "",
package/cliEnvHelpers.ts CHANGED
@@ -96,14 +96,18 @@ export function resolveAuthToken(
96
96
  }
97
97
 
98
98
  export function parseUserIdFromAuthToken(token: string) {
99
- const payload = token.trim().split(".")[0];
100
- if (!payload) return "";
101
- try {
102
- const parsed = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
103
- return typeof parsed?.userId === "string" ? parsed.userId : "";
104
- } catch {
105
- return "";
99
+ const parts = token.trim().split(".").filter(Boolean);
100
+ const payloadCandidates = parts.length >= 3 ? [parts[1], parts[0]] : [parts[0]];
101
+ for (const payload of payloadCandidates) {
102
+ if (!payload) continue;
103
+ try {
104
+ const parsed = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
105
+ if (typeof parsed?.userId === "string") return parsed.userId;
106
+ } catch {
107
+ // Try the next token shape.
108
+ }
106
109
  }
110
+ return "";
107
111
  }
108
112
 
109
113
  export function readOption(args: string[], flag: string) {
@@ -15,8 +15,8 @@ describe("agent config resolver", () => {
15
15
  cliProvider: "codex",
16
16
  customProviderUrl: "https://provider.example/v1",
17
17
  toolNames: ["legacyTool", "readFile"],
18
- tools: ["readFile", "applyEdit"],
19
- runtimeBinding: { machineId: "machine-1", localWorkspaceMode: "task-worktree" },
18
+ tools: ["readFile", "editFile"],
19
+ runtimeBinding: { machineId: "machine-1" },
20
20
  delegation: { target: "local" },
21
21
  temperature: 0.2,
22
22
  top_p: 0.9,
@@ -33,9 +33,8 @@ describe("agent config resolver", () => {
33
33
  apiSource: "platform",
34
34
  cliProvider: "codex",
35
35
  customProviderUrl: "https://provider.example/v1",
36
- toolNames: ["legacyTool", "readFile", "applyEdit"],
37
- runtimeBinding: { machineId: "machine-1", localWorkspaceMode: "task-worktree" },
38
- localWorkspaceMode: "task-worktree",
36
+ toolNames: ["legacyTool", "readFile", "editFile"],
37
+ runtimeBinding: { machineId: "machine-1" },
39
38
  delegation: { target: "local" },
40
39
  temperature: 0.2,
41
40
  top_p: 0.9,