nolo-cli 0.1.38 → 0.1.39

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 (52) hide show
  1. package/agent-runtime/agentRecordConfig.ts +0 -14
  2. package/agent-runtime/hostAdapter.ts +0 -1
  3. package/agent-runtime/index.ts +24 -18
  4. package/agent-runtime/localDialogRead.ts +23 -0
  5. package/agent-runtime/localToolPolicy.ts +2 -7
  6. package/agent-runtime/localWorkspaceTools.ts +9 -0
  7. package/agent-runtime/noloWorkspaceTools.ts +371 -0
  8. package/agent-runtime/runtimeToolPolicy.ts +1 -3
  9. package/agent-runtime/types.ts +1 -1
  10. package/agentAliases.ts +6 -0
  11. package/agentRunCommand.ts +57 -65
  12. package/agentRuntimeLocal.ts +0 -7
  13. package/chat/messages/fetchMessages.ts +84 -0
  14. package/chat/messages/types.ts +185 -0
  15. package/cli/agentAliases.ts +6 -0
  16. package/cli/agentRunCommand.ts +57 -65
  17. package/cli/agentRuntimeLocal.ts +0 -7
  18. package/cli/cliEnvHelpers.ts +11 -7
  19. package/cli/client/agentRun.ts +55 -7
  20. package/cli/client/localRuntimeAdapter.ts +87 -59
  21. package/cli/connectorRunArtifact.ts +0 -1
  22. package/cli/dialogCommands.ts +572 -0
  23. package/cli/dialogInternalCommandEntries.ts +4 -0
  24. package/cli/machineWsRunDispatch.ts +29 -37
  25. package/cli/scriptCommandEntries.ts +0 -2
  26. package/cli/tui/readlineWorkspace.ts +69 -0
  27. package/cli/tui/session.ts +176 -1
  28. package/cliEnvHelpers.ts +11 -7
  29. package/client/agentConfigResolver.test.ts +2 -3
  30. package/client/agentRun.test.ts +185 -1
  31. package/client/agentRun.ts +55 -7
  32. package/client/localRuntimeAdapter.test.ts +90 -25
  33. package/client/localRuntimeAdapter.ts +87 -59
  34. package/connectorRunArtifact.ts +0 -1
  35. package/database/server/db.ts +99 -0
  36. package/database/server/ensureDbOpen.ts +12 -0
  37. package/database/server/memoryAuthorityStore.ts +133 -0
  38. package/database/server/serverStoreFactory.ts +118 -0
  39. package/dialogCommands.ts +572 -0
  40. package/dialogInternalCommandEntries.ts +4 -0
  41. package/machineWsRunDispatch.ts +29 -37
  42. package/package.json +10 -9
  43. package/scriptCommandEntries.ts +0 -2
  44. package/tui/readlineWorkspace.ts +69 -0
  45. package/tui/session.ts +176 -1
  46. package/agent-runtime/taskWorkspace.ts +0 -193
  47. package/agent-runtime/workspaceSession.ts +0 -76
  48. package/cli/client/taskWorktree.ts +0 -8
  49. package/cli/client/workspaceSession.ts +0 -11
  50. package/client/taskWorktree.ts +0 -8
  51. package/client/workspaceSession.test.ts +0 -57
  52. 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,
