nolo-cli 0.1.13 → 0.1.15

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 (321) 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/agentRunCommand.ts +104 -0
  8. package/agentRuntimeCommands.ts +139 -22
  9. package/agentRuntimeLocal.ts +7 -0
  10. package/ai/agent/_executeModel.ts +118 -0
  11. package/ai/agent/agentSlice.ts +544 -1
  12. package/ai/agent/appWorkingMemory.ts +126 -0
  13. package/ai/agent/avatarUtils.ts +24 -0
  14. package/ai/agent/buildEditingContext.ts +373 -0
  15. package/ai/agent/buildSystemPrompt.ts +532 -0
  16. package/ai/agent/cleanAgentMessages.ts +140 -0
  17. package/ai/agent/cliChatClient.ts +119 -0
  18. package/ai/agent/contextCompiler.ts +107 -0
  19. package/ai/agent/contextLayerContract.ts +44 -0
  20. package/ai/agent/createAgentSchema.ts +234 -0
  21. package/ai/agent/executeToolCall.ts +58 -0
  22. package/ai/agent/fetchAgentContexts.ts +42 -0
  23. package/ai/agent/generatePrompt.ts +3 -0
  24. package/ai/agent/getFullChatContextKeys.ts +168 -0
  25. package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
  26. package/ai/agent/hooks/useAgentConfig.ts +61 -0
  27. package/ai/agent/hooks/useAgentDialog.ts +35 -0
  28. package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
  29. package/ai/agent/hooks/usePublicAgents.ts +473 -0
  30. package/ai/agent/persistMessageWithFixedId.ts +37 -0
  31. package/ai/agent/planSlice.ts +259 -0
  32. package/ai/agent/referenceUtils.ts +229 -0
  33. package/ai/agent/runAgentBackground.ts +238 -0
  34. package/ai/agent/runAgentClientLoop.ts +138 -0
  35. package/ai/agent/runtimeGuidance.ts +97 -0
  36. package/ai/agent/runtimeServerBase.ts +37 -0
  37. package/ai/agent/server/fetchPublicAgents.ts +128 -0
  38. package/ai/agent/startParallelAgentStreams.ts +424 -0
  39. package/ai/agent/startupProtocol.ts +53 -0
  40. package/ai/agent/streamAgentChatTurn.ts +1299 -0
  41. package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
  42. package/ai/agent/types.ts +71 -0
  43. package/ai/agent/utils/imageOutput.ts +39 -0
  44. package/ai/agent/utils/publicImageAgentMode.ts +26 -0
  45. package/ai/agent/utils/sortUtils.ts +250 -0
  46. package/ai/agent/web/referencePickerUtils.ts +146 -0
  47. package/ai/ai.locale.ts +1083 -0
  48. package/ai/chat/accumulateToolCallChunks.ts +95 -0
  49. package/ai/chat/fetchUtils.native.ts +276 -0
  50. package/ai/chat/fetchUtils.ts +153 -0
  51. package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
  52. package/ai/chat/parseApiError.ts +64 -0
  53. package/ai/chat/parseMultilineSSE.ts +95 -0
  54. package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
  55. package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
  56. package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
  57. package/ai/chat/shouldUseServerProxy.ts +18 -0
  58. package/ai/chat/sseClient.native.ts +91 -0
  59. package/ai/chat/sseClient.ts +67 -0
  60. package/ai/chat/streamReader.native.ts +31 -0
  61. package/ai/chat/streamReader.ts +62 -0
  62. package/ai/chat/updateTotalUsage.ts +72 -0
  63. package/ai/context/buildReferenceContext.ts +437 -0
  64. package/ai/context/calculateContextUsage.ts +133 -0
  65. package/ai/context/retention.ts +165 -0
  66. package/ai/context/tokenUtils.ts +78 -0
  67. package/ai/index.ts +1 -1
  68. package/ai/llm/agentCapabilities.ts +74 -0
  69. package/ai/llm/calculateGeminiImageTokens.ts +57 -0
  70. package/ai/llm/deepinfra.ts +28 -0
  71. package/ai/llm/fireworks.ts +68 -0
  72. package/ai/llm/generateRequestBody.ts +165 -0
  73. package/ai/llm/getModelContextWindow.ts +84 -0
  74. package/ai/llm/getNoloKey.ts +37 -0
  75. package/ai/llm/getPricing.ts +232 -0
  76. package/ai/llm/hooks/useModelPricing.ts +75 -0
  77. package/ai/llm/imagePricing.ts +66 -0
  78. package/ai/llm/isResponseAPIModel.ts +13 -0
  79. package/ai/llm/kimi.ts +18 -0
  80. package/ai/llm/mimo.ts +71 -0
  81. package/ai/llm/mistral.ts +22 -0
  82. package/ai/llm/modelAvatar.ts +427 -0
  83. package/ai/llm/models.ts +45 -0
  84. package/ai/llm/openrouterModels.ts +141 -0
  85. package/ai/llm/providers.ts +307 -0
  86. package/ai/llm/reasoningModels.ts +28 -0
  87. package/ai/llm/types.ts +59 -0
  88. package/ai/llm/usageRequestOptions.ts +59 -0
  89. package/ai/memory/capture.ts +148 -0
  90. package/ai/memory/consolidate.ts +104 -0
  91. package/ai/memory/delete.ts +147 -0
  92. package/ai/memory/overlay.ts +84 -0
  93. package/ai/memory/query.ts +38 -0
  94. package/ai/memory/queryShared.ts +160 -0
  95. package/ai/memory/rank.ts +105 -0
  96. package/ai/memory/recentRelationshipRecap.ts +247 -0
  97. package/ai/memory/remember.ts +167 -0
  98. package/ai/memory/runtime.ts +76 -0
  99. package/ai/memory/store.ts +20 -0
  100. package/ai/memory/storeShared.ts +76 -0
  101. package/ai/memory/types.ts +46 -0
  102. package/ai/memory/understanding.ts +349 -0
  103. package/ai/memory/understandingGreeting.ts +264 -0
  104. package/ai/messages/type.ts +20 -0
  105. package/ai/policy/personalizationDialog.ts +333 -0
  106. package/ai/policy/runtimePolicy.ts +440 -0
  107. package/ai/policy/selfUpdateFields.ts +48 -0
  108. package/ai/policy/types.ts +64 -0
  109. package/ai/skills/referenceRuntime.ts +274 -0
  110. package/ai/skills/skillDiagnostics.ts +251 -0
  111. package/ai/skills/skillDocBuilder.ts +139 -0
  112. package/ai/skills/skillDocProtocol.ts +434 -0
  113. package/ai/skills/skillReferenceSummary.ts +63 -0
  114. package/ai/skills/skillSummaryMarker.ts +26 -0
  115. package/ai/token/calculatePrice.ts +546 -0
  116. package/ai/token/db.ts +98 -0
  117. package/ai/token/externalToolCost.ts +321 -0
  118. package/ai/token/hooks/useRecords.ts +65 -0
  119. package/ai/token/missingUsageEstimate.ts +42 -0
  120. package/ai/token/modelUsageQuery.ts +252 -0
  121. package/ai/token/normalizeUsage.ts +84 -0
  122. package/ai/token/openaiImageGenerationUsage.ts +56 -0
  123. package/ai/token/prepareTokenUsageData.ts +88 -0
  124. package/ai/token/query.ts +88 -0
  125. package/ai/token/queryUserTokens.ts +59 -0
  126. package/ai/token/resolveBillingTarget.ts +52 -0
  127. package/ai/token/saveTokenRecord.ts +53 -0
  128. package/ai/token/serverDialogProjection.ts +78 -0
  129. package/ai/token/serverTokenWriter.ts +143 -0
  130. package/ai/token/stats.ts +21 -0
  131. package/ai/token/tokenThunks.ts +24 -0
  132. package/ai/token/types.ts +93 -0
  133. package/ai/tools/agent/agentTools.ts +176 -0
  134. package/ai/tools/agent/agentUpdateShared.ts +311 -0
  135. package/ai/tools/agent/callAgentTool.ts +139 -0
  136. package/ai/tools/agent/createAgentTool.ts +512 -0
  137. package/ai/tools/agent/createDialogTool.ts +69 -0
  138. package/ai/tools/agent/createSkillAgentTool.ts +62 -0
  139. package/ai/tools/agent/parallelBudget.ts +221 -0
  140. package/ai/tools/agent/presets/appBuilderPreset.ts +147 -0
  141. package/ai/tools/agent/runLlmTool.ts +96 -0
  142. package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
  143. package/ai/tools/agent/skillAgentArgs.ts +106 -0
  144. package/ai/tools/agent/skillAgentPreset.ts +89 -0
  145. package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
  146. package/ai/tools/agent/updateAgentTool.ts +96 -0
  147. package/ai/tools/agent/updateSelfTool.ts +113 -0
  148. package/ai/tools/amazonProductScraperTool.ts +86 -0
  149. package/ai/tools/apifyActorClient.ts +45 -0
  150. package/ai/tools/appEditGuard.ts +372 -0
  151. package/ai/tools/appReadSnapshot.ts +153 -0
  152. package/ai/tools/appTools.ts +1549 -0
  153. package/ai/tools/applyEditTool.ts +256 -0
  154. package/ai/tools/applyLineEditsTool.ts +312 -0
  155. package/ai/tools/browserTools/click.ts +33 -0
  156. package/ai/tools/browserTools/closeSession.ts +29 -0
  157. package/ai/tools/browserTools/common.ts +27 -0
  158. package/ai/tools/browserTools/openSession.ts +48 -0
  159. package/ai/tools/browserTools/readContent.ts +38 -0
  160. package/ai/tools/browserTools/selectOption.ts +46 -0
  161. package/ai/tools/browserTools/typeText.ts +42 -0
  162. package/ai/tools/category/createCategoryTool.ts +66 -0
  163. package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
  164. package/ai/tools/category/updateContentCategoryTool.ts +75 -0
  165. package/ai/tools/cfBrowserTools.ts +319 -0
  166. package/ai/tools/cfSpeechToTextTool.ts +49 -0
  167. package/ai/tools/checkEnvTool.ts +65 -0
  168. package/ai/tools/cloudflareCrawlTool.ts +289 -0
  169. package/ai/tools/codeSearchTool.ts +111 -0
  170. package/ai/tools/codeTools.ts +101 -0
  171. package/ai/tools/createDocTool.ts +132 -0
  172. package/ai/tools/createPlanTool.ts +999 -0
  173. package/ai/tools/createSkillDocTool.ts +155 -0
  174. package/ai/tools/createWorkflowTool.ts +154 -0
  175. package/ai/tools/deepseekOcrTool.ts +34 -0
  176. package/ai/tools/delayTool.ts +31 -0
  177. package/ai/tools/deleteSpacesTool.ts +325 -0
  178. package/ai/tools/deleteSpacesToolModel.ts +159 -0
  179. package/ai/tools/devReloadUtils.ts +29 -0
  180. package/ai/tools/dialogMessageSearch.ts +137 -0
  181. package/ai/tools/doctorSkillTool.ts +72 -0
  182. package/ai/tools/ecommerceScraperTool.ts +86 -0
  183. package/ai/tools/emailTools.ts +549 -0
  184. package/ai/tools/evalSkillTool.ts +92 -0
  185. package/ai/tools/exaSearchTool.ts +64 -0
  186. package/ai/tools/execBashTool.ts +379 -0
  187. package/ai/tools/executeSqlTool.ts +192 -0
  188. package/ai/tools/fetchWebpageSupport.ts +309 -0
  189. package/ai/tools/fetchWebpageTool.ts +84 -0
  190. package/ai/tools/geminiImagePreviewTool.ts +361 -0
  191. package/ai/tools/generateDocxTool.ts +215 -0
  192. package/ai/tools/googleSearchScraperTool.ts +106 -0
  193. package/ai/tools/importDataTool.ts +133 -0
  194. package/ai/tools/importSkillTool.ts +162 -0
  195. package/ai/tools/index.ts +1927 -0
  196. package/ai/tools/listFilesTool.ts +82 -0
  197. package/ai/tools/listUserSpacesTool.ts +113 -0
  198. package/ai/tools/modelUsageTools.ts +199 -0
  199. package/ai/tools/olmOcrTool.ts +34 -0
  200. package/ai/tools/openaiImageTool.ts +267 -0
  201. package/ai/tools/prepareTools.ts +23 -0
  202. package/ai/tools/readDocTool.ts +84 -0
  203. package/ai/tools/readFileTool.ts +211 -0
  204. package/ai/tools/readTool.ts +163 -0
  205. package/ai/tools/readXPostTool.ts +233 -0
  206. package/ai/tools/rememberMemoryTool.ts +84 -0
  207. package/ai/tools/remotionVideoTool.ts +151 -0
  208. package/ai/tools/searchDialogMessagesTool.ts +222 -0
  209. package/ai/tools/searchRepoTool.ts +115 -0
  210. package/ai/tools/searchWorkspaceTool.ts +259 -0
  211. package/ai/tools/skillFollowup.ts +86 -0
  212. package/ai/tools/surfWeatherTool.ts +169 -0
  213. package/ai/tools/table/addTableRowTool.ts +217 -0
  214. package/ai/tools/table/createTableTool.ts +315 -0
  215. package/ai/tools/table/rowTools.ts +366 -0
  216. package/ai/tools/table/schemaTools.ts +244 -0
  217. package/ai/tools/table/shareTableTool.ts +148 -0
  218. package/ai/tools/table/toolShared.ts +129 -0
  219. package/ai/tools/toolApiClient.ts +198 -0
  220. package/ai/tools/toolNameAliases.ts +57 -0
  221. package/ai/tools/toolResultError.ts +42 -0
  222. package/ai/tools/toolRunSlice.ts +303 -0
  223. package/ai/tools/toolSchemaCompatibility.ts +53 -0
  224. package/ai/tools/toolVisibility.ts +4 -0
  225. package/ai/tools/types.ts +20 -0
  226. package/ai/tools/uiAskChoiceTool.ts +104 -0
  227. package/ai/tools/updateContentTitleTool.ts +84 -0
  228. package/ai/tools/updateDocTool.ts +105 -0
  229. package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
  230. package/ai/tools/whisperTool.ts +77 -0
  231. package/ai/tools/writeFileTool.ts +210 -0
  232. package/ai/tools/youtubeScraperTool.ts +116 -0
  233. package/ai/tools/ziweiChartTool.ts +678 -0
  234. package/ai/types.ts +55 -0
  235. package/ai/workflow/workflowExecutor.ts +323 -0
  236. package/ai/workflow/workflowSlice.ts +73 -0
  237. package/ai/workflow/workflowTypes.ts +106 -0
  238. package/client/agentRun.test.ts +240 -0
  239. package/client/agentRun.ts +182 -19
  240. package/client/compactDialog.test.ts +238 -0
  241. package/client/localRuntimeAdapter.test.ts +135 -0
  242. package/client/localRuntimeAdapter.ts +244 -0
  243. package/client/profileConfig.test.ts +40 -0
  244. package/client/streamingOutput.test.ts +22 -0
  245. package/client/streamingOutput.ts +38 -0
  246. package/commandRegistry.ts +11 -2
  247. package/connector-experimental/index.ts +5 -0
  248. package/database/actions/cacheMergedUserData.ts +64 -0
  249. package/database/actions/common.ts +242 -0
  250. package/database/actions/deleteFile.ts +40 -0
  251. package/database/actions/fetchUserData.ts +16 -0
  252. package/database/actions/fileContent.ts +125 -0
  253. package/database/actions/patch.ts +155 -0
  254. package/database/actions/read.ts +337 -0
  255. package/database/actions/readAndWait.ts +224 -0
  256. package/database/actions/readRequestManager.ts +120 -0
  257. package/database/actions/remove.ts +94 -0
  258. package/database/actions/replication.ts +366 -0
  259. package/database/actions/upload.ts +174 -0
  260. package/database/actions/upsert.ts +56 -0
  261. package/database/actions/write.ts +126 -0
  262. package/database/client/db.native.ts +73 -0
  263. package/database/client/db.ts +51 -0
  264. package/database/client/fetchUserData.ts +61 -0
  265. package/database/client/handleError.ts +19 -0
  266. package/database/client/queryRequest.ts +21 -0
  267. package/database/config.ts +21 -0
  268. package/database/dbActionThunks.ts +1 -0
  269. package/database/dbSlice.ts +149 -0
  270. package/database/email.ts +42 -0
  271. package/database/fileRing.ts +51 -0
  272. package/database/fileSharding.ts +70 -0
  273. package/database/fileStorage.native.ts +92 -0
  274. package/database/fileStorage.ts +232 -0
  275. package/database/fileUrl.ts +34 -0
  276. package/database/hooks/useUserData.ts +489 -0
  277. package/database/index.ts +1 -0
  278. package/database/keys.ts +765 -0
  279. package/database/queryPrefixes.ts +14 -0
  280. package/database/requests.ts +443 -0
  281. package/database/runtimeServerContext.ts +35 -0
  282. package/database/server/MemoryDB.ts +76 -0
  283. package/database/server/actorAccess.ts +76 -0
  284. package/database/server/agentDelegation.ts +124 -0
  285. package/database/server/coreDataOwnership.ts +13 -0
  286. package/database/server/coreDataProxy.ts +76 -0
  287. package/database/server/cybotReadonly.ts +18 -0
  288. package/database/server/dataHandlers.ts +111 -0
  289. package/database/server/db.ts +118 -0
  290. package/database/server/dbPath.ts +20 -0
  291. package/database/server/delete.ts +499 -0
  292. package/database/server/emailRepository.ts +1480 -0
  293. package/database/server/ensureDbOpen.ts +12 -0
  294. package/database/server/fileRead.ts +337 -0
  295. package/database/server/fileService.ts +436 -0
  296. package/database/server/handleTransaction.ts +86 -0
  297. package/database/server/patch.ts +282 -0
  298. package/database/server/query.ts +138 -0
  299. package/database/server/read.ts +325 -0
  300. package/database/server/resourceAccess.ts +211 -0
  301. package/database/server/routes.ts +110 -0
  302. package/database/server/spaceMemberAuthority.ts +67 -0
  303. package/database/server/upload.ts +159 -0
  304. package/database/server/write.ts +494 -0
  305. package/database/server/writeAuthority.ts +133 -0
  306. package/database/sqliteDb.ts +46 -0
  307. package/database/table/deleteTable.ts +120 -0
  308. package/database/tenantPlacement.ts +57 -0
  309. package/database/tombstones.ts +52 -0
  310. package/database/userDataLoadDecision.ts +17 -0
  311. package/database/userDataMerge.ts +95 -0
  312. package/database/userPreferenceRegister.ts +108 -0
  313. package/database/utils/dbPath.ts +47 -0
  314. package/database/utils/ulid.native.ts +6 -0
  315. package/database/utils/ulid.ts +1 -0
  316. package/index.ts +37 -19
  317. package/localRuntimeDb.ts +28 -0
  318. package/package.json +17 -4
  319. package/runtimeModeArgs.ts +33 -0
  320. package/tui/readlineWorkspace.ts +1 -0
  321. package/tui/session.ts +22 -0
