nolo-cli 0.1.39 → 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 (35) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/dialogWritePlan.ts +39 -0
  3. package/agent-runtime/hostAdapter.ts +4 -0
  4. package/agent-runtime/hybridRecordStore.ts +1 -1
  5. package/agent-runtime/index.ts +15 -0
  6. package/agent-runtime/localLoop.ts +87 -7
  7. package/agent-runtime/localToolPolicy.ts +2 -13
  8. package/agent-runtime/localWorkspaceTools.ts +42 -110
  9. package/agent-runtime/runtimeToolPolicy.ts +2 -11
  10. package/agent-runtime/runtimeToolSurface.ts +198 -0
  11. package/agent-runtime/types.ts +1 -0
  12. package/agentAliases.ts +2 -1
  13. package/agentRunCommand.ts +10 -32
  14. package/agentRuntimeLocal.ts +1 -0
  15. package/ai/agent/cliExecutor.ts +249 -34
  16. package/cli/agentAliases.ts +2 -1
  17. package/cli/agentRunCommand.ts +10 -32
  18. package/cli/agentRuntimeLocal.ts +1 -0
  19. package/cli/client/agentRun.ts +40 -87
  20. package/cli/client/localRuntimeAdapter.ts +179 -26
  21. package/cli/commandRegistry.ts +2 -2
  22. package/cli/machineWsRunDispatch.ts +51 -6
  23. package/cli/offlineMarxistsAgentCommand.ts +6 -4
  24. package/client/agentConfigResolver.test.ts +2 -2
  25. package/client/agentRun.test.ts +123 -57
  26. package/client/agentRun.ts +40 -87
  27. package/client/localRuntimeAdapter.test.ts +245 -31
  28. package/client/localRuntimeAdapter.ts +179 -26
  29. package/client/localRuntimeDryRun.test.ts +23 -9
  30. package/client/localToolPolicy.test.ts +4 -4
  31. package/commandRegistry.ts +2 -2
  32. package/database/server/levelAuthorityStore.ts +20 -1
  33. package/machineWsRunDispatch.ts +51 -6
  34. package/offlineMarxistsAgentCommand.ts +6 -4
  35. package/package.json +2 -1
@@ -21,6 +21,7 @@ import {
21
21
  resolveCurrentRunRuntimeToolPolicy,
22
22
  resolveLocalWorkspaceExecutorOptionsFromPolicy,
23
23
  resolveRequestedRuntimeToolNames,
24
+ resolveRuntimeToolSurfaceForAgent,
24
25
  shouldUsePlatformChatProvider,
25
26
  } from "../agentRuntimeLocal";
26
27
  import type { AgentRuntimeChatMessage } from "../agent-runtime";
