nolo-cli 0.1.32 → 0.1.34

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 (234) hide show
  1. package/README.md +4 -10
  2. package/agent-runtime/agentRecordConfig.ts +10 -0
  3. package/agent-runtime/hostAdapter.ts +10 -1
  4. package/agent-runtime/index.ts +9 -20
  5. package/agent-runtime/localLoop.ts +49 -2
  6. package/agent-runtime/localToolPolicy.ts +76 -34
  7. package/agent-runtime/localWorkspaceTools.ts +30 -333
  8. package/agent-runtime/openAiCompatibleProvider.ts +7 -3
  9. package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
  10. package/agent-runtime/platformChatProvider.ts +47 -73
  11. package/agent-runtime/providerResolution.ts +244 -0
  12. package/agent-runtime/runtimeToolPolicy.ts +77 -29
  13. package/agent-runtime/shellCommandPolicy.ts +120 -0
  14. package/agentAliases.ts +14 -83
  15. package/agentCommandSupport.ts +3 -6
  16. package/agentInternalCommandEntries.ts +4 -13
  17. package/agentListCommands.ts +18 -27
  18. package/agentMachineCommands.ts +40 -336
  19. package/agentPullCommand.ts +30 -55
  20. package/agentRecordCommands.ts +84 -1
  21. package/agentRecordHelpers.ts +102 -186
  22. package/agentRunCommand.ts +438 -107
  23. package/agentRuntimeLocal.ts +2 -9
  24. package/ai/agent/cliExecutor.ts +19 -323
  25. package/ai/tools/toolNameAliases.ts +70 -0
  26. package/authCommands.ts +2 -36
  27. package/cli/agentAliases.ts +14 -83
  28. package/cli/agentCommandSupport.ts +3 -6
  29. package/cli/agentInternalCommandEntries.ts +4 -13
  30. package/cli/agentListCommands.ts +18 -27
  31. package/cli/agentMachineCommands.ts +40 -336
  32. package/cli/agentPullCommand.ts +30 -55
  33. package/cli/agentRecordCommands.ts +84 -1
  34. package/cli/agentRecordHelpers.ts +102 -186
  35. package/cli/agentRunCommand.ts +438 -107
  36. package/cli/agentRuntimeLocal.ts +2 -9
  37. package/cli/authCommands.ts +2 -36
  38. package/cli/cliCommandDispatch.ts +2 -2
  39. package/cli/cliEnvHelpers.ts +34 -107
  40. package/cli/client/agentRun.ts +854 -57
  41. package/cli/client/localRuntimeAdapter.ts +59 -159
  42. package/cli/client/streamingOutput.ts +3 -1
  43. package/cli/client/taskRunPrompt.ts +24 -7
  44. package/cli/commandRegistry.ts +4 -5
  45. package/cli/connectorRunArtifact.ts +10 -34
  46. package/cli/connectorWebSocketTarget.ts +6 -10
  47. package/cli/internalCommandEntries.ts +0 -8
  48. package/cli/localRuntimeDb.ts +2 -39
  49. package/cli/machineCommands.ts +189 -190
  50. package/cli/machineDaemonCommands.ts +165 -9
  51. package/cli/machineHeartbeatCommands.ts +2 -1
  52. package/cli/machineStatusCommands.ts +13 -5
  53. package/cli/machineWatchCommands.ts +1 -1
  54. package/cli/machineWsRunDispatch.ts +30 -44
  55. package/cli/machineWsSession.ts +0 -2
  56. package/cli/offlineMarxistsAgentCommand.ts +314 -0
  57. package/cli/runtimeDoctorCommands.ts +72 -15
  58. package/cli/scriptCommandEntries.ts +45 -5
  59. package/cli/tableCommands.ts +133 -33
  60. package/cli/taskRunCommand.ts +211 -16
  61. package/cli/tui/session.ts +372 -43
  62. package/cli/workflowInternalCommandEntries.ts +0 -20
  63. package/cliCommandDispatch.ts +2 -2
  64. package/cliEnvHelpers.ts +34 -107
  65. package/client/agentConfigResolver.test.ts +2 -0
  66. package/client/agentRun.test.ts +12 -208
  67. package/client/agentRun.ts +854 -57
  68. package/client/localProviderResolver.test.ts +26 -2
  69. package/client/localRuntimeAdapter.test.ts +296 -171
  70. package/client/localRuntimeAdapter.ts +59 -159
  71. package/client/localToolPolicy.test.ts +4 -28
  72. package/client/streamingOutput.ts +3 -1
  73. package/client/taskRunPrompt.ts +24 -7
  74. package/commandRegistry.ts +4 -5
  75. package/connectorRunArtifact.ts +10 -34
  76. package/connectorWebSocketTarget.ts +6 -10
  77. package/database/server/serverStoreFactory.ts +2 -8
  78. package/internalCommandEntries.ts +0 -8
  79. package/localRuntimeDb.ts +2 -39
  80. package/machineCommands.ts +189 -190
  81. package/machineDaemonCommands.ts +165 -9
  82. package/machineHeartbeatCommands.ts +2 -1
  83. package/machineStatusCommands.ts +13 -5
  84. package/machineWatchCommands.ts +1 -1
  85. package/machineWsRunDispatch.ts +30 -44
  86. package/machineWsSession.ts +0 -2
  87. package/offlineMarxistsAgentCommand.ts +314 -0
  88. package/package.json +52 -188
  89. package/runtimeDoctorCommands.ts +72 -15
  90. package/scriptCommandEntries.ts +45 -5
  91. package/tableCommands.ts +133 -33
  92. package/taskRunCommand.ts +211 -16
  93. package/tui/session.ts +372 -43
  94. package/workflowInternalCommandEntries.ts +0 -20
  95. package/agent-runtime/dialogStatus.ts +0 -139
  96. package/agent-runtime/hostRuntimeConfig.ts +0 -86
  97. package/agentCreateCustomCommands.ts +0 -278
  98. package/agentDialogsCommands.ts +0 -227
  99. package/agentDoctorCommands.ts +0 -203
  100. package/agentRunLocalWorkspace.ts +0 -182
  101. package/agentRunParsing.ts +0 -298
  102. package/agentSuperviseCommands.ts +0 -8
  103. package/agentUnpublishCommands.ts +0 -113
  104. package/ai/agent/agentRunDialogLineage.ts +0 -56
  105. package/ai/policy/types.ts +0 -64
  106. package/ai/skills/skillSummaryMarker.ts +0 -26
  107. package/app/types.ts +0 -358
  108. package/build.ts +0 -16
  109. package/buildPublish.ts +0 -138
  110. package/buildPublishArtifactAssembly.ts +0 -724
  111. package/buildPublishImportAnalysis.ts +0 -242
  112. package/chat/messages/fetchMessages.ts +0 -84
  113. package/chat/messages/types.ts +0 -185
  114. package/cli/agentCreateCustomCommands.ts +0 -278
  115. package/cli/agentDialogsCommands.ts +0 -227
  116. package/cli/agentDoctorCommands.ts +0 -203
  117. package/cli/agentRunLocalWorkspace.ts +0 -182
  118. package/cli/agentRunParsing.ts +0 -298
  119. package/cli/agentSuperviseCommands.ts +0 -8
  120. package/cli/agentUnpublishCommands.ts +0 -113
  121. package/cli/buildPublish.ts +0 -138
  122. package/cli/buildPublishArtifactAssembly.ts +0 -724
  123. package/cli/buildPublishImportAnalysis.ts +0 -242
  124. package/cli/cliIoTypes.ts +0 -3
  125. package/cli/cliRuntimeDoctorReport.ts +0 -193
  126. package/cli/client/agentRunInput.ts +0 -10
  127. package/cli/client/agentRunLocalExecution.ts +0 -124
  128. package/cli/client/agentRunLocalExecutor.ts +0 -179
  129. package/cli/client/agentRunRuntimeSelection.ts +0 -145
  130. package/cli/client/agentRunServerTransport.ts +0 -397
  131. package/cli/client/agentRunTaskRunRecorder.ts +0 -176
  132. package/cli/customCodingAgentFlow.ts +0 -208
  133. package/cli/customCodingAgentRecord.ts +0 -179
  134. package/cli/dialogDataAccess.ts +0 -186
  135. package/cli/dialogInternalCommandEntries.ts +0 -11
  136. package/cli/dialogReadCommands.ts +0 -180
  137. package/cli/dialogReadHelpers.ts +0 -226
  138. package/cli/dialogStatusCommands.ts +0 -123
  139. package/cli/docInternalCommandEntries.ts +0 -20
  140. package/cli/hybridReadHelpers.ts +0 -109
  141. package/cli/machineCommandPrompts.ts +0 -79
  142. package/cli/machineCommandResolution.ts +0 -43
  143. package/cli/noloReadBridgePrompt.ts +0 -16
  144. package/cli/pageCreateCommands.ts +0 -219
  145. package/cli/pageDeleteCommands.ts +0 -167
  146. package/cli/pageReadCommands.ts +0 -151
  147. package/cli/pageUpdateCommands.ts +0 -249
  148. package/cli/pageWriteHelpers.ts +0 -209
  149. package/cli/publishPackage.ts +0 -36
  150. package/cli/runtimeInternalCommandEntries.ts +0 -15
  151. package/cli/runtimePassthroughCommands.ts +0 -22
  152. package/cli/scriptPassthroughHelpers.ts +0 -28
  153. package/cli/secretScanHelpers.ts +0 -31
  154. package/cli/spaceAttachCommands.ts +0 -134
  155. package/cli/spaceCategoryCommands.ts +0 -263
  156. package/cli/spaceCommands.ts +0 -295
  157. package/cli/spaceDeleteCommands.ts +0 -312
  158. package/cli/spaceInternalCommandEntries.ts +0 -21
  159. package/cli/spaceMutationHelpers.ts +0 -135
  160. package/cli/spaceReadCommands.ts +0 -323
  161. package/cli/tableDataOutput.ts +0 -41
  162. package/cli/tableDataSafety.ts +0 -26
  163. package/cli/tableMetaCommands.ts +0 -307
  164. package/cli/tableMutationCommands.ts +0 -266
  165. package/cli/tableQueryParsing.ts +0 -90
  166. package/cli/tableQueryRequest.ts +0 -53
  167. package/cli/taskRunActions.ts +0 -122
  168. package/cli/taskRunParsing.ts +0 -86
  169. package/cli/taskRunPromptShared.ts +0 -55
  170. package/cli/tui/tuiCatalog.ts +0 -22
  171. package/cli/tui/tuiCommandInterpreter.ts +0 -253
  172. package/cli/tui/tuiPresentation.ts +0 -146
  173. package/cliIoTypes.ts +0 -3
  174. package/cliRuntimeDoctorReport.ts +0 -193
  175. package/client/agentRunInput.ts +0 -10
  176. package/client/agentRunLocalExecution.test.ts +0 -123
  177. package/client/agentRunLocalExecution.ts +0 -124
  178. package/client/agentRunLocalExecutor.ts +0 -179
  179. package/client/agentRunRuntimeSelection.test.ts +0 -159
  180. package/client/agentRunRuntimeSelection.ts +0 -145
  181. package/client/agentRunServerTransport.ts +0 -397
  182. package/client/agentRunTaskRunRecorder.ts +0 -176
  183. package/connector-experimental/daemonLauncher.ts +0 -223
  184. package/create/space/spaceKeys.ts +0 -65
  185. package/create/types.ts +0 -21
  186. package/customCodingAgentFlow.ts +0 -208
  187. package/customCodingAgentRecord.ts +0 -179
  188. package/database/keys.ts +0 -765
  189. package/database/tombstones.ts +0 -52
  190. package/database/utils/ulid.ts +0 -1
  191. package/dialogDataAccess.ts +0 -186
  192. package/dialogInternalCommandEntries.ts +0 -11
  193. package/dialogReadCommands.ts +0 -180
  194. package/dialogReadHelpers.ts +0 -226
  195. package/dialogStatusCommands.ts +0 -123
  196. package/docInternalCommandEntries.ts +0 -20
  197. package/hybridReadHelpers.ts +0 -109
  198. package/machineCommandPrompts.ts +0 -79
  199. package/machineCommandResolution.ts +0 -43
  200. package/noloReadBridgePrompt.ts +0 -16
  201. package/pageCreateCommands.ts +0 -219
  202. package/pageDeleteCommands.ts +0 -167
  203. package/pageReadCommands.ts +0 -151
  204. package/pageUpdateCommands.ts +0 -249
  205. package/pageWriteHelpers.ts +0 -209
  206. package/publishPackage.ts +0 -36
  207. package/runtimeInternalCommandEntries.ts +0 -15
  208. package/runtimePassthroughCommands.ts +0 -22
  209. package/scriptPassthroughHelpers.ts +0 -28
  210. package/secretScanHelpers.ts +0 -31
  211. package/share/helpers.ts +0 -302
  212. package/share/keys.ts +0 -243
  213. package/share/types.ts +0 -100
  214. package/spaceAttachCommands.ts +0 -134
  215. package/spaceCategoryCommands.ts +0 -263
  216. package/spaceCommands.ts +0 -295
  217. package/spaceDeleteCommands.ts +0 -312
  218. package/spaceInternalCommandEntries.ts +0 -21
  219. package/spaceMutationHelpers.ts +0 -135
  220. package/spaceReadCommands.ts +0 -323
  221. package/tableDataOutput.ts +0 -41
  222. package/tableDataSafety.ts +0 -26
  223. package/tableMetaCommands.ts +0 -307
  224. package/tableMutationCommands.ts +0 -266
  225. package/tableQueryParsing.ts +0 -90
  226. package/tableQueryRequest.ts +0 -53
  227. package/taskRunActions.ts +0 -122
  228. package/taskRunParsing.ts +0 -86
  229. package/taskRunPromptShared.ts +0 -55
  230. package/tui/tuiCatalog.ts +0 -22
  231. package/tui/tuiCommandInterpreter.test.ts +0 -169
  232. package/tui/tuiCommandInterpreter.ts +0 -253
  233. package/tui/tuiPresentation.test.ts +0 -154
  234. 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, "--machine-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 {