nolo-cli 0.1.32 → 0.1.33

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 (230) hide show
  1. package/README.md +0 -6
  2. package/agent-runtime/agentRecordConfig.ts +2 -0
  3. package/agent-runtime/hostAdapter.ts +1 -0
  4. package/agent-runtime/index.ts +0 -21
  5. package/agent-runtime/localToolPolicy.ts +6 -13
  6. package/agent-runtime/localWorkspaceTools.ts +30 -333
  7. package/agent-runtime/platformChatProvider.ts +1 -4
  8. package/agent-runtime/runtimeToolPolicy.ts +47 -32
  9. package/agentAliases.ts +14 -83
  10. package/agentCommandSupport.ts +3 -6
  11. package/agentInternalCommandEntries.ts +2 -13
  12. package/agentListCommands.ts +19 -28
  13. package/agentMachineCommands.ts +40 -336
  14. package/agentPullCommand.ts +36 -56
  15. package/agentRecordCommands.ts +3 -5
  16. package/agentRecordHelpers.ts +22 -189
  17. package/agentRunCommand.ts +438 -107
  18. package/agentRuntimeLocal.ts +0 -10
  19. package/ai/agent/cliExecutor.ts +19 -323
  20. package/authCommands.ts +2 -36
  21. package/cli/agentAliases.ts +14 -83
  22. package/cli/agentCommandSupport.ts +3 -6
  23. package/cli/agentInternalCommandEntries.ts +2 -13
  24. package/cli/agentListCommands.ts +19 -28
  25. package/cli/agentMachineCommands.ts +40 -336
  26. package/cli/agentPullCommand.ts +36 -56
  27. package/cli/agentRecordCommands.ts +3 -5
  28. package/cli/agentRecordHelpers.ts +22 -189
  29. package/cli/agentRunCommand.ts +438 -107
  30. package/cli/agentRuntimeLocal.ts +0 -10
  31. package/cli/authCommands.ts +2 -36
  32. package/cli/cliEnvHelpers.ts +21 -162
  33. package/cli/client/agentRun.ts +853 -57
  34. package/cli/client/localRuntimeAdapter.ts +22 -159
  35. package/cli/client/streamingOutput.ts +3 -1
  36. package/cli/client/taskRunPrompt.ts +24 -7
  37. package/cli/commandRegistry.ts +2 -3
  38. package/cli/connectorRunArtifact.ts +10 -34
  39. package/cli/connectorWebSocketTarget.ts +6 -10
  40. package/cli/internalCommandEntries.ts +0 -8
  41. package/cli/localRuntimeDb.ts +2 -39
  42. package/cli/machineCommands.ts +189 -190
  43. package/cli/machineDaemonCommands.ts +165 -9
  44. package/cli/machineHeartbeatCommands.ts +2 -1
  45. package/cli/machineStatusCommands.ts +13 -5
  46. package/cli/machineWatchCommands.ts +1 -1
  47. package/cli/machineWsRunDispatch.ts +14 -48
  48. package/cli/machineWsSession.ts +0 -2
  49. package/cli/offlineMarxistsAgentCommand.ts +314 -0
  50. package/cli/runtimeDoctorCommands.ts +69 -18
  51. package/cli/scriptCommandEntries.ts +45 -5
  52. package/cli/tableCommands.ts +133 -33
  53. package/cli/taskRunCommand.ts +211 -16
  54. package/cli/tui/session.ts +372 -43
  55. package/cli/workflowInternalCommandEntries.ts +0 -20
  56. package/cliEnvHelpers.ts +21 -162
  57. package/client/agentConfigResolver.test.ts +2 -0
  58. package/client/agentRun.test.ts +12 -208
  59. package/client/agentRun.ts +853 -57
  60. package/client/localRuntimeAdapter.test.ts +9 -168
  61. package/client/localRuntimeAdapter.ts +22 -159
  62. package/client/localToolPolicy.test.ts +4 -28
  63. package/client/streamingOutput.ts +3 -1
  64. package/client/taskRunPrompt.ts +24 -7
  65. package/commandRegistry.ts +2 -3
  66. package/connectorRunArtifact.ts +10 -34
  67. package/connectorWebSocketTarget.ts +6 -10
  68. package/database/server/db.ts +34 -15
  69. package/internalCommandEntries.ts +0 -8
  70. package/localRuntimeDb.ts +2 -39
  71. package/machineCommands.ts +189 -190
  72. package/machineDaemonCommands.ts +165 -9
  73. package/machineHeartbeatCommands.ts +2 -1
  74. package/machineStatusCommands.ts +13 -5
  75. package/machineWatchCommands.ts +1 -1
  76. package/machineWsRunDispatch.ts +14 -48
  77. package/machineWsSession.ts +0 -2
  78. package/offlineMarxistsAgentCommand.ts +314 -0
  79. package/package.json +49 -193
  80. package/runtimeDoctorCommands.ts +69 -18
  81. package/scriptCommandEntries.ts +45 -5
  82. package/tableCommands.ts +133 -33
  83. package/taskRunCommand.ts +211 -16
  84. package/tui/session.ts +372 -43
  85. package/workflowInternalCommandEntries.ts +0 -20
  86. package/agent-runtime/dialogStatus.ts +0 -139
  87. package/agent-runtime/hostRuntimeConfig.ts +0 -86
  88. package/agentCreateCustomCommands.ts +0 -278
  89. package/agentDialogsCommands.ts +0 -227
  90. package/agentDoctorCommands.ts +0 -203
  91. package/agentRunLocalWorkspace.ts +0 -182
  92. package/agentRunParsing.ts +0 -298
  93. package/agentSuperviseCommands.ts +0 -8
  94. package/agentUnpublishCommands.ts +0 -113
  95. package/ai/agent/agentRunDialogLineage.ts +0 -56
  96. package/ai/policy/types.ts +0 -64
  97. package/ai/skills/skillSummaryMarker.ts +0 -26
  98. package/app/types.ts +0 -358
  99. package/build.ts +0 -16
  100. package/buildPublish.ts +0 -138
  101. package/buildPublishArtifactAssembly.ts +0 -724
  102. package/buildPublishImportAnalysis.ts +0 -242
  103. package/chat/messages/fetchMessages.ts +0 -84
  104. package/chat/messages/types.ts +0 -185
  105. package/cli/agentCreateCustomCommands.ts +0 -278
  106. package/cli/agentDialogsCommands.ts +0 -227
  107. package/cli/agentDoctorCommands.ts +0 -203
  108. package/cli/agentRunLocalWorkspace.ts +0 -182
  109. package/cli/agentRunParsing.ts +0 -298
  110. package/cli/agentSuperviseCommands.ts +0 -8
  111. package/cli/agentUnpublishCommands.ts +0 -113
  112. package/cli/buildPublish.ts +0 -138
  113. package/cli/buildPublishArtifactAssembly.ts +0 -724
  114. package/cli/buildPublishImportAnalysis.ts +0 -242
  115. package/cli/cliIoTypes.ts +0 -3
  116. package/cli/cliRuntimeDoctorReport.ts +0 -193
  117. package/cli/client/agentRunInput.ts +0 -10
  118. package/cli/client/agentRunLocalExecution.ts +0 -124
  119. package/cli/client/agentRunLocalExecutor.ts +0 -179
  120. package/cli/client/agentRunRuntimeSelection.ts +0 -145
  121. package/cli/client/agentRunServerTransport.ts +0 -397
  122. package/cli/client/agentRunTaskRunRecorder.ts +0 -176
  123. package/cli/customCodingAgentFlow.ts +0 -208
  124. package/cli/customCodingAgentRecord.ts +0 -179
  125. package/cli/dialogDataAccess.ts +0 -186
  126. package/cli/dialogInternalCommandEntries.ts +0 -11
  127. package/cli/dialogReadCommands.ts +0 -180
  128. package/cli/dialogReadHelpers.ts +0 -226
  129. package/cli/dialogStatusCommands.ts +0 -123
  130. package/cli/docInternalCommandEntries.ts +0 -20
  131. package/cli/hybridReadHelpers.ts +0 -109
  132. package/cli/machineCommandPrompts.ts +0 -79
  133. package/cli/machineCommandResolution.ts +0 -43
  134. package/cli/noloReadBridgePrompt.ts +0 -16
  135. package/cli/pageCreateCommands.ts +0 -219
  136. package/cli/pageDeleteCommands.ts +0 -167
  137. package/cli/pageReadCommands.ts +0 -151
  138. package/cli/pageUpdateCommands.ts +0 -249
  139. package/cli/pageWriteHelpers.ts +0 -209
  140. package/cli/publishPackage.ts +0 -36
  141. package/cli/runtimeInternalCommandEntries.ts +0 -15
  142. package/cli/runtimePassthroughCommands.ts +0 -22
  143. package/cli/scriptPassthroughHelpers.ts +0 -28
  144. package/cli/secretScanHelpers.ts +0 -31
  145. package/cli/spaceAttachCommands.ts +0 -134
  146. package/cli/spaceCategoryCommands.ts +0 -263
  147. package/cli/spaceCommands.ts +0 -295
  148. package/cli/spaceDeleteCommands.ts +0 -312
  149. package/cli/spaceInternalCommandEntries.ts +0 -21
  150. package/cli/spaceMutationHelpers.ts +0 -135
  151. package/cli/spaceReadCommands.ts +0 -323
  152. package/cli/tableDataOutput.ts +0 -41
  153. package/cli/tableDataSafety.ts +0 -26
  154. package/cli/tableMetaCommands.ts +0 -307
  155. package/cli/tableMutationCommands.ts +0 -266
  156. package/cli/tableQueryParsing.ts +0 -90
  157. package/cli/tableQueryRequest.ts +0 -53
  158. package/cli/taskRunActions.ts +0 -122
  159. package/cli/taskRunParsing.ts +0 -86
  160. package/cli/taskRunPromptShared.ts +0 -55
  161. package/cli/tui/tuiCatalog.ts +0 -22
  162. package/cli/tui/tuiCommandInterpreter.ts +0 -253
  163. package/cli/tui/tuiPresentation.ts +0 -146
  164. package/cliIoTypes.ts +0 -3
  165. package/cliRuntimeDoctorReport.ts +0 -193
  166. package/client/agentRunInput.ts +0 -10
  167. package/client/agentRunLocalExecution.test.ts +0 -123
  168. package/client/agentRunLocalExecution.ts +0 -124
  169. package/client/agentRunLocalExecutor.ts +0 -179
  170. package/client/agentRunRuntimeSelection.test.ts +0 -159
  171. package/client/agentRunRuntimeSelection.ts +0 -145
  172. package/client/agentRunServerTransport.ts +0 -397
  173. package/client/agentRunTaskRunRecorder.ts +0 -176
  174. package/connector-experimental/daemonLauncher.ts +0 -223
  175. package/create/space/spaceKeys.ts +0 -65
  176. package/create/types.ts +0 -21
  177. package/customCodingAgentFlow.ts +0 -208
  178. package/customCodingAgentRecord.ts +0 -179
  179. package/database/keys.ts +0 -765
  180. package/database/server/authorityStoreTypes.ts +0 -31
  181. package/database/server/legacyServerDb.ts +0 -58
  182. package/database/server/levelAuthorityStore.ts +0 -75
  183. package/database/server/memoryAuthorityStore.ts +0 -133
  184. package/database/server/serverStoreFactory.ts +0 -124
  185. package/database/tombstones.ts +0 -52
  186. package/database/utils/ulid.ts +0 -1
  187. package/dialogDataAccess.ts +0 -186
  188. package/dialogInternalCommandEntries.ts +0 -11
  189. package/dialogReadCommands.ts +0 -180
  190. package/dialogReadHelpers.ts +0 -226
  191. package/dialogStatusCommands.ts +0 -123
  192. package/docInternalCommandEntries.ts +0 -20
  193. package/hybridReadHelpers.ts +0 -109
  194. package/machineCommandPrompts.ts +0 -79
  195. package/machineCommandResolution.ts +0 -43
  196. package/noloReadBridgePrompt.ts +0 -16
  197. package/pageCreateCommands.ts +0 -219
  198. package/pageDeleteCommands.ts +0 -167
  199. package/pageReadCommands.ts +0 -151
  200. package/pageUpdateCommands.ts +0 -249
  201. package/pageWriteHelpers.ts +0 -209
  202. package/publishPackage.ts +0 -36
  203. package/runtimeInternalCommandEntries.ts +0 -15
  204. package/runtimePassthroughCommands.ts +0 -22
  205. package/scriptPassthroughHelpers.ts +0 -28
  206. package/secretScanHelpers.ts +0 -31
  207. package/share/helpers.ts +0 -302
  208. package/share/keys.ts +0 -243
  209. package/share/types.ts +0 -100
  210. package/spaceAttachCommands.ts +0 -134
  211. package/spaceCategoryCommands.ts +0 -263
  212. package/spaceCommands.ts +0 -295
  213. package/spaceDeleteCommands.ts +0 -312
  214. package/spaceInternalCommandEntries.ts +0 -21
  215. package/spaceMutationHelpers.ts +0 -135
  216. package/spaceReadCommands.ts +0 -323
  217. package/tableDataOutput.ts +0 -41
  218. package/tableDataSafety.ts +0 -26
  219. package/tableMetaCommands.ts +0 -307
  220. package/tableMutationCommands.ts +0 -266
  221. package/tableQueryParsing.ts +0 -90
  222. package/tableQueryRequest.ts +0 -53
  223. package/taskRunActions.ts +0 -122
  224. package/taskRunParsing.ts +0 -86
  225. package/taskRunPromptShared.ts +0 -55
  226. package/tui/tuiCatalog.ts +0 -22
  227. package/tui/tuiCommandInterpreter.test.ts +0 -169
  228. package/tui/tuiCommandInterpreter.ts +0 -253
  229. package/tui/tuiPresentation.test.ts +0 -154
  230. package/tui/tuiPresentation.ts +0 -146
