@northflare/runner 0.0.23 → 0.0.25

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 (37) hide show
  1. package/README.md +3 -3
  2. package/bin/northflare-runner +234 -172
  3. package/dist/components/claude-sdk-manager.d.ts +6 -0
  4. package/dist/components/claude-sdk-manager.d.ts.map +1 -1
  5. package/dist/components/claude-sdk-manager.js +46 -5
  6. package/dist/components/claude-sdk-manager.js.map +1 -1
  7. package/dist/components/message-handler-sse.d.ts +3 -0
  8. package/dist/components/message-handler-sse.d.ts.map +1 -1
  9. package/dist/components/message-handler-sse.js +148 -72
  10. package/dist/components/message-handler-sse.js.map +1 -1
  11. package/dist/components/repository-manager.js +4 -4
  12. package/dist/components/repository-manager.js.map +1 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +49 -14
  16. package/dist/index.js.map +1 -1
  17. package/dist/runner-sse.d.ts +10 -6
  18. package/dist/runner-sse.d.ts.map +1 -1
  19. package/dist/runner-sse.js +130 -109
  20. package/dist/runner-sse.js.map +1 -1
  21. package/dist/services/RunnerAPIClient.d.ts +20 -0
  22. package/dist/services/RunnerAPIClient.d.ts.map +1 -1
  23. package/dist/services/RunnerAPIClient.js +50 -1
  24. package/dist/services/RunnerAPIClient.js.map +1 -1
  25. package/dist/services/SSEClient.d.ts.map +1 -1
  26. package/dist/services/SSEClient.js +22 -0
  27. package/dist/services/SSEClient.js.map +1 -1
  28. package/dist/types/index.d.ts +4 -2
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/runner-interface.d.ts +4 -1
  31. package/dist/types/runner-interface.d.ts.map +1 -1
  32. package/dist/utils/config.d.ts +7 -1
  33. package/dist/utils/config.d.ts.map +1 -1
  34. package/dist/utils/config.js +185 -62
  35. package/dist/utils/config.js.map +1 -1
  36. package/docs/claude-manager.md +1 -1
  37. package/package.json +2 -2
package/README.md CHANGED
@@ -32,7 +32,7 @@ export NORTHFLARE_RUNNER_TOKEN="your-auth-token"
32
32
 
33
33
  ```bash
34
34
  # Root directory for Git checkouts (optional, defaults to /workspace)
35
- export NORTHFLARE_WORKSPACE_DIR="/path/to/workspace/root"
35
+ export NORTHFLARE_WORKSPACE_PATH="/path/to/workspace/root"
36
36
 
37
37
  # Orchestrator API endpoint, for use when self-hosting
38
38
  export NORTHFLARE_ORCHESTRATOR_URL="https://your-orchestrator-url"
@@ -51,7 +51,7 @@ npx @northflare/runner start --config runner-config.json
51
51
  npx @northflare/runner start --debug
52
52
 
53
53
  # Override workspace directory
54
- npx @northflare/runner start --workspace-dir /custom/workspace
54
+ npx @northflare/runner start --workspace-path /custom/workspace
55
55
 
56
56
  ```
57
57
 
@@ -91,7 +91,7 @@ somewhere else:
91
91
  | Variable | Required | Description |
92
92
  | ----------------------------- | -------- | ------------------------------------------------------ |
93
93
  | `NORTHFLARE_RUNNER_TOKEN` | Yes | Authentication token from server |
94
- | `NORTHFLARE_WORKSPACE_DIR` | No | Root directory for Git checkouts (default: env-paths app data dir) |
94
+ | `NORTHFLARE_WORKSPACE_PATH` | No | Root directory for Git checkouts |
95
95
  | `NORTHFLARE_ORCHESTRATOR_URL` | Yes | Orchestrator API endpoint |
96
96
  | `NORTHFLARE_DATA_DIR` | No | Override data directory (default: env-paths app data dir) |
97
97
  | `DEBUG` | No | Enable debug logging |
@@ -9,6 +9,7 @@ import path from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import { createRequire } from "module";
11
11
  import fs from "fs/promises";
12
+ import { existsSync } from "fs";
12
13
  import crypto from "crypto";
13
14
  import envPaths from "env-paths";
14
15
 
