nolo-cli 0.1.22 → 0.1.26

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 (128) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/agentRecordConfig.ts +4 -0
  3. package/agent-runtime/hostAdapter.ts +2 -0
  4. package/agent-runtime/hybridRecordStore.ts +30 -9
  5. package/agent-runtime/index.ts +7 -0
  6. package/agent-runtime/localLoop.ts +14 -4
  7. package/agent-runtime/localToolPolicy.ts +22 -10
  8. package/agent-runtime/localWorkspaceTools.ts +140 -374
  9. package/agent-runtime/platformChatProvider.ts +3 -0
  10. package/agent-runtime/runtimeToolPolicy.ts +133 -0
  11. package/agent-runtime/types.ts +47 -0
  12. package/agentAliases.ts +1 -1
  13. package/agentCommandSupport.ts +77 -0
  14. package/agentInternalCommandEntries.ts +34 -0
  15. package/agentListCommands.ts +123 -0
  16. package/agentListHelpers.ts +187 -0
  17. package/agentMachineCommands.ts +402 -0
  18. package/agentRecordCommands.ts +141 -0
  19. package/agentRecordHelpers.ts +395 -0
  20. package/agentRunCommand.ts +75 -3
  21. package/agentRuntimeCommands.ts +7 -773
  22. package/ai/agent/_executeModel.ts +29 -3
  23. package/ai/agent/buildSystemPrompt.ts +17 -0
  24. package/ai/agent/cliChatClient.ts +38 -4
  25. package/ai/agent/cliExecutor.ts +304 -25
  26. package/ai/agent/createAgentSchema.ts +2 -2
  27. package/ai/agent/guidedCreation/assistant.ts +82 -0
  28. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  29. package/ai/agent/guidedCreation/draft.ts +62 -0
  30. package/ai/agent/guidedCreation/types.ts +38 -0
  31. package/ai/agent/machineRunPermissions.ts +21 -3
  32. package/ai/agent/runtimeGuidance.ts +1 -1
  33. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  34. package/ai/agent/streamAgentChatTurn.ts +593 -36
  35. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  36. package/ai/ai.locale.ts +14 -2
  37. package/ai/chat/fetchUtils.native.ts +2 -0
  38. package/ai/chat/fetchUtils.ts +2 -0
  39. package/ai/chat/sendOpenAICompletionsRequest.ts +82 -1
  40. package/ai/chat/sendOpenAIResponseRequest.ts +65 -0
  41. package/ai/llm/getPricing.ts +14 -0
  42. package/ai/llm/kimi.ts +4 -1
  43. package/ai/llm/modelAvatar.ts +1 -0
  44. package/ai/llm/providers.ts +4 -1
  45. package/ai/llm/reasoningModels.ts +1 -0
  46. package/ai/taskRun/taskRunProtocol.ts +25 -10
  47. package/ai/token/modelUsageQuery.ts +17 -0
  48. package/ai/tools/agent/agentTools.ts +38 -0
  49. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  50. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  51. package/ai/tools/checkEnvTool.ts +52 -20
  52. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  53. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  54. package/ai/tools/index.ts +24 -14
  55. package/ai/tools/marxistsOfflineBook.ts +253 -0
  56. package/ai/tools/modelUsageTools.ts +9 -2
  57. package/ai/tools/readTool.ts +31 -0
  58. package/ai/tools/toolApiClient.ts +35 -1
  59. package/ai/tools/toolNameAliases.ts +13 -0
  60. package/cliCommandAdapters.ts +28 -0
  61. package/cliCommandDispatch.ts +66 -0
  62. package/cliCommandFactories.ts +34 -0
  63. package/cliCommandTypes.ts +49 -0
  64. package/cliEnvHelpers.ts +44 -0
  65. package/client/agentRun.test.ts +330 -24
  66. package/client/agentRun.ts +117 -35
  67. package/client/hybridRecordStore.test.ts +42 -0
  68. package/client/hybridRecordStore.ts +11 -0
  69. package/client/localRuntimeAdapter.test.ts +191 -21
  70. package/client/localRuntimeAdapter.ts +99 -10
  71. package/client/localRuntimeDryRun.test.ts +5 -2
  72. package/client/taskRunPrompt.ts +1 -0
  73. package/commandRegistry.ts +11 -61
  74. package/connector-experimental/capabilities.ts +1 -0
  75. package/database/actions/common.ts +4 -3
  76. package/database/config.ts +19 -0
  77. package/database/server/coreDataProxy.ts +43 -26
  78. package/index.ts +54 -191
  79. package/internalCommandEntries.ts +12 -0
  80. package/machineCommands.ts +688 -64
  81. package/offlineMarxistsAgentCommand.ts +314 -0
  82. package/package.json +22 -2
  83. package/render/canvas/canvasEditContext.ts +127 -0
  84. package/render/canvas/canvasRuntime.ts +57 -0
  85. package/render/canvas/canvasSnapshotParser.ts +76 -0
  86. package/render/canvas/canvasTree.ts +308 -0
  87. package/render/canvas/types.ts +46 -0
  88. package/render/layout/deleteBehavior.ts +52 -0
  89. package/render/layout/mainLayoutSidebar.ts +17 -0
  90. package/render/layout/mainLayoutViewMode.ts +56 -0
  91. package/render/layout/topbarUtils.ts +87 -0
  92. package/render/layout/useDevReloadPending.ts +30 -0
  93. package/render/page/createPageAction.ts +183 -0
  94. package/render/page/docSlice.ts +468 -0
  95. package/render/page/server/createPage.ts +174 -0
  96. package/render/page/server/handleCreatePage.ts +91 -0
  97. package/render/page/server/index.ts +4 -0
  98. package/render/page/types.ts +17 -0
  99. package/render/page/useKeyboardSave.ts +48 -0
  100. package/render/styles/zIndex.ts +12 -0
  101. package/render/surf/WeatherIconStyles.ts +17 -0
  102. package/render/surf/color.ts +9 -0
  103. package/render/surf/config.ts +46 -0
  104. package/render/surf/screens/style.ts +1 -0
  105. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  106. package/render/surf/utils/groupedWeatherData.ts +32 -0
  107. package/render/surf/weatherUtils.ts +50 -0
  108. package/render/table/activityColumns.ts +6 -0
  109. package/render/table/createTableAction.ts +270 -0
  110. package/render/table/deleteTableAction.ts +129 -0
  111. package/render/table/fetchAndCacheTableRows.ts +174 -0
  112. package/render/table/tableSlice.ts +1106 -0
  113. package/render/table/tableView.ts +289 -0
  114. package/render/table/toolValueUtils.ts +363 -0
  115. package/render/table/types.ts +252 -0
  116. package/render/table/useCreateTable.ts +72 -0
  117. package/render/table/useTable.ts +61 -0
  118. package/render/table/utils/tableSerialization.ts +50 -0
  119. package/render/web/elements/artifactPreviewCode.ts +43 -0
  120. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  121. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  122. package/render/web/elements/mermaidPreview.ts +21 -0
  123. package/render/web/ui/useInlineEdit.ts +135 -0
  124. package/runtimeDoctorCommands.ts +118 -0
  125. package/scriptCommandEntries.ts +71 -0
  126. package/systemInternalCommandEntries.ts +71 -0
  127. package/tableCommands.ts +42 -5
  128. package/workflowInternalCommandEntries.ts +11 -0
