nolo-cli 0.1.7 → 0.1.9

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 (247) hide show
  1. package/README.md +107 -5
  2. package/agentRuntimeCommands.ts +464 -0
  3. package/ai/agent/_executeModel.ts +118 -0
  4. package/ai/agent/agentSlice.ts +525 -0
  5. package/ai/agent/appWorkingMemory.ts +126 -0
  6. package/ai/agent/avatarUtils.ts +24 -0
  7. package/ai/agent/buildEditingContext.ts +373 -0
  8. package/ai/agent/buildSystemPrompt.ts +532 -0
  9. package/ai/agent/cleanAgentMessages.ts +140 -0
  10. package/ai/agent/cliChatClient.ts +119 -0
  11. package/ai/agent/cliExecutor.ts +733 -0
  12. package/ai/agent/cliPrompt.ts +10 -0
  13. package/ai/agent/contextCompiler.ts +107 -0
  14. package/ai/agent/contextLayerContract.ts +44 -0
  15. package/ai/agent/createAgentSchema.ts +234 -0
  16. package/ai/agent/executeToolCall.ts +58 -0
  17. package/ai/agent/fetchAgentContexts.ts +42 -0
  18. package/ai/agent/generatePrompt.ts +3 -0
  19. package/ai/agent/getFullChatContextKeys.ts +168 -0
  20. package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
  21. package/ai/agent/hooks/useAgentConfig.ts +61 -0
  22. package/ai/agent/hooks/useAgentDialog.ts +35 -0
  23. package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
  24. package/ai/agent/hooks/usePublicAgents.ts +473 -0
  25. package/ai/agent/machineRunPermissions.ts +95 -0
  26. package/ai/agent/persistMessageWithFixedId.ts +37 -0
  27. package/ai/agent/planSlice.ts +259 -0
  28. package/ai/agent/referenceUtils.ts +229 -0
  29. package/ai/agent/runAgentBackground.ts +238 -0
  30. package/ai/agent/runAgentClientLoop.ts +138 -0
  31. package/ai/agent/runtimeGuidance.ts +97 -0
  32. package/ai/agent/runtimeServerBase.ts +37 -0
  33. package/ai/agent/server/fetchPublicAgents.ts +128 -0
  34. package/ai/agent/startParallelAgentStreams.ts +424 -0
  35. package/ai/agent/startupProtocol.ts +53 -0
  36. package/ai/agent/streamAgentChatTurn.ts +1278 -0
  37. package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
  38. package/ai/agent/types.ts +71 -0
  39. package/ai/agent/utils/imageOutput.ts +33 -0
  40. package/ai/agent/utils/sortUtils.ts +250 -0
  41. package/ai/agent/web/referencePickerUtils.ts +146 -0
  42. package/ai/ai.locale.ts +1075 -0
  43. package/ai/chat/accumulateToolCallChunks.ts +95 -0
  44. package/ai/chat/fetchUtils.native.ts +276 -0
  45. package/ai/chat/fetchUtils.ts +153 -0
  46. package/ai/chat/parseApiError.ts +64 -0
  47. package/ai/chat/parseMultilineSSE.ts +95 -0
  48. package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
  49. package/ai/chat/sendOpenAICompletionsRequest.ts +703 -0
  50. package/ai/chat/sendOpenAIResponseRequest.ts +491 -0
  51. package/ai/chat/shouldUseServerProxy.ts +18 -0
  52. package/ai/chat/sseClient.native.ts +91 -0
  53. package/ai/chat/sseClient.ts +67 -0
  54. package/ai/chat/streamReader.native.ts +31 -0
  55. package/ai/chat/streamReader.ts +62 -0
  56. package/ai/chat/updateTotalUsage.ts +72 -0
  57. package/ai/context/buildReferenceContext.ts +437 -0
  58. package/ai/context/calculateContextUsage.ts +133 -0
  59. package/ai/context/retention.ts +165 -0
  60. package/ai/context/tokenUtils.ts +78 -0
  61. package/ai/index.ts +1 -0
  62. package/ai/llm/calculateGeminiImageTokens.ts +57 -0
  63. package/ai/llm/deepinfra.ts +28 -0
  64. package/ai/llm/fireworks.ts +50 -0
  65. package/ai/llm/generateRequestBody.ts +165 -0
  66. package/ai/llm/getModelContextWindow.ts +84 -0
  67. package/ai/llm/getNoloKey.ts +31 -0
  68. package/ai/llm/getPricing.ts +199 -0
  69. package/ai/llm/hooks/useModelPricing.ts +75 -0
  70. package/ai/llm/imagePricing.ts +40 -0
  71. package/ai/llm/isResponseAPIModel.ts +13 -0
  72. package/ai/llm/mimo.ts +71 -0
  73. package/ai/llm/mistral.ts +22 -0
  74. package/ai/llm/modelAvatar.ts +427 -0
  75. package/ai/llm/models.ts +45 -0
  76. package/ai/llm/openrouterModels.ts +269 -0
  77. package/ai/llm/providers.ts +306 -0
  78. package/ai/llm/reasoningModels.ts +28 -0
  79. package/ai/llm/types.ts +59 -0
  80. package/ai/llm/usageRequestOptions.ts +59 -0
  81. package/ai/memory/capture.ts +148 -0
  82. package/ai/memory/consolidate.ts +104 -0
  83. package/ai/memory/delete.ts +147 -0
  84. package/ai/memory/overlay.ts +84 -0
  85. package/ai/memory/query.ts +38 -0
  86. package/ai/memory/queryShared.ts +160 -0
  87. package/ai/memory/rank.ts +105 -0
  88. package/ai/memory/recentRelationshipRecap.ts +249 -0
  89. package/ai/memory/remember.ts +167 -0
  90. package/ai/memory/runtime.ts +76 -0
  91. package/ai/memory/store.ts +20 -0
  92. package/ai/memory/storeShared.ts +76 -0
  93. package/ai/memory/types.ts +46 -0
  94. package/ai/memory/understanding.ts +349 -0
  95. package/ai/memory/understandingGreeting.ts +264 -0
  96. package/ai/messages/type.ts +20 -0
  97. package/ai/policy/personalizationDialog.ts +333 -0
  98. package/ai/policy/runtimePolicy.ts +440 -0
  99. package/ai/policy/selfUpdateFields.ts +48 -0
  100. package/ai/policy/types.ts +64 -0
  101. package/ai/skills/referenceRuntime.ts +274 -0
  102. package/ai/skills/skillDiagnostics.ts +251 -0
  103. package/ai/skills/skillDocBuilder.ts +139 -0
  104. package/ai/skills/skillDocProtocol.ts +434 -0
  105. package/ai/skills/skillReferenceSummary.ts +63 -0
  106. package/ai/skills/skillSummaryMarker.ts +26 -0
  107. package/ai/token/calculatePrice.ts +544 -0
  108. package/ai/token/db.ts +98 -0
  109. package/ai/token/externalToolCost.ts +330 -0
  110. package/ai/token/hooks/useRecords.ts +65 -0
  111. package/ai/token/missingUsageEstimate.ts +42 -0
  112. package/ai/token/modelUsageQuery.ts +252 -0
  113. package/ai/token/normalizeUsage.ts +84 -0
  114. package/ai/token/openaiImageGenerationUsage.ts +56 -0
  115. package/ai/token/prepareTokenUsageData.ts +88 -0
  116. package/ai/token/query.ts +88 -0
  117. package/ai/token/queryUserTokens.ts +59 -0
  118. package/ai/token/resolveBillingTarget.ts +52 -0
  119. package/ai/token/saveTokenRecord.ts +53 -0
  120. package/ai/token/serverDialogProjection.ts +78 -0
  121. package/ai/token/serverTokenWriter.ts +143 -0
  122. package/ai/token/stats.ts +21 -0
  123. package/ai/token/tokenThunks.ts +24 -0
  124. package/ai/token/types.ts +93 -0
  125. package/ai/tools/agent/agentTools.ts +176 -0
  126. package/ai/tools/agent/agentUpdateShared.ts +311 -0
  127. package/ai/tools/agent/callAgentTool.ts +139 -0
  128. package/ai/tools/agent/createAgentTool.ts +512 -0
  129. package/ai/tools/agent/createDialogTool.ts +69 -0
  130. package/ai/tools/agent/createSkillAgentTool.ts +62 -0
  131. package/ai/tools/agent/parallelBudget.ts +221 -0
  132. package/ai/tools/agent/presets/appBuilderPreset.ts +145 -0
  133. package/ai/tools/agent/runLlmTool.ts +96 -0
  134. package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
  135. package/ai/tools/agent/skillAgentArgs.ts +106 -0
  136. package/ai/tools/agent/skillAgentPreset.ts +89 -0
  137. package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
  138. package/ai/tools/agent/updateAgentTool.ts +96 -0
  139. package/ai/tools/agent/updateSelfTool.ts +113 -0
  140. package/ai/tools/amazonProductScraperTool.ts +86 -0
  141. package/ai/tools/apifyActorClient.ts +45 -0
  142. package/ai/tools/appEditGuard.ts +372 -0
  143. package/ai/tools/appReadSnapshot.ts +153 -0
  144. package/ai/tools/appTools.ts +1549 -0
  145. package/ai/tools/applyEditTool.ts +256 -0
  146. package/ai/tools/applyLineEditsTool.ts +312 -0
  147. package/ai/tools/browserTools/click.ts +33 -0
  148. package/ai/tools/browserTools/closeSession.ts +29 -0
  149. package/ai/tools/browserTools/common.ts +27 -0
  150. package/ai/tools/browserTools/openSession.ts +48 -0
  151. package/ai/tools/browserTools/readContent.ts +38 -0
  152. package/ai/tools/browserTools/selectOption.ts +46 -0
  153. package/ai/tools/browserTools/typeText.ts +42 -0
  154. package/ai/tools/category/createCategoryTool.ts +66 -0
  155. package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
  156. package/ai/tools/category/updateContentCategoryTool.ts +75 -0
  157. package/ai/tools/cfBrowserTools.ts +319 -0
  158. package/ai/tools/cfSpeechToTextTool.ts +49 -0
  159. package/ai/tools/checkEnvTool.ts +65 -0
  160. package/ai/tools/cloudflareCrawlTool.ts +289 -0
  161. package/ai/tools/codeSearchTool.ts +111 -0
  162. package/ai/tools/codeTools.ts +101 -0
  163. package/ai/tools/createDocTool.ts +132 -0
  164. package/ai/tools/createPlanTool.ts +999 -0
  165. package/ai/tools/createSkillDocTool.ts +155 -0
  166. package/ai/tools/createWorkflowTool.ts +154 -0
  167. package/ai/tools/deepseekOcrTool.ts +34 -0
  168. package/ai/tools/delayTool.ts +31 -0
  169. package/ai/tools/deleteSpacesTool.ts +325 -0
  170. package/ai/tools/deleteSpacesToolModel.ts +159 -0
  171. package/ai/tools/devReloadUtils.ts +29 -0
  172. package/ai/tools/dialogMessageSearch.ts +137 -0
  173. package/ai/tools/doctorSkillTool.ts +72 -0
  174. package/ai/tools/ecommerceScraperTool.ts +86 -0
  175. package/ai/tools/emailTools.ts +549 -0
  176. package/ai/tools/evalSkillTool.ts +92 -0
  177. package/ai/tools/exaSearchTool.ts +64 -0
  178. package/ai/tools/execBashTool.ts +379 -0
  179. package/ai/tools/executeSqlTool.ts +192 -0
  180. package/ai/tools/fetchWebpageSupport.ts +309 -0
  181. package/ai/tools/fetchWebpageTool.ts +84 -0
  182. package/ai/tools/geminiImagePreviewTool.ts +361 -0
  183. package/ai/tools/generateDocxTool.ts +215 -0
  184. package/ai/tools/googleSearchScraperTool.ts +106 -0
  185. package/ai/tools/importDataTool.ts +133 -0
  186. package/ai/tools/importSkillTool.ts +162 -0
  187. package/ai/tools/index.ts +1858 -0
  188. package/ai/tools/listFilesTool.ts +82 -0
  189. package/ai/tools/listUserSpacesTool.ts +113 -0
  190. package/ai/tools/modelUsageTools.ts +142 -0
  191. package/ai/tools/olmOcrTool.ts +34 -0
  192. package/ai/tools/openaiImageTool.ts +218 -0
  193. package/ai/tools/paddleOcrTool.ts +34 -0
  194. package/ai/tools/prepareTools.ts +23 -0
  195. package/ai/tools/readDocTool.ts +84 -0
  196. package/ai/tools/readFileTool.ts +211 -0
  197. package/ai/tools/readTool.ts +163 -0
  198. package/ai/tools/readXPostTool.ts +233 -0
  199. package/ai/tools/rememberMemoryTool.ts +84 -0
  200. package/ai/tools/remotionVideoTool.ts +151 -0
  201. package/ai/tools/searchDialogMessagesTool.ts +222 -0
  202. package/ai/tools/searchRepoTool.ts +115 -0
  203. package/ai/tools/searchWorkspaceTool.ts +259 -0
  204. package/ai/tools/skillFollowup.ts +86 -0
  205. package/ai/tools/surfWeatherTool.ts +169 -0
  206. package/ai/tools/table/addTableRowTool.ts +217 -0
  207. package/ai/tools/table/createTableTool.ts +315 -0
  208. package/ai/tools/table/rowTools.ts +366 -0
  209. package/ai/tools/table/schemaTools.ts +244 -0
  210. package/ai/tools/table/shareTableTool.ts +148 -0
  211. package/ai/tools/table/toolShared.ts +129 -0
  212. package/ai/tools/toolApiClient.ts +198 -0
  213. package/ai/tools/toolNameAliases.ts +57 -0
  214. package/ai/tools/toolResultError.ts +42 -0
  215. package/ai/tools/toolRunSlice.ts +303 -0
  216. package/ai/tools/toolSchemaCompatibility.ts +53 -0
  217. package/ai/tools/toolVisibility.ts +4 -0
  218. package/ai/tools/types.ts +20 -0
  219. package/ai/tools/uiAskChoiceTool.ts +104 -0
  220. package/ai/tools/updateContentTitleTool.ts +84 -0
  221. package/ai/tools/updateDocTool.ts +105 -0
  222. package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
  223. package/ai/tools/whisperTool.ts +77 -0
  224. package/ai/tools/writeFileTool.ts +210 -0
  225. package/ai/tools/youtubeScraperTool.ts +116 -0
  226. package/ai/tools/ziweiChartTool.ts +678 -0
  227. package/ai/types.ts +55 -0
  228. package/ai/workflow/workflowExecutor.ts +323 -0
  229. package/ai/workflow/workflowSlice.ts +73 -0
  230. package/ai/workflow/workflowTypes.ts +106 -0
  231. package/client/agentRun.ts +198 -167
  232. package/client/compactDialog.ts +222 -0
  233. package/commandRegistry.ts +14 -0
  234. package/connector-experimental/capabilities.ts +73 -0
  235. package/connector-experimental/codexBinary.ts +41 -0
  236. package/connector-experimental/heartbeatLoop.ts +22 -0
  237. package/connector-experimental/index.ts +5 -0
  238. package/connector-experimental/machineInfo.ts +46 -0
  239. package/connector-experimental/protocol.ts +54 -0
  240. package/connectorWebSocketTarget.ts +29 -0
  241. package/defaultServer.ts +1 -0
  242. package/index.ts +158 -104
  243. package/machineCommands.ts +382 -0
  244. package/package.json +12 -2
  245. package/tui/readlineWorkspace.ts +50 -0
  246. package/tui/session.ts +40 -2
  247. package/updateCommands.ts +70 -5
