nolo-cli 0.1.21 → 0.1.23

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 (72) hide show
  1. package/agent-runtime/agentRecordConfig.ts +4 -0
  2. package/agent-runtime/hostAdapter.ts +2 -0
  3. package/agent-runtime/index.ts +7 -0
  4. package/agent-runtime/localLoop.ts +2 -0
  5. package/agent-runtime/platformChatProvider.ts +3 -0
  6. package/agent-runtime/runtimeToolPolicy.ts +92 -0
  7. package/agent-runtime/types.ts +42 -0
  8. package/agentRunCommand.ts +74 -1
  9. package/agentRuntimeCommands.ts +17 -89
  10. package/ai/agent/streamAgentChatTurn.ts +104 -20
  11. package/ai/chat/fetchUtils.native.ts +2 -0
  12. package/ai/chat/fetchUtils.ts +2 -0
  13. package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
  14. package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
  15. package/ai/llm/kimi.ts +1 -1
  16. package/ai/llm/providers.ts +3 -0
  17. package/ai/llm/reasoningModels.ts +1 -0
  18. package/ai/skills/skillDocProtocol.ts +95 -3
  19. package/ai/taskRun/taskRunProtocol.ts +1 -0
  20. package/ai/tools/agent/agentTools.ts +17 -0
  21. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  22. package/ai/tools/modelUsageTools.ts +5 -0
  23. package/client/agentRun.test.ts +257 -7
  24. package/client/agentRun.ts +133 -34
  25. package/client/localRuntimeAdapter.test.ts +2 -0
  26. package/client/localRuntimeAdapter.ts +15 -2
  27. package/database/actions/common.ts +4 -3
  28. package/database/config.ts +19 -0
  29. package/machineCommands.ts +400 -45
  30. package/package.json +4 -2
  31. package/render/canvas/canvasEditContext.ts +127 -0
  32. package/render/canvas/canvasRuntime.ts +57 -0
  33. package/render/canvas/canvasSnapshotParser.ts +76 -0
  34. package/render/canvas/canvasTree.ts +308 -0
  35. package/render/canvas/types.ts +46 -0
  36. package/render/layout/deleteBehavior.ts +52 -0
  37. package/render/layout/mainLayoutSidebar.ts +17 -0
  38. package/render/layout/mainLayoutViewMode.ts +56 -0
  39. package/render/layout/topbarUtils.ts +87 -0
  40. package/render/layout/useDevReloadPending.ts +30 -0
  41. package/render/page/createPageAction.ts +183 -0
  42. package/render/page/docSlice.ts +468 -0
  43. package/render/page/server/createPage.ts +174 -0
  44. package/render/page/server/handleCreatePage.ts +91 -0
  45. package/render/page/server/index.ts +4 -0
  46. package/render/page/types.ts +17 -0
  47. package/render/page/useKeyboardSave.ts +48 -0
  48. package/render/styles/zIndex.ts +12 -0
  49. package/render/surf/WeatherIconStyles.ts +17 -0
  50. package/render/surf/color.ts +9 -0
  51. package/render/surf/config.ts +46 -0
  52. package/render/surf/screens/style.ts +1 -0
  53. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  54. package/render/surf/utils/groupedWeatherData.ts +32 -0
  55. package/render/surf/weatherUtils.ts +50 -0
  56. package/render/table/activityColumns.ts +6 -0
  57. package/render/table/createTableAction.ts +270 -0
  58. package/render/table/deleteTableAction.ts +129 -0
  59. package/render/table/fetchAndCacheTableRows.ts +174 -0
  60. package/render/table/tableSlice.ts +1106 -0
  61. package/render/table/tableView.ts +289 -0
  62. package/render/table/toolValueUtils.ts +363 -0
  63. package/render/table/types.ts +252 -0
  64. package/render/table/useCreateTable.ts +72 -0
  65. package/render/table/useTable.ts +61 -0
  66. package/render/table/utils/tableSerialization.ts +50 -0
  67. package/render/web/elements/artifactPreviewCode.ts +43 -0
  68. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  69. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  70. package/render/web/elements/mermaidPreview.ts +21 -0
  71. package/render/web/ui/useInlineEdit.ts +135 -0
  72. package/tableCommands.ts +42 -5