@@ -16,8 +17,26 @@ import envPaths from "env-paths";
16
17
  const require = createRequire(import.meta.url);
17
18
  const pkg = require("../package.json");
18
19
 
19
- const ENV_PATHS_APP_NAME = "northflare-runner";
20
+ function resolveEnvPathsAppName() {
21
+ if (process.env["NODE_ENV"] === "development") return "northflare-runner-dev";
22
+
23
+ // Heuristic: treat as local workspace when pnpm-workspace.yaml sits two levels up (repo root)
24
+ try {
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+ const repoRoot = path.resolve(__dirname, "../..");
27
+ if (existsSync(path.join(repoRoot, "pnpm-workspace.yaml"))) {
28
+ return "northflare-runner-dev";
29
+ }
30
+ } catch {
31
+ // ignore and fall back
32
+ }
33
+
34
+ return "northflare-runner";
35
+ }
36
+
37
+ const ENV_PATHS_APP_NAME = resolveEnvPathsAppName();
20
38
  const ENV_PATHS_OPTIONS = { suffix: "" };
39
+ const IS_DEV_APP = ENV_PATHS_APP_NAME.endsWith("-dev");
21
40
 
22
41
  let DEFAULT_DATA_DIR;
23
42
  try {
@@ -28,7 +47,7 @@ try {
28
47
  error
29
48
  );
30
49
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
- DEFAULT_DATA_DIR = path.resolve(__dirname, "../data");
50
+ DEFAULT_DATA_DIR = path.resolve(__dirname, IS_DEV_APP ? "../data/dev" : "../data");
32
51
  }
33
52
 
