nolo-cli 0.1.18 → 0.1.20

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 (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +14 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +78 -6
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +2 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +289 -54
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +823 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +361 -7
  79. package/client/agentRun.ts +449 -63
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +813 -20
  89. package/client/localRuntimeAdapter.ts +279 -232
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +237 -0
@@ -0,0 +1,181 @@
1
+ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
2
+
3
+ type EnvLike = Record<string, string | undefined>;
4
+ type OutputLike = { write(chunk: string): unknown };
5
+
6
+ type TableCommandDeps = {
7
+ env?: EnvLike;
8
+ output?: OutputLike;
9
+ fetchImpl?: typeof fetch;
10
+ };
11
+
12
+ type OutputMode = "full" | "raw" | "items" | "jsonl";
13
+
14
+ function readOption(args: string[], flag: string): string {
15
+ for (let i = 0; i < args.length; i++) {
16
+ const value = args[i];
17
+ if (value === flag) return args[i + 1] ?? "";
18
+ if (value.startsWith(`${flag}=`)) return value.slice(flag.length + 1);
19
+ }
20
+ return "";
21
+ }
22
+
23
+ function hasFlag(args: string[], flag: string): boolean {
24
+ return args.includes(flag);
25
+ }
26
+
27
+ function parseJsonOption<T>(args: string[], flag: string): T | undefined {
28
+ const raw = readOption(args, flag);
29
+ if (!raw) return undefined;
30
+ try {
31
+ return JSON.parse(raw) as T;
32
+ } catch (error) {
33
+ throw new Error(`${flag} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
34
+ }
35
+ }
36
+
37
+ function parseOutputMode(raw: string): OutputMode {
38
+ if (!raw) return "full";
39
+ if (raw === "full" || raw === "raw" || raw === "items" || raw === "jsonl") return raw;
40
+ throw new Error(`--output must be one of full, raw, items, jsonl; got ${raw}`);
41
+ }
42
+
43
+ function parseTableArg(raw: string): { tenantId?: string; tableId?: string } {
44
+ const trimmed = raw.trim();
45
+ if (!trimmed) return {};
46
+ if (!trimmed.startsWith("meta-")) return { tableId: trimmed };
47
+ const parts = trimmed.split("-");
48
+ return {
49
+ tenantId: parts[1],
50
+ tableId: parts.slice(2).join("-"),
51
+ };
52
+ }
53
+
54
+ function resolveServerUrl(args: string[], env: EnvLike): string {
55
+ return (
56
+ readOption(args, "--server-url") ||
57
+ readOption(args, "--server") ||
58
+ readOption(args, "--base-url") ||
59
+ env.NOLO_SERVER_URL ||
60
+ env.NOLO_SERVER ||
61
+ env.BASE_URL ||
62
+ DEFAULT_NOLO_SERVER_URL
63
+ ).replace(/\/+$/, "");
64
+ }
65
+
66
+ function resolveAuthToken(args: string[], env: EnvLike): string {
67
+ return (
68
+ readOption(args, "--api-key") ||
69
+ readOption(args, "--token") ||
70
+ env.AUTH_TOKEN ||
71
+ env.AUTH ||
72
+ ""
73
+ );
74
+ }
75
+
76
+ function parseJwtTenantId(token: string): string | undefined {
77
+ const [, payload] = token.split(".");
78
+ if (!payload) return undefined;
79
+ try {
80
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
81
+ const decoded = Buffer.from(normalized, "base64").toString("utf8");
82
+ const parsed = JSON.parse(decoded);
83
+ return typeof parsed.userId === "string" ? parsed.userId : typeof parsed.sub === "string" ? parsed.sub : undefined;
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ }
88
+
89
+ function formatOutput(input: { envelope: any; rawData: any; mode: OutputMode }): string {
90
+ if (input.mode === "full") return JSON.stringify(input.envelope, null, 2);
91
+ if (input.mode === "raw") return JSON.stringify(input.rawData, null, 2);
92
+ const items = Array.isArray(input.rawData?.items) ? input.rawData.items : input.rawData;
93
+ if (input.mode === "items") return JSON.stringify(items, null, 2);
94
+ return Array.isArray(items) ? items.map((item) => JSON.stringify(item)).join("\n") : JSON.stringify(items);
95
+ }
96
+
97
+ function usage(): string {
98
+ return [
99
+ "Usage:",
100
+ " nolo table query --table <tableId|metaKey> [--tenant-id <userId>] [--filters <json>] [--columns <json-array>] [--no-base-fields] [--output full|raw|items|jsonl]",
101
+ "",
102
+ ].join("\n");
103
+ }
104
+
105
+ export async function runTableQueryCommand(args: string[], deps: TableCommandDeps = {}): Promise<number> {
106
+ const env = deps.env ?? process.env;
107
+ const output = deps.output ?? process.stdout;
108
+ const fetchImpl = deps.fetchImpl ?? fetch;
109
+ if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
110
+ output.write(usage());
111
+ return 0;
112
+ }
113
+
114
+ let outputMode: OutputMode;
115
+ let columns: string[] | undefined;
116
+ let filters: Record<string, unknown> | undefined;
117
+ try {
118
+ outputMode = parseOutputMode(readOption(args, "--output"));
119
+ columns = parseJsonOption<string[]>(args, "--columns");
120
+ filters = parseJsonOption<Record<string, unknown>>(args, "--filters");
121
+ } catch (error) {
122
+ output.write(`[nolo] table query failed: ${error instanceof Error ? error.message : String(error)}\n`);
123
+ return 1;
124
+ }
125
+
126
+ const tableArg = parseTableArg(readOption(args, "--table"));
127
+ const authToken = resolveAuthToken(args, env);
128
+ const tenantId = readOption(args, "--tenant-id") || tableArg.tenantId || env.USER_ID || parseJwtTenantId(authToken);
129
+ const tableId = tableArg.tableId;
130
+ if (!tenantId || !tableId) {
131
+ output.write(usage());
132
+ return 1;
133
+ }
134
+ if (!authToken) {
135
+ output.write("[nolo] table query failed: AUTH_TOKEN is required.\n");
136
+ return 1;
137
+ }
138
+
139
+ const serverUrl = resolveServerUrl(args, env);
140
+ let response: Response;
141
+ try {
142
+ response = await fetchImpl(`${serverUrl}/api/table/query-rows`, {
143
+ method: "POST",
144
+ headers: {
145
+ Authorization: `Bearer ${authToken}`,
146
+ "Content-Type": "application/json",
147
+ },
148
+ body: JSON.stringify({
149
+ tenantId,
150
+ tableId,
151
+ filters: filters ?? {},
152
+ columns,
153
+ includeBaseFields: !hasFlag(args, "--no-base-fields"),
154
+ limit: Number(readOption(args, "--limit") || 20),
155
+ offset: Number(readOption(args, "--offset") || 0),
156
+ sortBy: readOption(args, "--sort-by") || "updatedAt",
157
+ sortOrder: readOption(args, "--sort-order") === "asc" ? "asc" : "desc",
158
+ }),
159
+ });
160
+ } catch (error) {
161
+ output.write(`[nolo] table query failed: ${error instanceof Error ? error.message : String(error)}\n`);
162
+ return 1;
163
+ }
164
+
165
+ const text = await response.text();
166
+ let payload: any;
167
+ try {
168
+ payload = text ? JSON.parse(text) : {};
169
+ } catch {
170
+ output.write(text ? `${text}\n` : "");
171
+ return response.ok ? 0 : 1;
172
+ }
173
+ if (!response.ok || payload?.error) {
174
+ output.write(`[nolo] table query failed: ${payload?.error ?? response.statusText}\n`);
175
+ return 1;
176
+ }
177
+
178
+ const rawData = payload.rawData ?? payload;
179
+ output.write(`${formatOutput({ envelope: payload, rawData, mode: outputMode })}\n`);
180
+ return 0;
181
+ }
@@ -0,0 +1,237 @@
1
+ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ type EnvLike = Record<string, string | undefined>;
7
+ type OutputLike = { write(chunk: string): unknown };
8
+
9
+ type TaskRunCommandDeps = {
10
+ env?: EnvLike;
11
+ output?: OutputLike;
12
+ fetchImpl?: typeof fetch;
13
+ };
14
+
15
+ function parseEnvFile(path: string): EnvLike {
16
+ if (!path || !existsSync(path)) return {};
17
+ const parsed: EnvLike = {};
18
+ for (const rawLine of readFileSync(path, "utf8").split(/\r?\n/)) {
19
+ const line = rawLine.trim();
20
+ if (!line || line.startsWith("#")) continue;
21
+ const index = line.indexOf("=");
22
+ if (index <= 0) continue;
23
+ const key = line.slice(0, index).trim();
24
+ let value = line.slice(index + 1).trim();
25
+ if (
26
+ (value.startsWith('"') && value.endsWith('"')) ||
27
+ (value.startsWith("'") && value.endsWith("'"))
28
+ ) {
29
+ value = value.slice(1, -1);
30
+ }
31
+ if (key) parsed[key] = value;
32
+ }
33
+ return parsed;
34
+ }
35
+
36
+ function loadTaskRunEnv(env: EnvLike, options: { includeDefaultEnvFiles?: boolean } = {}) {
37
+ const envFileCandidates = [
38
+ env.NOLO_TASK_RUN_ENV_FILE,
39
+ env.NOLO_MACHINE_ENV_FILE,
40
+ ...(options.includeDefaultEnvFiles
41
+ ? ["/etc/nolo/machine-runner.env", join(homedir(), ".nolo", "machine-runner.env")]
42
+ : []),
43
+ ].filter((path): path is string => Boolean(path));
44
+ const fileEnv = Object.assign({}, ...envFileCandidates.map(parseEnvFile));
45
+ return { ...fileEnv, ...env };
46
+ }
47
+
48
+ function readOption(args: string[], flag: string) {
49
+ for (let i = 0; i < args.length; i++) {
50
+ const value = args[i];
51
+ if (value === flag) return args[i + 1] ?? "";
52
+ if (value.startsWith(`${flag}=`)) return value.slice(flag.length + 1);
53
+ }
54
+ return "";
55
+ }
56
+
57
+ function csv(raw: string) {
58
+ return raw.split(",").map((item) => item.trim()).filter(Boolean);
59
+ }
60
+
61
+ function resolveServerUrl(args: string[], env: EnvLike) {
62
+ return (
63
+ readOption(args, "--server-url") ||
64
+ readOption(args, "--server") ||
65
+ readOption(args, "--base-url") ||
66
+ env.NOLO_SERVER_URL ||
67
+ env.NOLO_SERVER ||
68
+ env.BASE_URL ||
69
+ DEFAULT_NOLO_SERVER_URL
70
+ ).replace(/\/+$/, "");
71
+ }
72
+
73
+ function resolveAuthToken(args: string[], env: EnvLike) {
74
+ return (
75
+ readOption(args, "--api-key") ||
76
+ readOption(args, "--token") ||
77
+ env.NOLO_TASK_RUN_TOKEN ||
78
+ env.NOLO_MACHINE_API_KEY ||
79
+ env.AUTH_TOKEN ||
80
+ env.AUTH ||
81
+ ""
82
+ );
83
+ }
84
+
85
+ function required(args: string[], flag: string) {
86
+ const value = readOption(args, flag).trim();
87
+ if (!value) throw new Error(`${flag} is required`);
88
+ return value;
89
+ }
90
+
91
+ function optional(args: string[], flag: string) {
92
+ const value = readOption(args, flag).trim();
93
+ return value || undefined;
94
+ }
95
+
96
+ function buildBody(command: string, args: string[]) {
97
+ const dbKey = required(args, "--row-dbkey");
98
+ if (command === "read-context" || command === "readTaskContext") {
99
+ return { action: "readTaskContext", dbKey };
100
+ }
101
+ if (command === "claim-work-item" || command === "claimWorkItem") {
102
+ return {
103
+ action: "claimWorkItem",
104
+ dbKey,
105
+ workItemId: required(args, "--work-item-id"),
106
+ agentKey: required(args, "--agent-key"),
107
+ role: optional(args, "--role"),
108
+ };
109
+ }
110
+ if (command === "set-blocker" || command === "setBlocker") {
111
+ return {
112
+ action: "setBlocker",
113
+ dbKey,
114
+ workItemId: optional(args, "--work-item-id"),
115
+ blocker: {
116
+ layer: required(args, "--layer"),
117
+ code: required(args, "--code"),
118
+ message: required(args, "--message"),
119
+ },
120
+ };
121
+ }
122
+ if (command === "clear-blocker" || command === "clearBlocker") {
123
+ return {
124
+ action: "clearBlocker",
125
+ dbKey,
126
+ blockerId: required(args, "--blocker-id"),
127
+ workItemId: optional(args, "--work-item-id"),
128
+ };
129
+ }
130
+ if (command === "submit-outcome" || command === "submitOutcome") {
131
+ return {
132
+ action: "submitOutcome",
133
+ dbKey,
134
+ workItemId: required(args, "--work-item-id"),
135
+ summary: required(args, "--summary"),
136
+ ...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
137
+ ...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
138
+ };
139
+ }
140
+ if (command === "request-review" || command === "requestReview") {
141
+ return {
142
+ action: "requestReview",
143
+ dbKey,
144
+ workItemId: optional(args, "--work-item-id"),
145
+ reviewerAgentKey: required(args, "--reviewer-agent-key"),
146
+ ...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
147
+ ...(optional(args, "--message") ? { message: optional(args, "--message") } : {}),
148
+ ...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
149
+ };
150
+ }
151
+ if (command === "record-review" || command === "recordReview") {
152
+ return {
153
+ action: "recordReview",
154
+ dbKey,
155
+ ...(optional(args, "--work-item-id") ? { workItemId: optional(args, "--work-item-id") } : {}),
156
+ reviewStatus: required(args, "--review-status"),
157
+ ...(optional(args, "--reviewer-agent-key")
158
+ ? { reviewerAgentKey: optional(args, "--reviewer-agent-key") }
159
+ : {}),
160
+ ...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
161
+ ...(optional(args, "--findings") ? { findings: optional(args, "--findings") } : {}),
162
+ ...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
163
+ };
164
+ }
165
+ if (command === "close-work-item" || command === "complete-work-item" || command === "closeWorkItem" || command === "completeWorkItem") {
166
+ return {
167
+ action: "closeWorkItem",
168
+ dbKey,
169
+ workItemId: required(args, "--work-item-id"),
170
+ summary: required(args, "--summary"),
171
+ ...(optional(args, "--reason") ? { reason: optional(args, "--reason") } : {}),
172
+ ...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
173
+ ...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
174
+ };
175
+ }
176
+ throw new Error(`Unknown task-run command: ${command}`);
177
+ }
178
+
179
+ function usage() {
180
+ return [
181
+ "Usage:",
182
+ " nolo task-run read-context --row-dbkey <row>",
183
+ " nolo task-run claim-work-item --row-dbkey <row> --work-item-id <id> --agent-key <agent> [--role <role>]",
184
+ " nolo task-run set-blocker --row-dbkey <row> [--work-item-id <id>] --layer <layer> --code <code> --message <message>",
185
+ " nolo task-run clear-blocker --row-dbkey <row> --blocker-id <id> [--work-item-id <id>]",
186
+ " nolo task-run submit-outcome --row-dbkey <row> --work-item-id <id> --summary <summary> [--dialog-id <id>]",
187
+ " nolo task-run request-review --row-dbkey <row> --reviewer-agent-key <agent> [--work-item-id <id>]",
188
+ " nolo task-run record-review --row-dbkey <row> [--work-item-id <id>] --review-status <passed|needs_changes|blocked> [--findings <text>]",
189
+ " nolo task-run close-work-item --row-dbkey <row> --work-item-id <id> --summary <summary>",
190
+ "",
191
+ ].join("\n");
192
+ }
193
+
194
+ export async function runTaskRunCommand(args: string[], deps: TaskRunCommandDeps = {}) {
195
+ const env = loadTaskRunEnv(deps.env ?? process.env, { includeDefaultEnvFiles: deps.env === undefined });
196
+ const output = deps.output ?? process.stdout;
197
+ const fetchImpl = deps.fetchImpl ?? fetch;
198
+ const command = args[0];
199
+ if (!command || command === "--help" || command === "-h") {
200
+ output.write(usage());
201
+ return 0;
202
+ }
203
+
204
+ let body: Record<string, unknown>;
205
+ try {
206
+ body = buildBody(command, args.slice(1));
207
+ } catch (error) {
208
+ output.write(`[nolo] task-run failed: ${error instanceof Error ? error.message : String(error)}\n`);
209
+ return 1;
210
+ }
211
+
212
+ const authToken = resolveAuthToken(args, env);
213
+ if (!authToken) {
214
+ output.write("[nolo] task-run failed: AUTH_TOKEN or NOLO_MACHINE_API_KEY is required.\n");
215
+ return 1;
216
+ }
217
+
218
+ const serverUrl = resolveServerUrl(args, env);
219
+ let response: Response;
220
+ try {
221
+ response = await fetchImpl(`${serverUrl}/api/task-run`, {
222
+ method: "POST",
223
+ headers: {
224
+ Authorization: `Bearer ${authToken}`,
225
+ "Content-Type": "application/json",
226
+ },
227
+ body: JSON.stringify(body),
228
+ });
229
+ } catch (error) {
230
+ output.write(`[nolo] task-run failed: ${error instanceof Error ? error.message : String(error)}\n`);
231
+ return 1;
232
+ }
233
+
234
+ const text = await response.text();
235
+ output.write(text.trim() ? `${text.trim()}\n` : "");
236
+ return response.ok ? 0 : 1;
237
+ }