@@ -0,0 +1,238 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { compactDialog, parseTokenUserId } from "./compactDialog";
3
+
4
+ // A minimal valid JWT-style token with userId encoded in base64 payload.
5
+ // JWT format: header.payload.signature
6
+ // Payload: { "userId": "user01" }
7
+ const USER_ID = "user01";
8
+ const TOKEN_HEADER = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64");
9
+ const TOKEN_PAYLOAD = Buffer.from(JSON.stringify({ userId: USER_ID })).toString("base64");
10
+ const FAKE_TOKEN = `${TOKEN_HEADER}.${TOKEN_PAYLOAD}.fakesig`;
11
+
12
+ const OLD_DIALOG_ID = "01OLD0000000000000000000AB";
13
+ const OLD_DIALOG_KEY = `dialog-${USER_ID}-${OLD_DIALOG_ID}`;
14
+
15
+ const OLD_DIALOG_RECORD = {
16
+ id: OLD_DIALOG_ID,
17
+ dbKey: OLD_DIALOG_KEY,
18
+ type: "dialog",
19
+ title: "My dialog",
20
+ cybots: ["agent-pub-01NOLOAPPBLD000000019KCKT0"],
21
+ spaceId: "myspace",
22
+ createdAt: "2024-01-01T00:00:00.000Z",
23
+ updatedAt: "2024-01-01T00:00:00.000Z",
24
+ inputTokens: 100,
25
+ outputTokens: 200,
26
+ totalCost: 0.001,
27
+ // conversation summary/compression state that must NOT be carried to the fork
28
+ summary: "This is a long summary of the old conversation.",
29
+ summarizedBeforeId: "msg-old-summary-anchor",
30
+ proactiveSummary: "Short recap of prior topics.",
31
+ proactiveSummaryBeforeId: "msg-old-proactive-anchor",
32
+ compressionCount: 3,
33
+ summaryPending: true,
34
+ };
35
+
36
+ function makeFetchMock(options: {
37
+ dialogRecord?: Record<string, unknown>;
38
+ writeOk?: boolean;
39
+ patchOk?: boolean;
40
+ }) {
41
+ const calls: { url: string; method: string; body?: unknown }[] = [];
42
+
43
+ const fetchMock: typeof fetch = async (input, init) => {
44
+ const url = String(input);
45
+ const method = init?.method ?? "GET";
46
+ const bodyStr = typeof init?.body === "string" ? init.body : undefined;
47
+ const body = bodyStr ? JSON.parse(bodyStr) : undefined;
48
+ calls.push({ url, method, body });
49
+
50
+ if (method === "GET" || !method) {
51
+ return new Response(JSON.stringify(options.dialogRecord ?? OLD_DIALOG_RECORD), {
52
+ status: 200,
53
+ headers: { "Content-Type": "application/json" },
54
+ });
55
+ }
56
+ if (method === "POST") {
57
+ return new Response("{}", { status: options.writeOk !== false ? 200 : 500 });
58
+ }
59
+ if (method === "PATCH") {
60
+ return new Response("{}", { status: options.patchOk !== false ? 200 : 500 });
61
+ }
62
+ return new Response("{}", { status: 404 });
63
+ };
64
+
65
+ return { fetchMock, calls };
66
+ }
67
+
68
+ describe("compactDialog", () => {
69
+ test("reads current dialog, writes a forked copy, and returns the new dialog id", async () => {
70
+ const { fetchMock, calls } = makeFetchMock({});
71
+
72
+ const result = await compactDialog({
73
+ serverUrl: "http://localhost:8080",
74
+ authToken: FAKE_TOKEN,
75
+ dialogId: OLD_DIALOG_ID,
76
+ fetchImpl: fetchMock,
77
+ });
78
+
79
+ // Should have made 3 HTTP calls: read, write, patch (space)
80
+ expect(calls).toHaveLength(3);
81
+
82
+ // 1. Read old dialog
83
+ expect(calls[0]?.url).toContain(`/api/v1/db/read/${OLD_DIALOG_KEY}`);
84
+ expect(calls[0]?.method).toBe("GET");
85
+
86
+ // 2. Write new dialog
87
+ expect(calls[1]?.url).toContain("/api/v1/db/write/");
88
+ expect(calls[1]?.method).toBe("POST");
89
+ const writeBody = calls[1]?.body as any;
90
+ expect(writeBody?.data?.inheritedFromDialogKey).toBe(OLD_DIALOG_KEY);
91
+ expect(writeBody?.data?.cybots).toEqual(OLD_DIALOG_RECORD.cybots);
92
+ expect(writeBody?.data?.spaceId).toBe("myspace");
93
+ // Token stats should be reset
94
+ expect(writeBody?.data?.inputTokens).toBe(0);
95
+ expect(writeBody?.data?.outputTokens).toBe(0);
96
+ expect(writeBody?.data?.totalCost).toBe(0);
97
+ // Key should differ from old
98
+ expect(writeBody?.customKey).not.toBe(OLD_DIALOG_KEY);
99
+ expect(writeBody?.customKey).toMatch(/^dialog-user01-/);
100
+
101
+ // 3. Patch space
102
+ expect(calls[2]?.url).toContain("/api/v1/db/patch/space-myspace");
103
+ expect(calls[2]?.method).toBe("PATCH");
104
+
105
+ // Result
106
+ expect(result.dialogId).toBeDefined();
107
+ expect(result.dialogId).not.toBe(OLD_DIALOG_ID);
108
+ expect(result.dialogKey).toMatch(/^dialog-user01-/);
109
+ expect(result.spaceId).toBe("myspace");
110
+ });
111
+
112
+ test("does not patch space when the dialog has no spaceId", async () => {
113
+ const dialogWithoutSpace = { ...OLD_DIALOG_RECORD, spaceId: undefined };
114
+ const { fetchMock, calls } = makeFetchMock({ dialogRecord: dialogWithoutSpace });
115
+
116
+ await compactDialog({
117
+ serverUrl: "http://localhost:8080",
118
+ authToken: FAKE_TOKEN,
119
+ dialogId: OLD_DIALOG_ID,
120
+ fetchImpl: fetchMock,
121
+ });
122
+
123
+ // Only read + write, no patch
124
+ expect(calls).toHaveLength(2);
125
+ expect(calls.every((c) => c.method !== "PATCH")).toBe(true);
126
+ });
127
+
128
+ test("throws when the auth token is missing or invalid", async () => {
129
+ const { fetchMock } = makeFetchMock({});
130
+
131
+ await expect(
132
+ compactDialog({
133
+ serverUrl: "http://localhost:8080",
134
+ authToken: "not-a-valid-token",
135
+ dialogId: OLD_DIALOG_ID,
136
+ fetchImpl: fetchMock,
137
+ })
138
+ ).rejects.toThrow(/invalid or missing auth token/);
139
+ });
140
+
141
+ test("throws when the server read fails", async () => {
142
+ const fetchFailing: typeof fetch = async () =>
143
+ new Response("{}", { status: 404 });
144
+
145
+ await expect(
146
+ compactDialog({
147
+ serverUrl: "http://localhost:8080",
148
+ authToken: FAKE_TOKEN,
149
+ dialogId: OLD_DIALOG_ID,
150
+ fetchImpl: fetchFailing,
151
+ })
152
+ ).rejects.toThrow(/Failed to read dialog/);
153
+ });
154
+
155
+ test("space patch failure does not throw (best-effort)", async () => {
156
+ const { fetchMock } = makeFetchMock({ patchOk: false });
157
+
158
+ // Should resolve without throwing even though PATCH fails
159
+ const result = await compactDialog({
160
+ serverUrl: "http://localhost:8080",
161
+ authToken: FAKE_TOKEN,
162
+ dialogId: OLD_DIALOG_ID,
163
+ fetchImpl: fetchMock,
164
+ });
165
+
166
+ expect(result.dialogId).toBeDefined();
167
+ });
168
+
169
+ test("forked dialog does NOT inherit conversation summary/compression state", async () => {
170
+ const { fetchMock, calls } = makeFetchMock({});
171
+
172
+ await compactDialog({
173
+ serverUrl: "http://localhost:8080",
174
+ authToken: FAKE_TOKEN,
175
+ dialogId: OLD_DIALOG_ID,
176
+ fetchImpl: fetchMock,
177
+ });
178
+
179
+ const writeBody = calls[1]?.body as any;
180
+ const forked = writeBody?.data ?? {};
181
+
182
+ // Conversation-state fields must be absent (undefined / not present)
183
+ expect(forked.summary).toBeUndefined();
184
+ expect(forked.summarizedBeforeId).toBeUndefined();
185
+ expect(forked.proactiveSummary).toBeUndefined();
186
+ expect(forked.proactiveSummaryBeforeId).toBeUndefined();
187
+ expect(forked.compressionCount).toBeUndefined();
188
+ expect(forked.summaryPending).toBeUndefined();
189
+
190
+ // Config/identity fields must still be carried forward
191
+ expect(forked.cybots).toEqual(OLD_DIALOG_RECORD.cybots);
192
+ expect(forked.type).toBe("dialog");
193
+ expect(forked.spaceId).toBe("myspace");
194
+ expect(forked.inheritedFromDialogKey).toBe(OLD_DIALOG_KEY);
195
+ expect(forked.inheritedFromDialogTitle).toBe(OLD_DIALOG_RECORD.title);
196
+
197
+ // Stats must be reset
198
+ expect(forked.inputTokens).toBe(0);
199
+ expect(forked.outputTokens).toBe(0);
200
+ expect(forked.totalCost).toBe(0);
201
+ });
202
+ });
203
+
204
+ describe("parseTokenUserId", () => {
205
+ test("extracts userId from a valid JWT token (header.payload.signature)", () => {
206
+ const userId = "user-12345";
207
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64");
208
+ const payload = Buffer.from(JSON.stringify({ userId })).toString("base64");
209
+ const token = `${header}.${payload}.signature`;
210
+
211
+ expect(parseTokenUserId(token)).toBe(userId);
212
+ });
213
+
214
+ test("returns null when token has fewer than 2 segments", () => {
215
+ expect(parseTokenUserId("onlyonepart")).toBeNull();
216
+ });
217
+
218
+ test("returns null when payload is not valid JSON", () => {
219
+ const token = "header.not-valid-base64-json.signature";
220
+ expect(parseTokenUserId(token)).toBeNull();
221
+ });
222
+
223
+ test("returns null when payload does not contain userId", () => {
224
+ const header = Buffer.from(JSON.stringify({ alg: "HS256" })).toString("base64");
225
+ const payload = Buffer.from(JSON.stringify({ sub: "someone" })).toString("base64");
226
+ const token = `${header}.${payload}.signature`;
227
+
228
+ expect(parseTokenUserId(token)).toBeNull();
229
+ });
230
+
231
+ test("returns null when userId is not a string", () => {
232
+ const header = Buffer.from(JSON.stringify({ alg: "HS256" })).toString("base64");
233
+ const payload = Buffer.from(JSON.stringify({ userId: 12345 })).toString("base64");
234
+ const token = `${header}.${payload}.signature`;
235
+
236
+ expect(parseTokenUserId(token)).toBeNull();
237
+ });
238
+ });
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { runLocalAgentTurn } from "../../agent-runtime/localLoop";
4
+ import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
5
+
6
+ describe("CLI local runtime adapter", () => {
7
+ test("loads agent/history from LevelDB and saves dialog/message records back to LevelDB", async () => {
8
+ const requests: Array<{ url: string; body: any; auth: string | null }> = [];
9
+ const store = new Map<string, any>([
10
+ ["agent-user-1-frontend", {
11
+ dbKey: "agent-user-1-frontend",
12
+ id: "frontend",
13
+ name: "Frontend",
14
+ prompt: "Fix UI",
15
+ model: "gpt-4.1-mini",
16
+ provider: "openai-compatible",
17
+ }],
18
+ ["dialog-user-1-dialog-existing", {
19
+ dbKey: "dialog-user-1-dialog-existing",
20
+ id: "dialog-existing",
21
+ type: "dialog",
22
+ userId: "user-1",
23
+ }],
24
+ ["dialog-dialog-existing-msg-001", {
25
+ dbKey: "dialog-dialog-existing-msg-001",
26
+ id: "msg-001",
27
+ dialogId: "dialog-existing",
28
+ role: "assistant",
29
+ content: "previous answer",
30
+ }],
31
+ ]);
32
+ const batchOps: any[] = [];
33
+ const adapter = createCliLocalRuntimeAdapter({
34
+ env: {
35
+ NOLO_LOCAL_USER_ID: "user-1",
36
+ OPENAI_API_KEY: "sk-local",
37
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
38
+ },
39
+ db: {
40
+ get: async (key) => {
41
+ if (!store.has(key)) throw new Error(`not found: ${key}`);
42
+ return store.get(key);
43
+ },
44
+ put: async (key, value) => {
45
+ store.set(key, value);
46
+ },
47
+ batch: async (ops) => {
48
+ batchOps.push(...ops);
49
+ for (const op of ops) {
50
+ if (op.type === "put") store.set(op.key, op.value);
51
+ }
52
+ },
53
+ iterator: ({ gte, lte }) => (async function* () {
54
+ for (const entry of [...store.entries()].sort(([a], [b]) => a.localeCompare(b))) {
55
+ if (entry[0] >= gte && entry[0] <= lte) yield entry;
56
+ }
57
+ })(),
58
+ },
59
+ now: () => 1710000000000,
60
+ createId: () => "01LOCAL",
61
+ fetchImpl: async (url, init) => {
62
+ requests.push({
63
+ url: String(url),
64
+ body: JSON.parse(String(init?.body)),
65
+ auth: new Headers(init?.headers).get("Authorization"),
66
+ });
67
+ return Response.json({
68
+ choices: [{ message: { content: "local adapter ok" } }],
69
+ usage: { prompt_tokens: 4, completion_tokens: 3 },
70
+ });
71
+ },
72
+ });
73
+
74
+ const result = await runLocalAgentTurn({
75
+ adapter,
76
+ agentRef: "frontend",
77
+ input: "make it cleaner",
78
+ continueDialogId: "dialog-existing",
79
+ });
80
+
81
+ expect(result).toMatchObject({
82
+ content: "local adapter ok",
83
+ model: "gpt-4.1-mini",
84
+ dialogId: "01LOCAL",
85
+ });
86
+ expect(requests).toEqual([{
87
+ url: "http://127.0.0.1:11434/v1/chat/completions",
88
+ auth: "Bearer sk-local",
89
+ body: {
90
+ model: "gpt-4.1-mini",
91
+ messages: [
92
+ { role: "system", content: "Fix UI" },
93
+ { role: "assistant", content: "previous answer" },
94
+ { role: "user", content: "make it cleaner" },
95
+ ],
96
+ stream: false,
97
+ },
98
+ }]);
99
+ expect(batchOps.map((op) => op.key)).toEqual([
100
+ "dialog-user-1-01LOCAL",
101
+ "dialog-01LOCAL-msg-user",
102
+ "dialog-01LOCAL-msg-assistant",
103
+ ]);
104
+ expect(store.get("dialog-user-1-01LOCAL")).toMatchObject({
105
+ id: "01LOCAL",
106
+ dbKey: "dialog-user-1-01LOCAL",
107
+ type: "dialog",
108
+ primaryAgentKey: "agent-user-1-frontend",
109
+ status: "done",
110
+ });
111
+ expect(store.get("dialog-01LOCAL-msg-user")).toMatchObject({
112
+ dialogId: "01LOCAL",
113
+ role: "user",
114
+ content: "make it cleaner",
115
+ });
116
+ expect(store.get("dialog-01LOCAL-msg-assistant")).toMatchObject({
117
+ dialogId: "01LOCAL",
118
+ role: "assistant",
119
+ content: "local adapter ok",
120
+ });
121
+ });
122
+
123
+ test("rejects tools until local tool policy is implemented", async () => {
124
+ const adapter = createCliLocalRuntimeAdapter({
125
+ env: {},
126
+ fetchImpl: async () => Response.json({}),
127
+ });
128
+
129
+ await expect(adapter.executeTool({
130
+ id: "call-1",
131
+ name: "execShell",
132
+ arguments: "{}",
133
+ })).rejects.toThrow("Local runtime tools are not enabled");
134
+ });
135
+ });
@@ -0,0 +1,244 @@
1
+ import type {
2
+ AgentRuntimeAgentConfig,
3
+ AgentRuntimeHostAdapter,
4
+ AgentRuntimeSaveTurnInput,
5
+ } from "../agentRuntimeLocal";
6
+ import type { AgentRuntimeChatMessage } from "../../agent-runtime";
7
+ import { getDefaultCliLocalRuntimeDb } from "../localRuntimeDb";
8
+
9
+ type EnvLike = Record<string, string | undefined>;
10
+
11
+ export type CliLocalRuntimeDb = {
12
+ get(key: string): Promise<any>;
13
+ put(key: string, value: any): Promise<unknown>;
14
+ batch(ops: Array<{ type: "put"; key: string; value: any }>): Promise<unknown>;
15
+ iterator(options: { gte: string; lte?: string; lt?: string; reverse?: boolean; limit?: number }): AsyncIterable<[string, any]>;
16
+ };
17
+
18
+ export type CliLocalRuntimeAdapterDeps = {
19
+ env: EnvLike;
20
+ db?: CliLocalRuntimeDb;
21
+ now?: () => number;
22
+ createId?: () => string;
23
+ fetchImpl?: typeof fetch;
24
+ };
25
+
26
+ async function defaultLocalRuntimeDb(): Promise<CliLocalRuntimeDb> {
27
+ return getDefaultCliLocalRuntimeDb();
28
+ }
29
+
30
+ function createFallbackId() {
31
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`.toUpperCase();
32
+ }
33
+
34
+ function resolveOpenAiCompatibleBaseUrl(env: EnvLike) {
35
+ return (env.NOLO_LOCAL_OPENAI_BASE_URL || env.OPENAI_BASE_URL || "https://api.openai.com/v1")
36
+ .replace(/\/+$/, "");
37
+ }
38
+
39
+ function resolveApiKey(env: EnvLike) {
40
+ return env.OPENAI_API_KEY || env.NOLO_LOCAL_OPENAI_API_KEY || "";
41
+ }
42
+
43
+ function resolveLocalUserId(env: EnvLike) {
44
+ return env.NOLO_LOCAL_USER_ID || env.NOLO_USER_ID || "local";
45
+ }
46
+
47
+ async function resolveDb(deps: CliLocalRuntimeAdapterDeps) {
48
+ return deps.db ?? defaultLocalRuntimeDb();
49
+ }
50
+
51
+ async function readAgentFromDb(args: {
52
+ db: CliLocalRuntimeDb;
53
+ agentRef: string;
54
+ userId: string;
55
+ }): Promise<AgentRuntimeAgentConfig | null> {
56
+ const candidates = [
57
+ args.agentRef,
58
+ `agent-${args.userId}-${args.agentRef}`,
59
+ `cybot-${args.userId}-${args.agentRef}`,
60
+ ];
61
+ for (const key of candidates) {
62
+ try {
63
+ const record = await args.db.get(key);
64
+ if (!record || typeof record !== "object") continue;
65
+ return {
66
+ key,
67
+ ...(typeof record.name === "string" ? { name: record.name } : {}),
68
+ ...(typeof record.prompt === "string" ? { prompt: record.prompt } : {}),
69
+ ...(typeof record.model === "string" ? { model: record.model } : {}),
70
+ ...(typeof record.provider === "string" ? { provider: record.provider } : {}),
71
+ ...(Array.isArray(record.toolNames) ? { toolNames: record.toolNames } : {}),
72
+ };
73
+ } catch {
74
+ // Try the next local LevelDB key convention.
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function toOpenAiMessages(messages: AgentRuntimeChatMessage[]) {
81
+ return messages.map((message) => ({
82
+ role: message.role,
83
+ content: typeof message.content === "string"
84
+ ? message.content
85
+ : Array.isArray(message.content)
86
+ ? message.content
87
+ .map((part) => part.type === "text" ? part.text : `[image:${part.image_url.url}]`)
88
+ .join("\n")
89
+ : "",
90
+ }));
91
+ }
92
+
93
+ async function readDialogMessages(args: {
94
+ db: CliLocalRuntimeDb;
95
+ dialogId: string;
96
+ }) {
97
+ const messages: AgentRuntimeChatMessage[] = [];
98
+ const prefix = `dialog-${args.dialogId}-msg-`;
99
+ const iterator = args.db.iterator({ gte: prefix, lte: `${prefix}\uffff` });
100
+ for await (const [, value] of iterator) {
101
+ if (!value || typeof value !== "object") continue;
102
+ if (value.role === "system") continue;
103
+ if (value.role === "user" || value.role === "assistant" || value.role === "tool") {
104
+ messages.push({
105
+ role: value.role,
106
+ content: value.content ?? null,
107
+ ...(typeof value.toolCallId === "string" ? { tool_call_id: value.toolCallId } : {}),
108
+ ...(Array.isArray(value.tool_calls) ? { tool_calls: value.tool_calls } : {}),
109
+ });
110
+ }
111
+ }
112
+ return messages;
113
+ }
114
+
115
+ async function writeDialog(args: {
116
+ db: CliLocalRuntimeDb;
117
+ input: AgentRuntimeSaveTurnInput;
118
+ userId: string;
119
+ now: () => number;
120
+ createId: () => string;
121
+ }) {
122
+ const dialogId = args.createId();
123
+ const now = args.now();
124
+ const nowIso = new Date(now).toISOString();
125
+ const dialogKey = `dialog-${args.userId}-${dialogId}`;
126
+ const lastUser = [...args.input.messages].reverse().find((message) => message.role === "user");
127
+ const ops: Array<{ type: "put"; key: string; value: any }> = [
128
+ {
129
+ type: "put",
130
+ key: dialogKey,
131
+ value: {
132
+ id: dialogId,
133
+ dbKey: dialogKey,
134
+ type: "dialog",
135
+ userId: args.userId,
136
+ cybots: [args.input.agentKey],
137
+ primaryAgentKey: args.input.agentKey,
138
+ title: typeof lastUser?.content === "string" && lastUser.content.trim()
139
+ ? lastUser.content.trim().slice(0, 80)
140
+ : "Local agent run",
141
+ status: "done",
142
+ triggerType: "cli-local",
143
+ executionMode: "foreground",
144
+ createdAt: nowIso,
145
+ updatedAt: nowIso,
146
+ finishedAt: now,
147
+ usage: args.input.result.usage,
148
+ },
149
+ },
150
+ {
151
+ type: "put",
152
+ key: `dialog-${dialogId}-msg-user`,
153
+ value: {
154
+ id: "msg-user",
155
+ dbKey: `dialog-${dialogId}-msg-user`,
156
+ dialogId,
157
+ role: "user",
158
+ content: lastUser?.content ?? "",
159
+ userId: args.userId,
160
+ createdAt: nowIso,
161
+ },
162
+ },
163
+ {
164
+ type: "put",
165
+ key: `dialog-${dialogId}-msg-assistant`,
166
+ value: {
167
+ id: "msg-assistant",
168
+ dbKey: `dialog-${dialogId}-msg-assistant`,
169
+ dialogId,
170
+ role: "assistant",
171
+ content: args.input.result.content,
172
+ agentKey: args.input.agentKey,
173
+ cybotKey: args.input.agentKey,
174
+ createdAt: nowIso,
175
+ },
176
+ },
177
+ ];
178
+ await args.db.batch(ops);
179
+ return { dialogId };
180
+ }
181
+
182
+ export function createCliLocalRuntimeAdapter(
183
+ deps: CliLocalRuntimeAdapterDeps
184
+ ): AgentRuntimeHostAdapter {
185
+ const now = deps.now ?? Date.now;
186
+ const createId = deps.createId ?? createFallbackId;
187
+ const fetchImpl = deps.fetchImpl ?? fetch;
188
+ const userId = resolveLocalUserId(deps.env);
189
+
190
+ return {
191
+ host: "cli",
192
+ capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence"],
193
+ loadAgentConfig: async (agentRef) => readAgentFromDb({
194
+ agentRef,
195
+ db: await resolveDb(deps),
196
+ userId,
197
+ }),
198
+ loadDialogHistory: async (dialogId) => readDialogMessages({
199
+ dialogId,
200
+ db: await resolveDb(deps),
201
+ }),
202
+ saveTurn: async (input) => writeDialog({
203
+ db: await resolveDb(deps),
204
+ input,
205
+ userId,
206
+ now,
207
+ createId,
208
+ }),
209
+ resolveProvider: async (agentConfig) => ({
210
+ model: agentConfig.model || "gpt-4.1-mini",
211
+ complete: async (messages) => {
212
+ const res = await fetchImpl(`${resolveOpenAiCompatibleBaseUrl(deps.env)}/chat/completions`, {
213
+ method: "POST",
214
+ headers: {
215
+ "Content-Type": "application/json",
216
+ ...(resolveApiKey(deps.env)
217
+ ? { Authorization: `Bearer ${resolveApiKey(deps.env)}` }
218
+ : {}),
219
+ },
220
+ body: JSON.stringify({
221
+ model: agentConfig.model || "gpt-4.1-mini",
222
+ messages: toOpenAiMessages(messages),
223
+ stream: false,
224
+ }),
225
+ });
226
+ const data = await res.json().catch(() => ({}));
227
+ if (!res.ok) {
228
+ throw new Error(`local provider failed: HTTP ${res.status} ${JSON.stringify(data)}`);
229
+ }
230
+ const content = String(data?.choices?.[0]?.message?.content ?? "");
231
+ return {
232
+ content,
233
+ model: agentConfig.model || "gpt-4.1-mini",
234
+ provider: agentConfig.provider || "openai-compatible",
235
+ usage: data?.usage,
236
+ trace: messages,
237
+ };
238
+ },
239
+ }),
240
+ executeTool: async () => {
241
+ throw new Error("Local runtime tools are not enabled for text-only CLI runs.");
242
+ },
243
+ };
244
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import {
7
+ buildEnvFromProfile,
8
+ loadProfileConfig,
9
+ saveDefaultProfile,
10
+ } from "./profileConfig";
11
+
12
+ describe("cli profile config", () => {
13
+ test("saves and loads the default profile", () => {
14
+ const dir = mkdtempSync(join(tmpdir(), "nolo-profile-"));
15
+ try {
16
+ const path = join(dir, "config.json");
17
+
18
+ saveDefaultProfile(path, {
19
+ serverUrl: "https://nolo.chat",
20
+ authToken: "token-123",
21
+ agentKey: "agent-pub-abc",
22
+ agentName: "app-builder",
23
+ });
24
+
25
+ const config = loadProfileConfig(path);
26
+ expect(config.currentProfile).toBe("default");
27
+ expect(config.profiles.default.serverUrl).toBe("https://nolo.chat");
28
+ expect(config.profiles.default.authToken).toBe("token-123");
29
+ expect(buildEnvFromProfile(config)).toEqual({
30
+ NOLO_PROFILE: "default",
31
+ NOLO_SERVER: "https://nolo.chat",
32
+ AUTH_TOKEN: "token-123",
33
+ NOLO_AGENT: "agent-pub-abc",
34
+ NOLO_AGENT_NAME: "app-builder",
35
+ });
36
+ } finally {
37
+ rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ });
40
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { createStreamingTextWriter, splitStreamingText } from "./streamingOutput";
4
+
5
+ describe("streamingOutput", () => {
6
+ test("splits text into user-visible characters instead of UTF-16 code units", () => {
7
+ expect(splitStreamingText("A你👍🏽B")).toEqual(["A", "你", "👍🏽", "B"]);
8
+ });
9
+
10
+ test("flushes text in small character batches", () => {
11
+ const chunks: string[] = [];
12
+ const writer = createStreamingTextWriter({
13
+ write: (chunk) => chunks.push(chunk),
14
+ batchSize: 2,
15
+ });
16
+
17
+ writer.push("abcd");
18
+ writer.flushAll();
19
+
20
+ expect(chunks).toEqual(["ab", "cd"]);
21
+ });
22
+ });