nolo-cli 0.1.13 → 0.1.14

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 (320) hide show
  1. package/README.md +9 -2
  2. package/agent-runtime/hostAdapter.ts +53 -0
  3. package/agent-runtime/index.ts +28 -0
  4. package/agent-runtime/localLoop.ts +62 -0
  5. package/agent-runtime/runtimeDecision.ts +70 -0
  6. package/agent-runtime/types.ts +87 -0
  7. package/agentRuntimeCommands.ts +139 -22
  8. package/agentRuntimeLocal.ts +7 -0
  9. package/ai/agent/_executeModel.ts +118 -0
  10. package/ai/agent/agentSlice.ts +544 -1
  11. package/ai/agent/appWorkingMemory.ts +126 -0
  12. package/ai/agent/avatarUtils.ts +24 -0
  13. package/ai/agent/buildEditingContext.ts +373 -0
  14. package/ai/agent/buildSystemPrompt.ts +532 -0
  15. package/ai/agent/cleanAgentMessages.ts +140 -0
  16. package/ai/agent/cliChatClient.ts +119 -0
  17. package/ai/agent/contextCompiler.ts +107 -0
  18. package/ai/agent/contextLayerContract.ts +44 -0
  19. package/ai/agent/createAgentSchema.ts +234 -0
  20. package/ai/agent/executeToolCall.ts +58 -0
  21. package/ai/agent/fetchAgentContexts.ts +42 -0
  22. package/ai/agent/generatePrompt.ts +3 -0
  23. package/ai/agent/getFullChatContextKeys.ts +168 -0
  24. package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
  25. package/ai/agent/hooks/useAgentConfig.ts +61 -0
  26. package/ai/agent/hooks/useAgentDialog.ts +35 -0
  27. package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
  28. package/ai/agent/hooks/usePublicAgents.ts +473 -0
  29. package/ai/agent/persistMessageWithFixedId.ts +37 -0
  30. package/ai/agent/planSlice.ts +259 -0
  31. package/ai/agent/referenceUtils.ts +229 -0
  32. package/ai/agent/runAgentBackground.ts +238 -0
  33. package/ai/agent/runAgentClientLoop.ts +138 -0
  34. package/ai/agent/runtimeGuidance.ts +97 -0
  35. package/ai/agent/runtimeServerBase.ts +37 -0
  36. package/ai/agent/server/fetchPublicAgents.ts +128 -0
  37. package/ai/agent/startParallelAgentStreams.ts +424 -0
  38. package/ai/agent/startupProtocol.ts +53 -0
  39. package/ai/agent/streamAgentChatTurn.ts +1299 -0
  40. package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
  41. package/ai/agent/types.ts +71 -0
  42. package/ai/agent/utils/imageOutput.ts +39 -0
  43. package/ai/agent/utils/publicImageAgentMode.ts +26 -0
  44. package/ai/agent/utils/sortUtils.ts +250 -0
  45. package/ai/agent/web/referencePickerUtils.ts +146 -0
  46. package/ai/ai.locale.ts +1083 -0
  47. package/ai/chat/accumulateToolCallChunks.ts +95 -0
  48. package/ai/chat/fetchUtils.native.ts +276 -0
  49. package/ai/chat/fetchUtils.ts +153 -0
  50. package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
  51. package/ai/chat/parseApiError.ts +64 -0
  52. package/ai/chat/parseMultilineSSE.ts +95 -0
  53. package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
  54. package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
  55. package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
  56. package/ai/chat/shouldUseServerProxy.ts +18 -0
  57. package/ai/chat/sseClient.native.ts +91 -0
  58. package/ai/chat/sseClient.ts +67 -0
  59. package/ai/chat/streamReader.native.ts +31 -0
  60. package/ai/chat/streamReader.ts +62 -0
  61. package/ai/chat/updateTotalUsage.ts +72 -0
  62. package/ai/context/buildReferenceContext.ts +437 -0
  63. package/ai/context/calculateContextUsage.ts +133 -0
  64. package/ai/context/retention.ts +165 -0
  65. package/ai/context/tokenUtils.ts +78 -0
  66. package/ai/index.ts +1 -1
  67. package/ai/llm/agentCapabilities.ts +74 -0
  68. package/ai/llm/calculateGeminiImageTokens.ts +57 -0
  69. package/ai/llm/deepinfra.ts +28 -0
  70. package/ai/llm/fireworks.ts +68 -0
  71. package/ai/llm/generateRequestBody.ts +165 -0
  72. package/ai/llm/getModelContextWindow.ts +84 -0
  73. package/ai/llm/getNoloKey.ts +37 -0
  74. package/ai/llm/getPricing.ts +232 -0
  75. package/ai/llm/hooks/useModelPricing.ts +75 -0
  76. package/ai/llm/imagePricing.ts +66 -0
  77. package/ai/llm/isResponseAPIModel.ts +13 -0
  78. package/ai/llm/kimi.ts +18 -0
  79. package/ai/llm/mimo.ts +71 -0
  80. package/ai/llm/mistral.ts +22 -0
  81. package/ai/llm/modelAvatar.ts +427 -0
  82. package/ai/llm/models.ts +45 -0
  83. package/ai/llm/openrouterModels.ts +141 -0
  84. package/ai/llm/providers.ts +307 -0
  85. package/ai/llm/reasoningModels.ts +28 -0
  86. package/ai/llm/types.ts +59 -0
  87. package/ai/llm/usageRequestOptions.ts +59 -0
  88. package/ai/memory/capture.ts +148 -0
  89. package/ai/memory/consolidate.ts +104 -0
  90. package/ai/memory/delete.ts +147 -0
  91. package/ai/memory/overlay.ts +84 -0
  92. package/ai/memory/query.ts +38 -0
  93. package/ai/memory/queryShared.ts +160 -0
  94. package/ai/memory/rank.ts +105 -0
  95. package/ai/memory/recentRelationshipRecap.ts +247 -0
  96. package/ai/memory/remember.ts +167 -0
  97. package/ai/memory/runtime.ts +76 -0
  98. package/ai/memory/store.ts +20 -0
  99. package/ai/memory/storeShared.ts +76 -0
  100. package/ai/memory/types.ts +46 -0
  101. package/ai/memory/understanding.ts +349 -0
  102. package/ai/memory/understandingGreeting.ts +264 -0
  103. package/ai/messages/type.ts +20 -0
  104. package/ai/policy/personalizationDialog.ts +333 -0
  105. package/ai/policy/runtimePolicy.ts +440 -0
  106. package/ai/policy/selfUpdateFields.ts +48 -0
  107. package/ai/policy/types.ts +64 -0
  108. package/ai/skills/referenceRuntime.ts +274 -0
  109. package/ai/skills/skillDiagnostics.ts +251 -0
  110. package/ai/skills/skillDocBuilder.ts +139 -0
  111. package/ai/skills/skillDocProtocol.ts +434 -0
  112. package/ai/skills/skillReferenceSummary.ts +63 -0
  113. package/ai/skills/skillSummaryMarker.ts +26 -0
  114. package/ai/token/calculatePrice.ts +546 -0
  115. package/ai/token/db.ts +98 -0
  116. package/ai/token/externalToolCost.ts +321 -0
  117. package/ai/token/hooks/useRecords.ts +65 -0
  118. package/ai/token/missingUsageEstimate.ts +42 -0
  119. package/ai/token/modelUsageQuery.ts +252 -0
  120. package/ai/token/normalizeUsage.ts +84 -0
  121. package/ai/token/openaiImageGenerationUsage.ts +56 -0
  122. package/ai/token/prepareTokenUsageData.ts +88 -0
  123. package/ai/token/query.ts +88 -0
  124. package/ai/token/queryUserTokens.ts +59 -0
  125. package/ai/token/resolveBillingTarget.ts +52 -0
  126. package/ai/token/saveTokenRecord.ts +53 -0
  127. package/ai/token/serverDialogProjection.ts +78 -0
  128. package/ai/token/serverTokenWriter.ts +143 -0
  129. package/ai/token/stats.ts +21 -0
  130. package/ai/token/tokenThunks.ts +24 -0
  131. package/ai/token/types.ts +93 -0
  132. package/ai/tools/agent/agentTools.ts +176 -0
  133. package/ai/tools/agent/agentUpdateShared.ts +311 -0
  134. package/ai/tools/agent/callAgentTool.ts +139 -0
  135. package/ai/tools/agent/createAgentTool.ts +512 -0
  136. package/ai/tools/agent/createDialogTool.ts +69 -0
  137. package/ai/tools/agent/createSkillAgentTool.ts +62 -0
  138. package/ai/tools/agent/parallelBudget.ts +221 -0
  139. package/ai/tools/agent/presets/appBuilderPreset.ts +147 -0
  140. package/ai/tools/agent/runLlmTool.ts +96 -0
  141. package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
  142. package/ai/tools/agent/skillAgentArgs.ts +106 -0
  143. package/ai/tools/agent/skillAgentPreset.ts +89 -0
  144. package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
  145. package/ai/tools/agent/updateAgentTool.ts +96 -0
  146. package/ai/tools/agent/updateSelfTool.ts +113 -0
  147. package/ai/tools/amazonProductScraperTool.ts +86 -0
  148. package/ai/tools/apifyActorClient.ts +45 -0
  149. package/ai/tools/appEditGuard.ts +372 -0
  150. package/ai/tools/appReadSnapshot.ts +153 -0
  151. package/ai/tools/appTools.ts +1549 -0
  152. package/ai/tools/applyEditTool.ts +256 -0
  153. package/ai/tools/applyLineEditsTool.ts +312 -0
  154. package/ai/tools/browserTools/click.ts +33 -0
  155. package/ai/tools/browserTools/closeSession.ts +29 -0
  156. package/ai/tools/browserTools/common.ts +27 -0
  157. package/ai/tools/browserTools/openSession.ts +48 -0
  158. package/ai/tools/browserTools/readContent.ts +38 -0
  159. package/ai/tools/browserTools/selectOption.ts +46 -0
  160. package/ai/tools/browserTools/typeText.ts +42 -0
  161. package/ai/tools/category/createCategoryTool.ts +66 -0
  162. package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
  163. package/ai/tools/category/updateContentCategoryTool.ts +75 -0
  164. package/ai/tools/cfBrowserTools.ts +319 -0
  165. package/ai/tools/cfSpeechToTextTool.ts +49 -0
  166. package/ai/tools/checkEnvTool.ts +65 -0
  167. package/ai/tools/cloudflareCrawlTool.ts +289 -0
  168. package/ai/tools/codeSearchTool.ts +111 -0
  169. package/ai/tools/codeTools.ts +101 -0
  170. package/ai/tools/createDocTool.ts +132 -0
  171. package/ai/tools/createPlanTool.ts +999 -0
  172. package/ai/tools/createSkillDocTool.ts +155 -0
  173. package/ai/tools/createWorkflowTool.ts +154 -0
  174. package/ai/tools/deepseekOcrTool.ts +34 -0
  175. package/ai/tools/delayTool.ts +31 -0
  176. package/ai/tools/deleteSpacesTool.ts +325 -0
  177. package/ai/tools/deleteSpacesToolModel.ts +159 -0
  178. package/ai/tools/devReloadUtils.ts +29 -0
  179. package/ai/tools/dialogMessageSearch.ts +137 -0
  180. package/ai/tools/doctorSkillTool.ts +72 -0
  181. package/ai/tools/ecommerceScraperTool.ts +86 -0
  182. package/ai/tools/emailTools.ts +549 -0
  183. package/ai/tools/evalSkillTool.ts +92 -0
  184. package/ai/tools/exaSearchTool.ts +64 -0
  185. package/ai/tools/execBashTool.ts +379 -0
  186. package/ai/tools/executeSqlTool.ts +192 -0
  187. package/ai/tools/fetchWebpageSupport.ts +309 -0
  188. package/ai/tools/fetchWebpageTool.ts +84 -0
  189. package/ai/tools/geminiImagePreviewTool.ts +361 -0
  190. package/ai/tools/generateDocxTool.ts +215 -0
  191. package/ai/tools/googleSearchScraperTool.ts +106 -0
  192. package/ai/tools/importDataTool.ts +133 -0
  193. package/ai/tools/importSkillTool.ts +162 -0
  194. package/ai/tools/index.ts +1927 -0
  195. package/ai/tools/listFilesTool.ts +82 -0
  196. package/ai/tools/listUserSpacesTool.ts +113 -0
  197. package/ai/tools/modelUsageTools.ts +199 -0
  198. package/ai/tools/olmOcrTool.ts +34 -0
  199. package/ai/tools/openaiImageTool.ts +267 -0
  200. package/ai/tools/prepareTools.ts +23 -0
  201. package/ai/tools/readDocTool.ts +84 -0
  202. package/ai/tools/readFileTool.ts +211 -0
  203. package/ai/tools/readTool.ts +163 -0
  204. package/ai/tools/readXPostTool.ts +233 -0
  205. package/ai/tools/rememberMemoryTool.ts +84 -0
  206. package/ai/tools/remotionVideoTool.ts +151 -0
  207. package/ai/tools/searchDialogMessagesTool.ts +222 -0
  208. package/ai/tools/searchRepoTool.ts +115 -0
  209. package/ai/tools/searchWorkspaceTool.ts +259 -0
  210. package/ai/tools/skillFollowup.ts +86 -0
  211. package/ai/tools/surfWeatherTool.ts +169 -0
  212. package/ai/tools/table/addTableRowTool.ts +217 -0
  213. package/ai/tools/table/createTableTool.ts +315 -0
  214. package/ai/tools/table/rowTools.ts +366 -0
  215. package/ai/tools/table/schemaTools.ts +244 -0
  216. package/ai/tools/table/shareTableTool.ts +148 -0
  217. package/ai/tools/table/toolShared.ts +129 -0
  218. package/ai/tools/toolApiClient.ts +198 -0
  219. package/ai/tools/toolNameAliases.ts +57 -0
  220. package/ai/tools/toolResultError.ts +42 -0
  221. package/ai/tools/toolRunSlice.ts +303 -0
  222. package/ai/tools/toolSchemaCompatibility.ts +53 -0
  223. package/ai/tools/toolVisibility.ts +4 -0
  224. package/ai/tools/types.ts +20 -0
  225. package/ai/tools/uiAskChoiceTool.ts +104 -0
  226. package/ai/tools/updateContentTitleTool.ts +84 -0
  227. package/ai/tools/updateDocTool.ts +105 -0
  228. package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
  229. package/ai/tools/whisperTool.ts +77 -0
  230. package/ai/tools/writeFileTool.ts +210 -0
  231. package/ai/tools/youtubeScraperTool.ts +116 -0
  232. package/ai/tools/ziweiChartTool.ts +678 -0
  233. package/ai/types.ts +55 -0
  234. package/ai/workflow/workflowExecutor.ts +323 -0
  235. package/ai/workflow/workflowSlice.ts +73 -0
  236. package/ai/workflow/workflowTypes.ts +106 -0
  237. package/client/agentRun.test.ts +240 -0
  238. package/client/agentRun.ts +182 -19
  239. package/client/compactDialog.test.ts +238 -0
  240. package/client/localRuntimeAdapter.test.ts +135 -0
  241. package/client/localRuntimeAdapter.ts +244 -0
  242. package/client/profileConfig.test.ts +40 -0
  243. package/client/streamingOutput.test.ts +22 -0
  244. package/client/streamingOutput.ts +38 -0
  245. package/commandRegistry.ts +9 -2
  246. package/connector-experimental/index.ts +5 -0
  247. package/database/actions/cacheMergedUserData.ts +64 -0
  248. package/database/actions/common.ts +242 -0
  249. package/database/actions/deleteFile.ts +40 -0
  250. package/database/actions/fetchUserData.ts +16 -0
  251. package/database/actions/fileContent.ts +125 -0
  252. package/database/actions/patch.ts +155 -0
  253. package/database/actions/read.ts +337 -0
  254. package/database/actions/readAndWait.ts +224 -0
  255. package/database/actions/readRequestManager.ts +120 -0
  256. package/database/actions/remove.ts +94 -0
  257. package/database/actions/replication.ts +366 -0
  258. package/database/actions/upload.ts +174 -0
  259. package/database/actions/upsert.ts +56 -0
  260. package/database/actions/write.ts +126 -0
  261. package/database/client/db.native.ts +73 -0
  262. package/database/client/db.ts +51 -0
  263. package/database/client/fetchUserData.ts +61 -0
  264. package/database/client/handleError.ts +19 -0
  265. package/database/client/queryRequest.ts +21 -0
  266. package/database/config.ts +21 -0
  267. package/database/dbActionThunks.ts +1 -0
  268. package/database/dbSlice.ts +149 -0
  269. package/database/email.ts +42 -0
  270. package/database/fileRing.ts +51 -0
  271. package/database/fileSharding.ts +70 -0
  272. package/database/fileStorage.native.ts +92 -0
  273. package/database/fileStorage.ts +232 -0
  274. package/database/fileUrl.ts +34 -0
  275. package/database/hooks/useUserData.ts +489 -0
  276. package/database/index.ts +1 -0
  277. package/database/keys.ts +765 -0
  278. package/database/queryPrefixes.ts +14 -0
  279. package/database/requests.ts +443 -0
  280. package/database/runtimeServerContext.ts +35 -0
  281. package/database/server/MemoryDB.ts +76 -0
  282. package/database/server/actorAccess.ts +76 -0
  283. package/database/server/agentDelegation.ts +124 -0
  284. package/database/server/coreDataOwnership.ts +13 -0
  285. package/database/server/coreDataProxy.ts +76 -0
  286. package/database/server/cybotReadonly.ts +18 -0
  287. package/database/server/dataHandlers.ts +111 -0
  288. package/database/server/db.ts +118 -0
  289. package/database/server/dbPath.ts +20 -0
  290. package/database/server/delete.ts +499 -0
  291. package/database/server/emailRepository.ts +1480 -0
  292. package/database/server/ensureDbOpen.ts +12 -0
  293. package/database/server/fileRead.ts +337 -0
  294. package/database/server/fileService.ts +436 -0
  295. package/database/server/handleTransaction.ts +86 -0
  296. package/database/server/patch.ts +282 -0
  297. package/database/server/query.ts +138 -0
  298. package/database/server/read.ts +325 -0
  299. package/database/server/resourceAccess.ts +211 -0
  300. package/database/server/routes.ts +110 -0
  301. package/database/server/spaceMemberAuthority.ts +67 -0
  302. package/database/server/upload.ts +159 -0
  303. package/database/server/write.ts +494 -0
  304. package/database/server/writeAuthority.ts +133 -0
  305. package/database/sqliteDb.ts +46 -0
  306. package/database/table/deleteTable.ts +120 -0
  307. package/database/tenantPlacement.ts +57 -0
  308. package/database/tombstones.ts +52 -0
  309. package/database/userDataLoadDecision.ts +17 -0
  310. package/database/userDataMerge.ts +95 -0
  311. package/database/userPreferenceRegister.ts +108 -0
  312. package/database/utils/dbPath.ts +47 -0
  313. package/database/utils/ulid.native.ts +6 -0
  314. package/database/utils/ulid.ts +1 -0
  315. package/index.ts +25 -15
  316. package/localRuntimeDb.ts +28 -0
  317. package/package.json +16 -4
  318. package/runtimeModeArgs.ts +33 -0
  319. package/tui/readlineWorkspace.ts +1 -0
  320. package/tui/session.ts +22 -0