@@ -43,6 +44,7 @@ import {
43
44
  import { executeLocalToolWithPolicy } from "./localToolPolicy";
44
45
  import { taskRunFunctionSchema } from "../ai/tools/agent/taskRunTool";
45
46
  import {
47
+ MIMO_MONTH_AGENT_KEY,
46
48
  NOLO_DEFAULT_AGENT_ID,
47
49
  NOLO_DEFAULT_AGENT_KEY,
48
50
  } from "../agentAliases";
@@ -50,14 +52,26 @@ import {
50
52
  buildNoloWorkspaceCliToolExecutors,
51
53
  buildNoloWorkspaceOpenAiTools,
52
54
  filterNoloWorkspaceToolNames,
53
- NOLO_WORKSPACE_TOOL_NAMES,
54
- NOLO_WORKSPACE_TOOL_PROMPT,
55
55
  parseNoloWorkspaceToolArguments,
56
56
  } from "../agent-runtime/noloWorkspaceTools";
57
+ import { executeCli as defaultExecuteCli, type CliExecuteResult, type CliImageInput } from "../ai/agent/cliExecutor";
58
+ import { buildCliPrompt } from "../ai/agent/cliPrompt";
57
59
 
58
60
  type EnvLike = Record<string, string | undefined>;
59
61
  type FetchInput = Parameters<typeof fetch>[0];
60
62
  type FetchInit = Parameters<typeof fetch>[1];
63
+ type LocalCliExecutor = (
64
+ provider: string,
65
+ prompt: string,
66
+ options: {
67
+ model?: string;
68
+ timeout?: number;
69
+ cwd?: string;
70
+ yolo?: boolean;
71
+ env?: Record<string, string | undefined>;
72
+ imageInputs?: CliImageInput[];
73
+ }
74
+ ) => Promise<CliExecuteResult>;
61
75
  const TRANSIENT_FETCH_MAX_ATTEMPTS = 8;
62
76
  const TRANSIENT_FETCH_RETRY_BASE_DELAY_MS = 250;
63
77
  export const BUILTIN_NOLO_AGENT_ID = NOLO_DEFAULT_AGENT_ID;
@@ -77,6 +91,7 @@ export type CliLocalRuntimeAdapterDeps = {
77
91
  cwd?: string;
78
92
  output?: { write(chunk: string): unknown };
79
93
  localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
94
+ executeCli?: LocalCliExecutor;
80
95
  sleep?: (ms: number) => Promise<void>;
81
96
  loopbackRequest?: (input: FetchInput, init?: FetchInit) => Promise<Response>;
82
97
  };
@@ -93,10 +108,6 @@ function resolveLocalUserId(env: EnvLike) {
93
108
  return env.NOLO_LOCAL_USER_ID || env.NOLO_USER_ID || "local";
94
109
  }
95
110
 
96
- function shellModeEnabled(env: EnvLike) {
97
- return env.NOLO_LOCAL_SHELL_MODE === "worktree";
98
- }
99
-
100
111
  function parseLocalToolBudgets(env: EnvLike) {
101
112
  const raw = env.NOLO_LOCAL_TOOL_BUDGETS?.trim();
102
113
  if (!raw) return {};
@@ -251,7 +262,79 @@ function parseJsonObject(raw: string) {
251
262
  }
252
263
  }
253
264
 
265
+ function isCliProviderAgent(agentConfig: AgentRuntimeAgentConfig) {
266
+ return Boolean(
267
+ agentConfig.apiSource === "cli" ||
268
+ agentConfig.provider === "cli" ||
269
+ agentConfig.cliProvider
270
+ );
271
+ }
272
+
273
+ function resolveCliProviderName(agentConfig: AgentRuntimeAgentConfig) {
274
+ return (agentConfig.cliProvider || agentConfig.provider || "codex").trim() || "codex";
275
+ }
276
+
277
+ function stringifyRuntimeMessageContent(content: AgentRuntimeChatMessage["content"]) {
278
+ if (typeof content === "string") return content;
279
+ if (Array.isArray(content)) {
280
+ return content
281
+ .map((part) => {
282
+ if (typeof part === "string") return part;
283
+ if (part && typeof part === "object" && "text" in part) {
284
+ return String((part as any).text ?? "");
285
+ }
286
+ return JSON.stringify(part);
287
+ })
288
+ .filter((part) => part.trim())
289
+ .join("\n");
290
+ }
291
+ return content == null ? "" : String(content);
292
+ }
293
+
294
+ function buildPromptForCliProvider(messages: AgentRuntimeChatMessage[]) {
295
+ const systemPrompt = messages
296
+ .filter((message) => message.role === "system")
297
+ .map((message) => stringifyRuntimeMessageContent(message.content).trim())
298
+ .filter(Boolean)
299
+ .join("\n\n");
300
+ const taskPrompt = messages
301
+ .filter((message) => message.role !== "system")
302
+ .map((message) => {
303
+ const content = stringifyRuntimeMessageContent(message.content).trim();
304
+ if (!content) return "";
305
+ return `[${message.role}]\n${content}`;
306
+ })
307
+ .filter(Boolean)
308
+ .join("\n\n");
309
+ return buildCliPrompt(systemPrompt, taskPrompt);
310
+ }
311
+
312
+ function collectCliProviderImageInputs(messages: AgentRuntimeChatMessage[]) {
313
+ const urls: string[] = [];
314
+ for (const message of messages) {
315
+ if (!Array.isArray(message.content)) continue;
316
+ for (const part of message.content) {
317
+ if (
318
+ part &&
319
+ typeof part === "object" &&
320
+ (part as any).type === "image_url" &&
321
+ typeof (part as any).image_url?.url === "string" &&
322
+ (part as any).image_url.url.trim()
323
+ ) {
324
+ urls.push((part as any).image_url.url.trim());
325
+ }
326
+ }
327
+ }
328
+ return urls;
329
+ }
330
+
254
331
  function logLocalRuntimeDiagnostic(event: string, fields: Record<string, unknown>) {
332
+ if (
333
+ process.env.NOLO_LOCAL_RUNTIME_DEBUG !== "1" &&
334
+ process.env.NOLO_DEBUG !== "1"
335
+ ) {
336
+ return;
337
+ }
255
338
  console.error(`[nolo-local-runtime] ${event} ${JSON.stringify(fields)}`);
256
339
  }
257
340
 
@@ -266,31 +349,38 @@ function summarizeOpenAiToolNames(tools: Array<Record<string, unknown>>) {
266
349
  .filter((name): name is string => Boolean(name));
267
350
  }
268
351
 
269
- function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
352
+ function shouldExposeLocalPlatformTools(agentKey?: string) {
353
+ return agentKey !== MIMO_MONTH_AGENT_KEY;
354
+ }
355
+
356
+ function buildOpenAiTools(args: { agentKey?: string; toolNames?: string[]; env: EnvLike }) {
270
357
  const toolset = buildLocalWorkspaceToolsetForEnv(args);
358
+ const exposePlatformTools = shouldExposeLocalPlatformTools(args.agentKey);
271
359
  return [
272
360
  ...buildLocalWorkspaceOpenAiTools({
273
361
  toolNames: toolset.toolNames,
274
362
  exposeShellTools: toolset.exposeShellTools,
275
363
  }),
276
- ...buildServerPlatformOpenAiTools({ toolNames: args.toolNames }),
277
- ...buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }),
364
+ ...(exposePlatformTools ? buildServerPlatformOpenAiTools({ toolNames: args.toolNames }) : []),
365
+ ...(exposePlatformTools ? buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }) : []),
278
366
  ];