@@ -65,7 +64,6 @@ type MachineWsRunDispatchDeps = {
65
64
  collectConnectorRunArtifact?: typeof defaultCollectConnectorRunArtifact;
66
65
  materializeLargeConnectorPrompt?: typeof defaultMaterializeLargeConnectorPrompt;
67
66
  readRuntimePromptPageMeta?: typeof defaultReadRuntimePromptPageMeta;
68
- prepareTaskWorktree?: typeof prepareTaskWorktree;
69
67
  buildConnectorCliPrompt?: (
70
68
  agentConfig: any,
71
69
  userInput: string,
@@ -97,6 +95,18 @@ function logConnectorRun(message: string, fields: Record<string, unknown> = {})
97
95
  console.error(`[nolo-connector] ${message} ${JSON.stringify(fields)}`);
98
96
  }
99
97
 
98
+ function summarizeRuntimePolicyForLogs(policy?: ConnectorRuntimePolicy) {
99
+ return {
100
+ hasPolicy: Boolean(policy),
101
+ runtimeTools: policy?.runtimeTools ?? [],
102
+ agentTools: policy?.agentTools ?? [],
103
+ workspaceMode: policy?.workspace?.mode ?? null,
104
+ workspaceCwd: policy?.workspace?.cwd ?? null,
105
+ shellEnabled: policy?.shell?.enabled ?? null,
106
+ shellMode: policy?.shell?.mode ?? null,
107
+ };
108
+ }
109
+
100
110
  function summarizeEndpoint(value: unknown) {
101
111
  if (typeof value !== "string" || !value.trim()) return undefined;
102
112
  try {
@@ -139,14 +149,9 @@ function runtimePolicyFromConnectorPayload(parsed: any): ConnectorRuntimePolicy
139
149
  return policy ? policy as ConnectorRuntimePolicy : undefined;
140
150
  }
141
151
 
142
- function requestsTaskWorktree(policy?: ConnectorRuntimePolicy) {
143
- return policy?.workspace?.mode === "task-worktree" || policy?.workspace?.mode === "lease";
144
- }
145
-
146
152
  function requestsLocalWorkspaceRuntime(policy?: ConnectorRuntimePolicy) {
147
153
  return Boolean(
148
- Array.isArray(policy?.runtimeTools) && policy.runtimeTools.length > 0 ||
149
- requestsTaskWorktree(policy),
154
+ Array.isArray(policy?.runtimeTools) && policy.runtimeTools.length > 0,
150
155
  );
151
156
  }
152
157
 
@@ -183,13 +188,13 @@ function runtimeWorkspacePermissionPolicy(
183
188
  runtimeEnv: EnvLike,
184
189
  cwd: string
185
190
  ): PermissionPolicy | null {
186
- if (!requestsTaskWorktree(runtimePolicy)) return null;
191
+ if (!requestsLocalWorkspaceRuntime(runtimePolicy)) return null;
187
192
  return {
188
193
  mode: "full_access",
189
194
  allowFilesystemRead: true,
190
195
  allowFilesystemWrite: true,
191
196
  allowShell:
192
- ["worktree", "dangerous", "1", "true"].includes(runtimeEnv.NOLO_LOCAL_SHELL_MODE || "") &&
197
+ runtimeEnv.NOLO_LOCAL_SHELL_MODE === "worktree" &&
193
198
  Array.isArray(runtimePolicy?.runtimeTools) &&
194
199
  runtimePolicy.runtimeTools.includes("execShell"),
195
200
  writableRoots: [cwd],
@@ -201,7 +206,7 @@ function scopePermissionPolicyToRuntimeWorkspace(
201
206
  runtimePolicy: ConnectorRuntimePolicy | undefined,
202
207
  cwd: string
203
208
  ): PermissionPolicy {
204
- if (!requestsTaskWorktree(runtimePolicy)) return policy;
209
+ if (!requestsLocalWorkspaceRuntime(runtimePolicy)) return policy;
205
210
  return {
206
211
  ...policy,
207
212
  writableRoots: policy.allowFilesystemWrite ? [cwd] : [],
@@ -313,7 +318,6 @@ async function defaultRunConnectorLocalRuntimeAgent(args: {
313
318
  policy?.runtimeTools,
314
319
  ),
315
320
  ...(policy ? { runtimeToolPolicy: policy } : {}),
316
- ...(requestsTaskWorktree(policy) ? { localWorkspaceMode: "task-worktree" } : {}),
317
321
  };
318
322
  const store = new Map<string, any>([
319
323
  [agentKey, agentRecord],
@@ -324,19 +328,17 @@ async function defaultRunConnectorLocalRuntimeAgent(args: {
324
328
  withForwardedUserAuthToken(args.runtimeEnv, args.parsed),
325
329
  policy
326
330
  );
327
- const wantsTaskWorktree = requestsTaskWorktree(policy);
331
+ logConnectorRun("agent.run.local-runtime.env", {
332
+ requestId: args.parsed.requestId,
333
+ agentKey,
334
+ cwd: args.cwd,
335
+ runtimeEnvShellModeBeforePolicy: args.runtimeEnv.NOLO_LOCAL_SHELL_MODE ?? null,
336
+ runtimeEnvShellModeAfterPolicy: env.NOLO_LOCAL_SHELL_MODE ?? null,
337
+ ...summarizeRuntimePolicyForLogs(policy),
338
+ });
328
339
  const adapter = createCliLocalRuntimeAdapter({
329
340
  env,
330
341
  cwd: args.cwd,
331
- useCwdAsTaskWorkspaceBase: wantsTaskWorktree,
332
- ...(wantsTaskWorktree
333
- ? {
334
- prepareTaskWorktree: (input) => prepareTaskWorktree({
335
- ...input,
336
- cwd: args.cwd,
337
- }),
338
- }
339
- : {}),
340
342
  fetchImpl: args.fetchImpl ?? fetch,
341
343
  store: {
342
344
  read: async (key) => store.get(key) ?? null,
@@ -447,7 +449,6 @@ export async function handleConnectorRunMessage(
447
449
  deps.materializeLargeConnectorPrompt ?? defaultMaterializeLargeConnectorPrompt;
448
450
  const readRuntimePromptPageMeta =
449
451
  deps.readRuntimePromptPageMeta ?? defaultReadRuntimePromptPageMeta;
450
- const prepareTaskWorktreeImpl = deps.prepareTaskWorktree ?? prepareTaskWorktree;
451
452
  const runConnectorLocalRuntimeAgent =
452
453
  deps.runConnectorLocalRuntimeAgent ?? defaultRunConnectorLocalRuntimeAgent;
453
454
  try {
@@ -465,15 +466,6 @@ export async function handleConnectorRunMessage(
465
466
  if (!buildConnectorCliPrompt) {
466
467
  throw new Error("buildConnectorCliPrompt is required for CLI connector runs.");
467
468
  }
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
469
  const runtimePermissionPolicy = !hasExplicitMachinePermissions(agentConfig)
478
470
  ? runtimeWorkspacePermissionPolicy(runtimePolicy, runtimeEnv, cwd)
479
471
  : null;
@@ -507,7 +499,7 @@ export async function handleConnectorRunMessage(
507
499
  ...(runtimePromptPage?.dbKey ? { promptPageKey: runtimePromptPage.dbKey } : {}),
508
500
  ...(runtimePromptPage?.promptHash ? { promptPageHash: runtimePromptPage.promptHash } : {}),
509
501
  workspaceRoot: artifactCwd,
510
- workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
502
+ workspaceKind: "current",
511
503
  };
512
504
  logConnectorRun("agent.run.cli.start", {
513
505
  requestId: parsed.requestId,
@@ -580,9 +572,9 @@ export async function handleConnectorRunMessage(
580
572
  artifacts,
581
573
  runtimePolicy,
582
574
  workspaceRoot: artifactCwd,
583
- workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
575
+ workspaceKind: "current",
584
576
  });
585
- if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
577
+ if (artifactProgress) sendProgress(artifactProgress);
586
578
  send(JSON.stringify({
587
579
  type: "agent.run.result",
588
580
  requestId: parsed.requestId,
@@ -628,9 +620,9 @@ export async function handleConnectorRunMessage(
628
620
  artifacts,
629
621
  runtimePolicy,
630
622
  workspaceRoot: artifactCwd,
631
- workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
623
+ workspaceKind: "current",
632
624
  });
633
- if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
625
+ if (artifactProgress) sendProgress(artifactProgress);
634
626
  send(JSON.stringify({
635
627
  type: "agent.run.result",
636
628
  requestId: parsed.requestId,
@@ -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) {
@@ -16,7 +16,7 @@ describe("agent config resolver", () => {
16
16
  customProviderUrl: "https://provider.example/v1",
17
17
  toolNames: ["legacyTool", "readFile"],
18
18
  tools: ["readFile", "applyEdit"],
19
- runtimeBinding: { machineId: "machine-1", localWorkspaceMode: "task-worktree" },
19
+ runtimeBinding: { machineId: "machine-1" },
20
20
  delegation: { target: "local" },
21
21
  temperature: 0.2,
22
22
  top_p: 0.9,
@@ -34,8 +34,7 @@ describe("agent config resolver", () => {
34
34
  cliProvider: "codex",
35
35
  customProviderUrl: "https://provider.example/v1",
36
36
  toolNames: ["legacyTool", "readFile", "applyEdit"],
37
- runtimeBinding: { machineId: "machine-1", localWorkspaceMode: "task-worktree" },
38
- localWorkspaceMode: "task-worktree",
37
+ runtimeBinding: { machineId: "machine-1" },
39
38
  delegation: { target: "local" },
40
39
  temperature: 0.2,
41
40
  top_p: 0.9,