@@ -0,0 +1,240 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Writable } from "node:stream";
3
+
4
+ import { runAgentTurn, shouldUseScriptBridge } from "./agentRun";
5
+
6
+ class CaptureOutput extends Writable {
7
+ chunks: string[] = [];
8
+
9
+ _write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
10
+ this.chunks.push(String(chunk));
11
+ callback();
12
+ }
13
+
14
+ text() {
15
+ return this.chunks.join("");
16
+ }
17
+ }
18
+
19
+ describe("cli agent run client", () => {
20
+ test("uses the repo script bridge only when no auth token is available", () => {
21
+ expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: true })).toBe(true);
22
+ expect(shouldUseScriptBridge({ hasAuthToken: true, scriptPathExists: true })).toBe(false);
23
+ expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: false })).toBe(false);
24
+ });
25
+
26
+ test("calls the Nolo HTTP API directly when AUTH_TOKEN is present", async () => {
27
+ const output = new CaptureOutput();
28
+ const requests: Array<{ url: string; body: any; auth: string | null }> = [];
29
+
30
+ const result = await runAgentTurn({
31
+ agentName: "nolo",
32
+ agentKey: "agent-pub-test",
33
+ serverUrl: "https://nolo.chat",
34
+ message: "hello",
35
+ continueDialogId: "dialog-existing",
36
+ scriptDir: "C:/missing/scripts",
37
+ env: { AUTH_TOKEN: "token-123" },
38
+ output,
39
+ scriptPathExists: () => false,
40
+ fetchImpl: async (url, init) => {
41
+ requests.push({
42
+ url: String(url),
43
+ body: JSON.parse(String(init?.body)),
44
+ auth: new Headers(init?.headers).get("Authorization"),
45
+ });
46
+ return Response.json({
47
+ content: "hi",
48
+ dialogId: "dialog-1",
49
+ usage: { input_tokens: 2, output_tokens: 3 },
50
+ });
51
+ },
52
+ });
53
+
54
+ expect(requests).toEqual([
55
+ {
56
+ url: "https://nolo.chat/api/agent/run",
57
+ auth: "Bearer token-123",
58
+ body: {
59
+ agentKey: "agent-pub-test",
60
+ userInput: "hello",
61
+ continueDialogId: "dialog-existing",
62
+ runtimeContext: {
63
+ surface: "cli",
64
+ host: "terminal",
65
+ runtime: "bun",
66
+ entrypoint: "nolo-cli",
67
+ capabilities: ["text-io", "streaming", "slash-commands"],
68
+ },
69
+ stream: true,
70
+ },
71
+ },
72
+ ]);
73
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
74
+ expect(output.text()).toContain("nolo -> working");
75
+ expect(output.text()).toContain("nolo > hi");
76
+ expect(output.text()).not.toContain("tokens=");
77
+ });
78
+
79
+ test("runs forced local turns through the injected runtime adapter without HTTP", async () => {
80
+ const output = new CaptureOutput();
81
+ const result = await runAgentTurn({
82
+ agentName: "frontend",
83
+ agentKey: "frontend-local",
84
+ serverUrl: "https://nolo.chat",
85
+ message: "polish notifications",
86
+ scriptDir: "C:/missing/scripts",
87
+ env: { AUTH_TOKEN: "token-123" },
88
+ output,
89
+ runtimeMode: "local",
90
+ localRuntimeAdapter: {
91
+ host: "cli",
92
+ capabilities: ["local-provider", "local-persistence"],
93
+ loadAgentConfig: async (agentRef) => ({
94
+ key: agentRef,
95
+ name: "Frontend",
96
+ prompt: "Fix UI",
97
+ model: "fake-local",
98
+ }),
99
+ loadDialogHistory: async () => [],
100
+ saveTurn: async () => ({ dialogId: "dialog-local" }),
101
+ resolveProvider: async () => ({
102
+ model: "fake-local",
103
+ complete: async (messages) => ({
104
+ content: `local:${messages.at(-1)?.content}`,
105
+ model: "fake-local",
106
+ trace: messages,
107
+ }),
108
+ }),
109
+ executeTool: async () => {
110
+ throw new Error("no tools expected");
111
+ },
112
+ },
113
+ scriptPathExists: () => false,
114
+ fetchImpl: async () => {
115
+ throw new Error("HTTP should not be called for forced local runs");
116
+ },
117
+ });
118
+
119
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local" });
120
+ expect(output.text()).toContain("frontend -> working locally");
121
+ expect(output.text()).toContain("frontend > local:polish notifications");
122
+ });
123
+
124
+ test("builds the default local adapter when env requests local mode", async () => {
125
+ const output = new CaptureOutput();
126
+ const builtModes: string[] = [];
127
+
128
+ const result = await runAgentTurn({
129
+ agentName: "frontend",
130
+ agentKey: "frontend-local",
131
+ serverUrl: "https://nolo.chat",
132
+ message: "polish notifications",
133
+ scriptDir: "C:/missing/scripts",
134
+ env: { NOLO_RUNTIME_MODE: "local" },
135
+ output,
136
+ localRuntimeAdapterFactory: (env) => {
137
+ builtModes.push(env.NOLO_RUNTIME_MODE ?? "");
138
+ return {
139
+ host: "cli",
140
+ capabilities: ["local-provider", "local-persistence"],
141
+ loadAgentConfig: async (agentRef) => ({ key: agentRef, prompt: "Fix UI" }),
142
+ loadDialogHistory: async () => [],
143
+ saveTurn: async () => ({ dialogId: "dialog-local-env" }),
144
+ resolveProvider: async () => ({
145
+ model: "fake-local",
146
+ complete: async () => ({ content: "local env ok", model: "fake-local" }),
147
+ }),
148
+ executeTool: async () => {
149
+ throw new Error("no tools expected");
150
+ },
151
+ };
152
+ },
153
+ scriptPathExists: () => false,
154
+ fetchImpl: async () => {
155
+ throw new Error("HTTP should not be called for env local runs");
156
+ },
157
+ });
158
+
159
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-env" });
160
+ expect(builtModes).toEqual(["local"]);
161
+ expect(output.text()).toContain("frontend -> working locally");
162
+ });
163
+
164
+ test("streams agent text responses to terminal output", async () => {
165
+ const output = new CaptureOutput();
166
+
167
+ const result = await runAgentTurn({
168
+ agentName: "nolo",
169
+ agentKey: "agent-pub-test",
170
+ serverUrl: "https://nolo.chat",
171
+ message: "hello",
172
+ scriptDir: "C:/missing/scripts",
173
+ env: { AUTH_TOKEN: "token-123" },
174
+ output,
175
+ scriptPathExists: () => false,
176
+ fetchImpl: async () =>
177
+ new Response(
178
+ [
179
+ `data: ${JSON.stringify({ type: "text", content: "你" })}`,
180
+ "",
181
+ `data: ${JSON.stringify({ type: "text", content: "好" })}`,
182
+ "",
183
+ `data: ${JSON.stringify({ type: "done", dialogId: "dialog-stream" })}`,
184
+ "",
185
+ ].join("\n"),
186
+ {
187
+ status: 200,
188
+ headers: { "Content-Type": "text/event-stream" },
189
+ }
190
+ ),
191
+ });
192
+
193
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-stream" });
194
+ expect(output.text()).toContain("nolo -> working");
195
+ expect(output.text()).toContain("nolo > 你好");
196
+ });
197
+
198
+ test("prints an auth hint when installed without repo scripts or AUTH_TOKEN", async () => {
199
+ const output = new CaptureOutput();
200
+
201
+ const result = await runAgentTurn({
202
+ agentName: "nolo",
203
+ agentKey: "agent-pub-test",
204
+ serverUrl: "https://nolo.chat",
205
+ message: "hello",
206
+ scriptDir: "C:/missing/scripts",
207
+ env: {},
208
+ output,
209
+ scriptPathExists: () => false,
210
+ fetchImpl: async () => {
211
+ throw new Error("fetch should not be called");
212
+ },
213
+ });
214
+
215
+ expect(result.exitCode).toBe(1);
216
+ expect(output.text()).toContain("Set AUTH_TOKEN");
217
+ });
218
+
219
+ test("prints a friendly connection hint instead of crashing on transport errors", async () => {
220
+ const output = new CaptureOutput();
221
+
222
+ const result = await runAgentTurn({
223
+ agentName: "nolo",
224
+ agentKey: "agent-pub-test",
225
+ serverUrl: "http://127.0.0.1:38123",
226
+ message: "hello",
227
+ scriptDir: "C:/missing/scripts",
228
+ env: { AUTH_TOKEN: "token-123" },
229
+ output,
230
+ scriptPathExists: () => false,
231
+ fetchImpl: async () => {
232
+ throw new Error("ConnectionRefused");
233
+ },
234
+ });
235
+
236
+ expect(result).toEqual({ exitCode: 1 });
237
+ expect(output.text()).toContain("Could not reach http://127.0.0.1:38123/api/agent/run");
238
+ expect(output.text()).toContain("set NOLO_SERVER");
239
+ });
240
+ });
@@ -1,5 +1,9 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { runLocalAgentTurn } from "../agentRuntimeLocal";
4
+ import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
5
+ import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
6
+ import { createStreamingTextWriter } from "./streamingOutput";
3
7
 