@@ -1,10 +1,14 @@
1
1
  import type { MachineHeartbeat } from "../connector-experimental/protocol";
2
- import { NOLO_CLUSTER_SERVERS, normalizeKnownServerOrigin } from "../database/config";
3
2
  import { detectMachineInfo } from "../connector-experimental/machineInfo";
3
+ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
4
4
  import {
5
5
  type HeartbeatLoopOptions,
6
6
  runHeartbeatLoop as defaultRunHeartbeatLoop,
7
7
  } from "../connector-experimental/heartbeatLoop";
8
+ import {
9
+ buildMachinePermissionPromptBlock,
10
+ resolveMachineRunPermissionPolicy,
11
+ } from "../ai/agent/machineRunPermissions";
8
12
  import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
9
13
  import {
10
14
  checkConnectorWorkspaceLinks,
@@ -21,24 +25,14 @@ import {
21
25
  runMachineWsSession,
22
26
  type ConnectorWebSocketOptions,
23
27
  } from "./machineWsSession";
24
- import {
25
- buildConnectorCliPrompt,
26
- } from "./machineCommandPrompts";
27
- import {
28
- hasFlag,
29
- readOption,
30
- resolveConnectAuthToken,
31
- resolveConnectServerUrl,
32
- resolveConnectorReconnectDelayMs,
33
- writeAuthMissing,
34
- type EnvLike,
35
- type OutputLike,
36
- } from "./machineCommandResolution";
37
28
  export {
38
29
  formatMachineStatus,
39
30
  runMachineStatusCommand,
40
31
  type MachineSummary,
41
32
  } from "./machineStatusCommands";
33
+
34
+ type EnvLike = Record<string, string | undefined>;
35
+ type OutputLike = { write(chunk: string): unknown };
42
36
 
43
37
  type MachineCommandDeps = {
44
38
  env?: EnvLike;
@@ -61,8 +55,50 @@ type MachineCommandDeps = {
61
55
  }) => { pid?: number };
62
56
  };