@@ -1,6 +1,13 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { runLocalAgentTurn } from "../agentRuntimeLocal";
4
+ import {
5
+ FRONTEND_IMPLEMENTER_AGENT_KEY,
6
+ NOLO_FULLSTACK_AGENT_KEY,
7
+ NOLO_PROJECT_MANAGER_AGENT_KEY,
8
+ NOLO_REVIEW_AGENT_KEY,
9
+ NOLO_SENIOR_FULLSTACK_AGENT_KEY,
10
+ } from "../agentAliases";
4
11
  import type { LocalAgentToolEvent } from "../agent-runtime/localLoop";
5
12
  import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
6
13
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
@@ -52,7 +59,7 @@ type RunAgentTurnOptions = {
52
59
  taskRunContext?: TaskRunPromptContext;
53
60
  taskRunRecorder?: (args: {
54
61
  options: RunAgentTurnOptions;
55
- status: "completed" | "failed";
62
+ status: "running" | "completed" | "failed";
56
63
  dialogId?: string;
57
64
  resultSummary?: string;
58
65
  errorCode?: string;
@@ -62,10 +69,11 @@ type RunAgentTurnOptions = {
62
69
  fetchImpl?: typeof fetch;
63
70
  };
64
71
 
65
- export type RunAgentTurnResult = {
66
- exitCode: number;
67
- dialogId?: string;
68
- };
72
+ export type RunAgentTurnResult = {
73
+ exitCode: number;
74
+ dialogId?: string;
75
+ streamInterrupted?: boolean;
76
+ };
69
77
 
70
78
  type ScriptBridgeDecision = {
71
79
  hasAuthToken: boolean;
@@ -84,6 +92,38 @@ const SERVER_PLATFORM_TOOL_NAMES = new Set([
84
92
  "updateTableRows",
85
93
  ]);
86
94
 
95
+ const KNOWN_SERVER_PLATFORM_AGENT_KEYS = new Set([
96
+ FRONTEND_IMPLEMENTER_AGENT_KEY,
97
+ NOLO_FULLSTACK_AGENT_KEY,
98
+ NOLO_PROJECT_MANAGER_AGENT_KEY,
99
+ NOLO_REVIEW_AGENT_KEY,
100
+ NOLO_SENIOR_FULLSTACK_AGENT_KEY,
101
+ ]);
102
+
103
+ const KNOWN_SERVER_PLATFORM_AGENT_ALIASES = new Set([
104
+ "code-review",
105
+ "frontend",
106
+ "frontend-agent",
107
+ "frontend-implementer",
108
+ "full-stack",
109
+ "fullstack",
110
+ "nolo code review",
111
+ "nolo frontend",
112
+ "nolo fullstack",
113
+ "nolo project manager",
114
+ "nolo reviewer",
115
+ "nolo-code-review",
116
+ "nolo-frontend",
117
+ "nolo-fullstack",
118
+ "nolo-pm",
119
+ "nolo-project-manager",
120
+ "nolo-reviewer",
121
+ "pm",
122
+ "project-manager",
123
+ "review",
124
+ "reviewer",
125
+ ]);
126
+
87
127
  export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
88
128
  return !decision.hasAuthToken && decision.scriptPathExists;
89
129
  }
@@ -92,6 +132,25 @@ export function findServerPlatformTools(toolNames?: string[]) {
92
132
  if (!Array.isArray(toolNames)) return [];
93
133
  return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
94
134
  }
135
+
136
+ function resolveServerPlatformToolNames(agentConfig: any) {
137
+ return findServerPlatformTools([
138
+ ...(Array.isArray(agentConfig?.toolNames) ? agentConfig.toolNames : []),
139
+ ...(Array.isArray(agentConfig?.runtimeToolPolicy?.agentTools)
140
+ ? agentConfig.runtimeToolPolicy.agentTools
141
+ : []),
142
+ ]);
143
+ }
144
+
145
+ function normalizeAgentRef(ref?: string) {
146
+ return ref?.trim().toLowerCase().replace(/\s+/g, " ");
147
+ }
148
+
149
+ function isKnownServerPlatformAgent(options: RunAgentTurnOptions) {
150
+ if (KNOWN_SERVER_PLATFORM_AGENT_KEYS.has(options.agentKey)) return true;
151
+ const normalizedKey = normalizeAgentRef(options.agentKey);
152
+ return Boolean(normalizedKey && KNOWN_SERVER_PLATFORM_AGENT_ALIASES.has(normalizedKey));
153
+ }
95
154
 
96
155
  function resolveAuthToken(env: EnvLike) {
97
156
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
@@ -126,6 +185,13 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
126
185
  }
127
186
 
128
187
  async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
188
+ if (isKnownServerPlatformAgent(options)) {
189
+ options.output.write(
190
+ `[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a known platform agent. ` +
191
+ "Use --local explicitly to force local workspace tools.\n"
192
+ );
193
+ return true;
194
+ }
129
195
  const adapter = resolveLocalRuntimeAdapter(options);
130
196
  if (!adapter) return false;
131
197
  let agentConfig;
@@ -134,7 +200,7 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
134
200
  } catch {
135
201
  return false;
136
202
  }
137
- const serverTools = findServerPlatformTools(agentConfig?.toolNames);
203
+ const serverTools = resolveServerPlatformToolNames(agentConfig);
138
204
  if (serverTools.length === 0) return false;
139
205
  options.output.write(
140
206
  `[nolo] auto runtime: skipping local runtime because ${options.agentKey} declares server platform tools ` +
@@ -292,7 +358,7 @@ async function runTaskRunCli(args: {
292
358
 
293
359
  async function defaultTaskRunRecorder(args: {
294
360
  options: RunAgentTurnOptions;
295
- status: "completed" | "failed";
361
+ status: "running" | "completed" | "failed";
296
362
  dialogId?: string;
297
363
  resultSummary?: string;
298
364
  errorCode?: string;
@@ -301,7 +367,7 @@ async function defaultTaskRunRecorder(args: {
301
367
  const context = args.options.taskRunContext;
302
368
  if (!context?.rowDbKey) return;
303
369
  const role = inferTaskRunAgentRole(args.options.agentName || args.options.agentKey);
304
- if (role === "reviewer") {
370
+ if (role === "reviewer" && args.status !== "running") {
305
371
  await runTaskRunCli({
306
372
  options: args.options,
307
373
  cliArgs: [
@@ -359,7 +425,7 @@ async function defaultTaskRunRecorder(args: {
359
425
  ],
360
426
  });
361
427
  }
362
- if (role === "reviewer") {
428
+ if (role === "reviewer" && args.status !== "running") {
363
429
  const reviewStatus =
364
430
  args.status === "failed" ? "blocked" : classifyTaskRunReviewStatus(args.resultSummary);
365
431
  if (reviewStatus) {
@@ -385,7 +451,7 @@ async function defaultTaskRunRecorder(args: {
385
451
 
386
452
  async function recordTaskRunOutcome(args: {
387
453
  options: RunAgentTurnOptions;
388
- status: "completed" | "failed";
454
+ status: "running" | "completed" | "failed";
389
455
  dialogId?: string;
390
456
  resultSummary?: string;
391
457
  errorCode?: string;
@@ -510,12 +576,28 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
510
576
  options.imageUrls
511
577
  ),
512
578
  runtimeContext: {
513
- surface: "cli",
514
- host: "terminal",
515
- runtime: "bun",
516
- entrypoint: "nolo-cli",
517
- capabilities: ["text-io", "streaming", "slash-commands"],
518
- },
579
+ surface: "cli",
580
+ host: "terminal",
581
+ runtime: "bun",
582
+ entrypoint: "nolo-cli",
583
+ capabilities: ["text-io", "streaming", "slash-commands"],
584
+ ...(options.taskRunContext
585
+ ? {
586
+ taskRun: {
587
+ rowDbKey: options.taskRunContext.rowDbKey,
588
+ ...(options.taskRunContext.taskRunId
589
+ ? { taskRunId: options.taskRunContext.taskRunId }
590
+ : {}),
591
+ ...(options.taskRunContext.workItemId
592
+ ? { workItemId: options.taskRunContext.workItemId }
593
+ : {}),
594
+ ...(options.taskRunContext.artifactIds?.length
595
+ ? { artifactIds: options.taskRunContext.artifactIds }
596
+ : {}),
597
+ },
598
+ }
599
+ : {}),
600
+ },
519
601
  ...(options.continueDialogId
520
602
  ? { continueDialogId: options.continueDialogId }
521
603
  : {}),
@@ -541,7 +623,11 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
541
623
  const result = await readStreamingAgentRun(options, res);
542
624
  await recordTaskRunOutcome({
543
625
  options,
544
- status: result.exitCode === 0 ? "completed" : "failed",
626
+ status: result.streamInterrupted
627
+ ? "running"
628
+ : result.exitCode === 0
629
+ ? "completed"
630
+ : "failed",
545
631
  dialogId: result.dialogId,
546
632
  ...(result.exitCode !== 0 ? { errorCode: "STREAM_FAILED" } : {}),
547
633
  });
@@ -586,9 +672,9 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
586
672
  };
587
673
  await recordTaskRunOutcome({
588
674
  options,
589
- status: "completed",
675
+ status: options.background ? "running" : "completed",
590
676
  dialogId: result.dialogId,
591
- resultSummary: content || undefined,
677
+ ...(!options.background && content ? { resultSummary: content } : {}),
592
678
  });
593
679
  return result;
594
680
  }
@@ -691,9 +777,9 @@ async function runLocalAgentTurnForCli(
691
777
  }
692
778
  await recordTaskRunOutcome({
693
779
  options,
694
- status: "completed",
780
+ status: options.background ? "running" : "completed",
695
781
  dialogId: result.dialogId,
696
- resultSummary: content || undefined,
782
+ ...(!options.background && content ? { resultSummary: content } : {}),
697
783
  toolEvidence: summarizeLocalToolEvidence(toolEvents),
698
784
  });
699
785
  return { exitCode: 0, dialogId: result.dialogId };
@@ -735,15 +821,20 @@ async function readStreamingAgentRun(
735
821
  hasPrintedLabel = true;
736
822
  };
737
823
 
738
- const handlePayload = (payload: any) => {
739
- if (payload?.error || payload?.type === "error") {
740
- throw new Error(String(payload.error || payload.message || "Agent stream failed"));
741
- }
742
- if (payload?.type === "done") {
743
- if (typeof payload.dialogId === "string") dialogId = payload.dialogId;
744
- usage = payload.usage;
745
- return;
746
- }
824
+ const handlePayload = (payload: any) => {
825
+ if (typeof payload?.dialogId === "string" && payload.dialogId.trim()) {
826
+ dialogId = payload.dialogId;
827
+ }
828
+ if (payload?.error || payload?.type === "error") {
829
+ throw new Error(String(payload.error || payload.message || "Agent stream failed"));
830
+ }
831
+ if (payload?.type === "done") {
832
+ usage = payload.usage;
833
+ return;
834
+ }
835
+ if (payload?.type === "dialog" || payload?.type === "status") {
836
+ return;
837
+ }
747
838
 
748
839
  const chunk =
749
840
  payload?.type === "text"
@@ -786,10 +877,18 @@ async function readStreamingAgentRun(
786
877
  .trim();
787
878
  if (raw) handlePayload(JSON.parse(raw));
788
879
  }
789
- } catch (error) {
790
- options.output.write(`\n[nolo] Agent stream failed: ${error instanceof Error ? error.message : String(error)}\n`);
791
- return { exitCode: 1 };
792
- } finally {
880
+ } catch (error) {
881
+ const message = error instanceof Error ? error.message : String(error);
882
+ if (dialogId) {
883
+ options.output.write(
884
+ `\n[nolo] Agent stream transport interrupted after dialog ${dialogId} was created: ${message}\n`
885
+ );
886
+ options.output.write("[nolo] The agent run may still finish on the server; read the dialog before retrying.\n");
887
+ return { exitCode: 0, dialogId, streamInterrupted: true };
888
+ }
889
+ options.output.write(`\n[nolo] Agent stream failed: ${message}\n`);
890
+ return { exitCode: 1 };
891
+ } finally {
793
892
  writer.flushAll();
794
893
  }
795
894
 
@@ -353,6 +353,7 @@ describe("CLI local runtime adapter", () => {
353
353
  tool_choice: "auto",
354
354
  url: "https://api.fireworks.ai/inference/v1/chat/completions",
355
355
  provider: "fireworks",
356
+ agentKey: "agent-user-1-frontend",
356
357
  },
357
358
  });
358
359
  expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
@@ -419,6 +420,7 @@ describe("CLI local runtime adapter", () => {
419
420
  body: {
420
421
  provider: "fireworks",
421
422
  apiSource: "platform",
423
+ agentKey: "agent-user-1-frontend",
422
424
  },
423
425
  });
424
426
  });
@@ -58,6 +58,7 @@ export type CliLocalRuntimeAdapterDeps = {
58
58
  createId?: () => string;
59
59
  fetchImpl?: typeof fetch;
60
60
  cwd?: string;
61
+ useCwdAsTaskWorkspaceBase?: boolean;
61
62
  output?: { write(chunk: string): unknown };
62
63
  prepareTaskWorktree?: typeof prepareTaskWorktree;
63
64
  localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
@@ -254,7 +255,11 @@ export function createCliLocalRuntimeAdapter(
254
255
  const localToolBudgets = parseLocalToolBudgets(deps.env);
255
256
  const localToolUsage = new Map<string, number>();
256
257
  let activeAgentToolNames: string[] = [];
257
- let workspaceSession: WorkspaceSession = createWorkspaceSession({ cwd: deps.cwd });
258
+ let workspaceSession: WorkspaceSession = createWorkspaceSession(
259
+ deps.useCwdAsTaskWorkspaceBase
260
+ ? { defaultCwd: deps.cwd }
261
+ : { cwd: deps.cwd }
262
+ );
258
263
  let localToolExecutors = buildLocalToolExecutors({
259
264
  workspaceRoot: workspaceSession.workspaceRoot,
260
265
  localToolExecutors: deps.localToolExecutors,
@@ -378,12 +383,20 @@ export function createCliLocalRuntimeAdapter(
378
383
  budgets: localToolBudgets,
379
384
  usage: localToolUsage,
380
385
  });
381
- return executeLocalToolWithPolicy({
386
+ const result = await executeLocalToolWithPolicy({
382
387
  env: deps.env,
383
388
  agentToolNames: activeAgentToolNames,
384
389
  call,
385
390
  executors: localToolExecutors,
386
391
  });
392
+ return {
393
+ ...result,
394
+ metadata: {
395
+ ...(result.metadata ?? {}),
396
+ workspaceRoot: workspaceSession.workspaceRoot,
397
+ workspaceKind: workspaceSession.kind,
398
+ },
399
+ };
387
400
  },
388
401
  };
389
402
  }
@@ -2,7 +2,8 @@
2
2
 
3
3
  import pino from "pino";
4
4
  import { getIsDesktopApp } from "app/utils/env";
5
- import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS } from "../config";
5
+ import { fetchWithTransientReadRetry } from "app/utils/retryFetch";
6
+ import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS, normalizeKnownServerOrigin } from "../config";
6
7
 
7
8
  // RN 下 pino 的 browser 写法可能有兼容性问题
8
9
  // 使用简单的 console 封装作为 fallback
@@ -23,7 +24,7 @@ export const logger = isRN ? {
23
24
  // },
24
25
  });
25
26
  const normalizeServer = (server: string): string =>
26
- server.trim().replace(/\/+$/, "");
27
+ normalizeKnownServerOrigin(server) ?? server.trim().replace(/\/+$/, "");
27
28
  const isNoloClusterServer = (server: string): boolean =>
28
29
  /^https?:\/\/(?:us\.)?nolo\.chat$/i.test(normalizeServer(server));
29
30
 
@@ -202,7 +203,7 @@ export const fetchFromServer = async (
202
203
  signal?.addEventListener("abort", onExternalAbort);
203
204
 
204
205
  try {
205
- const res = await fetch(
206
+ const res = await fetchWithTransientReadRetry(
206
207
  `${server}${buildReadUrl(dbKey)}`,
207
208
  {
208
209
  signal: controller.signal as any,
@@ -8,6 +8,25 @@ export const SERVERS = {
8
8
  } as const;
9
9
  export const NOLO_CLUSTER_SERVERS = Object.values(SERVERS);
10
10
 
11
+ const LEGACY_SERVER_ORIGIN_MAP: Record<string, string> = {
12
+ "https://nolotus.com": SERVERS.MAIN,
13
+ "https://www.nolotus.com": SERVERS.MAIN,
14
+ "https://us.nolotus.com": SERVERS.US,
15
+ "https://www.us.nolotus.com": SERVERS.US,
16
+ };
17
+
18
+ export const normalizeKnownServerOrigin = (server: unknown): string | null => {
19
+ if (typeof server !== "string" || server.trim().length === 0) return null;
20
+ const trimmed = server.trim();
21
+ let origin: string;
22
+ try {
23
+ origin = new URL(trimmed).origin;
24
+ } catch {
25
+ origin = trimmed.replace(/\/+$/, "");
26
+ }
27
+ return LEGACY_SERVER_ORIGIN_MAP[origin.toLowerCase()] ?? origin;
28
+ };
29
+
11
30
  export const API_ENDPOINTS = {
12
31
  DATABASE: `${API_VERSION}/db`,
13
32
  SHARE: `${API_VERSION}/share`,