4
8
  type EnvLike = Record<string, string | undefined>;
5
9
 
@@ -15,10 +19,13 @@ type RunAgentTurnOptions = {
15
19
  continueDialogId?: string;
16
20
  scriptDir: string;
17
21
  env: EnvLike;
18
- output: OutputLike;
19
- scriptPathExists?: (path: string) => boolean;
20
- fetchImpl?: typeof fetch;
21
- };
22
+ output: OutputLike;
23
+ runtimeMode?: AgentRuntimeRequestedMode;
24
+ localRuntimeAdapter?: AgentRuntimeHostAdapter;
25
+ localRuntimeAdapterFactory?: (env: EnvLike) => AgentRuntimeHostAdapter;
26
+ scriptPathExists?: (path: string) => boolean;
27
+ fetchImpl?: typeof fetch;
28
+ };
22
29
 
23
30
  export type RunAgentTurnResult = {
24
31
  exitCode: number;
@@ -38,9 +45,23 @@ function resolveAuthToken(env: EnvLike) {
38
45
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
39
46
  }
40
47
 
41
- function shouldShowUsage(env: EnvLike) {
42
- return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
43
- }
48
+ function shouldShowUsage(env: EnvLike) {
49
+ return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
50
+ }
51
+
52
+ function resolveRequestedRuntimeMode(options: RunAgentTurnOptions) {
53
+ const envMode = options.env.NOLO_RUNTIME_MODE;
54
+ if (options.runtimeMode) return options.runtimeMode;
55
+ if (envMode === "local" || envMode === "server" || envMode === "auto") return envMode;
56
+ return "auto";
57
+ }
58
+
59
+ function buildDefaultLocalRuntimeAdapter(options: RunAgentTurnOptions) {
60
+ return createCliLocalRuntimeAdapter({
61
+ env: options.env,
62
+ fetchImpl: options.fetchImpl,
63
+ });
64
+ }
44
65
 
45
66
  function formatUsage(usage: any, dialogId: unknown) {
46
67
  const parts: string[] = [];
@@ -112,7 +133,7 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
112
133
  return { exitCode };
113
134
  }
114
135
 
115
- async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
136
+ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
116
137
  options.output.write(`\n${options.agentName} -> working...\n`);
117
138
 
118
139
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -132,12 +153,12 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
132
153
  host: "terminal",
133
154
  runtime: "bun",
134
155
  entrypoint: "nolo-cli",
135
- capabilities: ["text-io", "slash-commands"],
156
+ capabilities: ["text-io", "streaming", "slash-commands"],
136
157
  },