63
57
 
58
+ function resolveServerUrl(env: EnvLike) {
59
+ return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
60
+ }
61
+
62
+ function resolveAuthToken(env: EnvLike) {
63
+ return env.AUTH_TOKEN || env.AUTH || "";
64
+ }
65
+
66
+ function writeAuthMissing(output: OutputLike) {
67
+ output.write(
68
+ "[nolo] Machine commands require an auth token. Run `nolo login` or set AUTH_TOKEN.\n"
69
+ );
70
+ }
71
+
72
+ function hasFlag(args: string[], flag: string) {
73
+ return args.includes(flag);
74
+ }
75
+
76
+ function readOption(args: string[], flag: string) {
77
+ for (let i = 0; i < args.length; i++) {
78
+ const arg = args[i];
79
+ if (arg === flag) {
80
+ const next = args[i + 1];
81
+ if (typeof next === "string" && !next.startsWith("-")) return next;
82
+ return "";
83
+ }
84
+ if (arg.startsWith(flag + "=")) {
85
+ return arg.slice(flag.length + 1);
86
+ }
87
+ }
88
+ return "";
89
+ }
90
+
91
+ function resolveConnectServerUrl(args: string[], env: EnvLike) {
92
+ const fromFlag = readOption(args, "--server-url") || readOption(args, "--server");
93
+ const raw = fromFlag || env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL;
94
+ return String(raw).replace(/\/+$|\s+$/g, "").replace(/\/+$/g, "").replace(/\s+$/g, "");
95
+ }
96
+
97
+ function resolveConnectAuthToken(args: string[], env: EnvLike) {
98
+ return readOption(args, "--api-key") || readOption(args, "--token") || resolveAuthToken(env);
99
+ }
100
+
64
101
  const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
