nolo-cli 0.1.23 → 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 (76) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/hybridRecordStore.ts +30 -9
  3. package/agent-runtime/localLoop.ts +12 -4
  4. package/agent-runtime/localToolPolicy.ts +22 -10
  5. package/agent-runtime/localWorkspaceTools.ts +140 -374
  6. package/agent-runtime/runtimeToolPolicy.ts +43 -2
  7. package/agent-runtime/types.ts +5 -0
  8. package/agentAliases.ts +1 -1
  9. package/agentCommandSupport.ts +77 -0
  10. package/agentInternalCommandEntries.ts +34 -0
  11. package/agentListCommands.ts +123 -0
  12. package/agentListHelpers.ts +187 -0
  13. package/agentMachineCommands.ts +402 -0
  14. package/agentRecordCommands.ts +141 -0
  15. package/agentRecordHelpers.ts +395 -0
  16. package/agentRunCommand.ts +1 -2
  17. package/agentRuntimeCommands.ts +7 -701
  18. package/ai/agent/_executeModel.ts +29 -3
  19. package/ai/agent/buildSystemPrompt.ts +17 -0
  20. package/ai/agent/cliChatClient.ts +38 -4
  21. package/ai/agent/cliExecutor.ts +304 -25
  22. package/ai/agent/createAgentSchema.ts +2 -2
  23. package/ai/agent/guidedCreation/assistant.ts +82 -0
  24. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  25. package/ai/agent/guidedCreation/draft.ts +62 -0
  26. package/ai/agent/guidedCreation/types.ts +38 -0
  27. package/ai/agent/machineRunPermissions.ts +21 -3
  28. package/ai/agent/runtimeGuidance.ts +1 -1
  29. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  30. package/ai/agent/streamAgentChatTurn.ts +492 -19
  31. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  32. package/ai/ai.locale.ts +14 -2
  33. package/ai/chat/sendOpenAICompletionsRequest.ts +28 -3
  34. package/ai/chat/sendOpenAIResponseRequest.ts +3 -2
  35. package/ai/llm/getPricing.ts +14 -0
  36. package/ai/llm/kimi.ts +3 -0
  37. package/ai/llm/modelAvatar.ts +1 -0
  38. package/ai/llm/providers.ts +1 -1
  39. package/ai/taskRun/taskRunProtocol.ts +24 -10
  40. package/ai/token/modelUsageQuery.ts +17 -0
  41. package/ai/tools/agent/agentTools.ts +21 -0
  42. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  43. package/ai/tools/checkEnvTool.ts +52 -20
  44. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  45. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  46. package/ai/tools/index.ts +24 -14
  47. package/ai/tools/marxistsOfflineBook.ts +253 -0
  48. package/ai/tools/modelUsageTools.ts +4 -2
  49. package/ai/tools/readTool.ts +31 -0
  50. package/ai/tools/toolApiClient.ts +35 -1
  51. package/ai/tools/toolNameAliases.ts +13 -0
  52. package/cliCommandAdapters.ts +28 -0
  53. package/cliCommandDispatch.ts +66 -0
  54. package/cliCommandFactories.ts +34 -0
  55. package/cliCommandTypes.ts +49 -0
  56. package/cliEnvHelpers.ts +44 -0
  57. package/client/agentRun.test.ts +123 -19
  58. package/client/agentRun.ts +40 -1
  59. package/client/hybridRecordStore.test.ts +42 -0
  60. package/client/hybridRecordStore.ts +11 -0
  61. package/client/localRuntimeAdapter.test.ts +189 -21
  62. package/client/localRuntimeAdapter.ts +84 -8
  63. package/client/localRuntimeDryRun.test.ts +5 -2
  64. package/client/taskRunPrompt.ts +1 -0
  65. package/commandRegistry.ts +11 -61
  66. package/connector-experimental/capabilities.ts +1 -0
  67. package/database/server/coreDataProxy.ts +43 -26
  68. package/index.ts +54 -191
  69. package/internalCommandEntries.ts +12 -0
  70. package/machineCommands.ts +301 -32
  71. package/offlineMarxistsAgentCommand.ts +314 -0
  72. package/package.json +20 -2
  73. package/runtimeDoctorCommands.ts +118 -0
  74. package/scriptCommandEntries.ts +71 -0
  75. package/systemInternalCommandEntries.ts +71 -0
  76. package/workflowInternalCommandEntries.ts +11 -0