279
367
  }
280
368
 
281
369
  function buildLocalWorkspaceToolsetForEnv(args: { toolNames?: string[]; env: EnvLike }) {
370
+ void args.env;
282
371
  const toolset = buildLocalWorkspaceToolset({
283
372
  declaredToolNames: args.toolNames,
284
- exposeShellTools: shellModeEnabled(args.env),
373
+ exposeShellTools: true,
285
374
  });
286
375
  return toolset;
287
376
  }
288
377
 
289
378
  function buildLocalPolicyToolNames(args: { toolNames?: string[]; env: EnvLike }) {
379
+ void args.env;
290
380
  return [
291
381
  ...buildLocalWorkspacePolicyToolNames({
292
382
  declaredToolNames: args.toolNames,
293
- exposeShellTools: shellModeEnabled(args.env),
383
+ exposeShellTools: true,
294
384
  }),
295
385
  ...((args.toolNames ?? []).includes("taskRun") ? ["taskRun"] : []),
296
386
  ...filterNoloWorkspaceToolNames(args.toolNames),
@@ -336,16 +426,32 @@ function isBuiltinNoloAgentConfig(agentConfig: AgentRuntimeAgentConfig | null |
336
426
  return isBuiltinNoloAgentRef(key) || isBuiltinNoloAgentRef(id);
337
427
  }
338
428
 
339
- function withBuiltinNoloCliTools(agentConfig: AgentRuntimeAgentConfig | null) {
340
- if (!agentConfig || !isBuiltinNoloAgentConfig(agentConfig)) return agentConfig;
341
- const toolNames = [...new Set([...(agentConfig.toolNames ?? []), ...NOLO_WORKSPACE_TOOL_NAMES])];
429
+ function withResolvedRuntimeToolSurface(
430
+ agentConfig: AgentRuntimeAgentConfig | null,
431
+ env: EnvLike
432
+ ) {
433
+ if (!agentConfig) return agentConfig;
434
+ const currentUserId = resolveLocalUserId(env);
435
+ const rawRecord = (agentConfig as any).rawRecord ?? {};
436
+ const ownerId =
437
+ typeof rawRecord.userId === "string" && rawRecord.userId.trim()
438
+ ? rawRecord.userId.trim()
439
+ : null;
440
+ const toolSurface = resolveRuntimeToolSurfaceForAgent({
441
+ explicitToolNames: agentConfig.toolNames,
442
+ currentUserId,
443
+ agentOwnerId: ownerId,
444
+ agentKey: rawRecord.dbKey ?? agentConfig.key,
445
+ isPublic: !isBuiltinNoloAgentConfig(agentConfig) && rawRecord.isPublic === true,
446
+ sharingLevel: typeof rawRecord.sharingLevel === "string" ? rawRecord.sharingLevel : null,
447
+ trustedPrivateInvocation: isBuiltinNoloAgentConfig(agentConfig),
448
+ runtimeHost: "cli",
449
+ });
342
450
  return {
343
451
  ...agentConfig,
344
- toolNames,
345
- prompt: [
346
- agentConfig.prompt?.trim() ?? "",
347
- NOLO_WORKSPACE_TOOL_PROMPT,
348
- ].filter(Boolean).join("\n\n"),
452
+ toolNames: toolSurface.finalToolNames,
453
+ toolSurface,
454
+ prompt: agentConfig.prompt,
349
455
  };
350
456
  }
351
457
 
@@ -500,11 +606,14 @@ export function createCliLocalRuntimeAdapter(
500
606
  host: "cli",
501
607
  capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence", "local-tools"],
502
608
  loadAgentConfig: async (agentRef) => {
503
- const agentConfig = withBuiltinNoloCliTools(await readAgentFromStore({
504
- agentRef,
505
- store: await resolveStore(deps),
506
- userId,
507
- }));
609
+ const agentConfig = withResolvedRuntimeToolSurface(
610
+ await readAgentFromStore({
611
+ agentRef,
612
+ store: await resolveStore(deps),
613
+ userId,
614
+ }),
615
+ deps.env
616
+ );
508
617
  const requestedToolNames = agentConfig
509
618
  ? resolveRequestedRuntimeToolNames({ agentConfig })
510
619
  : [];
@@ -537,6 +646,50 @@ export function createCliLocalRuntimeAdapter(
537
646
  cwd: workspaceRoot,
538
647
  }),
539
648
  resolveProvider: async (agentConfig) => {
649
+ if (isCliProviderAgent(agentConfig)) {
650
+ const provider = resolveCliProviderName(agentConfig);
651
+ logLocalRuntimeDiagnostic("provider.selected", {
652
+ agentKey: agentConfig.key,
653
+ transport: "local-cli",
654
+ apiSource: agentConfig.apiSource ?? null,
655
+ provider,
656
+ model: agentConfig.model ?? null,
657
+ cwd: workspaceRoot,
658
+ });
659
+ return {
660
+ model: agentConfig.model || provider,
661
+ complete: async (messages, options) => {
662
+ const executeCli = deps.executeCli ?? (defaultExecuteCli as LocalCliExecutor);
663
+ const imageUrls = collectCliProviderImageInputs(messages);
664
+ const imageInputs: CliImageInput[] | undefined =
665
+ imageUrls.length > 0
666
+ ? imageUrls.map((url) => ({ source: url }))
667
+ : undefined;
668
+ const prompt = buildPromptForCliProvider(messages);
669
+ try {
670
+ const result = await executeCli(provider, prompt, {
671
+ ...(agentConfig.model ? { model: agentConfig.model } : {}),
672
+ ...(options?.timeoutMs ? { timeout: options.timeoutMs } : {}),
673
+ cwd: workspaceRoot,
674
+ yolo: true,
675
+ env: deps.env,
676
+ ...(imageInputs ? { imageInputs } : {}),
677
+ });
678
+ return {
679
+ content: result.text,
680
+ model: agentConfig.model || provider,
681
+ raw: result.raw,
682
+ };
683
+ } catch (error) {
684
+ const message = error instanceof Error ? error.message : String(error);
685
+ throw new Error(
686
+ `Local CLI provider "${provider}" is unavailable or failed: ${message}`
687
+ );
688
+ }
689
+ },
690
+ };
691
+ }
692
+
540
693
  if (shouldUsePlatformChatProvider(deps.env, agentConfig)) {
541
694
  const providerConfig = resolvePlatformChatProviderConfig({
542
695
  agentConfig,
@@ -563,6 +716,7 @@ export function createCliLocalRuntimeAdapter(
563
716
  agentConfig,
564
717
  });
565
718
  const tools = buildOpenAiTools({
719
+ agentKey: agentConfig.key,
566
720
  toolNames: requestedToolNames,
567
721
  env: deps.env,
568
722
  });
@@ -582,7 +736,6 @@ export function createCliLocalRuntimeAdapter(
582
736
  toolCount: tools.length,
583
737
  requestedToolNames,
584
738
  openAiToolNames: summarizeOpenAiToolNames(tools),
585
- shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
586
739
  timeoutMs: options?.timeoutMs ?? null,
587
740
  });
588
741
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
@@ -636,6 +789,7 @@ export function createCliLocalRuntimeAdapter(
636
789
  agentConfig,
637
790
  });
638
791
  const tools = buildOpenAiTools({
792
+ agentKey: agentConfig.key,
639
793
  toolNames: requestedToolNames,
640
794
  env: deps.env,
641
795
  });
@@ -654,7 +808,6 @@ export function createCliLocalRuntimeAdapter(
654
808
  toolCount: tools.length,
655
809
  requestedToolNames,
656
810
  openAiToolNames: summarizeOpenAiToolNames(tools),
657
- shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
658
811
  timeoutMs: options?.timeoutMs ?? null,
659
812
  });
660
813
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
@@ -7,6 +7,26 @@ import { runLocalAgentTurn } from "../agent-runtime/localLoop";
7
7
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
8
8
 
9
9
  describe("CLI local runtime dry run", () => {
10
+ const DEFAULT_PRIVATE_LOCAL_TOOL_NAMES = [
11
+ "listFiles",
12
+ "readFile",
13
+ "writeFile",
14
+ "editFile",
15
+ "searchFiles",
16
+ "execShell",
17
+ "listDialogs",
18
+ "readDialog",
19
+ "listAgents",
20
+ "readAgent",
21
+ "listSpaces",
22
+ "readSpace",
23
+ "readDoc",
24
+ "readSkillDoc",
25
+ "queryTableRows",
26
+ "cliWhoami",
27
+ "cliDoctor",
28
+ ];
29
+
10
30
  test("lets a declared workspace file tool write a file and save the tool trace", async () => {
11
31
  const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-local-runtime-dry-run-"));
12
32
  try {
@@ -18,7 +38,7 @@ describe("CLI local runtime dry run", () => {
18
38
  prompt: "Use workspace file tools to edit files.",
19
39
  model: "qwen-coder",
20
40
  tools: [
21
- { type: "function", function: { name: "writeWorkspaceFile" } },
41
+ { type: "function", function: { name: "writeFile" } },
22
42
  ],
23
43
  }],
24
44
  ]);
@@ -52,13 +72,7 @@ describe("CLI local runtime dry run", () => {
52
72
  const body = JSON.parse(String(init?.body));
53
73
  completeCount += 1;
54
74
  if (completeCount === 1) {
55
- expect(body.tools.map((tool: any) => tool.function.name)).toEqual([
56
- "writeWorkspaceFile",
57
- "listFiles",
58
- "readFile",
59
- "editFile",
60
- "searchFiles",
61
- ]);
75
+ expect(body.tools.map((tool: any) => tool.function.name)).toEqual(DEFAULT_PRIVATE_LOCAL_TOOL_NAMES);
62
76
  return Response.json({
63
77
  model: "qwen-coder",
64
78
  choices: [{
@@ -68,7 +82,7 @@ describe("CLI local runtime dry run", () => {
68
82
  id: "call-1",
69
83
  type: "function",
70
84
  function: {
71
- name: "writeWorkspaceFile",
85
+ name: "writeFile",
72
86
  arguments: JSON.stringify({
73
87
  path: "src/notification.css",
74
88
  content: ".notification { border-radius: 8px; }\n",
@@ -3,20 +3,20 @@ import { describe, expect, test } from "bun:test";
3
3
  import { executeLocalToolWithPolicy, resolveLocalToolPolicy } from "./localToolPolicy";
4
4
 
5
5
  describe("CLI local tool policy", () => {
6
- test("allows execShell in explicit worktree shell mode", () => {
6
+ test("allows execShell by default", () => {
7
7
  expect(resolveLocalToolPolicy({
8
- env: { NOLO_LOCAL_SHELL_MODE: "worktree" },
8
+ env: {},
9
9
  agentToolNames: [],
10
10
  toolName: "execShell",
11
11
  })).toEqual({ allowed: true, toolName: "execShell" });
12
12
  });
13
13
 
14
- test("blocks execShell unless explicit shell mode is enabled", () => {
14
+ test("allows execShell even without env allowlist", () => {
15
15
  expect(resolveLocalToolPolicy({
16
16
  env: { NOLO_LOCAL_ALLOWED_TOOLS: "execShell" },
17
17
  agentToolNames: ["execShell"],
18
18
  toolName: "execShell",
19
- })).toMatchObject({ allowed: false });
19
+ })).toEqual({ allowed: true, toolName: "execShell" });
20
20
  });
21
21
 
22
22
  test("requires both env allowlist and agent declaration", () => {
@@ -69,8 +69,8 @@ export function renderHelpText() {
69
69
  " nolo space read 01KKY77TT0DA9NY7TNW3R7255N --content-key page-user-id --brief",
70
70
  " nolo space delete --name-prefix rn_owner_verify_0504 --yes",
71
71
  " nolo table query --table 01ABCXYZ",
72
- ' nolo table query --table meta-b2e06f801f-NOLOTASKBOARD --columns \'["title","status","owner","priority","codeStatus"]\' --no-base-fields --output items',
73
- ' nolo table update-row --table meta-b2e06f801f-NOLOTASKBOARD --row 01ROWID --changes \'{"status":"已完成"}\'',
72
+ ' nolo table query --table meta-0e95801d90-NOLOTASKBOARD --columns \'["title","status","owner","priority","codeStatus"]\' --no-base-fields --output items',
73
+ ' nolo table update-row --table meta-0e95801d90-NOLOTASKBOARD --row 01ROWID --changes \'{"status":"已完成"}\'',
74
74
  " nolo task-run read-context --row-dbkey row-user-table-row",
75
75
  " nolo llama status",
76
76
  ];
@@ -48,6 +48,25 @@ export function createLevelAuthorityStore(
48
48
  const levelDb: LevelLike = typeof source === "string"
49
49
  ? new Level<string, any>(source, { valueEncoding: safeJsonEncoding })
50
50
  : source;
51
+ const writeBatchOps = async (ops: AuthorityBatchOperation[]) => {
52
+ const result = levelDb.batch(ops);
53
+ if (result && typeof (result as any).then === "function") {
54
+ await result;
55
+ return;
56
+ }
57
+ if (result && typeof (result as any).write === "function") {
58
+ for (const op of ops) {
59
+ if (op.type === "put") {
60
+ (result as LevelBatchWriterLike).put(op.key, op.value);
61
+ } else {
62
+ (result as LevelBatchWriterLike).del(op.key);
63
+ }
64
+ }
65
+ await (result as LevelBatchWriterLike).write();
66
+ return;
67
+ }
68
+ throw new Error("Level backing does not support batch writes");
69
+ };
51
70
 
52
71
  return {
53
72
  get location() {
@@ -72,7 +91,7 @@ export function createLevelAuthorityStore(
72
91
  await levelDb.del(key);
73
92
  },
74
93
  async batchWrite(ops: AuthorityBatchOperation[]) {
75
- await levelDb.batch(ops);
94
+ await writeBatchOps(ops);
76
95
  },
77
96
  createBatch(): AuthorityBatchWriter {
78
97
  const batch = levelDb.batch();
@@ -20,9 +20,52 @@ type EnvLike = Record<string, string | undefined>;
20
20
  export type LocalCliExecutor = (
21
21
  provider: string,
22
22
  prompt: string,
23
- 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
+ }
24
31
  ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
25
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
+
26
69
  type ConnectorRuntimePolicy = {
27
70
  runtimeTools?: string[];
28
71
  agentTools?: string[];
@@ -103,7 +146,6 @@ function summarizeRuntimePolicyForLogs(policy?: ConnectorRuntimePolicy) {
103
146
  workspaceMode: policy?.workspace?.mode ?? null,
104
147
  workspaceCwd: policy?.workspace?.cwd ?? null,
105
148
  shellEnabled: policy?.shell?.enabled ?? null,
106
- shellMode: policy?.shell?.mode ?? null,
107
149
  };
108
150
  }
109
151
 
@@ -188,13 +230,13 @@ function runtimeWorkspacePermissionPolicy(
188
230
  runtimeEnv: EnvLike,
189
231
  cwd: string
190
232
  ): PermissionPolicy | null {
233
+ void runtimeEnv;
191
234
  if (!requestsLocalWorkspaceRuntime(runtimePolicy)) return null;
192
235
  return {
193
236
  mode: "full_access",
194
237
  allowFilesystemRead: true,
195
238
  allowFilesystemWrite: true,
196
239
  allowShell:
197
- runtimeEnv.NOLO_LOCAL_SHELL_MODE === "worktree" &&
198
240
  Array.isArray(runtimePolicy?.runtimeTools) &&
199
241
  runtimePolicy.runtimeTools.includes("execShell"),
200
242
  writableRoots: [cwd],
@@ -332,8 +374,6 @@ async function defaultRunConnectorLocalRuntimeAgent(args: {
332
374
  requestId: args.parsed.requestId,
333
375
  agentKey,
334
376
  cwd: args.cwd,
335
- runtimeEnvShellModeBeforePolicy: args.runtimeEnv.NOLO_LOCAL_SHELL_MODE ?? null,
336
- runtimeEnvShellModeAfterPolicy: env.NOLO_LOCAL_SHELL_MODE ?? null,
337
377
  ...summarizeRuntimePolicyForLogs(policy),
338
378
  });
339
379
  const adapter = createCliLocalRuntimeAdapter({
@@ -454,7 +494,9 @@ export async function handleConnectorRunMessage(
454
494
  try {
455
495
  const machinePermissionPolicy = resolveMachineRunPermissionPolicy(agentConfig);
456
496
  const runtimePolicy = runtimePolicyFromConnectorPayload(parsed);
457
- const userInput = String(parsed.payload?.userInput ?? "");
497
+ const extractedInput = extractMultimodalUserInput(parsed.payload?.userInput);
498
+ const userInput = extractedInput.text;
499
+ const connectorImageUrls = extractedInput.imageUrls;
458
500
  const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
459
501
  let cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy: machinePermissionPolicy });
460
502
  let runContent = "";
@@ -553,6 +595,9 @@ export async function handleConnectorRunMessage(
553
595
  effectiveRuntimeEnv.AUTH_TOKEN ||
554
596
  effectiveRuntimeEnv.AUTH,
555
597
  },
598
+ ...(connectorImageUrls.length > 0
599
+ ? { imageInputs: connectorImageUrls.map((url) => ({ source: url })) }
600
+ : {}),
556
601
  }).finally(() => clearInterval(heartbeat));
557
602
  logConnectorRun("agent.run.cli.result", {
558
603
  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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
@@ -169,6 +169,7 @@
169
169
  "agent-runtime/hybridRecordStore.ts",
170
170
  "agent-runtime/localToolPolicy.ts",
171
171
  "agent-runtime/shellCommandPolicy.ts",
172
+ "agent-runtime/runtimeToolSurface.ts",
172
173
  "connector-experimental/heartbeatLoop.ts",
173
174
  "connector-experimental/capabilities.ts",
174
175
  "connector-experimental/codexBinary.ts",