@@ -0,0 +1,382 @@
1
+ import type { MachineHeartbeat } from "./connector-experimental/protocol";
2
+ import { detectMachineInfo } from "./connector-experimental/machineInfo";
3
+ import { mkdirSync, openSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
7
+ import {
8
+ type HeartbeatLoopOptions,
9
+ runHeartbeatLoop as defaultRunHeartbeatLoop,
10
+ } from "./connector-experimental/heartbeatLoop";
11
+ import {
12
+ assertMachineRunAllowed,
13
+ buildMachinePermissionPromptBlock,
14
+ resolveMachineRunPermissionPolicy,
15
+ } from "./ai/agent/machineRunPermissions";
16
+ import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
17
+
18
+ type EnvLike = Record<string, string | undefined>;
19
+ type OutputLike = { write(chunk: string): unknown };
20
+ type ConnectorWebSocketOptions = {
21
+ headers: Record<string, string>;
22
+ onMessage: (message: string) => void | Promise<void>;
23
+ sentMessages: string[];
24
+ };
25
+ type LocalCliExecutor = (
26
+ provider: string,
27
+ prompt: string,
28
+ options: { model?: string; yolo?: boolean }
29
+ ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
30
+
31
+ export type MachineSummary = {
32
+ machineId: string;
33
+ name: string;
34
+ platform: string;
35
+ arch: string;
36
+ connectorVersion: string | null;
37
+ capabilities: string[];
38
+ connectorStatus?: "connected" | "disconnected";
39
+ status: "online" | "offline";
40
+ lastSeenAt: number;
41
+ };
42
+
43
+ type MachineCommandDeps = {
44
+ env?: EnvLike;
45
+ output?: OutputLike;
46
+ fetchImpl?: typeof fetch;
47
+ machineInfo?: () => MachineHeartbeat;
48
+ runHeartbeatLoop?: (options: HeartbeatLoopOptions) => Promise<void>;
49
+ connectWebSocket?: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
50
+ executeCli?: LocalCliExecutor;
51
+ spawnDaemon?: (args: {
52
+ cmd: string[];
53
+ cwd: string;
54
+ env: EnvLike;
55
+ logPath: string;
56
+ }) => { pid?: number };
57
+ };
58
+
59
+ function resolveServerUrl(env: EnvLike) {
60
+ return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
61
+ }
62
+
63
+ function resolveAuthToken(env: EnvLike) {
64
+ return env.AUTH_TOKEN || env.AUTH || "";
65
+ }
66
+
67
+ function writeAuthMissing(output: OutputLike) {
68
+ output.write(
69
+ "[nolo] Machine commands require an auth token. Run `nolo login` or set AUTH_TOKEN.\n"
70
+ );
71
+ }
72
+
73
+ function hasFlag(args: string[], flag: string) {
74
+ return args.includes(flag);
75
+ }
76
+
77
+ function resolveDaemonLogPath(env: EnvLike) {
78
+ return env.NOLO_CONNECT_LOG || join(homedir(), ".nolo", "logs", "connector.log");
79
+ }
80
+
81
+ function defaultSpawnDaemon(args: {
82
+ cmd: string[];
83
+ cwd: string;
84
+ env: EnvLike;
85
+ logPath: string;
86
+ }) {
87
+ mkdirSync(dirname(args.logPath), { recursive: true });
88
+ const out = openSync(args.logPath, "a");
89
+ const env = Object.fromEntries(
90
+ Object.entries(args.env).filter((entry): entry is [string, string] => typeof entry[1] === "string")
91
+ );
92
+ const proc = Bun.spawn({
93
+ cmd: args.cmd,
94
+ cwd: args.cwd,
95
+ env,
96
+ stdin: "ignore",
97
+ stdout: out,
98
+ stderr: out,
99
+ });
100
+ proc.unref();
101
+ return { pid: proc.pid };
102
+ }
103
+
104
+ function resolveHeartbeatIntervalMs(env: EnvLike) {
105
+ const raw = Number(env.NOLO_CONNECT_HEARTBEAT_MS ?? "");
106
+ return Number.isFinite(raw) && raw > 0 ? raw : 30_000;
107
+ }
108
+
109
+ async function defaultConnectWebSocket(url: string, options: ConnectorWebSocketOptions) {
110
+ const WebSocketCtor = globalThis.WebSocket;
111
+ if (!WebSocketCtor) {
112
+ throw new Error("WebSocket is not available in this runtime");
113
+ }
114
+ await new Promise<void>((resolve, reject) => {
115
+ const ws = new WebSocketCtor(url, {
116
+ headers: options.headers,
117
+ } as any);
118
+ let opened = false;
119
+ ws.addEventListener("open", () => {
120
+ opened = true;
121
+ }, { once: true });
122
+ ws.addEventListener("error", () => reject(new Error("connector websocket failed")));
123
+ ws.addEventListener("close", () => {
124
+ if (opened) resolve();
125
+ else reject(new Error("connector websocket closed before opening"));
126
+ }, { once: true });
127
+ ws.addEventListener("message", (event) => {
128
+ const startIndex = options.sentMessages.length;
129
+ Promise.resolve(options.onMessage(String(event.data))).then(() => {
130
+ for (const message of options.sentMessages.slice(startIndex)) {
131
+ ws.send(message);
132
+ }
133
+ }).catch(() => undefined);
134
+ });
135
+ });
136
+ }
137
+
138
+ async function defaultExecuteCli(provider: string, prompt: string, options: { model?: string; yolo?: boolean }) {
139
+ const { executeCli } = await import("ai/agent/cliExecutor");
140
+ return executeCli(provider as any, prompt, options);
141
+ }
142
+
143
+ function detectLaunchableMachineInfo() {
144
+ return detectMachineInfo({ probeLaunchable: true });
145
+ }
146
+
147
+ function buildConnectorCliPrompt(agentConfig: any, userInput: string) {
148
+ const policy = resolveMachineRunPermissionPolicy(agentConfig);
149
+ return [
150
+ typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
151
+ buildMachinePermissionPromptBlock(policy),
152
+ `--- User task ---\n${userInput}`,
153
+ ].filter(Boolean).join("\n\n");
154
+ }
155
+
156
+ async function handleConnectorRunMessage(
157
+ machine: MachineHeartbeat,
158
+ message: string,
159
+ send: (message: string) => void,
160
+ executeCli: LocalCliExecutor
161
+ ) {
162
+ let parsed: any;
163
+ try {
164
+ parsed = JSON.parse(message);
165
+ } catch {
166
+ return;
167
+ }
168
+ if (parsed?.type !== "agent.run" || typeof parsed.requestId !== "string") return;
169
+ const agentConfig = parsed.payload?.agentConfig ?? {};
170
+ try {
171
+ if (agentConfig.apiSource !== "cli") {
172
+ throw new Error("Connector can only execute CLI agents. Set the agent apiSource to cli and choose a cliProvider.");
173
+ }
174
+ const provider = String(agentConfig.cliProvider || "copilot");
175
+ const policy = resolveMachineRunPermissionPolicy(agentConfig);
176
+ const userInput = String(parsed.payload?.userInput ?? "");
177
+ assertMachineRunAllowed(userInput, policy);
178
+ const result = await executeCli(
179
+ provider,
180
+ buildConnectorCliPrompt(agentConfig, userInput),
181
+ {
182
+ model: agentConfig.model || undefined,
183
+ yolo: true,
184
+ }
185
+ );
186
+ send(JSON.stringify({
187
+ type: "agent.run.result",
188
+ requestId: parsed.requestId,
189
+ result: {
190
+ content: result.text,
191
+ model: agentConfig.model ?? provider,
192
+ trace: [{ role: "assistant", content: result.text }],
193
+ },
194
+ }));
195
+ } catch (error) {
196
+ send(JSON.stringify({
197
+ type: "agent.run.result",
198
+ requestId: parsed.requestId,
199
+ error: error instanceof Error ? error.message : String(error),
200
+ }));
201
+ }
202
+ }
203
+
204
+ export function formatMachineStatus(machines: MachineSummary[]) {
205
+ if (machines.length === 0) {
206
+ return "No connected machines.\nRun `nolo connect` on this computer to register it once.\n";
207
+ }
208
+
209
+ return [
210
+ "Connected machines:",
211
+ ...machines.map((machine) => {
212
+ const caps = machine.capabilities.length ? machine.capabilities.join(", ") : "no capabilities";
213
+ const connector = machine.connectorStatus ? ` ws:${machine.connectorStatus}` : "";
214
+ return `- ${machine.name} ${machine.status}${connector} ${machine.platform}/${machine.arch} ${caps}`;
215
+ }),
216
+ "",
217
+ ].join("\n");
218
+ }
219
+
220
+ export async function runMachineConnectCommand(
221
+ args: string[],
222
+ deps: MachineCommandDeps = {}
223
+ ) {
224
+ const env = deps.env ?? process.env;
225
+ const output = deps.output ?? process.stdout;
226
+ const authToken = resolveAuthToken(env);
227
+ if (!authToken) {
228
+ writeAuthMissing(output);
229
+ return 1;
230
+ }
231
+
232
+ const serverUrl = resolveServerUrl(env);
233
+ const fetchImpl = deps.fetchImpl ?? fetch;
234
+ const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
235
+ const sendHeartbeat = async () => {
236
+ const res = await fetchImpl(`${serverUrl}/api/machines/heartbeat`, {
237
+ method: "POST",
238
+ headers: {
239
+ Authorization: `Bearer ${authToken}`,
240
+ "Content-Type": "application/json",
241
+ },
242
+ body: JSON.stringify(machine),
243
+ });
244
+
245
+ if (!res.ok) {
246
+ const text = await res.text().catch(() => "");
247
+ throw new Error(`HTTP ${res.status}\n${text}`);
248
+ }
249
+ };
250
+
251
+ if (hasFlag(args, "--daemon") || hasFlag(args, "--background")) {
252
+ const logPath = resolveDaemonLogPath(env);
253
+ const result = (deps.spawnDaemon ?? defaultSpawnDaemon)({
254
+ cmd: [process.execPath, import.meta.path, "connect", "--ws"],
255
+ cwd: process.cwd(),
256
+ env,
257
+ logPath,
258
+ });
259
+ output.write(`Connector daemon started${result.pid ? ` pid=${result.pid}` : ""}. Log: ${logPath}\n`);
260
+ return 0;
261
+ }
262
+
263
+ if (hasFlag(args, "--ws")) {
264
+ const sentMessages: string[] = [];
265
+ const heartbeatAbort = new AbortController();
266
+ try {
267
+ await sendHeartbeat();
268
+ output.write(`Connector websocket connected: ${machine.name} (${machine.machineId})\n`);
269
+ const heartbeatLoopPromise = (deps.runHeartbeatLoop ?? defaultRunHeartbeatLoop)({
270
+ intervalMs: resolveHeartbeatIntervalMs(env),
271
+ sendHeartbeat,
272
+ signal: heartbeatAbort.signal,
273
+ });
274
+ const wsTarget = await resolveConnectorWebSocketTarget({
275
+ serverUrl,
276
+ machineId: machine.machineId,
277
+ headers: { Authorization: `Bearer ${authToken}` },
278
+ fetchImpl,
279
+ });
280
+ const websocketPromise = (deps.connectWebSocket ?? defaultConnectWebSocket)(
281
+ wsTarget,
282
+ {
283
+ headers: { Authorization: `Bearer ${authToken}` },
284
+ sentMessages,
285
+ onMessage: (message) => handleConnectorRunMessage(
286
+ machine,
287
+ message,
288
+ (response) => sentMessages.push(response),
289
+ deps.executeCli ?? defaultExecuteCli
290
+ ),
291
+ }
292
+ );
293
+ await Promise.race([websocketPromise, heartbeatLoopPromise]);
294
+ heartbeatAbort.abort();
295
+ await Promise.allSettled([websocketPromise, heartbeatLoopPromise]);
296
+ return 0;
297
+ } catch (error) {
298
+ heartbeatAbort.abort();
299
+ output.write(
300
+ `[nolo] Connector websocket failed: ${
301
+ error instanceof Error ? error.message : String(error)
302
+ }\n`
303
+ );
304
+ return 1;
305
+ }
306
+ }
307
+
308
+ if (hasFlag(args, "--watch")) {
309
+ output.write(`Connecting machine heartbeat loop: ${machine.name} (${machine.platform}/${machine.arch})\n`);
310
+ try {
311
+ await (deps.runHeartbeatLoop ?? defaultRunHeartbeatLoop)({
312
+ intervalMs: resolveHeartbeatIntervalMs(env),
313
+ sendHeartbeat,
314
+ });
315
+ return 0;
316
+ } catch (error) {
317
+ output.write(
318
+ `[nolo] Machine heartbeat loop failed: ${
319
+ error instanceof Error ? error.message : String(error)
320
+ }\n`
321
+ );
322
+ return 1;
323
+ }
324
+ }
325
+
326
+ try {
327
+ await sendHeartbeat();
328
+ } catch (error) {
329
+ output.write(
330
+ `[nolo] Machine connect failed: ${
331
+ error instanceof Error ? error.message : String(error)
332
+ }\n`
333
+ );
334
+ return 1;
335
+ }
336
+
337
+ output.write(`Connected machine: ${machine.name} (${machine.platform}/${machine.arch})\n`);
338
+ return 0;
339
+ }
340
+
341
+ export async function runMachineStatusCommand(
342
+ _args: string[],
343
+ deps: MachineCommandDeps = {}
344
+ ) {
345
+ const env = deps.env ?? process.env;
346
+ const output = deps.output ?? process.stdout;
347
+ const authToken = resolveAuthToken(env);
348
+ if (!authToken) {
349
+ writeAuthMissing(output);
350
+ return 1;
351
+ }
352
+
353
+ const serverUrl = resolveServerUrl(env);
354
+ const fetchImpl = deps.fetchImpl ?? fetch;
355
+ let res: Response;
356
+ try {
357
+ res = await fetchImpl(`${serverUrl}/api/machines`, {
358
+ method: "GET",
359
+ headers: {
360
+ Authorization: `Bearer ${authToken}`,
361
+ },
362
+ });
363
+ } catch (error) {
364
+ output.write(
365
+ `[nolo] Machine status failed: ${
366
+ error instanceof Error ? error.message : String(error)
367
+ }\n`
368
+ );
369
+ return 1;
370
+ }
371
+
372
+ if (!res.ok) {
373
+ const text = await res.text().catch(() => "");
374
+ output.write(`[nolo] Machine status failed: HTTP ${res.status}\n${text}\n`);
375
+ return 1;
376
+ }
377
+
378
+ const data = await res.json().catch(() => ({ machines: [] }));
379
+ const machines = Array.isArray(data?.machines) ? data.machines : [];
380
+ output.write(formatMachineStatus(machines));
381
+ return 0;
382
+ }
package/package.json CHANGED
@@ -1,22 +1,32 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
7
7
  "nolo": "index.ts"
8
8
  },
9
9
  "module": "index.ts",
10
+ "scripts": {
11
+ "build:publish": "bun build.ts"
12
+ },
10
13
  "files": [
11
14
  "index.ts",
15
+ "agentRuntimeCommands.ts",
12
16
  "authCommands.ts",
13
17
  "commandRegistry.ts",
18
+ "connectorWebSocketTarget.ts",
19
+ "defaultServer.ts",
20
+ "machineCommands.ts",
14
21
  "updateCommands.ts",
15
22
  "client/agentRun.ts",
23
+ "client/compactDialog.ts",
16
24
  "client/profileConfig.ts",
17
25
  "tui/readlineWorkspace.ts",
18
26
  "tui/session.ts",
19
- "README.md"
27
+ "README.md",
28
+ "ai/**/*.ts",
29
+ "connector-experimental/**/*.ts"
20
30
  ],
21
31
  "publishConfig": {
22
32
  "access": "public"
@@ -1,6 +1,8 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { stdin as defaultInput, stdout as defaultOutput } from "node:process";
3
3
  import { runAgentTurn, type RunAgentTurnResult } from "../client/agentRun";
4
+ import { compactDialog, type CompactDialogResult } from "../client/compactDialog";
5
+ import { runSelfUpdate } from "../updateCommands";
4
6
  import {
5
7
  createInitialTuiState,
6
8
  handleTuiInput,
@@ -10,12 +12,22 @@ import {
10
12
  type TuiState,
11
13
  } from "./session";
12
14
 
15
+ export type SelfUpdater = (
16
+ output: NodeJS.WritableStream
17
+ ) => Promise<number>;
18
+
13
19
  type WorkspaceOptions = {
14
20
  scriptDir: string;
15
21
  env?: NodeJS.ProcessEnv;
16
22
  input?: NodeJS.ReadableStream;
17
23
  output?: NodeJS.WritableStream;
18
24
  agentRunner?: typeof runAgentTurn;
25
+ compactRunner?: (options: {
26
+ serverUrl: string;
27
+ authToken: string;
28
+ dialogId: string;
29
+ }) => Promise<CompactDialogResult>;
30
+ selfUpdater?: SelfUpdater;
19
31
  };
20
32
 
21
33
  async function runAgentChat(
@@ -43,6 +55,8 @@ export async function startTuiWorkspace(options: WorkspaceOptions) {
43
55
  let state = createInitialTuiState(options.env ?? process.env);
44
56
  const input = options.input ?? defaultInput;
45
57
  const output = options.output ?? defaultOutput;
58
+ const selfUpdater: SelfUpdater =
59
+ options.selfUpdater ?? ((target) => runSelfUpdate({ output: target }));
46
60
  const rl = createInterface({ input, output });
47
61
 
48
62
  output.write(renderWelcome(state));
@@ -62,6 +76,42 @@ export async function startTuiWorkspace(options: WorkspaceOptions) {
62
76
  break;
63
77
  }
64
78
 
79
+ if (result.action?.type === "compact") {
80
+ const runner = options.compactRunner ?? compactDialog;
81
+ const authToken =
82
+ options.env?.AUTH_TOKEN ?? options.env?.AUTH ?? options.env?.BENCHMARK_AUTH_TOKEN ?? "";
83
+ try {
84
+ const compactResult = await runner({
85
+ serverUrl: state.serverUrl,
86
+ authToken,
87
+ dialogId: result.action.dialogId,
88
+ });
89
+ state = {
90
+ ...state,
91
+ dialogId: compactResult.dialogId,
92
+ dialogLabel: compactResult.dialogId,
93
+ };
94
+ } catch (error: any) {
95
+ output.write(
96
+ `[nolo] Compact failed: ${error?.message ?? String(error)}\n`
97
+ );
98
+ }
99
+ }
100
+
101
+ if (result.action?.type === "self-update") {
102
+ try {
103
+ const exitCode = await selfUpdater(output);
104
+ if (exitCode === 0) {
105
+ output.write("Update finished. Restart nolo to use the new version.\n");
106
+ } else {
107
+ output.write("Update failed. Fix the npm error above, then run /update again or use nolo update.\n");
108
+ }
109
+ } catch (error) {
110
+ output.write(`${error instanceof Error ? error.message : String(error)}\n`);
111
+ output.write("Update failed. Fix the npm error above, then run /update again or use nolo update.\n");
112
+ }
113
+ }
114
+
65
115
  if (result.action?.type === "chat") {
66
116
  const runResult = await runAgentChat(
67
117
  options.scriptDir,
package/tui/session.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { DEFAULT_NOLO_SERVER_URL } from "../defaultServer";
2
+
1
3
  export const DEFAULT_TUI_AGENT_KEY = "agent-pub-01NOLOAPPBLD000000019KCKT0";
2
- export const DEFAULT_TUI_SERVER_URL = "http://127.0.0.1:38123";
4
+ export const DEFAULT_TUI_SERVER_URL = DEFAULT_NOLO_SERVER_URL;
3
5
 
4
6
  const KNOWN_AGENTS: Array<{
5
7
  name: string;
@@ -51,6 +53,13 @@ export type TuiAction =
51
53
  agentKey: string;
52
54
  continueDialogId?: string;
53
55
  }
56
+ | {
57
+ type: "compact";
58
+ dialogId: string;
59
+ }
60
+ | {
61
+ type: "self-update";
62
+ }
54
63
  | {
55
64
  type: "exit";
56
65
  };
@@ -73,7 +82,10 @@ export function createInitialTuiState(env: EnvLike = process.env): TuiState {
73
82
  dialogId: env.NOLO_DIALOG_ID?.trim() || undefined,
74
83
  dialogLabel: env.NOLO_DIALOG?.trim() || "new",
75
84
  profileName: env.NOLO_PROFILE?.trim() || "local",
76
- serverUrl: (env.NOLO_SERVER || env.BASE_URL || DEFAULT_TUI_SERVER_URL).replace(/\/+$/, ""),
85
+ serverUrl: (env.NOLO_SERVER || env.BASE_URL || DEFAULT_TUI_SERVER_URL).replace(
86
+ /\/+$/,
87
+ ""
88
+ ),
77
89
  cliVersion: env.NOLO_CLI_VERSION?.trim() || undefined,
78
90
  attachedDocs: [],
79
91
  };
@@ -117,6 +129,7 @@ export function renderTuiHelp() {
117
129
  "Commands:",
118
130
  " /help Show this help",
119
131
  " /new Start a fresh dialog",
132
+ " /compact Compact current dialog and fork a new one",
120
133
  " /context Show workspace context and next actions",
121
134
  " /agent Show the current agent",
122
135
  " /agents List built-in agent shortcuts",
@@ -127,6 +140,7 @@ export function renderTuiHelp() {
127
140
  " /customize Describe how you want to tune nolo",
128
141
  " /login Show login/profile hint",
129
142
  " /profile Show active profile",
143
+ " /update Update the global nolo install",
130
144
  " /version Show version/update hint",
131
145
  " /exit Leave the workspace",
132
146
  "",
@@ -221,6 +235,24 @@ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
221
235
  },
222
236
  output: "Started a fresh dialog.",
223
237
  };
238
+ case "/compact":
239
+ if (argText) {
240
+ return {
241
+ nextState: state,
242
+ output: `Unknown command: ${trimmed}\n\n${renderTuiHelp()}`,
243
+ };
244
+ }
245
+ if (!state.dialogId) {
246
+ return {
247
+ nextState: state,
248
+ output: "Current dialog: new (nothing to compact yet)",
249
+ };
250
+ }
251
+ return {
252
+ nextState: state,
253
+ output: "Compacting current dialog...",
254
+ action: { type: "compact", dialogId: state.dialogId },
255
+ };
224
256
  case "/agent":
225
257
  return {
226
258
  nextState: state,
@@ -302,6 +334,12 @@ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
302
334
  nextState: state,
303
335
  output: `Profile: ${state.profileName}`,
304
336
  };
337
+ case "/update":
338
+ return {
339
+ nextState: state,
340
+ output: "Starting self-update...",
341
+ action: { type: "self-update" },
342
+ };
305
343
  case "/version":
306
344
  return {
307
345
  nextState: state,
package/updateCommands.ts CHANGED
@@ -15,6 +15,13 @@ type DoctorInfo = {
15
15
  profileName: string;
16
16
  };
17
17
 
18
+ type RunSelfUpdateOptions = {
19
+ output?: NodeJS.WritableStream;
20
+ spawn?: typeof Bun.spawn;
21
+ };
22
+
23
+ type SpawnOutputChunk = string | ArrayBuffer | ArrayBufferView;
24
+
18
25
  const CLI_DIR = dirname(fileURLToPath(import.meta.url));
19
26
  const PACKAGE_JSON_PATH = join(CLI_DIR, "package.json");
20
27
 
@@ -53,17 +60,75 @@ export function buildCliDoctorText(info: DoctorInfo) {
53
60
  ].join("\n");
54
61
  }
55
62
 
56
- export async function runSelfUpdate(output: NodeJS.WritableStream = process.stdout) {
63
+ function isRunSelfUpdateOptions(
64
+ value: NodeJS.WritableStream | RunSelfUpdateOptions
65
+ ): value is RunSelfUpdateOptions {
66
+ return (
67
+ typeof value === "object" &&
68
+ value !== null &&
69
+ ("output" in value || "spawn" in value)
70
+ );
71
+ }
72
+
73
+ async function forwardSpawnOutput(
74
+ stream: AsyncIterable<SpawnOutputChunk> | undefined,
75
+ output: NodeJS.WritableStream
76
+ ) {
77
+ if (!stream) {
78
+ return;
79
+ }
80
+
81
+ for await (const chunk of stream) {
82
+ output.write(normalizeSpawnChunk(chunk));
83
+ }
84
+ }
85
+
86
+ function normalizeSpawnChunk(
87
+ chunk: SpawnOutputChunk
88
+ ): string | Uint8Array<ArrayBufferLike> {
89
+ if (typeof chunk === "string" || chunk instanceof Uint8Array) {
90
+ return chunk;
91
+ }
92
+
93
+ if (chunk instanceof ArrayBuffer) {
94
+ return new Uint8Array(chunk);
95
+ }
96
+
97
+ return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
98
+ }
99
+
100
+ export async function runSelfUpdate(
101
+ outputOrOptions?: NodeJS.WritableStream | RunSelfUpdateOptions
102
+ ) {
103
+ const options =
104
+ outputOrOptions === undefined
105
+ ? {}
106
+ : isRunSelfUpdateOptions(outputOrOptions)
107
+ ? outputOrOptions
108
+ : { output: outputOrOptions };
109
+ const output = options.output ?? process.stdout;
110
+ const spawn = options.spawn ?? Bun.spawn;
57
111
  const command = buildSelfUpdateCommand();
58
112
  output.write(`Updating nolo with: ${command.join(" ")}\n`);
59
113
 
60
- const proc = Bun.spawn({
114
+ const useCustomSink = options.output !== undefined;
115
+ const proc = spawn({
61
116
  cmd: command,
62
117
  stdin: "inherit",
63
- stdout: "inherit",
64
- stderr: "inherit",
118
+ stdout: useCustomSink ? "pipe" : "inherit",
119
+ stderr: useCustomSink ? "pipe" : "inherit",
65
120
  env: process.env,
66
121
  });
67
122
 
68
- return await proc.exited;
123
+ if (!useCustomSink) {
124
+ return await proc.exited;
125
+ }
126
+
127
+ const [exitCode] = await Promise.all([
128
+ proc.exited,
129
+ forwardSpawnOutput(proc.stdout, output),
130
+ forwardSpawnOutput(proc.stderr, output),
131
+ ]);
132
+
133
+ return exitCode;
69
134
  }