@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.
- package/README.md +3 -3
- package/bin/northflare-runner +234 -172
- package/dist/components/claude-sdk-manager.d.ts +6 -0
- package/dist/components/claude-sdk-manager.d.ts.map +1 -1
- package/dist/components/claude-sdk-manager.js +46 -5
- package/dist/components/claude-sdk-manager.js.map +1 -1
- package/dist/components/message-handler-sse.d.ts +3 -0
- package/dist/components/message-handler-sse.d.ts.map +1 -1
- package/dist/components/message-handler-sse.js +148 -72
- package/dist/components/message-handler-sse.js.map +1 -1
- package/dist/components/repository-manager.js +4 -4
- package/dist/components/repository-manager.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -14
- package/dist/index.js.map +1 -1
- package/dist/runner-sse.d.ts +10 -6
- package/dist/runner-sse.d.ts.map +1 -1
- package/dist/runner-sse.js +130 -109
- package/dist/runner-sse.js.map +1 -1
- package/dist/services/RunnerAPIClient.d.ts +20 -0
- package/dist/services/RunnerAPIClient.d.ts.map +1 -1
- package/dist/services/RunnerAPIClient.js +50 -1
- package/dist/services/RunnerAPIClient.js.map +1 -1
- package/dist/services/SSEClient.d.ts.map +1 -1
- package/dist/services/SSEClient.js +22 -0
- package/dist/services/SSEClient.js.map +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/runner-interface.d.ts +4 -1
- package/dist/types/runner-interface.d.ts.map +1 -1
- package/dist/utils/config.d.ts +7 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +185 -62
- package/dist/utils/config.js.map +1 -1
- package/docs/claude-manager.md +1 -1
- 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
|
|
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-
|
|
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
|
-
| `
|
|
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 |
|
package/bin/northflare-runner
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
77
|
-
"workspace directory for Git checkouts
|
|
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.
|
|
104
|
-
process.env.
|
|
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-
|
|
140
|
-
"workspace directory for Git checkouts
|
|
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.
|
|
153
|
-
process.env.
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
212
|
-
|
|
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
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
const repos =
|
|
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
|
-
//
|
|
297
|
-
const paths = envPaths(
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
await
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
407
|
+
} else {
|
|
408
|
+
debug(
|
|
409
|
+
"Snapshot fetch failed (continuing)",
|
|
410
|
+
snapshotResp.status,
|
|
411
|
+
await snapshotResp.text().catch(() => "")
|
|
448
412
|
);
|
|
449
|
-
|
|
450
|
-
|
|
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;
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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)
|