34
53
  /**
@@ -73,8 +92,8 @@ const startCommand = new Command("start")
73
92
  )
74
93
  .option("--data-dir <path>", "override data directory", DEFAULT_DATA_DIR)
75
94
  .option(
76
- "--workspace-dir <path>",
77
- "workspace directory for Git checkouts (default: uses env-paths data directory)"
95
+ "--workspace-path <path>",
96
+ "workspace directory for Git checkouts"
78
97
  )
79
98
  .option("--token <token>", "authentication token")
80
99
  .option(
@@ -100,8 +119,8 @@ const startCommand = new Command("start")
100
119
  if (options.dataDir) {
101
120
  process.env.NORTHFLARE_DATA_DIR = options.dataDir;
102
121
  }
103
- if (options.workspaceDir) {
104
- process.env.NORTHFLARE_WORKSPACE_DIR = options.workspaceDir;
122
+ if (options.workspacePath) {
123
+ process.env.NORTHFLARE_WORKSPACE_PATH = options.workspacePath;
105
124
  }
106
125
  const token = options.token || command.parent?.opts().token;
107
126
  if (token) {
@@ -113,6 +132,31 @@ const startCommand = new Command("start")
113
132
  process.env.NORTHFLARE_RUNNER_INACTIVITY_TIMEOUT = inactivityTimeout;
114
133
  }
115
134
 
135
+ const debugEnabled =
136
+ !!process.env.DEBUG || !!process.env.NORTHFLARE_RUNNER_DEBUG;
137
+ // Determine config path for logging purposes (actual load happens in main)
138
+ let resolvedConfigPath =
139
+ options.config || command.parent?.opts().config || null;
140
+ if (!resolvedConfigPath) {
141
+ try {
142
+ const paths = envPaths(ENV_PATHS_APP_NAME, ENV_PATHS_OPTIONS);
143
+ resolvedConfigPath = path.join(paths.config, "config.json");
144
+ } catch {
145
+ const localDir = IS_DEV_APP ? "./config/dev" : "./config";
146
+ resolvedConfigPath = path.resolve(localDir, "config.json");
147
+ }
148
+ }
149
+ if (debugEnabled) {
150
+ console.log(
151
+ "[start] Config file location:",
152
+ path.resolve(resolvedConfigPath)
153
+ );
154
+ console.log(
155
+ "[start] Orchestrator URL (env):",
156
+ process.env.NORTHFLARE_ORCHESTRATOR_URL || "(not set)"
157
+ );
158
+ }
159
+
116
160
  // Load the main module with config path if provided
117
161
  // Clear argv[2] since it contains 'start' command
118
162
  process.argv.splice(2);
@@ -136,8 +180,8 @@ const validateCommand = new Command("validate")
136
180
  .option("--token <token>", "authentication token")
137
181
  .option("--data-dir <path>", "override data directory", DEFAULT_DATA_DIR)
138
182
  .option(
139
- "--workspace-dir <path>",
140
- "workspace directory for Git checkouts (default: uses env-paths data directory)"
183
+ "--workspace-path <path>",
184
+ "workspace directory for Git checkouts"
141
185
  )
142
186
  .action(async (options, command) => {
143
187
  try {
@@ -149,8 +193,8 @@ const validateCommand = new Command("validate")
149
193
  if (options.dataDir) {
150
194
  process.env.NORTHFLARE_DATA_DIR = options.dataDir;
151
195
  }
152
- if (options.workspaceDir) {
153
- process.env.NORTHFLARE_WORKSPACE_DIR = options.workspaceDir;
196
+ if (options.workspacePath) {
197
+ process.env.NORTHFLARE_WORKSPACE_PATH = options.workspacePath;
154
198
  }
155
199
 
156
200
  const { ConfigManager } = await import("../dist/utils/config.js");
@@ -160,7 +204,7 @@ const validateCommand = new Command("validate")
160
204
  console.log("Runner ID:", config.runnerId);
161
205
  console.log("ElectricSQL URL:", config.electricUrl);
162
206
  console.log("Orchestrator URL:", config.orchestratorUrl);
163
- console.log("Workspace Directory:", process.env.NORTHFLARE_WORKSPACE_DIR);
207
+ console.log("Workspace Directory:", process.env.NORTHFLARE_WORKSPACE_PATH);
164
208
  process.exit(0);
165
209
  } catch (error) {
166
210
  console.error("Configuration validation failed:", error.message);
@@ -190,70 +234,54 @@ const listReposCommand = new Command("list-repos")
190
234
  process.exit(1);
191
235
  }
192
236
 
193
- // Get default config path
194
- const paths = envPaths("northflare-runner", { suffix: "" });
195
-
196
- // Determine config path from command options, parent options, or default
197
- const configOption = options.config || command.parent?.opts().config;
198
- let configPath;
199
- if (configOption) {
200
- configPath = path.resolve(configOption);
201
- } else {
202
- configPath = path.join(paths.config, "config.json");
203
- }
204
-
205
- // Load config file
237
+ // Optional config load for orchestrator URL
206
238
  let config = {};
239
+ const paths = envPaths(ENV_PATHS_APP_NAME, ENV_PATHS_OPTIONS);
240
+ const configOption = options.config || command.parent?.opts().config;
241
+ const configPath = configOption
242
+ ? path.resolve(configOption)
243
+ : path.join(paths.config, "config.json");
207
244
  try {
208
245
  const content = await fs.readFile(configPath, "utf-8");
209
246
  config = JSON.parse(content);
210
247
  } catch (error) {
211
- if (error.code === "ENOENT") {
212
- if (options.config) {
213
- console.error(`Configuration file not found: ${configPath}`);
214
- process.exit(1);
215
- } else {
216
- console.log(
217
- `No configuration file found at default location: ${configPath}`
218
- );
219
- console.log(
220
- "Use -c/--config to specify a config file or add-repo to create one."
221
- );
222
- process.exit(0);
223
- }
248
+ if (error.code !== "ENOENT") {
249
+ throw error;
224
250
  }
225
- throw error;
226
251
  }
227
252
 
228
- // Log config file location in debug mode
229
- if (process.env.DEBUG) {
230
- console.log(`Config file location: ${configPath}`);
231
- }
232
-
233
- // Get orchestrator URL from config or environment
234
253
  const orchestratorUrl =
235
- config.orchestratorUrl ||
236
254
  process.env.NORTHFLARE_ORCHESTRATOR_URL ||
255
+ config.orchestratorUrl ||
237
256
  "https://api.northflare.app";
238
257
 
239
258
  // Fetch runner ID
240
259
  const runnerId = await fetchRunnerId(orchestratorUrl, token);
241
260
  console.log(`Runner ID: ${runnerId}\n`);
242
261
 
243
- // Ensure runnerRepos is properly structured
244
- if (!config.runnerRepos || typeof config.runnerRepos !== "object") {
245
- config.runnerRepos = {};
262
+ // Fetch snapshot from server
263
+ const resp = await fetch(`${orchestratorUrl}/api/runner/repos`, {
264
+ method: "GET",
265
+ headers: {
266
+ Authorization: `Bearer ${token}`,
267
+ "X-Runner-Id": runnerId,
268
+ "Content-Type": "application/json",
269
+ },
270
+ });
271
+
272
+ if (!resp.ok) {
273
+ const error = await resp.text();
274
+ throw new Error(`Failed to fetch runner repos: ${resp.status} - ${error}`);
246
275
  }
247
276
 
248
- // Get repos for this runner
249
- const repos = config.runnerRepos[runnerId] || [];
277
+ const data = await resp.json();
278
+ const repos = Array.isArray(data.repos) ? data.repos : [];
250
279
 
251
- // List runner repos
252
280
  if (repos.length > 0) {
253
281
  console.log(`Found ${repos.length} runner repositories:`);
254
282
  repos.forEach((repo, index) => {
255
283
  console.log(
256
- `${index + 1}. ${repo.name} (${repo.path})${
284
+ `${index + 1}. ${repo.name} (${repo.runnerPath || repo.path})${
257
285
  repo.uuid ? ` [UUID: ${repo.uuid}]` : ""
258
286
  }`
259
287
  );
@@ -279,9 +307,17 @@ const addRepoCommand = new Command("add-repo")
279
307
  )
280
308
  .option("-n, --name <name>", "repository name (defaults to folder name)")
281
309
  .option("--token <token>", "authentication token")
310
+ .option(
311
+ "-d, --debug",
312
+ "enable verbose logging for add-repo (prints config paths, URLs, etc.)"
313
+ )
282
314
  .action(async (repoPath, options, command) => {
315
+ const debugEnabled = Boolean(options.debug || process.env.DEBUG);
316
+ const debug = (...args) => {
317
+ if (debugEnabled) console.log(...args);
318
+ };
319
+
283
320
  try {
284
- // Get token from command options, parent options, or environment
285
321
  const token =
286
322
  options.token ||
287
323
  command.parent?.opts().token ||
@@ -293,72 +329,33 @@ const addRepoCommand = new Command("add-repo")
293
329
  process.exit(1);
294
330
  }
295
331
 
296
- // Get default config path
297
- const paths = envPaths("northflare-runner", { suffix: "" });
298
-
299
- // Determine config path from command options, parent options, or default
332
+ // Optional config for orchestrator/workspace hints
333
+ const paths = envPaths(ENV_PATHS_APP_NAME, ENV_PATHS_OPTIONS);
300
334
  const configOption = options.config || command.parent?.opts().config;
301
- let configPath;
302
- if (configOption) {
303
- configPath = path.resolve(configOption);
304
- } else {
305
- configPath = path.join(paths.config, "config.json");
306
- // Ensure config directory exists
307
- await fs.mkdir(paths.config, { recursive: true });
308
- }
309
-
310
- // Load existing config
335
+ const configPath = configOption
336
+ ? path.resolve(configOption)
337
+ : path.join(paths.config, "config.json");
311
338
  let config = {};
312
339
  try {
313
340
  const content = await fs.readFile(configPath, "utf-8");
314
341
  config = JSON.parse(content);
315
342
  } catch (error) {
316
- if (error.code === "ENOENT") {
317
- console.log(
318
- `Configuration file not found: ${configPath}. Creating new config file.`
319
- );
320
- config = {};
321
- } else {
322
- throw error;
323
- }
324
- }
325
-
326
- // Log config file location in debug mode
327
- if (process.env.DEBUG) {
328
- console.log(`Config file location: ${configPath}`);
343
+ if (error.code !== "ENOENT") throw error;
329
344
  }
330
345
 
331
- // Get orchestrator URL from config or environment
332
346
  const orchestratorUrl =
333
- config.orchestratorUrl ||
334
347
  process.env.NORTHFLARE_ORCHESTRATOR_URL ||
348
+ config.orchestratorUrl ||
335
349
  "https://api.northflare.app";
350
+ debug("Orchestrator URL:", orchestratorUrl);
336
351
 
337
- // Fetch runner ID
338
352
  const runnerId = await fetchRunnerId(orchestratorUrl, token);
339
353
  console.log(`Runner ID: ${runnerId}`);
340
354
 
341
- // Ensure runnerRepos is properly structured
342
- if (
343
- !config.runnerRepos ||
344
- typeof config.runnerRepos !== "object" ||
345
- Array.isArray(config.runnerRepos)
346
- ) {
347
- config.runnerRepos = {};
348
- }
349
-
350
- // Initialize repos array for this runner if not present
351
- if (
352
- !config.runnerRepos[runnerId] ||
353
- !Array.isArray(config.runnerRepos[runnerId])
354
- ) {
355
- config.runnerRepos[runnerId] = [];
356
- }
357
-
358
- // Resolve the repository path
359
355
  const absoluteRepoPath = path.resolve(repoPath);
356
+ debug("Repository path (absolute):", absoluteRepoPath);
360
357
 
361
- // Check if path exists
358
+ // Validate directory
362
359
  try {
363
360
  const stats = await fs.stat(absoluteRepoPath);
364
361
  if (!stats.isDirectory()) {
@@ -372,82 +369,78 @@ const addRepoCommand = new Command("add-repo")
372
369
  process.exit(1);
373
370
  }
374
371
 
375
- // Check for duplicates
376
- const duplicate = config.runnerRepos[runnerId].find(
377
- (repo) => repo.path === absoluteRepoPath
378
- );
379
- if (duplicate) {
380
- console.error(
381
- `Error: Repository already exists in configuration: ${duplicate.name} (${duplicate.path})`
382
- );
383
- process.exit(1);
384
- }
385
-
386
- // Determine repository name
387
372
  const repoName = options.name || path.basename(absoluteRepoPath);
388
-
389
- // Generate UUID for the repository
390
373
  const repoUuid = crypto.randomUUID();
391
374
 
392
- // Add new repository
393
- const newRepo = {
394
- uuid: repoUuid,
395
- name: repoName,
396
- path: absoluteRepoPath,
397
- };
398
-
399
- config.runnerRepos[runnerId].push(newRepo);
400
-
401
- // Save updated config
402
- const { ConfigManager } = await import("../dist/utils/config.js");
403
- await ConfigManager.saveConfigFile(configPath, config);
404
-
405
- console.log(
406
- `Successfully added repository: ${repoName} (${absoluteRepoPath})`
407
- );
408
- console.log(`UUID: ${repoUuid}`);
409
- console.log(
410
- `Total repositories for this runner: ${config.runnerRepos[runnerId].length}`
411
- );
412
-
413
- // Sync all runner repos with the orchestrator
414
- console.log("\nSyncing runner repositories with orchestrator...");
415
- try {
416
- const response = await fetch(`${orchestratorUrl}/api/runner/repos/sync`, {
417
- method: "POST",
418
- headers: {
419
- Authorization: `Bearer ${token}`,
420
- "Content-Type": "application/json",
421
- },
422
- body: JSON.stringify({
423
- repos: config.runnerRepos[runnerId].map((r) => ({
424
- uuid: r.uuid,
425
- name: r.name,
426
- path: r.path,
427
- })),
428
- }),
429
- });
430
-
431
- if (!response.ok) {
432
- const error = await response.text();
433
- console.warn(
434
- `Warning: Failed to sync repos with orchestrator: ${error}`
435
- );
375
+ // Derive workspacePath for external flag
376
+ const workspacePath =
377
+ process.env.NORTHFLARE_WORKSPACE_PATH || config.workspacePath;
378
+ const insideWorkspace =
379
+ workspacePath &&
380
+ path
381
+ .resolve(absoluteRepoPath)
382
+ .startsWith(path.resolve(workspacePath) + path.sep);
383
+ const external = !insideWorkspace;
384
+
385
+ // Preflight snapshot to warn on duplicates
386
+ const snapshotResp = await fetch(`${orchestratorUrl}/api/runner/repos`, {
387
+ method: "GET",
388
+ headers: {
389
+ Authorization: `Bearer ${token}`,
390
+ "X-Runner-Id": runnerId,
391
+ "Content-Type": "application/json",
392
+ },
393
+ });
394
+ let existingRepos = [];
395
+ if (snapshotResp.ok) {
396
+ const data = await snapshotResp.json();
397
+ existingRepos = Array.isArray(data.repos) ? data.repos : [];
398
+ const dup = existingRepos.find(
399
+ (r) =>
400
+ path.resolve(r.runnerPath || r.path || "") === absoluteRepoPath
401
+ );
402
+ if (dup) {
436
403
  console.warn(
437
- "The repository was added to your local config, but may not be visible in the web UI."
438
- );
439
- } else {
440
- const data = await response.json();
441
- console.log(
442
- `Successfully synced ${data.syncedCount} repositories with orchestrator`
404
+ `Warning: Repository already exists on server: ${dup.name} (${dup.runnerPath || dup.path})`
443
405
  );
444
406
  }
445
- } catch (syncError) {
446
- console.warn(
447
- `Warning: Failed to sync repos with orchestrator: ${syncError.message}`
407
+ } else {
408
+ debug(
409
+ "Snapshot fetch failed (continuing)",
410
+ snapshotResp.status,
411
+ await snapshotResp.text().catch(() => "")
448
412
  );
449
- console.warn(
450
- "The repository was added to your local config, but may not be visible in the web UI."
413
+ }
414
+
415
+ // Upsert server-side
416
+ const resp = await fetch(`${orchestratorUrl}/api/runner/repos`, {
417
+ method: "POST",
418
+ headers: {
419
+ Authorization: `Bearer ${token}`,
420
+ "X-Runner-Id": runnerId,
421
+ "Content-Type": "application/json",
422
+ },
423
+ body: JSON.stringify({
424
+ uuid: repoUuid,
425
+ name: repoName,
426
+ path: absoluteRepoPath,
427
+ external,
428
+ }),
429
+ });
430
+
431
+ if (!resp.ok) {
432
+ const error = await resp.text();
433
+ throw new Error(`Failed to upsert repo: ${resp.status} - ${error}`);
434
+ }
435
+
436
+ const data = await resp.json();
437
+ console.log(
438
+ `Successfully registered repository: ${data.repo?.name || repoName} (${data.repo?.runnerPath || absoluteRepoPath})`
439
+ );
440
+ console.log(`UUID: ${data.repo?.uuid || repoUuid}`);
441
+ if (external) {
442
+ console.log(
443
+ "Marked as external (outside workspacePath) - only added via CLI."
451
444
  );
452
445
  }
453
446
 
@@ -480,6 +473,75 @@ program
480
473
  .addCommand(startCommand, { isDefault: true })
481
474
  .addCommand(validateCommand)
482
475
  .addCommand(listReposCommand)
483
- .addCommand(addRepoCommand);
476
+ .addCommand(addRepoCommand)
477
+ .addCommand(
478
+ new Command("sync-repos")
479
+ .description("pull a fresh runner repo snapshot from server (no config writes)")
480
+ .option("--token <token>", "authentication token")
481
+ .option("-c, --config <path>", "path to configuration file")
482
+ .action(async (options, command) => {
483
+ try {
484
+ const token =
485
+ options.token ||
486
+ command.parent?.opts().token ||
487
+ process.env.NORTHFLARE_RUNNER_TOKEN;
488
+ if (!token) {
489
+ console.error(
490
+ "Error: NORTHFLARE_RUNNER_TOKEN is required. Provide it via --token or environment variable."
491
+ );
492
+ process.exit(1);
493
+ }
494
+
495
+ let config = {};
496
+ const paths = envPaths(ENV_PATHS_APP_NAME, ENV_PATHS_OPTIONS);
497
+ const configOption = options.config || command.parent?.opts().config;
498
+ const configPath = configOption
499
+ ? path.resolve(configOption)
500
+ : path.join(paths.config, "config.json");
501
+ try {
502
+ const content = await fs.readFile(configPath, "utf-8");
503
+ config = JSON.parse(content);
504
+ } catch (err) {
505
+ if (err.code !== "ENOENT") throw err;
506
+ }
507
+
508
+ const orchestratorUrl =
509
+ process.env.NORTHFLARE_ORCHESTRATOR_URL ||
510
+ config.orchestratorUrl ||
511
+ "https://api.northflare.app";
512
+
513
+ const runnerId = await fetchRunnerId(orchestratorUrl, token);
514
+ console.log(`Runner ID: ${runnerId}`);
515
+
516
+ const resp = await fetch(`${orchestratorUrl}/api/runner/repos`, {
517
+ method: "GET",
518
+ headers: {
519
+ Authorization: `Bearer ${token}`,
520
+ "X-Runner-Id": runnerId,
521
+ "Content-Type": "application/json",
522
+ },
523
+ });
524
+
525
+ if (!resp.ok) {
526
+ const error = await resp.text();
527
+ throw new Error(`Failed to fetch runner repos: ${resp.status} - ${error}`);
528
+ }
529
+
530
+ const data = await resp.json();
531
+ const repos = Array.isArray(data.repos) ? data.repos : [];
532
+ console.log(`Synced ${repos.length} repos from server snapshot.`);
533
+ repos.forEach((r, idx) => {
534
+ console.log(
535
+ `${idx + 1}. ${r.name} (${r.runnerPath || r.path})${r.external ? " [external]" : ""}`
536
+ );
537
+ });
538
+
539
+ process.exit(0);
540
+ } catch (error) {
541
+ console.error("Error syncing repositories:", error.message);
542
+ process.exit(1);
543
+ }
544
+ })
545
+ );
484
546
 
485
547
  program.parse(process.argv);
@@ -45,6 +45,12 @@ export declare class ClaudeManager {
45
45
  * Normalize arbitrary content shapes into a plain string for the CLI
46
46
  */