137
158
  ...(options.continueDialogId
138
159
  ? { continueDialogId: options.continueDialogId }
139
160
  : {}),
140
- stream: false,
161
+ stream: true,
141
162
  }),
142
163
  });
143
164
  } catch (error) {
@@ -145,6 +166,11 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
145
166
  return { exitCode: 1 };
146
167
  }
147
168
 
169
+ const contentType = res.headers.get("content-type") || "";
170
+ if (contentType.includes("text/event-stream") && res.body) {
171
+ return readStreamingAgentRun(options, res);
172
+ }
173
+
148
174
  let data: any = {};
149
175
  try {
150
176
  data = await res.json();
@@ -175,17 +201,154 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
175
201
  ? { dialogId: data.dialogId }
176
202
  : {}),
177
203
  };
178
- }
204
+ }
205
+
206
+ async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
207
+ const adapter =
208
+ options.localRuntimeAdapter ||
209
+ options.localRuntimeAdapterFactory?.(options.env) ||
210
+ buildDefaultLocalRuntimeAdapter(options);
211
+ if (!adapter) {
212
+ options.output.write("[nolo] Local runtime was requested but no local runtime adapter is available.\n");
213
+ return { exitCode: 1 };
214
+ }
215
+
216
+ options.output.write(`\n${options.agentName} -> working locally...\n`);
217
+ try {
218
+ const result = await runLocalAgentTurn({
219
+ adapter,
220
+ agentRef: options.agentKey,
221
+ input: options.message,
222
+ continueDialogId: options.continueDialogId,
223
+ });
224
+ const content = result.content.trim();
225
+ if (content) {
226
+ options.output.write(`\n${options.agentName} > ${content}\n`);
227
+ } else {
228
+ options.output.write(`\n${options.agentName} > (no text response)\n`);
229
+ }
230
+ return { exitCode: 0, dialogId: result.dialogId };
231
+ } catch (error) {
232
+ options.output.write(
233
+ `[nolo] Local agent run failed: ${
234
+ error instanceof Error ? error.message : String(error)
235
+ }\n`
236
+ );
237
+ return { exitCode: 1 };
238
+ }
239
+ }
240
+
241
+ async function readStreamingAgentRun(
242
+ options: RunAgentTurnOptions,
243
+ res: Response,
244
+ ): Promise<RunAgentTurnResult> {
245
+ const reader = res.body?.getReader();
246
+ if (!reader) {
247
+ options.output.write("[nolo] Agent stream response did not include a readable body.\n");
248
+ return { exitCode: 1 };
249
+ }
250
+
251
+ const decoder = new TextDecoder();
252
+ const writer = createStreamingTextWriter({
253
+ write: (chunk) => options.output.write(chunk),
254
+ });
255
+ let buffer = "";
256
+ let content = "";
257
+ let dialogId: string | undefined;
258
+ let usage: any;
259
+ let hasPrintedLabel = false;
260
+
261
+ const printLabel = () => {
262
+ if (hasPrintedLabel) return;
263
+ options.output.write(`\n${options.agentName} > `);
264
+ hasPrintedLabel = true;
265
+ };
266
+
267
+ const handlePayload = (payload: any) => {
268
+ if (payload?.error || payload?.type === "error") {
269
+ throw new Error(String(payload.error || payload.message || "Agent stream failed"));
270
+ }
271
+ if (payload?.type === "done") {
272
+ if (typeof payload.dialogId === "string") dialogId = payload.dialogId;
273
+ usage = payload.usage;
274
+ return;
275
+ }
276
+
277
+ const chunk =
278
+ payload?.type === "text"
279
+ ? payload.content
280
+ : typeof payload?.chunk === "string"
281
+ ? payload.chunk
282
+ : "";
283
+ if (!chunk) return;
284
+
285
+ printLabel();
286
+ content += chunk;
287
+ writer.push(chunk);
288
+ };
289
+
290
+ try {
291
+ while (true) {
292
+ const { done, value } = await reader.read();
293
+ if (done) break;
179
294
 
180
- export async function runAgentTurn(options: RunAgentTurnOptions) {
181
- const authToken = resolveAuthToken(options.env);
182
- const scriptPath = join(options.scriptDir, "chatWithAgent.ts");
183
- const scriptPathExists = (options.scriptPathExists ?? existsSync)(scriptPath);
295
+ buffer += decoder.decode(value, { stream: true });
296
+ const events = buffer.split("\n\n");
297
+ buffer = events.pop() ?? "";
184
298
 
185
- if (shouldUseScriptBridge({ hasAuthToken: Boolean(authToken), scriptPathExists })) {
186
- return runScriptBridge(options, scriptPath);
299
+ for (const event of events) {
300
+ const dataLines = event
301
+ .split("\n")
302
+ .filter((line) => line.startsWith("data:"))
303
+ .map((line) => line.slice(5).trim())
304
+ .filter(Boolean);
305
+ for (const raw of dataLines) {
306
+ handlePayload(JSON.parse(raw));
307
+ }
308
+ }
309
+ }
310
+ if (buffer.trim()) {
311
+ const raw = buffer
312
+ .split("\n")
313
+ .find((line) => line.startsWith("data:"))
314
+ ?.slice(5)
315
+ .trim();
316
+ if (raw) handlePayload(JSON.parse(raw));
317
+ }
318
+ } catch (error) {
319
+ options.output.write(`\n[nolo] Agent stream failed: ${error instanceof Error ? error.message : String(error)}\n`);
320
+ return { exitCode: 1 };
321
+ } finally {
322
+ writer.flushAll();
187
323
  }
188
324
 
325
+ if (!content) {
326
+ options.output.write(`\n${options.agentName} > (no text response)\n`);
327
+ } else {
328
+ options.output.write("\n");
329
+ }
330
+
331
+ const usageText = formatUsage(usage, dialogId);
332
+ if (usageText && shouldShowUsage(options.env)) options.output.write(`${usageText}\n`);
333
+ return {
334
+ exitCode: 0,
335
+ ...(dialogId ? { dialogId } : {}),
336
+ };
337
+ }
338
+
339
+ export async function runAgentTurn(options: RunAgentTurnOptions) {
340
+ const authToken = resolveAuthToken(options.env);
341
+ const scriptPath = join(options.scriptDir, "chatWithAgent.ts");
342
+ const scriptPathExists = (options.scriptPathExists ?? existsSync)(scriptPath);
343
+
344
+ if (resolveRequestedRuntimeMode(options) === "local") {
345
+ return runInjectedLocalAgentTurn(options);
346
+ }
347
+
348
+ if (shouldUseScriptBridge({ hasAuthToken: Boolean(authToken), scriptPathExists })) {
349
+ return runScriptBridge(options, scriptPath);
350
+ }
351
+
189
352
  if (!authToken) {
190
353
  options.output.write(
191
354
  "[nolo] This install needs an auth token before it can talk to agents.\n" +