65
- const MACHINE_TOKEN_PREFIX = "sk_machine_";
66
102
 
67
103
  async function sleepWithAbort(ms: number, sleep: (ms: number) => Promise<void>, signal?: AbortSignal) {
68
104
  if (!signal) {
@@ -83,80 +119,6 @@ function resolveConnectorReconnectDelayMs(env: EnvLike) {
83
119
  return Number.isFinite(raw) && raw >= 0 ? raw : 5_000;
84
120
  }
85
121
 
86
- function isMachineToken(token: string) {
87
- return token.trim().startsWith(MACHINE_TOKEN_PREFIX);
88
- }
89
-
90
- function resolveClusterConnectServerUrls(serverUrl: string, authToken: string) {
91
- const normalizedPrimary = normalizeKnownServerOrigin(serverUrl) ?? serverUrl.trim().replace(/\/+$/, "");
92
- if (isMachineToken(authToken)) return [normalizedPrimary];
93
- const normalizedCluster = NOLO_CLUSTER_SERVERS
94
- .map((baseUrl) => normalizeKnownServerOrigin(baseUrl) ?? baseUrl)
95
- .filter((value, index, list) => value && list.indexOf(value) === index);
96
- if (!normalizedCluster.includes(normalizedPrimary)) return [normalizedPrimary];
97
- return [normalizedPrimary, ...normalizedCluster.filter((baseUrl) => baseUrl !== normalizedPrimary)];
98
- }
99
-
100
- function linkAbortSignal(signal: AbortSignal | undefined, controller: AbortController) {
101
- if (!signal) return;
102
- if (signal.aborted) {
103
- controller.abort();
104
- return;
105
- }
106
- signal.addEventListener("abort", () => controller.abort(), { once: true });
107
- }
108
-
109
- async function sendMachineHeartbeat(args: {
110
- serverUrl: string;
111
- authToken: string;
112
- fetchImpl: typeof fetch;
113
- machine: MachineHeartbeat;
114
- }) {
115
- const res = await args.fetchImpl(`${args.serverUrl}/api/machines/heartbeat`, {
116
- method: "POST",
117
- headers: {
118
- Authorization: `Bearer ${args.authToken}`,
119
- "Content-Type": "application/json",
120
- },
121
- body: JSON.stringify(args.machine),
122
- });
123
-
124
- if (!res.ok) {
125
- const text = await res.text().catch(() => "");
126
- throw new Error(`HTTP ${res.status}\n${text}`);
127
- }
128
- }
129
-
130
- async function sendHeartbeatAcrossServers(args: {
131
- serverUrls: string[];
132
- authToken: string;
133
- fetchImpl: typeof fetch;
134
- machine: MachineHeartbeat;
135
- output: OutputLike;
136
- }) {
137
- let primaryError: Error | null = null;
138
- for (const [index, serverUrl] of args.serverUrls.entries()) {
139
- try {
140
- await sendMachineHeartbeat({
141
- serverUrl,
142
- authToken: args.authToken,
143
- fetchImpl: args.fetchImpl,
144
- machine: args.machine,
145
- });
146
- } catch (error) {
147
- const resolvedError = error instanceof Error ? error : new Error(String(error));
148
- if (index === 0) {
149
- primaryError = resolvedError;
150
- } else {
151
- args.output.write(
152
- `[nolo] secondary connector registration failed for ${serverUrl}: ${resolvedError.message}\n`
153
- );
154
- }
155
- }
156
- }
157
- if (primaryError) throw primaryError;
158
- }
159
-
160
122
  async function defaultExecuteCli(
161
123
  provider: string,
162
124
  prompt: string,
@@ -169,11 +131,75 @@ async function defaultExecuteCli(
169
131
  function detectLaunchableMachineInfo() {
170
132
  return detectMachineInfo({ probeLaunchable: true });
171
133
  }
134
+
135
+ function inferTaskRunRole(agentKey: string, agentConfig: any) {
136
+ const haystack = [
137
+ agentKey,
138
+ agentConfig?.name,
139
+ agentConfig?.prompt,
140
+ ].filter(Boolean).join("\n").toLowerCase();
141
+ if (haystack.includes("frontend") || haystack.includes("前端")) return "frontend";
142
+ if (haystack.includes("fullstack") || haystack.includes("全栈")) return "fullstack";
143
+ if (haystack.includes("review") || haystack.includes("审查")) return "reviewer";
144
+ if (haystack.includes("project-manager") || haystack.includes("项目经理")) return "pm";
145
+ return "codex";
146
+ }
147
+
148
+ function buildTaskRunBridgePrompt(args: {
149
+ agentKey: string;
150
+ agentConfig: any;
151
+ runtimeContext: any;
152
+ }) {
153
+ const taskRun = args.runtimeContext?.taskRun;
154
+ const rowDbKey = typeof taskRun?.rowDbKey === "string" ? taskRun.rowDbKey.trim() : "";
155
+ if (!rowDbKey) return "";
156
+ const workItemId = typeof taskRun?.workItemId === "string" ? taskRun.workItemId.trim() : "";
157
+ const role = inferTaskRunRole(args.agentKey, args.agentConfig);
158
+ return [
159
+ "--- Nolo task-run tool bridge ---",
160
+ "This CLI runtime does not receive server-side function tools directly.",
161
+ "Use the local task-run bridge command for durable task state writes. Do not edit meta.taskRun with table or database commands.",
162
+ "Run commands from the repository root. The runner already provides server URL and auth in the environment.",
163
+ "Release boundary: after review passes, AI/reviewer/Codex may advance alpha for verification. Do not merge, push, or release main/release unless the human owner explicitly authorizes it in the current task context.",
164
+ "Cost boundary: read taskRun.control from context. Prefer low-cost/free-window models for routine work, and reserve Codex for supervision, quality gates, complex unblocking, and main/release decisions.",
165
+ "Handoff context: start from read-context, then inspect existing refs such as workItems.dialogIds, workItems.outcome, review.dialogId, activityRefs, agentRuns.dialogId/resultSummary, and artifactRefs. Use dialog read/search for exact evidence; do not infer completion from handoff text alone.",
166
+ `Read context: bun packages/cli/index.ts task-run read-context --row-dbkey ${JSON.stringify(rowDbKey)}`,
167
+ ...(workItemId
168
+ ? [
169
+ `Claim work item: bun packages/cli/index.ts task-run claim-work-item --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --agent-key ${JSON.stringify(args.agentKey)} --role ${role}`,
170
+ `Set blocker: bun packages/cli/index.ts task-run set-blocker --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --layer tool --code <CODE> --message <MESSAGE>`,
171
+ `Clear resolved blocker: bun packages/cli/index.ts task-run clear-blocker --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --blocker-id <BLOCKER_ID>`,
172
+ `Submit outcome: bun packages/cli/index.ts task-run submit-outcome --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --summary <SUMMARY>`,
173
+ `Request review: bun packages/cli/index.ts task-run request-review --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --reviewer-agent-key <AGENT_KEY> --message <MESSAGE>`,
174
+ `Record review: bun packages/cli/index.ts task-run record-review --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --review-status <passed|needs_changes|blocked> --reviewer-agent-key ${JSON.stringify(args.agentKey)} --findings <FINDINGS>`,
175
+ `Close work item: bun packages/cli/index.ts task-run close-work-item --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --summary <SUMMARY>`,
176
+ ]
177
+ : []),
178
+ "If a bridge command fails, report the exact command and error.",
179
+ ].join("\n");
180
+ }
181
+
182
+ function buildConnectorCliPrompt(agentConfig: any, userInput: string, bridgeArgs?: {
183
+ agentKey: string;
184
+ runtimeContext: any;
185
+ }, permissionPolicy?: ReturnType<typeof resolveMachineRunPermissionPolicy>) {
186
+ const policy = permissionPolicy ?? resolveMachineRunPermissionPolicy(agentConfig);
187
+ return [
188
+ typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
189
+ bridgeArgs ? buildTaskRunBridgePrompt({
190
+ agentKey: bridgeArgs.agentKey,
191
+ agentConfig,
192
+ runtimeContext: bridgeArgs.runtimeContext,
193
+ }) : "",
194
+ buildMachinePermissionPromptBlock(policy),
195
+ `--- User task ---\n${userInput}`,
196
+ ].filter(Boolean).join("\n\n");
197
+ }
172
198
 
173
199
  export async function runMachineConnectCommand(
174
- args: string[],
175
- deps: MachineCommandDeps = {}
176
- ) {
200
+ args: string[],
201
+ deps: MachineCommandDeps = {}
202
+ ) {
177
203
  const env = deps.env ?? process.env;
178
204
  const output = deps.output ?? process.stdout;
179
205
  const authToken = resolveConnectAuthToken(args, env);
@@ -181,31 +207,29 @@ export async function runMachineConnectCommand(
181
207
  writeAuthMissing(output);
182
208
  return 1;
183
209
  }
184
-
210
+
185
211
  const serverUrl = resolveConnectServerUrl(args, env);
186
- const serverUrls = resolveClusterConnectServerUrls(serverUrl, authToken);
187
212
  const fetchImpl = deps.fetchImpl ?? fetch;
188
213
  const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
189
214
  const sendHeartbeat = async () => {
190
- await sendHeartbeatAcrossServers({
191
- serverUrls,
192
- authToken,
193
- fetchImpl,
194
- machine,
195
- output,
196
- });
197
- };
215
+ const res = await fetchImpl(`${serverUrl}/api/machines/heartbeat`, {
216
+ method: "POST",
217
+ headers: {
218
+ Authorization: `Bearer ${authToken}`,
219
+ "Content-Type": "application/json",
220
+ },
221
+ body: JSON.stringify(machine),
222
+ });
223
+
224
+ if (!res.ok) {
225
+ const text = await res.text().catch(() => "");
226
+ throw new Error(`HTTP ${res.status}\n${text}`);
227
+ }
228
+ };
198
229
 
199
230
  if (hasFlag(args, "--daemon") || hasFlag(args, "--background")) {
200
231
  return runMachineDaemonCommand({
201
- env: {
202
- ...env,
203
- NOLO_SERVER: serverUrl,
204
- NOLO_SERVER_URL: serverUrl,
205
- BASE_URL: serverUrl,
206
- AUTH_TOKEN: authToken,
207
- NOLO_MACHINE_API_KEY: authToken,
208
- },
232
+ env,
209
233
  output,
210
234
  cliEntrypointPath: deps.cliEntrypointPath,
211
235
  validateWorkspaceLinks: deps.validateWorkspaceLinks,
@@ -221,88 +245,63 @@ export async function runMachineConnectCommand(
221
245
  const maxAttempts = deps.maxConnectorAttempts ?? Infinity;
222
246
  const sleep = deps.sleep ?? defaultSleep;
223
247
  const reconnectDelayMs = resolveConnectorReconnectDelayMs(env);
224
- const runServerSession = async (targetServerUrl: string, signal?: AbortSignal) => {
225
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
226
- if (signal?.aborted) {
227
- return { serverUrl: targetServerUrl, exitCode: 0 };
228
- }
229
- const runtimeServerUrl =
230
- env.NOLO_CONNECT_RUNTIME_SERVER_URL ||
231
- env.NOLO_RUNTIME_SERVER_URL ||
232
- targetServerUrl;
233
- const sessionResult = await runMachineWsSession({
234
- env,
235
- output,
236
- signal,
237
- machine,
238
- sendHeartbeat: () =>
239
- sendMachineHeartbeat({
240
- serverUrl: targetServerUrl,
241
- authToken,
242
- fetchImpl,
243
- machine,
244
- }),
245
- runHeartbeatLoop: deps.runHeartbeatLoop ?? defaultRunHeartbeatLoop,
246
- connectWebSocket: deps.connectWebSocket ?? defaultConnectWebSocket,
247
- serverUrl: targetServerUrl,
248
- authToken,
249
- resolveConnectorWebSocketTarget: (options) =>
250
- resolveConnectorWebSocketTarget({
251
- ...options,
252
- fetchImpl,
253
- }),
254
- onMessage: (message, send) =>
255
- handleConnectorRunMessage(
256
- message,
257
- send,
258
- deps.executeCli ?? defaultExecuteCli,
259
- {
260
- ...env,
261
- NOLO_SERVER: runtimeServerUrl,
262
- NOLO_SERVER_URL: runtimeServerUrl,
263
- BASE_URL: runtimeServerUrl,
264
- AUTH_TOKEN: authToken,
265
- NOLO_MACHINE_API_KEY: authToken,
266
- },
267
- fetchImpl,
268
- {
269
- buildConnectorCliPrompt,
270
- }
271
- ),
272
- });
273
- const exitCode = sessionResult.exitCode;
274
- if (exitCode === 2) return { serverUrl: targetServerUrl, exitCode: 2 };
275
- if (signal?.aborted) {
276
- return { serverUrl: targetServerUrl, exitCode: 0 };
277
- }
278
- if (attempt >= maxAttempts) return { serverUrl: targetServerUrl, exitCode };
279
- const retryDelayMs = sessionResult.retryAfterMs ?? reconnectDelayMs;
280
- const reconnectContext =
281
- sessionResult.reconnectReason === "core_draining" ? " during core draining" : "";
282
- output.write(
283
- `[nolo] Connector websocket disconnected${reconnectContext}. Reconnecting in ${retryDelayMs}ms.\n`
284
- );
285
- await sleepWithAbort(retryDelayMs, sleep, signal);
248
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
249
+ if (deps.signal?.aborted) {
250
+ return 0;
286
251
  }
287
- return { serverUrl: targetServerUrl, exitCode: 0 };
288
- };
289
-
290
- const sessionAbort = new AbortController();
291
- linkAbortSignal(deps.signal, sessionAbort);
292
-
293
- const sessionPromises = serverUrls.map(async (targetServerUrl, index) => {
294
- const result = await runServerSession(targetServerUrl, sessionAbort.signal);
295
- if (index > 0 && result.exitCode !== 0 && !sessionAbort.signal.aborted) {
296
- output.write(
297
- `[nolo] secondary connector registration failed for ${targetServerUrl}: exit ${result.exitCode}\n`
298
- );
252
+ const runtimeServerUrl =
253
+ env.NOLO_CONNECT_RUNTIME_SERVER_URL ||
254
+ env.NOLO_RUNTIME_SERVER_URL ||
255
+ serverUrl;
256
+ const sessionResult = await runMachineWsSession({
257
+ env,
258
+ output,
259
+ signal: deps.signal,
260
+ machine,
261
+ sendHeartbeat,
262
+ runHeartbeatLoop: deps.runHeartbeatLoop ?? defaultRunHeartbeatLoop,
263
+ connectWebSocket: deps.connectWebSocket ?? defaultConnectWebSocket,
264
+ serverUrl,
265
+ authToken,
266
+ resolveConnectorWebSocketTarget: (options) =>
267
+ resolveConnectorWebSocketTarget({
268
+ ...options,
269
+ fetchImpl,
270
+ }),
271
+ onMessage: (message, send) =>
272
+ handleConnectorRunMessage(
273
+ message,
274
+ send,
275
+ deps.executeCli ?? defaultExecuteCli,
276
+ {
277
+ ...env,
278
+ NOLO_SERVER: runtimeServerUrl,
279
+ NOLO_SERVER_URL: runtimeServerUrl,
280
+ BASE_URL: runtimeServerUrl,
281
+ AUTH_TOKEN: authToken,
282
+ NOLO_MACHINE_API_KEY: authToken,
283
+ },
284
+ fetchImpl,
285
+ {
286
+ buildConnectorCliPrompt,
287
+ }
288
+ ),
289
+ });
290
+ const exitCode = sessionResult.exitCode;
291
+ if (exitCode === 2) return 1;
292
+ if (deps.signal?.aborted) {
293
+ return 0;
299
294
  }
300
- return result;
301
- });
302
- const results = await Promise.all(sessionPromises);
303
- if (results.some((result) => result.exitCode === 0)) return 0;
304
- if (results.some((result) => result.exitCode === 2)) return 1;
305
- return results[0]?.exitCode ?? 1;
295
+ if (attempt >= maxAttempts) return exitCode;
296
+ const retryDelayMs = sessionResult.retryAfterMs ?? reconnectDelayMs;
297
+ const reconnectContext =
298
+ sessionResult.reconnectReason === "core_draining" ? " during core draining" : "";
299
+ output.write(
300
+ `[nolo] Connector websocket disconnected${reconnectContext}. Reconnecting in ${retryDelayMs}ms.\n`
301
+ );
302
+ await sleepWithAbort(retryDelayMs, sleep, deps.signal);
303
+ }
304
+ return 0;
306
305
  }
307
306
 
308
307
  if (hasFlag(args, "--watch")) {
@@ -1,11 +1,153 @@
1
- import {
2
- checkConnectorWorkspaceLinks,
3
- runConnectorDaemon,
4
- type ConnectorDaemonDeps,
5
- } from "../connector-experimental/daemonLauncher";
6
- import type { OutputLike } from "./cliIoTypes";
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, openSync, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
7
5
 
8
6
  type EnvLike = Record<string, string | undefined>;
7
+ type OutputLike = { write(chunk: string): unknown };
8
+
9
+ type PackageJson = {
10
+ name?: string;
11
+ workspaces?: string[] | { packages?: string[] };
12
+ };
13
+
14
+ const CONNECTOR_RUNTIME_WORKSPACE_PACKAGES = new Set([
15
+ "connector-experimental",
16
+ "ai",
17
+ ]);
18
+
19
+ function readJsonFile<T>(path: string): T | null {
20
+ try {
21
+ return JSON.parse(readFileSync(path, "utf8")) as T;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function getWorkspacePatterns(packageJson: PackageJson) {
28
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
29
+ return packageJson.workspaces?.packages ?? [];
30
+ }
31
+
32
+ function normalizePath(path: string) {
33
+ const resolved = resolve(path);
34
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
35
+ }
36
+
37
+ function isInsideOrSame(path: string, parent: string) {
38
+ const relativePath = relative(parent, path);
39
+ return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
40
+ }
41
+
42
+ function findWorkspaceRoot(startPath: string) {
43
+ let current = resolve(startPath);
44
+ while (true) {
45
+ const packageJsonPath = join(current, "package.json");
46
+ if (existsSync(packageJsonPath)) {
47
+ const packageJson = readJsonFile<PackageJson>(packageJsonPath);
48
+ if (packageJson && getWorkspacePatterns(packageJson).length > 0) {
49
+ return current;
50
+ }
51
+ }
52
+ const parent = dirname(current);
53
+ if (parent === current) return null;
54
+ current = parent;
55
+ }
56
+ }
57
+
58
+ function validateConnectorWorkspaceLinks(cwd: string) {
59
+ const repoRoot = findWorkspaceRoot(cwd);
60
+ if (!repoRoot) return [];
61
+
62
+ const rootPackageJson = readJsonFile<PackageJson>(join(repoRoot, "package.json"));
63
+ if (!rootPackageJson) return [];
64
+
65
+ const normalizedRepoRoot = normalizePath(realpathSync(repoRoot));
66
+ const errors: string[] = [];
67
+
68
+ for (const pattern of getWorkspacePatterns(rootPackageJson)) {
69
+ if (!pattern.endsWith("/*")) continue;
70
+ const workspaceParent = join(repoRoot, pattern.slice(0, -2));
71
+ if (!existsSync(workspaceParent)) continue;
72
+
73
+ for (const entry of Array.from(new Bun.Glob("*/package.json").scanSync({ cwd: workspaceParent }))) {
74
+ const packageDir = join(workspaceParent, dirname(entry));
75
+ const packageJson = readJsonFile<PackageJson>(join(workspaceParent, entry));
76
+ if (!packageJson?.name || !CONNECTOR_RUNTIME_WORKSPACE_PACKAGES.has(packageJson.name)) {
77
+ continue;
78
+ }
79
+
80
+ const linkPath = join(repoRoot, "node_modules", ...packageJson.name.split("/"));
81
+ if (!existsSync(linkPath)) {
82
+ errors.push(`${linkPath} is missing. Run bun install in ${repoRoot}.`);
83
+ continue;
84
+ }
85
+
86
+ const actualPath = normalizePath(realpathSync(linkPath));
87
+ const expectedPath = normalizePath(realpathSync(packageDir));
88
+ if (!isInsideOrSame(actualPath, normalizedRepoRoot)) {
89
+ errors.push(
90
+ `${linkPath} points outside this checkout: ${actualPath}. Run bun install in ${repoRoot}.`,
91
+ );
92
+ continue;
93
+ }
94
+ if (actualPath !== expectedPath) {
95
+ errors.push(
96
+ `${linkPath} points at ${actualPath}, expected ${expectedPath}. Run bun install in ${repoRoot}.`,
97
+ );
98
+ }
99
+ }
100
+ }
101
+
102
+ return errors;
103
+ }
104
+
105
+ export async function checkConnectorWorkspaceLinks(
106
+ cwd: string,
107
+ output: OutputLike,
108
+ validateWorkspaceLinks: (cwd: string) => string[] | Promise<string[]>,
109
+ ) {
110
+ const errors = await validateWorkspaceLinks(cwd);
111
+ if (errors.length === 0) return true;
112
+ output.write(
113
+ [
114
+ "[nolo] Connector workspace package links are unsafe.",
115
+ "The connector would import workspace packages from a different checkout, so machine capabilities and runtime behavior may be stale.",
116
+ ...errors,
117
+ "",
118
+ ].join("\n"),
119
+ );
120
+ return false;
121
+ }
122
+
123
+ function resolveDaemonLogPath(env: EnvLike) {
124
+ return env.NOLO_CONNECT_LOG || join(homedir(), ".nolo", "logs", "connector.log");
125
+ }
126
+
127
+ function buildDaemonCommand(cliEntrypointPath: string | undefined) {
128
+ return [process.execPath, cliEntrypointPath || import.meta.path, "connect", "--ws"];
129
+ }
130
+
131
+ function defaultSpawnDaemon(args: {
132
+ cmd: string[];
133
+ cwd: string;
134
+ env: EnvLike;
135
+ logPath: string;
136
+ }) {
137
+ mkdirSync(dirname(args.logPath), { recursive: true });
138
+ const out = openSync(args.logPath, "a");
139
+ const env = Object.fromEntries(
140
+ Object.entries(args.env).filter((entry): entry is [string, string] => typeof entry[1] === "string")
141
+ );
142
+ const proc = spawn(args.cmd[0], args.cmd.slice(1), {
143
+ cwd: args.cwd,
144
+ env,
145
+ detached: true,
146
+ stdio: ["ignore", out, out],
147
+ });
148
+ proc.unref();
149
+ return { pid: proc.pid };
150
+ }
9
151
 
10
152
  export type MachineDaemonCommandDeps = {
11
153
  env?: EnvLike;
@@ -23,7 +165,21 @@ export type MachineDaemonCommandDeps = {
23
165
  export async function runMachineDaemonCommand(
24
166
  deps: MachineDaemonCommandDeps = {}
25
167
  ) {
26
- return runConnectorDaemon(deps as ConnectorDaemonDeps);
27
- }
168
+ const env = deps.env ?? process.env;
169
+ const output = deps.output ?? process.stdout;
170
+ const validateWorkspaceLinks = deps.validateWorkspaceLinks ?? validateConnectorWorkspaceLinks;
28
171
 
29
- export { checkConnectorWorkspaceLinks };
172
+ if (!(await checkConnectorWorkspaceLinks(process.cwd(), output, validateWorkspaceLinks))) {
173
+ return 1;
174
+ }
175
+
176
+ const logPath = resolveDaemonLogPath(env);
177
+ const result = (deps.spawnDaemon ?? defaultSpawnDaemon)({
178
+ cmd: buildDaemonCommand(deps.cliEntrypointPath),
179
+ cwd: process.cwd(),
180
+ env,
181
+ logPath,
182
+ });
183
+ output.write(`Connector daemon started${result.pid ? ` pid=${result.pid}` : ""}. Log: ${logPath}\n`);
184
+ return 0;
185
+ }
@@ -1,5 +1,6 @@
1
1
  import type { MachineHeartbeat } from "../connector-experimental/protocol";
2
- import type { OutputLike } from "./cliIoTypes";
2
+
3
+ type OutputLike = { write(chunk: string): unknown };
3
4
 
4
5
  export type MachineHeartbeatConnectDeps = {
5
6
  output?: OutputLike;
@@ -1,7 +1,7 @@
1
- import type { OutputLike } from "./cliIoTypes";
2
- import { resolveAuthToken, resolveServerUrl } from "./cliEnvHelpers";
1
+ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
3
2
 
4
3
  type EnvLike = Record<string, string | undefined>;
4
+ type OutputLike = { write(chunk: string): unknown };
5
5
 
6
6
  export type MachineSummary = {
7
7
  machineId: string;
@@ -21,6 +21,14 @@ export type MachineStatusCommandDeps = {
21
21
  fetchImpl?: typeof fetch;
22
22
  };
23
23
 
24
+ function resolveServerUrl(env: EnvLike) {
25
+ return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
26
+ }
27
+
28
+ function resolveAuthToken(env: EnvLike) {
29
+ return env.AUTH_TOKEN || env.AUTH || "";
30
+ }
31
+
24
32
  function writeAuthMissing(output: OutputLike) {
25
33
  output.write(
26
34
  "[nolo] Machine commands require an auth token. Run `nolo login` or set AUTH_TOKEN.\n"
@@ -44,18 +52,18 @@ export function formatMachineStatus(machines: MachineSummary[]) {
44
52
  }
45
53
 
46
54
  export async function runMachineStatusCommand(
47
- args: string[],
55
+ _args: string[],
48
56
  deps: MachineStatusCommandDeps = {}
49
57
  ) {
50
58
  const env = deps.env ?? process.env;
51
59
  const output = deps.output ?? process.stdout;
52
- const authToken = resolveAuthToken(args, env);
60
+ const authToken = resolveAuthToken(env);
53
61
  if (!authToken) {
54
62
  writeAuthMissing(output);
55
63
  return 1;
56
64
  }
57
65
 
58
- const serverUrl = resolveServerUrl(args, env);
66
+ const serverUrl = resolveServerUrl(env);
59
67
  const fetchImpl = deps.fetchImpl ?? fetch;
60
68
  let res: Response;
61
69
  try {