47
47
  private normalizeToText;
48
+ /**
49
+ * Normalize arbitrary content shapes into content blocks array for multimodal support.
50
+ * Returns the array of content blocks if the content contains image blocks,
51
+ * otherwise returns null to indicate text-only content.
52
+ */
53
+ private normalizeToContentBlocks;
48
54
  private handleStreamedMessage;
49
55
  }
50
56
  //# sourceMappingURL=claude-sdk-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude-sdk-manager.d.ts","sourceRoot":"","sources":["../../src/components/claude-sdk-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAC7E,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAuBrF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,iBAAiB,CAA4B;gBAGnD,MAAM,EAAE,UAAU,EAClB,iBAAiB,EAAE,yBAAyB;IAiB9C,OAAO,CAAC,sBAAsB;IAKxB,iBAAiB,CACrB,sBAAsB,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,EAC3D,oBAAoB,EAAE,MAAM,EAC5B,MAAM,EAAE,kBAAkB,EAC1B,eAAe,EAAE,OAAO,EAAE,EAC1B,gBAAgB,CAAC,EAAE;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,kBAAkB,EAAE,MAAM,CAAC;QAC3B,qBAAqB,EAAE,MAAM,CAAC;QAC9B,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;QAChC,gBAAgB,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;KACzC,GACA,OAAO,CAAC,mBAAmB,CAAC;IA2YzB,gBAAgB,CACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,mBAAmB,EAC5B,gBAAgB,GAAE,OAAe,EACjC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAmCV,kBAAkB,CACtB,sBAAsB,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,EAC3D,oBAAoB,EAAE,MAAM,EAC5B,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,kBAAkB,EAC1B,gBAAgB,CAAC,EAAE,GAAG,EACtB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC;YAuCJ,qBAAqB;IA2C7B,eAAe,CACnB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,GAAG,EACZ,MAAM,CAAC,EAAE,kBAAkB,EAC3B,sBAAsB,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,EAC5D,oBAAoB,CAAC,EAAE,MAAM,EAC7B,YAAY,CAAC,EAAE,GAAG,EAClB,uBAAuB,CAAC,EAAE,MAAM,GAC/B,OAAO,CAAC,IAAI,CAAC;YAoIF,iBAAiB;YA2BjB,wBAAwB;IA2BtC,OAAO,CAAC,aAAa;IAerB;;OAEG;IACH,OAAO,CAAC,eAAe;YA2CT,qBAAqB;CA0dpC"}