@@ -1,9 +1,11 @@
1
1
  import type { MachineHeartbeat } from "./connector-experimental/protocol";
2
2
  import { detectMachineInfo } from "./connector-experimental/machineInfo";
3
+ import { createHash } from "node:crypto";
3
4
  import { spawn } from "node:child_process";
4
- import { mkdirSync, openSync } from "node:fs";
5
+ import { existsSync, mkdirSync, openSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
5
6
  import { homedir } from "node:os";
6
- import { dirname, join } from "node:path";
7
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import { runLocalAgentTurn, type LocalAgentToolEvent } from "./agent-runtime/localLoop";
7
9
  import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
8
10
  import {
9
11
  collectConnectorRunArtifact,
@@ -19,7 +21,9 @@ import {
19
21
  buildMachinePermissionPromptBlock,
20
22
  resolveMachineRunPermissionPolicy,
21
23
  } from "./ai/agent/machineRunPermissions";
22
- import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
24
+ import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
25
+ import { createCliLocalRuntimeAdapter } from "./client/localRuntimeAdapter";
26
+ import { prepareTaskWorktree } from "./client/taskWorktree";
23
27
 
24
28
  type EnvLike = Record<string, string | undefined>;
25
29
  type OutputLike = { write(chunk: string): unknown };
@@ -28,11 +32,42 @@ type ConnectorWebSocketOptions = {
28
32
  onMessage: (message: string) => void | Promise<void>;
29
33
  sentMessages: string[];
30
34
  };
31
- type LocalCliExecutor = (
35
+ export type LocalCliExecutor = (
32
36
  provider: string,
33
37
  prompt: string,
34
38
  options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean; env?: EnvLike }
35
39
  ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
40
+
41
+ type ConnectorRuntimePolicy = {
42
+ runtimeTools?: string[];
43
+ agentTools?: string[];
44
+ workspace?: {
45
+ mode?: string;
46
+ cwd?: string;
47
+ };
48
+ shell?: {
49
+ enabled?: boolean;
50
+ mode?: string;
51
+ };
52
+ };
53
+
54
+ type ConnectorRunProgress = {
55
+ eventType?: LocalAgentToolEvent["type"];
56
+ toolCallCount?: number;
57
+ toolResultCount?: number;
58
+ lastToolNames?: string[];
59
+ provider?: string;
60
+ promptBytes?: number;
61
+ promptHash?: string;
62
+ promptRef?: string;
63
+ promptPageKey?: string;
64
+ promptPageHash?: string;
65
+ requestId?: string;
66
+ workspaceRoot?: string;
67
+ workspaceKind?: string;
68
+ message?: string;
69
+ updatedAt?: number;
70
+ };
36
71
 
37
72
  export type MachineSummary = {
38
73
  machineId: string;
@@ -55,15 +90,26 @@ type MachineCommandDeps = {
55
90
  fetchImpl?: typeof fetch;
56
91
  machineInfo?: () => MachineHeartbeat;
57
92
  runHeartbeatLoop?: (options: HeartbeatLoopOptions) => Promise<void>;
58
- connectWebSocket?: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
59
- executeCli?: LocalCliExecutor;
60
- spawnDaemon?: (args: {
93
+ connectWebSocket?: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
94
+ executeCli?: LocalCliExecutor;
95
+ validateWorkspaceLinks?: (cwd: string) => string[] | Promise<string[]>;
96
+ spawnDaemon?: (args: {
61
97
  cmd: string[];
62
98
  cwd: string;
63
99
  env: EnvLike;
64
100
  logPath: string;
65
101
  }) => { pid?: number };
66
- };
102
+ };
103
+
104
+ type PackageJson = {
105
+ name?: string;
106
+ workspaces?: string[] | { packages?: string[] };
107
+ };
108
+
109
+ const CONNECTOR_RUNTIME_WORKSPACE_PACKAGES = new Set([
110
+ "connector-experimental",
111
+ "ai",
112
+ ]);
67
113
 
68
114
  function resolveServerUrl(env: EnvLike) {
69
115
  return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
@@ -79,9 +125,113 @@ function writeAuthMissing(output: OutputLike) {
79
125
  );
80
126
  }
81
127
 
82
- function hasFlag(args: string[], flag: string) {
83
- return args.includes(flag);
84
- }
128
+ function hasFlag(args: string[], flag: string) {
129
+ return args.includes(flag);
130
+ }
131
+
132
+ function normalizePath(path: string) {
133
+ const resolved = resolve(path);
134
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
135
+ }
136
+
137
+ function isInsideOrSame(path: string, parent: string) {
138
+ const relativePath = relative(parent, path);
139
+ return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
140
+ }
141
+
142
+ function readJsonFile<T>(path: string): T | null {
143
+ try {
144
+ return JSON.parse(readFileSync(path, "utf8")) as T;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function getWorkspacePatterns(packageJson: PackageJson) {
151
+ if (Array.isArray(packageJson.workspaces)) return packageJson.workspaces;
152
+ return packageJson.workspaces?.packages ?? [];
153
+ }
154
+
155
+ function findWorkspaceRoot(startPath: string) {
156
+ let current = resolve(startPath);
157
+ while (true) {
158
+ const packageJsonPath = join(current, "package.json");
159
+ if (existsSync(packageJsonPath)) {
160
+ const packageJson = readJsonFile<PackageJson>(packageJsonPath);
161
+ if (packageJson && getWorkspacePatterns(packageJson).length > 0) {
162
+ return current;
163
+ }
164
+ }
165
+ const parent = dirname(current);
166
+ if (parent === current) return null;
167
+ current = parent;
168
+ }
169
+ }
170
+
171
+ function validateConnectorWorkspaceLinks(cwd: string) {
172
+ const repoRoot = findWorkspaceRoot(cwd);
173
+ if (!repoRoot) return [];
174
+
175
+ const rootPackageJson = readJsonFile<PackageJson>(join(repoRoot, "package.json"));
176
+ if (!rootPackageJson) return [];
177
+
178
+ const normalizedRepoRoot = normalizePath(realpathSync(repoRoot));
179
+ const errors: string[] = [];
180
+
181
+ for (const pattern of getWorkspacePatterns(rootPackageJson)) {
182
+ if (!pattern.endsWith("/*")) continue;
183
+ const workspaceParent = join(repoRoot, pattern.slice(0, -2));
184
+ if (!existsSync(workspaceParent)) continue;
185
+
186
+ for (const entry of Array.from(new Bun.Glob("*/package.json").scanSync({ cwd: workspaceParent }))) {
187
+ const packageDir = join(workspaceParent, dirname(entry));
188
+ const packageJson = readJsonFile<PackageJson>(join(workspaceParent, entry));
189
+ if (!packageJson?.name || !CONNECTOR_RUNTIME_WORKSPACE_PACKAGES.has(packageJson.name)) {
190
+ continue;
191
+ }
192
+
193
+ const linkPath = join(repoRoot, "node_modules", ...packageJson.name.split("/"));
194
+ if (!existsSync(linkPath)) {
195
+ errors.push(`${linkPath} is missing. Run bun install in ${repoRoot}.`);
196
+ continue;
197
+ }
198
+
199
+ const actualPath = normalizePath(realpathSync(linkPath));
200
+ const expectedPath = normalizePath(realpathSync(packageDir));
201
+ if (!isInsideOrSame(actualPath, normalizedRepoRoot)) {
202
+ errors.push(
203
+ `${linkPath} points outside this checkout: ${actualPath}. Run bun install in ${repoRoot}.`,
204
+ );
205
+ continue;
206
+ }
207
+ if (actualPath !== expectedPath) {
208
+ errors.push(
209
+ `${linkPath} points at ${actualPath}, expected ${expectedPath}. Run bun install in ${repoRoot}.`,
210
+ );
211
+ }
212
+ }
213
+ }
214
+
215
+ return errors;
216
+ }
217
+
218
+ async function checkConnectorWorkspaceLinks(
219
+ cwd: string,
220
+ output: OutputLike,
221
+ validateWorkspaceLinks: (cwd: string) => string[] | Promise<string[]>,
222
+ ) {
223
+ const errors = await validateWorkspaceLinks(cwd);
224
+ if (errors.length === 0) return true;
225
+ output.write(
226
+ [
227
+ "[nolo] Connector workspace package links are unsafe.",
228
+ "The connector would import workspace packages from a different checkout, so machine capabilities and runtime behavior may be stale.",
229
+ ...errors,
230
+ "",
231
+ ].join("\n"),
232
+ );
233
+ return false;
234
+ }
85
235
 
86
236
  function readOption(args: string[], flag: string) {
87
237
  for (let i = 0; i < args.length; i++) {
@@ -112,6 +262,90 @@ function resolveDaemonLogPath(env: EnvLike) {
112
262
  return env.NOLO_CONNECT_LOG || join(homedir(), ".nolo", "logs", "connector.log");
113
263
  }
114
264
 
265
+ function byteLength(value: string) {
266
+ return Buffer.byteLength(value, "utf8");
267
+ }
268
+
269
+ function shortHash(value: string) {
270
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
271
+ }
272
+
273
+ function resolvePromptInlineMaxBytes(env: EnvLike) {
274
+ const raw = Number(env.NOLO_CONNECTOR_PROMPT_INLINE_MAX_BYTES);
275
+ return Number.isFinite(raw) && raw >= 1024 ? raw : 32_000;
276
+ }
277
+
278
+ function materializeLargeConnectorPrompt(args: {
279
+ prompt: string;
280
+ cwd: string;
281
+ env: EnvLike;
282
+ requestId: string;
283
+ runtimePromptPage?: {
284
+ dbKey?: string;
285
+ promptHash?: string;
286
+ contentBytes?: number;
287
+ } | null;
288
+ }) {
289
+ const promptBytes = byteLength(args.prompt);
290
+ const promptHash = shortHash(args.prompt);
291
+ const maxInlineBytes = resolvePromptInlineMaxBytes(args.env);
292
+ if (promptBytes <= maxInlineBytes) {
293
+ return {
294
+ prompt: args.prompt,
295
+ promptBytes,
296
+ promptHash,
297
+ promptRef: null as string | null,
298
+ };
299
+ }
300
+
301
+ const promptDir = join(args.cwd, ".nolo", "agent-prompts");
302
+ mkdirSync(promptDir, { recursive: true });
303
+ const promptRef = join(promptDir, `${args.requestId}-${promptHash}.md`);
304
+ writeFileSync(promptRef, args.prompt, "utf8");
305
+
306
+ return {
307
+ prompt: [
308
+ "A large Nolo runtime prompt has been materialized outside this command argument.",
309
+ ...(args.runtimePromptPage?.dbKey
310
+ ? [
311
+ "Cloud prompt page is the shared audit/context source.",
312
+ `Cloud prompt page: ${args.runtimePromptPage.dbKey}`,
313
+ `Read cloud page: bun packages/cli/index.ts doc read ${JSON.stringify(args.runtimePromptPage.dbKey)}`,
314
+ ...(args.runtimePromptPage.promptHash
315
+ ? [`Cloud prompt sha256 prefix: ${args.runtimePromptPage.promptHash}`]
316
+ : []),
317
+ "",
318
+ ]
319
+ : []),
320
+ "Local prompt file is the execution-machine fallback and should match the cloud page content at run start.",
321
+ `Prompt file: ${promptRef}`,
322
+ `Prompt bytes: ${promptBytes}`,
323
+ `Prompt sha256 prefix: ${promptHash}`,
324
+ "",
325
+ "Prefer the cloud prompt page for shared context when readable; use the local prompt file as fallback.",
326
+ "Local repository edits, diffs, test output, and commits are still authoritative for local execution facts.",
327
+ "If neither source is readable, stop and report PROMPT_REF_UNREADABLE with both references.",
328
+ ].join("\n"),
329
+ promptBytes,
330
+ promptHash,
331
+ promptRef,
332
+ };
333
+ }
334
+
335
+ function readRuntimePromptPageMeta(parsed: any) {
336
+ const page = parsed?.payload?.meta?.runtimePromptPage;
337
+ if (!page || typeof page !== "object") return null;
338
+ return {
339
+ dbKey: typeof page.dbKey === "string" ? page.dbKey : undefined,
340
+ promptHash: typeof page.promptHash === "string" ? page.promptHash : undefined,
341
+ contentBytes: typeof page.contentBytes === "number" ? page.contentBytes : undefined,
342
+ };
343
+ }
344
+
345
+ function logConnectorRun(message: string, fields: Record<string, unknown> = {}) {
346
+ console.error(`[nolo-connector] ${message} ${JSON.stringify(fields)}`);
347
+ }
348
+
115
349
  function buildDaemonCommand(cliEntrypointPath: string | undefined) {
116
350
  return [process.execPath, cliEntrypointPath || import.meta.path, "connect", "--ws"];
117
351
  }
@@ -245,6 +479,7 @@ function buildTaskRunBridgePrompt(args: {
245
479
  "Run commands from the repository root. The runner already provides server URL and auth in the environment.",
246
480
  "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.",
247
481
  "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.",
482
+ "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.",
248
483
  `Read context: bun packages/cli/index.ts task-run read-context --row-dbkey ${JSON.stringify(rowDbKey)}`,
249
484
  ...(workItemId
250
485
  ? [
@@ -264,8 +499,8 @@ function buildTaskRunBridgePrompt(args: {
264
499
  function buildConnectorCliPrompt(agentConfig: any, userInput: string, bridgeArgs?: {
265
500
  agentKey: string;
266
501
  runtimeContext: any;
267
- }) {
268
- const policy = resolveMachineRunPermissionPolicy(agentConfig);
502
+ }, permissionPolicy?: ReturnType<typeof resolveMachineRunPermissionPolicy>) {
503
+ const policy = permissionPolicy ?? resolveMachineRunPermissionPolicy(agentConfig);
269
504
  return [
270
505
  typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
271
506
  bridgeArgs ? buildTaskRunBridgePrompt({
@@ -278,6 +513,248 @@ function buildConnectorCliPrompt(agentConfig: any, userInput: string, bridgeArgs
278
513
  ].filter(Boolean).join("\n\n");
279
514
  }
280
515
 
516
+ function isRecord(value: unknown): value is Record<string, any> {
517
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
518
+ }
519
+
520
+ function runtimePolicyFromConnectorPayload(parsed: any): ConnectorRuntimePolicy | undefined {
521
+ const fromMeta = parsed?.payload?.meta?.runtimeToolPolicySnapshot;
522
+ const fromAgent = parsed?.payload?.agentConfig?.runtimeToolPolicy;
523
+ const policy = isRecord(fromMeta) ? fromMeta : isRecord(fromAgent) ? fromAgent : null;
524
+ return policy ? policy as ConnectorRuntimePolicy : undefined;
525
+ }
526
+
527
+ function requestsTaskWorktree(policy?: ConnectorRuntimePolicy) {
528
+ return policy?.workspace?.mode === "task-worktree" || policy?.workspace?.mode === "lease";
529
+ }
530
+
531
+ function hasExplicitMachinePermissions(agentConfig: any) {
532
+ const runtimeBinding = isRecord(agentConfig?.runtimeBinding) ? agentConfig.runtimeBinding : {};
533
+ return Boolean(
534
+ isRecord(agentConfig?.machinePermissions) ||
535
+ isRecord(runtimeBinding.permissions) ||
536
+ isRecord(runtimeBinding.machinePermissions) ||
537
+ isRecord(agentConfig?.boundRuntimeMachine?.permissions)
538
+ );
539
+ }
540
+
541
+ function runtimeWorkspacePermissionPolicy(
542
+ runtimePolicy: ConnectorRuntimePolicy | undefined,
543
+ cwd: string
544
+ ): ReturnType<typeof resolveMachineRunPermissionPolicy> | null {
545
+ if (!requestsTaskWorktree(runtimePolicy)) return null;
546
+ return {
547
+ mode: "full_access",
548
+ allowFilesystemRead: true,
549
+ allowFilesystemWrite: true,
550
+ allowShell: runtimePolicy?.shell?.enabled !== false,
551
+ writableRoots: [cwd],
552
+ };
553
+ }
554
+
555
+ function scopePermissionPolicyToRuntimeWorkspace(
556
+ policy: ReturnType<typeof resolveMachineRunPermissionPolicy>,
557
+ runtimePolicy: ConnectorRuntimePolicy | undefined,
558
+ cwd: string
559
+ ): ReturnType<typeof resolveMachineRunPermissionPolicy> {
560
+ if (!requestsTaskWorktree(runtimePolicy)) return policy;
561
+ return {
562
+ ...policy,
563
+ writableRoots: policy.allowFilesystemWrite ? [cwd] : [],
564
+ };
565
+ }
566
+
567
+ function mergeToolNames(...values: unknown[]) {
568
+ const names: string[] = [];
569
+ for (const value of values) {
570
+ if (!Array.isArray(value)) continue;
571
+ for (const item of value) {
572
+ const name = typeof item === "string"
573
+ ? item
574
+ : isRecord(item) && typeof item.name === "string"
575
+ ? item.name
576
+ : isRecord(item) && typeof item.function?.name === "string"
577
+ ? item.function.name
578
+ : "";
579
+ if (name && !names.includes(name)) names.push(name);
580
+ }
581
+ }
582
+ return names;
583
+ }
584
+
585
+ function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
586
+ return {
587
+ ...runtimeEnv,
588
+ ...(policy?.shell?.enabled && policy.shell.mode === "worktree"
589
+ ? { NOLO_LOCAL_SHELL_MODE: "worktree" }
590
+ : {}),
591
+ ...(policy?.workspace?.cwd ? { NOLO_LOCAL_WORKTREE: policy.workspace.cwd } : {}),
592
+ };
593
+ }
594
+
595
+ function runtimeWorkspaceRootFromTrace(trace: unknown): string | undefined {
596
+ if (!Array.isArray(trace)) return undefined;
597
+ for (const message of trace) {
598
+ if (!isRecord(message)) continue;
599
+ const metadata = isRecord(message.tool_result_metadata)
600
+ ? message.tool_result_metadata
601
+ : null;
602
+ const workspaceRoot = metadata && typeof metadata.workspaceRoot === "string"
603
+ ? metadata.workspaceRoot.trim()
604
+ : "";
605
+ if (workspaceRoot) return workspaceRoot;
606
+ }
607
+ return undefined;
608
+ }
609
+
610
+ function buildArtifactProgress(args: {
611
+ artifacts: unknown;
612
+ runtimePolicy?: ConnectorRuntimePolicy;
613
+ workspaceRoot: string;
614
+ workspaceKind?: string;
615
+ }): ConnectorRunProgress | null {
616
+ if (!isRecord(args.artifacts)) return null;
617
+ const changedFiles = Array.isArray(args.artifacts.changedFiles)
618
+ ? args.artifacts.changedFiles
619
+ : [];
620
+ const statusShort = typeof args.artifacts.statusShort === "string"
621
+ ? args.artifacts.statusShort.trim()
622
+ : "";
623
+ if (changedFiles.length === 0 && !statusShort) return null;
624
+ const runtimeTools = Array.isArray(args.runtimePolicy?.runtimeTools)
625
+ ? args.runtimePolicy.runtimeTools
626
+ : [];
627
+ const shellTool = runtimeTools.includes("execShell")
628
+ ? "execShell"
629
+ : "workspaceArtifact";
630
+ return {
631
+ eventType: "tool-result",
632
+ toolCallCount: 1,
633
+ toolResultCount: 1,
634
+ lastToolNames: [shellTool],
635
+ workspaceRoot: args.workspaceRoot,
636
+ ...(args.workspaceKind ? { workspaceKind: args.workspaceKind } : {}),
637
+ message: statusShort || `${changedFiles.length} changed file(s)`,
638
+ updatedAt: Date.now(),
639
+ };
640
+ }
641
+
642
+ async function runConnectorLocalRuntimeAgent(args: {
643
+ parsed: any;
644
+ runtimeEnv: EnvLike;
645
+ cwd: string;
646
+ fetchImpl?: typeof fetch;
647
+ onProgress?: (progress: ConnectorRunProgress) => void;
648
+ }): Promise<Awaited<ReturnType<typeof runLocalAgentTurn>> & { runtimeWorkspaceRoot?: string }> {
649
+ const agentKey = String(args.parsed.payload?.agentKey ?? "");
650
+ const payloadAgentConfig = isRecord(args.parsed.payload?.agentConfig)
651
+ ? args.parsed.payload.agentConfig
652
+ : {};
653
+ const policy = runtimePolicyFromConnectorPayload(args.parsed);
654
+ const agentRecord = {
655
+ ...payloadAgentConfig,
656
+ dbKey: agentKey,
657
+ id: agentKey,
658
+ key: agentKey,
659
+ apiSource: payloadAgentConfig.apiSource ?? "platform",
660
+ provider: payloadAgentConfig.provider ?? payloadAgentConfig.apiSource ?? "openai",
661
+ toolNames: mergeToolNames(
662
+ payloadAgentConfig.toolNames,
663
+ payloadAgentConfig.tools,
664
+ policy?.agentTools,
665
+ policy?.runtimeTools,
666
+ ),
667
+ ...(policy ? { runtimeToolPolicy: policy } : {}),
668
+ ...(policy?.workspace?.mode === "task-worktree" || policy?.workspace?.mode === "lease"
669
+ ? { localWorkspaceMode: "task-worktree" }
670
+ : {}),
671
+ };
672
+ const store = new Map<string, any>([
673
+ [agentKey, agentRecord],
674
+ [`agent-${args.runtimeEnv.NOLO_USER_ID || "local"}-${agentKey}`, agentRecord],
675
+ [`agent-${args.runtimeEnv.NOLO_LOCAL_USER_ID || args.runtimeEnv.NOLO_USER_ID || "local"}-${agentKey}`, agentRecord],
676
+ ]);
677
+ const env = localRuntimeEnvFromPolicy(args.runtimeEnv, policy);
678
+ const wantsTaskWorktree = requestsTaskWorktree(policy);
679
+ const adapter = createCliLocalRuntimeAdapter({
680
+ env,
681
+ cwd: args.cwd,
682
+ useCwdAsTaskWorkspaceBase: wantsTaskWorktree,
683
+ ...(wantsTaskWorktree
684
+ ? {
685
+ prepareTaskWorktree: (input) => prepareTaskWorktree({
686
+ ...input,
687
+ cwd: args.cwd,
688
+ }),
689
+ }
690
+ : {}),
691
+ fetchImpl: args.fetchImpl ?? fetch,
692
+ store: {
693
+ read: async (key) => store.get(key) ?? null,
694
+ batch: async (ops) => {
695
+ for (const op of ops) {
696
+ if (op.type === "put") store.set(op.key, op.value);
697
+ }
698
+ },
699
+ iterator: async function* (options) {
700
+ const keys = [...store.keys()].sort();
701
+ for (const key of keys) {
702
+ if (key < options.gte) continue;
703
+ if (options.lte && key > options.lte) continue;
704
+ if (options.lt && key >= options.lt) continue;
705
+ yield [key, store.get(key)];
706
+ }
707
+ },
708
+ },
709
+ });
710
+ let toolCallCount = 0;
711
+ let toolResultCount = 0;
712
+ const lastToolNames: string[] = [];
713
+ let runtimeWorkspaceRoot: string | undefined;
714
+ let runtimeWorkspaceKind: string | undefined;
715
+ const noteToolName = (toolName: string) => {
716
+ if (!toolName) return;
717
+ const existingIndex = lastToolNames.indexOf(toolName);
718
+ if (existingIndex >= 0) lastToolNames.splice(existingIndex, 1);
719
+ lastToolNames.push(toolName);
720
+ while (lastToolNames.length > 8) lastToolNames.shift();
721
+ };
722
+ const result = await runLocalAgentTurn({
723
+ adapter,
724
+ agentRef: agentKey,
725
+ input: String(args.parsed.payload?.userInput ?? ""),
726
+ continueDialogId: typeof args.parsed.payload?.continueDialogId === "string"
727
+ ? args.parsed.payload.continueDialogId
728
+ : undefined,
729
+ onToolEvent: (event) => {
730
+ if (event.type === "tool-call") toolCallCount += 1;
731
+ if (event.type === "tool-result") toolResultCount += 1;
732
+ noteToolName(event.toolName);
733
+ const metadata = isRecord(event.metadata) ? event.metadata : {};
734
+ if (typeof metadata.workspaceRoot === "string" && metadata.workspaceRoot.trim()) {
735
+ runtimeWorkspaceRoot = metadata.workspaceRoot.trim();
736
+ }
737
+ if (typeof metadata.workspaceKind === "string" && metadata.workspaceKind.trim()) {
738
+ runtimeWorkspaceKind = metadata.workspaceKind.trim();
739
+ }
740
+ args.onProgress?.({
741
+ eventType: event.type,
742
+ toolCallCount,
743
+ toolResultCount,
744
+ lastToolNames: [...lastToolNames],
745
+ ...(runtimeWorkspaceRoot ? { workspaceRoot: runtimeWorkspaceRoot } : {}),
746
+ ...(runtimeWorkspaceKind ? { workspaceKind: runtimeWorkspaceKind } : {}),
747
+ ...(event.message ? { message: event.message } : {}),
748
+ updatedAt: Date.now(),
749
+ });
750
+ },
751
+ });
752
+ return {
753
+ ...result,
754
+ runtimeWorkspaceRoot: runtimeWorkspaceRootFromTrace(result.trace) ?? runtimeWorkspaceRoot,
755
+ };
756
+ }
757
+
281
758
  function normalizeConnectorRunTimeoutMs(value: unknown): number | undefined {
282
759
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
283
760
  return undefined;
@@ -285,12 +762,13 @@ function normalizeConnectorRunTimeoutMs(value: unknown): number | undefined {
285
762
  return Math.floor(value);
286
763
  }
287
764
 
288
- async function handleConnectorRunMessage(
765
+ export async function handleConnectorRunMessage(
289
766
  machine: MachineHeartbeat,
290
767
  message: string,
291
768
  send: (message: string) => void,
292
769
  executeCli: LocalCliExecutor,
293
- runtimeEnv: EnvLike
770
+ runtimeEnv: EnvLike,
771
+ fetchImpl?: typeof fetch
294
772
  ) {
295
773
  let parsed: any;
296
774
  try {
@@ -298,59 +776,193 @@ async function handleConnectorRunMessage(
298
776
  } catch {
299
777
  return;
300
778
  }
301
- if (parsed?.type !== "agent.run" || typeof parsed.requestId !== "string") return;
302
- const agentConfig = parsed.payload?.agentConfig ?? {};
303
- try {
304
- if (agentConfig.apiSource !== "cli") {
305
- throw new Error("Connector can only execute CLI agents. Set the agent apiSource to cli and choose a cliProvider.");
306
- }
307
- const provider = String(agentConfig.cliProvider || "copilot");
308
- const policy = resolveMachineRunPermissionPolicy(agentConfig);
779
+ if (parsed?.type !== "agent.run" || typeof parsed.requestId !== "string") return;
780
+ const agentConfig = parsed.payload?.agentConfig ?? {};
781
+ const sendProgress = (progress: ConnectorRunProgress) => {
782
+ send(JSON.stringify({
783
+ type: "agent.run.progress",
784
+ requestId: parsed.requestId,
785
+ progress,
786
+ }));
787
+ };
788
+ try {
789
+ const machinePermissionPolicy = resolveMachineRunPermissionPolicy(agentConfig);
790
+ const runtimePolicy = runtimePolicyFromConnectorPayload(parsed);
309
791
  const userInput = String(parsed.payload?.userInput ?? "");
310
792
  const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
311
- assertMachineRunAllowed(userInput, policy);
312
- const cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy });
313
- const baseSha = await readConnectorGitHead(cwd);
314
- const result = await executeCli(
315
- provider,
316
- buildConnectorCliPrompt(agentConfig, userInput, {
793
+ let cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy: machinePermissionPolicy });
794
+ let runContent = "";
795
+ let runModel = agentConfig.model;
796
+ let runTrace: unknown[] = [];
797
+ let artifactCwd = cwd;
798
+ let baseSha: string | null = null;
799
+ if (agentConfig.apiSource === "cli") {
800
+ if (requestsTaskWorktree(runtimePolicy)) {
801
+ const prepared = await prepareTaskWorktree({
802
+ agentKey: String(parsed.payload?.agentKey ?? "agent"),
803
+ cwd,
804
+ env: runtimeEnv,
805
+ });
806
+ cwd = prepared.path;
807
+ artifactCwd = prepared.path;
808
+ }
809
+ const runtimePermissionPolicy = !hasExplicitMachinePermissions(agentConfig)
810
+ ? runtimeWorkspacePermissionPolicy(runtimePolicy, cwd)
811
+ : null;
812
+ const effectivePermissionPolicy = scopePermissionPolicyToRuntimeWorkspace(
813
+ runtimePermissionPolicy ?? machinePermissionPolicy,
814
+ runtimePolicy,
815
+ cwd,
816
+ );
817
+ assertMachineRunAllowed(userInput, effectivePermissionPolicy);
818
+ baseSha = await readConnectorGitHead(artifactCwd);
819
+ const provider = String(agentConfig.cliProvider || "copilot");
820
+ const builtPrompt = buildConnectorCliPrompt(agentConfig, userInput, {
317
821
  agentKey: String(parsed.payload?.agentKey ?? ""),
318
822
  runtimeContext: parsed.payload?.runtimeContext,
319
- }),
320
- {
321
- model: agentConfig.model || undefined,
322
- timeout,
823
+ }, effectivePermissionPolicy);
824
+ const materializedPrompt = materializeLargeConnectorPrompt({
825
+ prompt: builtPrompt,
323
826
  cwd,
324
- yolo: true,
325
- env: {
326
- NOLO_SERVER: runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL || runtimeEnv.BASE_URL,
327
- NOLO_SERVER_URL: runtimeEnv.NOLO_SERVER_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL,
328
- BASE_URL: runtimeEnv.BASE_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL,
329
- AUTH_TOKEN: runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH || runtimeEnv.NOLO_MACHINE_API_KEY,
330
- NOLO_MACHINE_API_KEY: runtimeEnv.NOLO_MACHINE_API_KEY || runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH,
827
+ env: runtimeEnv,
828
+ requestId: parsed.requestId,
829
+ runtimePromptPage: readRuntimePromptPageMeta(parsed),
830
+ });
831
+ const runtimePromptPage = readRuntimePromptPageMeta(parsed);
832
+ const startAt = Date.now();
833
+ const progressBase = {
834
+ provider,
835
+ requestId: parsed.requestId,
836
+ promptBytes: materializedPrompt.promptBytes,
837
+ promptHash: materializedPrompt.promptHash,
838
+ ...(materializedPrompt.promptRef ? { promptRef: materializedPrompt.promptRef } : {}),
839
+ ...(runtimePromptPage?.dbKey ? { promptPageKey: runtimePromptPage.dbKey } : {}),
840
+ ...(runtimePromptPage?.promptHash ? { promptPageHash: runtimePromptPage.promptHash } : {}),
841
+ workspaceRoot: artifactCwd,
842
+ workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
843
+ };
844
+ logConnectorRun("agent.run.cli.start", {
845
+ requestId: parsed.requestId,
846
+ agentKey: parsed.payload?.agentKey,
847
+ provider,
848
+ cwd,
849
+ artifactCwd,
850
+ timeout,
851
+ promptBytes: materializedPrompt.promptBytes,
852
+ promptHash: materializedPrompt.promptHash,
853
+ promptRef: materializedPrompt.promptRef,
854
+ promptPageKey: runtimePromptPage?.dbKey,
855
+ });
856
+ sendProgress({
857
+ ...progressBase,
858
+ message: "cli-started",
859
+ updatedAt: startAt,
860
+ });
861
+ const heartbeat = setInterval(() => {
862
+ sendProgress({
863
+ ...progressBase,
864
+ message: "cli-running",
865
+ updatedAt: Date.now(),
866
+ });
867
+ }, 15_000);
868
+ const result = await executeCli(
869
+ provider,
870
+ materializedPrompt.prompt,
871
+ {
872
+ model: agentConfig.model || undefined,
873
+ timeout,
874
+ cwd,
875
+ yolo: true,
876
+ env: {
877
+ NOLO_SERVER: runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL || runtimeEnv.BASE_URL,
878
+ NOLO_SERVER_URL: runtimeEnv.NOLO_SERVER_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL,
879
+ BASE_URL: runtimeEnv.BASE_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL,
880
+ AUTH_TOKEN: runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH || runtimeEnv.NOLO_MACHINE_API_KEY,
881
+ NOLO_MACHINE_API_KEY: runtimeEnv.NOLO_MACHINE_API_KEY || runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH,
882
+ },
883
+ }
884
+ ).finally(() => clearInterval(heartbeat));
885
+ logConnectorRun("agent.run.cli.result", {
886
+ requestId: parsed.requestId,
887
+ provider,
888
+ elapsed: result.elapsed,
889
+ outputBytes: byteLength(result.raw ?? result.text ?? ""),
890
+ });
891
+ runContent = result.text;
892
+ runModel = String(agentConfig.model || "").trim() || provider;
893
+ runTrace = [{ role: "assistant", content: result.text }];
894
+ const artifacts = await collectConnectorRunArtifact({
895
+ cwd: artifactCwd,
896
+ baseSha,
897
+ exitStatus: "completed",
898
+ });
899
+ const artifactProgress = buildArtifactProgress({
900
+ artifacts,
901
+ runtimePolicy,
902
+ workspaceRoot: artifactCwd,
903
+ workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
904
+ });
905
+ if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
906
+ send(JSON.stringify({
907
+ type: "agent.run.result",
908
+ requestId: parsed.requestId,
909
+ result: {
910
+ content: runContent,
911
+ model: runModel,
912
+ trace: runTrace,
913
+ artifacts,
331
914
  },
915
+ }));
916
+ return;
917
+ } else {
918
+ if (!runtimePolicy) {
919
+ throw new Error("Connector can only execute non-CLI agents when runtimeToolPolicySnapshot requests a local workspace runtime.");
332
920
  }
333
- );
921
+ baseSha = await readConnectorGitHead(cwd);
922
+ const result = await runConnectorLocalRuntimeAgent({
923
+ parsed,
924
+ runtimeEnv,
925
+ cwd,
926
+ fetchImpl,
927
+ onProgress: sendProgress,
928
+ });
929
+ runContent = result.content;
930
+ runModel = result.model;
931
+ runTrace = result.trace ?? [];
932
+ artifactCwd = result.runtimeWorkspaceRoot ?? cwd;
933
+ }
334
934
  const artifacts = await collectConnectorRunArtifact({
335
- cwd,
935
+ cwd: artifactCwd,
336
936
  baseSha,
337
937
  exitStatus: "completed",
338
938
  });
939
+ const artifactProgress = buildArtifactProgress({
940
+ artifacts,
941
+ runtimePolicy,
942
+ workspaceRoot: artifactCwd,
943
+ workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
944
+ });
945
+ if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
339
946
  send(JSON.stringify({
340
- type: "agent.run.result",
341
- requestId: parsed.requestId,
342
- result: {
343
- content: result.text,
344
- model: agentConfig.model ?? provider,
345
- trace: [{ role: "assistant", content: result.text }],
947
+ type: "agent.run.result",
948
+ requestId: parsed.requestId,
949
+ result: {
950
+ content: runContent,
951
+ model: runModel,
952
+ trace: runTrace,
346
953
  artifacts,
347
954
  },
348
955
  }));
349
- } catch (error) {
350
- send(JSON.stringify({
351
- type: "agent.run.result",
352
- requestId: parsed.requestId,
353
- error: error instanceof Error ? error.message : String(error),
956
+ } catch (error) {
957
+ logConnectorRun("agent.run.error", {
958
+ requestId: parsed.requestId,
959
+ agentKey: parsed.payload?.agentKey,
960
+ error: error instanceof Error ? error.message : String(error),
961
+ });
962
+ send(JSON.stringify({
963
+ type: "agent.run.result",
964
+ requestId: parsed.requestId,
965
+ error: error instanceof Error ? error.message : String(error),
354
966
  }));
355
967
  }
356
968
  }
@@ -385,6 +997,10 @@ async function runConnectorWebSocketSession(options: {
385
997
  headers: { Authorization: `Bearer ${options.authToken}` },
386
998
  fetchImpl: options.fetchImpl,
387
999
  });
1000
+ const runtimeServerUrl =
1001
+ options.env.NOLO_CONNECT_RUNTIME_SERVER_URL ||
1002
+ options.env.NOLO_RUNTIME_SERVER_URL ||
1003
+ options.serverUrl;
388
1004
  const websocketPromise = options.connectWebSocket(
389
1005
  wsTarget,
390
1006
  {
@@ -397,12 +1013,13 @@ async function runConnectorWebSocketSession(options: {
397
1013
  options.executeCli,
398
1014
  {
399
1015
  ...options.env,
400
- NOLO_SERVER: options.serverUrl,
401
- NOLO_SERVER_URL: options.serverUrl,
402
- BASE_URL: options.serverUrl,
1016
+ NOLO_SERVER: runtimeServerUrl,
1017
+ NOLO_SERVER_URL: runtimeServerUrl,
1018
+ BASE_URL: runtimeServerUrl,
403
1019
  AUTH_TOKEN: options.authToken,
404
1020
  NOLO_MACHINE_API_KEY: options.authToken,
405
- }
1021
+ },
1022
+ options.fetchImpl
406
1023
  ),
407
1024
  }
408
1025
  );
@@ -449,10 +1066,11 @@ export async function runMachineConnectCommand(
449
1066
  return 1;
450
1067
  }
451
1068
 
452
- const serverUrl = resolveConnectServerUrl(args, env);
453
- const fetchImpl = deps.fetchImpl ?? fetch;
454
- const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
455
- const sendHeartbeat = async () => {
1069
+ const serverUrl = resolveConnectServerUrl(args, env);
1070
+ const fetchImpl = deps.fetchImpl ?? fetch;
1071
+ const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
1072
+ const validateWorkspaceLinks = deps.validateWorkspaceLinks ?? validateConnectorWorkspaceLinks;
1073
+ const sendHeartbeat = async () => {
456
1074
  const res = await fetchImpl(`${serverUrl}/api/machines/heartbeat`, {
457
1075
  method: "POST",
458
1076
  headers: {
@@ -467,8 +1085,11 @@ export async function runMachineConnectCommand(
467
1085
  throw new Error(`HTTP ${res.status}\n${text}`);
468
1086
  }
469
1087
  };
470
-
1088
+
471
1089
  if (hasFlag(args, "--daemon") || hasFlag(args, "--background")) {
1090
+ if (!(await checkConnectorWorkspaceLinks(process.cwd(), output, validateWorkspaceLinks))) {
1091
+ return 1;
1092
+ }
472
1093
  const logPath = resolveDaemonLogPath(env);
473
1094
  const result = (deps.spawnDaemon ?? defaultSpawnDaemon)({
474
1095
  cmd: buildDaemonCommand(deps.cliEntrypointPath),
@@ -478,9 +1099,12 @@ export async function runMachineConnectCommand(
478
1099
  });
479
1100
  output.write(`Connector daemon started${result.pid ? ` pid=${result.pid}` : ""}. Log: ${logPath}\n`);
480
1101
  return 0;
481
- }
482
-
1102
+ }
1103
+
483
1104
  if (hasFlag(args, "--ws")) {
1105
+ if (!(await checkConnectorWorkspaceLinks(process.cwd(), output, validateWorkspaceLinks))) {
1106
+ return 1;
1107
+ }
484
1108
  const maxAttempts = deps.maxConnectorAttempts ?? Infinity;
485
1109
  const sleep = deps.sleep ?? defaultSleep;
486
1110
  const reconnectDelayMs = resolveConnectorReconnectDelayMs(env);