@@ -1,9 +1,10 @@
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";
7
8
  import { runLocalAgentTurn, type LocalAgentToolEvent } from "./agent-runtime/localLoop";
8
9
  import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
9
10
  import {
@@ -55,6 +56,13 @@ type ConnectorRunProgress = {
55
56
  toolCallCount?: number;
56
57
  toolResultCount?: number;
57
58
  lastToolNames?: string[];
59
+ provider?: string;
60
+ promptBytes?: number;
61
+ promptHash?: string;
62
+ promptRef?: string;
63
+ promptPageKey?: string;
64
+ promptPageHash?: string;
65
+ requestId?: string;
58
66
  workspaceRoot?: string;
59
67
  workspaceKind?: string;
60
68
  message?: string;
@@ -82,15 +90,26 @@ type MachineCommandDeps = {
82
90
  fetchImpl?: typeof fetch;
83
91
  machineInfo?: () => MachineHeartbeat;
84
92
  runHeartbeatLoop?: (options: HeartbeatLoopOptions) => Promise<void>;
85
- connectWebSocket?: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
86
- executeCli?: LocalCliExecutor;
87
- spawnDaemon?: (args: {
93
+ connectWebSocket?: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
94
+ executeCli?: LocalCliExecutor;
95
+ validateWorkspaceLinks?: (cwd: string) => string[] | Promise<string[]>;
96
+ spawnDaemon?: (args: {
88
97
  cmd: string[];
89
98
  cwd: string;
90
99
  env: EnvLike;
91
100
  logPath: string;
92
101
  }) => { pid?: number };
93
- };
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
+ ]);
94
113
 
95
114
  function resolveServerUrl(env: EnvLike) {
96
115
  return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
@@ -106,9 +125,113 @@ function writeAuthMissing(output: OutputLike) {
106
125
  );
107
126
  }
108
127
 
109
- function hasFlag(args: string[], flag: string) {
110
- return args.includes(flag);
111
- }
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
+ }
112
235
 
113
236
  function readOption(args: string[], flag: string) {
114
237
  for (let i = 0; i < args.length; i++) {
@@ -139,6 +262,90 @@ function resolveDaemonLogPath(env: EnvLike) {
139
262
  return env.NOLO_CONNECT_LOG || join(homedir(), ".nolo", "logs", "connector.log");
140
263
  }
141
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
+
142
349
  function buildDaemonCommand(cliEntrypointPath: string | undefined) {
143
350
  return [process.execPath, cliEntrypointPath || import.meta.path, "connect", "--ws"];
144
351
  }
@@ -272,6 +479,7 @@ function buildTaskRunBridgePrompt(args: {
272
479
  "Run commands from the repository root. The runner already provides server URL and auth in the environment.",
273
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.",
274
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.",
275
483
  `Read context: bun packages/cli/index.ts task-run read-context --row-dbkey ${JSON.stringify(rowDbKey)}`,
276
484
  ...(workItemId
277
485
  ? [
@@ -416,11 +624,9 @@ function buildArtifactProgress(args: {
416
624
  const runtimeTools = Array.isArray(args.runtimePolicy?.runtimeTools)
417
625
  ? args.runtimePolicy.runtimeTools
418
626
  : [];
419
- const shellTool = runtimeTools.includes("execBash")
420
- ? "execBash"
421
- : runtimeTools.includes("execShell")
422
- ? "execShell"
423
- : "workspaceArtifact";
627
+ const shellTool = runtimeTools.includes("execShell")
628
+ ? "execShell"
629
+ : "workspaceArtifact";
424
630
  return {
425
631
  eventType: "tool-result",
426
632
  toolCallCount: 1,
@@ -611,12 +817,57 @@ export async function handleConnectorRunMessage(
611
817
  assertMachineRunAllowed(userInput, effectivePermissionPolicy);
612
818
  baseSha = await readConnectorGitHead(artifactCwd);
613
819
  const provider = String(agentConfig.cliProvider || "copilot");
820
+ const builtPrompt = buildConnectorCliPrompt(agentConfig, userInput, {
821
+ agentKey: String(parsed.payload?.agentKey ?? ""),
822
+ runtimeContext: parsed.payload?.runtimeContext,
823
+ }, effectivePermissionPolicy);
824
+ const materializedPrompt = materializeLargeConnectorPrompt({
825
+ prompt: builtPrompt,
826
+ cwd,
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);
614
868
  const result = await executeCli(
615
869
  provider,
616
- buildConnectorCliPrompt(agentConfig, userInput, {
617
- agentKey: String(parsed.payload?.agentKey ?? ""),
618
- runtimeContext: parsed.payload?.runtimeContext,
619
- }, effectivePermissionPolicy),
870
+ materializedPrompt.prompt,
620
871
  {
621
872
  model: agentConfig.model || undefined,
622
873
  timeout,
@@ -630,9 +881,15 @@ export async function handleConnectorRunMessage(
630
881
  NOLO_MACHINE_API_KEY: runtimeEnv.NOLO_MACHINE_API_KEY || runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH,
631
882
  },
632
883
  }
633
- );
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
+ });
634
891
  runContent = result.text;
635
- runModel = agentConfig.model ?? provider;
892
+ runModel = String(agentConfig.model || "").trim() || provider;
636
893
  runTrace = [{ role: "assistant", content: result.text }];
637
894
  const artifacts = await collectConnectorRunArtifact({
638
895
  cwd: artifactCwd,
@@ -696,11 +953,16 @@ export async function handleConnectorRunMessage(
696
953
  artifacts,
697
954
  },
698
955
  }));
699
- } catch (error) {
700
- send(JSON.stringify({
701
- type: "agent.run.result",
702
- requestId: parsed.requestId,
703
- 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),
704
966
  }));
705
967
  }
706
968
  }
@@ -804,10 +1066,11 @@ export async function runMachineConnectCommand(
804
1066
  return 1;
805
1067
  }
806
1068
 
807
- const serverUrl = resolveConnectServerUrl(args, env);
808
- const fetchImpl = deps.fetchImpl ?? fetch;
809
- const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
810
- 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 () => {
811
1074
  const res = await fetchImpl(`${serverUrl}/api/machines/heartbeat`, {
812
1075
  method: "POST",
813
1076
  headers: {
@@ -822,8 +1085,11 @@ export async function runMachineConnectCommand(
822
1085
  throw new Error(`HTTP ${res.status}\n${text}`);
823
1086
  }
824
1087
  };
825
-
1088
+
826
1089
  if (hasFlag(args, "--daemon") || hasFlag(args, "--background")) {
1090
+ if (!(await checkConnectorWorkspaceLinks(process.cwd(), output, validateWorkspaceLinks))) {
1091
+ return 1;
1092
+ }
827
1093
  const logPath = resolveDaemonLogPath(env);
828
1094
  const result = (deps.spawnDaemon ?? defaultSpawnDaemon)({
829
1095
  cmd: buildDaemonCommand(deps.cliEntrypointPath),
@@ -833,9 +1099,12 @@ export async function runMachineConnectCommand(
833
1099
  });
834
1100
  output.write(`Connector daemon started${result.pid ? ` pid=${result.pid}` : ""}. Log: ${logPath}\n`);
835
1101
  return 0;
836
- }
837
-
1102
+ }
1103
+
838
1104
  if (hasFlag(args, "--ws")) {
1105
+ if (!(await checkConnectorWorkspaceLinks(process.cwd(), output, validateWorkspaceLinks))) {
1106
+ return 1;
1107
+ }
839
1108
  const maxAttempts = deps.maxConnectorAttempts ?? Infinity;
840
1109
  const sleep = deps.sleep ?? defaultSleep;
841
1110
  const reconnectDelayMs = resolveConnectorReconnectDelayMs(env);