1
+ {"version":3,"file":"claude-sdk-manager.d.ts","sourceRoot":"","sources":["../../src/components/claude-sdk-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAC7E,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAuBrF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,iBAAiB,CAA4B;gBAGnD,MAAM,EAAE,UAAU,EAClB,iBAAiB,EAAE,yBAAyB;IAiB9C,OAAO,CAAC,sBAAsB;IAKxB,iBAAiB,CACrB,sBAAsB,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,EAC3D,oBAAoB,EAAE,MAAM,EAC5B,MAAM,EAAE,kBAAkB,EAC1B,eAAe,EAAE,OAAO,EAAE,EAC1B,gBAAgB,CAAC,EAAE;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,kBAAkB,EAAE,MAAM,CAAC;QAC3B,qBAAqB,EAAE,MAAM,CAAC;QAC9B,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;QAChC,gBAAgB,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;KACzC,GACA,OAAO,CAAC,mBAAmB,CAAC;IAyZzB,gBAAgB,CACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,mBAAmB,EAC5B,gBAAgB,GAAE,OAAe,EACjC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAmCV,kBAAkB,CACtB,sBAAsB,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,EAC3D,oBAAoB,EAAE,MAAM,EAC5B,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,kBAAkB,EAC1B,gBAAgB,CAAC,EAAE,GAAG,EACtB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC;YAuCJ,qBAAqB;IA2C7B,eAAe,CACnB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,GAAG,EACZ,MAAM,CAAC,EAAE,kBAAkB,EAC3B,sBAAsB,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,EAC5D,oBAAoB,CAAC,EAAE,MAAM,EAC7B,YAAY,CAAC,EAAE,GAAG,EAClB,uBAAuB,CAAC,EAAE,MAAM,GAC/B,OAAO,CAAC,IAAI,CAAC;YAoIF,iBAAiB;YA2BjB,wBAAwB;IA2BtC,OAAO,CAAC,aAAa;IAerB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2CvB;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;YAyBlB,qBAAqB;CA0dpC"}
@@ -334,11 +334,25 @@ export class ClaudeManager {
334
334
  // Send initial messages
335
335
  try {
336
336
  for (const message of initialMessages) {
337
- const initialText = this.normalizeToText(message.content);
338
- conversation.send({
339
- type: "text",
340
- text: initialText,
341
- });
337
+ // Check if the message content contains multimodal content blocks (e.g., images)
338
+ const contentBlocks = this.normalizeToContentBlocks(message.content);
339
+ if (contentBlocks && Array.isArray(contentBlocks) && contentBlocks.some(b => b.type === 'image')) {
340
+ // Send as multimodal message with content array
341
+ // The SDK types may not include multimodal support, but the underlying API does
342
+ console.log('[ClaudeManager] Sending multimodal message with', contentBlocks.length, 'content blocks');
343
+ conversation.send({
344
+ type: "user",
345
+ content: contentBlocks,
346
+ });
347
+ }
348
+ else {
349
+ // Fallback to text-only message
350
+ const initialText = this.normalizeToText(message.content);
351
+ conversation.send({
352
+ type: "text",
353
+ text: initialText,
354
+ });
355
+ }
342
356
  }
343
357
  console.log(`Started conversation for ${conversationObjectType} ${conversationObjectId} in workspace ${workspacePath}`);
344
358
  // Return the conversation context directly
@@ -625,6 +639,33 @@ export class ClaudeManager {
625
639
  return String(value);
626
640
  }
627
641
  }
642
+ /**
643
+ * Normalize arbitrary content shapes into content blocks array for multimodal support.
644
+ * Returns the array of content blocks if the content contains image blocks,
645
+ * otherwise returns null to indicate text-only content.
646
+ */
647
+ normalizeToContentBlocks(value) {
648
+ // If already an array, check if it contains image blocks
649
+ if (Array.isArray(value)) {
650
+ const hasImages = value.some(b => b && b.type === 'image');
651
+ if (hasImages) {
652
+ // Return the content blocks as-is, they're already in the right format
653
+ return value;
654
+ }
655
+ return null;
656
+ }
657
+ // Check if it's a wrapped content object
658
+ if (typeof value === 'object' && value !== null) {
659
+ // Check for nested content array
660
+ if (Array.isArray(value.content)) {
661
+ const hasImages = value.content.some((b) => b && b.type === 'image');
662
+ if (hasImages) {
663
+ return value.content;
664
+ }
665
+ }
666
+ }
667
+ return null;
668
+ }
628
669
  async handleStreamedMessage(context, message, sessionId) {
629
670
  /*
630
671
  * SDK tool call payload reference (observed shapes)