@camstack/agent 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,861 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ createAgentHttpServer: () => createAgentHttpServer,
34
+ createAgentService: () => createAgentService,
35
+ loadAgentConfig: () => loadAgentConfig,
36
+ startAgent: () => startAgent,
37
+ startAgentHttpServer: () => startAgentHttpServer
38
+ });
39
+ module.exports = __toCommonJS(src_exports);
40
+
41
+ // src/agent-config.ts
42
+ var fs = __toESM(require("fs"));
43
+ var path = __toESM(require("path"));
44
+ var crypto = __toESM(require("crypto"));
45
+ var os = __toESM(require("os"));
46
+ function ensurePersistedNodeId(configPath, dataDir) {
47
+ const resolvedPath = path.resolve(dataDir, configPath);
48
+ let raw = {};
49
+ if (fs.existsSync(resolvedPath)) {
50
+ try {
51
+ raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
52
+ } catch {
53
+ }
54
+ }
55
+ if (typeof raw.nodeId === "string" && raw.nodeId.length > 0) {
56
+ return raw.nodeId;
57
+ }
58
+ const envSeed = process.env.CAMSTACK_NODE_ID;
59
+ const id = envSeed && envSeed.length > 0 ? envSeed : `agent-${crypto.randomBytes(4).toString("hex")}`;
60
+ raw.nodeId = id;
61
+ try {
62
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
63
+ fs.writeFileSync(resolvedPath, JSON.stringify(raw, null, 2), "utf-8");
64
+ } catch {
65
+ }
66
+ return id;
67
+ }
68
+ function loadAgentConfig(configPath, dataDirOverride) {
69
+ const filePath = configPath ?? process.env.CAMSTACK_AGENT_CONFIG ?? "agent.json";
70
+ const dataDir = dataDirOverride ?? process.env.CAMSTACK_DATA_DIR ?? "./camstack-data";
71
+ const resolvedDataDir = path.resolve(dataDir);
72
+ const envName = process.env.CAMSTACK_NODE_ID ?? process.env.CAMSTACK_AGENT_NAME ?? `${os.hostname()}-${os.arch()}`;
73
+ const envHubAddress = process.env.CAMSTACK_HUB_ADDRESS || void 0;
74
+ const envSecret = process.env.CAMSTACK_CLUSTER_SECRET || void 0;
75
+ const configFullPath = path.resolve(resolvedDataDir, filePath);
76
+ const nodeId = ensurePersistedNodeId(filePath, resolvedDataDir);
77
+ let fileHubAddress;
78
+ let fileName;
79
+ let fileSecret;
80
+ if (fs.existsSync(configFullPath)) {
81
+ try {
82
+ const raw = JSON.parse(fs.readFileSync(configFullPath, "utf-8"));
83
+ fileHubAddress = typeof raw.hubAddress === "string" ? raw.hubAddress : void 0;
84
+ fileName = typeof raw.name === "string" ? raw.name : void 0;
85
+ fileSecret = typeof raw.secret === "string" ? raw.secret : void 0;
86
+ } catch {
87
+ }
88
+ }
89
+ const effectiveName = fileName ?? envName;
90
+ const effectiveHub = fileHubAddress ?? envHubAddress;
91
+ const effectiveSecret = fileSecret ?? envSecret;
92
+ return {
93
+ nodeId,
94
+ name: effectiveName,
95
+ hubAddress: effectiveHub,
96
+ dataDir: resolvedDataDir,
97
+ addonsDir: path.resolve(resolvedDataDir, "addons"),
98
+ logLevel: process.env.CAMSTACK_LOG_LEVEL ?? "info",
99
+ secret: effectiveSecret,
100
+ configPath: configFullPath,
101
+ statusPort: Number(process.env.CAMSTACK_STATUS_PORT) || 4444
102
+ };
103
+ }
104
+
105
+ // src/agent-service.ts
106
+ var os2 = __toESM(require("os"));
107
+ var fs2 = __toESM(require("fs"));
108
+ var path2 = __toESM(require("path"));
109
+ function getLocalIps() {
110
+ const interfaces = os2.networkInterfaces();
111
+ const ips = [];
112
+ for (const ifaces of Object.values(interfaces)) {
113
+ if (!ifaces) continue;
114
+ for (const iface of ifaces) {
115
+ if (iface.internal) continue;
116
+ ips.push(iface.address);
117
+ }
118
+ }
119
+ return ips;
120
+ }
121
+ function readHubAddressFromConfig(configPath) {
122
+ if (!configPath) return null;
123
+ try {
124
+ if (!fs2.existsSync(configPath)) return null;
125
+ const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
126
+ return typeof raw.hubAddress === "string" && raw.hubAddress.length > 0 ? raw.hubAddress : null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ function isHubConnected(broker) {
132
+ try {
133
+ const registry = broker.registry;
134
+ const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
135
+ return nodes.some((n) => n.id === "hub");
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+ function createAgentService(deps) {
141
+ return {
142
+ name: "$agent",
143
+ actions: {
144
+ status: {
145
+ handler: async (ctx) => {
146
+ const cpus2 = os2.cpus();
147
+ let cpuPercent = 0;
148
+ let memoryPercent = 0;
149
+ const metrics = deps.getMetricsProvider?.();
150
+ if (metrics) {
151
+ const snapshot = await metrics.getCached();
152
+ if (snapshot) {
153
+ cpuPercent = snapshot.cpu.total;
154
+ memoryPercent = snapshot.memory.percent;
155
+ }
156
+ }
157
+ return {
158
+ nodeId: ctx.broker.nodeID,
159
+ name: deps.agentName,
160
+ platform: os2.platform(),
161
+ arch: os2.arch(),
162
+ hostname: os2.hostname(),
163
+ cpuCores: cpus2.length,
164
+ cpuModel: cpus2[0]?.model,
165
+ totalMemoryMB: Math.round(os2.totalmem() / 1024 / 1024),
166
+ freeMemoryMB: Math.round(os2.freemem() / 1024 / 1024),
167
+ cpuPercent,
168
+ memoryPercent,
169
+ uptime: os2.uptime(),
170
+ localIps: getLocalIps(),
171
+ addons: [...deps.loadedAddons.values()].map((a) => ({
172
+ id: a.id,
173
+ status: a.status,
174
+ version: a.version,
175
+ packageName: a.packageName
176
+ }))
177
+ };
178
+ }
179
+ },
180
+ health: {
181
+ handler: async (ctx) => {
182
+ let cpuPercent = 0;
183
+ let memoryPercent = 0;
184
+ const metrics = deps.getMetricsProvider?.();
185
+ if (metrics) {
186
+ try {
187
+ const snapshot = await metrics.getCached();
188
+ if (snapshot) {
189
+ cpuPercent = snapshot.cpu.total;
190
+ memoryPercent = snapshot.memory.percent;
191
+ }
192
+ } catch {
193
+ }
194
+ }
195
+ let total = 0;
196
+ let running = 0;
197
+ let errored = 0;
198
+ for (const a of deps.loadedAddons.values()) {
199
+ total++;
200
+ if (a.status === "running") running++;
201
+ else if (a.status === "error") errored++;
202
+ }
203
+ const hubAddress = readHubAddressFromConfig(deps.configPath);
204
+ return {
205
+ ok: errored === 0,
206
+ nodeId: ctx.broker.nodeID,
207
+ name: deps.agentName,
208
+ version: deps.agentVersion ?? "unknown",
209
+ uptimeSeconds: Math.round(process.uptime()),
210
+ pid: process.pid,
211
+ hubConnected: isHubConnected(ctx.broker),
212
+ hubAddress,
213
+ addons: { total, running, error: errored },
214
+ cpuPercent,
215
+ memoryPercent,
216
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
217
+ };
218
+ }
219
+ },
220
+ shutdown: {
221
+ handler() {
222
+ setTimeout(() => process.exit(0), 500);
223
+ return { success: true };
224
+ }
225
+ },
226
+ rename: {
227
+ handler(ctx) {
228
+ const newName = ctx.params.name;
229
+ if (!newName || typeof newName !== "string") {
230
+ throw new Error("$agent.rename: name is required");
231
+ }
232
+ const oldName = deps.agentName;
233
+ deps.agentName = newName.trim();
234
+ ctx.broker.logger.info(`Agent renamed: "${oldName}" \u2192 "${deps.agentName}"`);
235
+ try {
236
+ const configFile = path2.resolve(deps.configPath);
237
+ let raw = {};
238
+ if (fs2.existsSync(configFile)) {
239
+ try {
240
+ raw = JSON.parse(fs2.readFileSync(configFile, "utf-8"));
241
+ } catch {
242
+ }
243
+ }
244
+ raw.name = deps.agentName;
245
+ fs2.mkdirSync(path2.dirname(configFile), { recursive: true });
246
+ fs2.writeFileSync(configFile, JSON.stringify(raw, null, 2), "utf-8");
247
+ ctx.broker.logger.info(`Agent name persisted to ${configFile}`);
248
+ } catch (err) {
249
+ ctx.broker.logger.warn("Agent rename: config file write failed (in-memory rename still active)", { error: String(err) });
250
+ }
251
+ return { success: true, name: deps.agentName };
252
+ }
253
+ },
254
+ listAddons: {
255
+ handler() {
256
+ return [...deps.loadedAddons.keys()];
257
+ }
258
+ },
259
+ deploy: {
260
+ handler: async (ctx) => {
261
+ const { addonId, bundle } = ctx.params;
262
+ const addonDir = path2.join(deps.addonsDir, addonId);
263
+ fs2.mkdirSync(deps.addonsDir, { recursive: true });
264
+ const bundlePath = path2.join(deps.addonsDir, `${addonId}.tgz`);
265
+ const bufferData = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
266
+ fs2.writeFileSync(bundlePath, bufferData);
267
+ const { execSync } = await import("child_process");
268
+ fs2.mkdirSync(addonDir, { recursive: true });
269
+ execSync(`tar -xzf "${bundlePath}" -C "${addonDir}" --strip-components=1`, {
270
+ timeout: 3e4
271
+ });
272
+ fs2.unlinkSync(bundlePath);
273
+ return { success: true, addonId, path: addonDir };
274
+ }
275
+ },
276
+ undeploy: {
277
+ handler: async (ctx) => {
278
+ const { addonId } = ctx.params;
279
+ const addonDir = path2.join(deps.addonsDir, addonId);
280
+ deps.loadedAddons.delete(addonId);
281
+ if (fs2.existsSync(addonDir)) {
282
+ fs2.rmSync(addonDir, { recursive: true, force: true });
283
+ }
284
+ return { success: true, addonId };
285
+ }
286
+ },
287
+ restart: {
288
+ handler: async (ctx) => {
289
+ const { addonId } = ctx.params;
290
+ return { success: true, addonId, action: "restart-queued" };
291
+ }
292
+ }
293
+ }
294
+ };
295
+ }
296
+
297
+ // src/agent-bootstrap.ts
298
+ var fs4 = __toESM(require("fs"));
299
+ var path4 = __toESM(require("path"));
300
+
301
+ // src/agent-http.ts
302
+ var import_fastify = __toESM(require("fastify"));
303
+ var fs3 = __toESM(require("fs"));
304
+ var path3 = __toESM(require("path"));
305
+ function resolveUiDistDir(dataDir) {
306
+ const candidates = [
307
+ path3.resolve(__dirname, "../../addon-agent-ui/dist"),
308
+ path3.join(dataDir, "addons", "@camstack", "addon-agent-ui", "dist"),
309
+ path3.join(dataDir, "agent-ui")
310
+ ];
311
+ for (const dir of candidates) {
312
+ if (fs3.existsSync(path3.join(dir, "index.html"))) return dir;
313
+ }
314
+ return null;
315
+ }
316
+ function readConfigFile(configPath) {
317
+ if (!fs3.existsSync(configPath)) return {};
318
+ try {
319
+ return JSON.parse(fs3.readFileSync(configPath, "utf-8"));
320
+ } catch {
321
+ return {};
322
+ }
323
+ }
324
+ function writeConfigFile(configPath, data) {
325
+ fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
326
+ fs3.writeFileSync(configPath, JSON.stringify(data, null, 2), "utf-8");
327
+ }
328
+ function getRegistryNodes(broker) {
329
+ try {
330
+ const registry = broker.registry;
331
+ return registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
332
+ } catch {
333
+ return [];
334
+ }
335
+ }
336
+ function getEffectiveConfig(configPath, nodeId) {
337
+ const raw = readConfigFile(configPath);
338
+ return {
339
+ name: typeof raw.name === "string" ? raw.name : nodeId,
340
+ hubAddress: typeof raw.hubAddress === "string" ? raw.hubAddress : null,
341
+ hasSecret: typeof raw.secret === "string" && raw.secret.length > 0
342
+ };
343
+ }
344
+ async function createAgentHttpServer(getBroker, config) {
345
+ const app = (0, import_fastify.default)({ logger: false });
346
+ const cors = await import("@fastify/cors");
347
+ await app.register(cors.default);
348
+ app.get("/health", async (_req, reply) => {
349
+ try {
350
+ const result = await getBroker().call("$agent.health");
351
+ return result;
352
+ } catch (err) {
353
+ const message = err instanceof Error ? err.message : String(err);
354
+ return reply.status(503).send({
355
+ ok: false,
356
+ nodeId: config.nodeId,
357
+ error: message
358
+ });
359
+ }
360
+ });
361
+ app.get("/api/agent/status", async () => {
362
+ const broker = getBroker();
363
+ const eff = getEffectiveConfig(config.configPath, config.nodeId);
364
+ const nodes = getRegistryNodes(broker);
365
+ const hubConnected = nodes.some((n) => n.id === "hub");
366
+ const discoveryMode = !eff.hubAddress;
367
+ try {
368
+ const status = await broker.call("$agent.status");
369
+ return {
370
+ ...status,
371
+ name: eff.name,
372
+ hubAddress: eff.hubAddress,
373
+ hubConnected,
374
+ discoveryMode,
375
+ hasSecret: eff.hasSecret,
376
+ discoveredNodes: nodes.filter((n) => n.id !== broker.nodeID).map((n) => n.id)
377
+ };
378
+ } catch {
379
+ return {
380
+ nodeId: config.nodeId,
381
+ name: eff.name,
382
+ hubAddress: eff.hubAddress,
383
+ hubConnected,
384
+ discoveryMode,
385
+ hasSecret: eff.hasSecret,
386
+ discoveredNodes: [],
387
+ addons: [],
388
+ localIps: []
389
+ };
390
+ }
391
+ });
392
+ app.get("/api/agent/processes", async () => {
393
+ try {
394
+ return await getBroker().call("$process.list");
395
+ } catch {
396
+ return [];
397
+ }
398
+ });
399
+ app.post("/api/agent/addon/restart", async (req, reply) => {
400
+ const addonId = req.body?.addonId;
401
+ if (!addonId) return reply.status(400).send({ error: "addonId required" });
402
+ return getBroker().call("$agent.restart", { addonId });
403
+ });
404
+ app.post("/api/agent/process/restart", async (req, reply) => {
405
+ const name = req.body?.name;
406
+ if (!name) return reply.status(400).send({ error: "name required" });
407
+ return getBroker().call("$process.restart", { name });
408
+ });
409
+ app.get("/api/agent/config", async () => {
410
+ const persisted = readConfigFile(config.configPath);
411
+ const eff = getEffectiveConfig(config.configPath, config.nodeId);
412
+ return {
413
+ nodeId: config.nodeId,
414
+ name: eff.name,
415
+ hubAddress: eff.hubAddress,
416
+ hasSecret: eff.hasSecret,
417
+ configPath: config.configPath,
418
+ dataDir: config.dataDir,
419
+ // Include all persisted fields except raw secret
420
+ ...Object.fromEntries(
421
+ Object.entries(persisted).filter(([k]) => k !== "secret")
422
+ )
423
+ };
424
+ });
425
+ app.post("/api/agent/config", async (req) => {
426
+ const patch = req.body ?? {};
427
+ const existing = readConfigFile(config.configPath);
428
+ const merged = { ...existing, ...patch };
429
+ writeConfigFile(config.configPath, merged);
430
+ if (typeof patch.name === "string" && patch.name.trim()) {
431
+ try {
432
+ await getBroker().call("$agent.rename", { name: patch.name.trim() });
433
+ console.log(`[Agent] Name changed to "${patch.name.trim()}"`);
434
+ } catch {
435
+ }
436
+ }
437
+ const needsReconnect = Boolean(
438
+ patch.hubAddress !== void 0 || patch.secret !== void 0
439
+ );
440
+ return {
441
+ success: true,
442
+ restartRequired: needsReconnect
443
+ };
444
+ });
445
+ app.post("/api/agent/restart", async () => {
446
+ if (!config.onReconnect) {
447
+ return { success: false, message: "Reconnect not available" };
448
+ }
449
+ console.log("[Agent] Reconnect requested from UI");
450
+ void config.onReconnect().catch((err) => {
451
+ console.error("[Agent] Reconnect failed:", err);
452
+ });
453
+ return { success: true, message: "Agent reconnecting..." };
454
+ });
455
+ app.get("/api/agent/discovered-nodes", async () => {
456
+ const b = getBroker();
457
+ const nodes = getRegistryNodes(b);
458
+ return nodes.filter((n) => n.id !== b.nodeID).map((n) => ({ id: n.id, isHub: n.id === "hub" }));
459
+ });
460
+ const uiDir = resolveUiDistDir(config.dataDir);
461
+ if (uiDir) {
462
+ const fastifyStatic = await import("@fastify/static");
463
+ await app.register(fastifyStatic.default, {
464
+ root: uiDir,
465
+ prefix: "/",
466
+ wildcard: false,
467
+ decorateReply: false
468
+ });
469
+ app.setNotFoundHandler(async (req, reply) => {
470
+ if (req.url.startsWith("/api/") || req.url.startsWith("/health")) {
471
+ return reply.status(404).send({ error: "Not found" });
472
+ }
473
+ return reply.type("text/html").sendFile("index.html");
474
+ });
475
+ console.log(`[Agent] UI served from: ${uiDir}`);
476
+ }
477
+ return app;
478
+ }
479
+ async function startAgentHttpServer(getBroker, config) {
480
+ try {
481
+ const app = await createAgentHttpServer(getBroker, config);
482
+ await app.listen({ port: config.port, host: "0.0.0.0" });
483
+ console.log(`[Agent] HTTP server: http://localhost:${config.port}`);
484
+ } catch (err) {
485
+ console.warn(`[Agent] HTTP server failed to start on port ${config.port}:`, err);
486
+ }
487
+ }
488
+
489
+ // src/agent-bootstrap.ts
490
+ var import_kernel = require("@camstack/kernel");
491
+ var import_types = require("@camstack/types");
492
+ var import_types2 = require("@camstack/types");
493
+ var import_core = require("@camstack/core");
494
+ var agentLogManager = new import_core.LogManager(5e3);
495
+ async function startAgent(configPath) {
496
+ const config = loadAgentConfig(configPath);
497
+ if (config.hubAddress) {
498
+ console.log(`[Agent] Starting node "${config.nodeId}" (name="${config.name}") connecting to ${config.hubAddress}`);
499
+ } else {
500
+ console.log(`[Agent] Starting node "${config.nodeId}" (name="${config.name}") in discovery mode`);
501
+ }
502
+ console.log(`[Agent] Config: ${config.configPath}`);
503
+ console.log(`[Agent] Data dir: ${config.dataDir}`);
504
+ const explicitSource = process.env["CAMSTACK_INSTALL_SOURCE"];
505
+ const workspaceDir = explicitSource === "npm" ? null : (0, import_kernel.detectWorkspacePackagesDir)(config.dataDir);
506
+ const installer = new import_kernel.AddonInstaller({
507
+ addonsDir: config.addonsDir,
508
+ workspacePackagesDir: workspaceDir ?? void 0,
509
+ installSource: explicitSource
510
+ });
511
+ await installer.ensureRequiredPackages(import_kernel.AddonInstaller.AGENT_PACKAGES);
512
+ console.log(`[Agent] Addon packages ready in ${config.addonsDir}`);
513
+ let broker = (0, import_kernel.createBroker)({
514
+ nodeID: config.nodeId,
515
+ mode: "agent",
516
+ hubAddress: config.hubAddress,
517
+ logLevel: config.logLevel,
518
+ secret: config.secret
519
+ });
520
+ const reconnect = async () => {
521
+ console.log("[Agent] Reconnecting with updated config...");
522
+ try {
523
+ await broker.stop();
524
+ } catch {
525
+ }
526
+ const fresh = loadAgentConfig(void 0, config.dataDir);
527
+ console.log(`[Agent] New config: hub=${fresh.hubAddress ?? "discovery"}, secret=${fresh.secret ? "yes" : "none"}`);
528
+ broker = (0, import_kernel.createBroker)({
529
+ nodeID: config.nodeId,
530
+ mode: "agent",
531
+ hubAddress: fresh.hubAddress,
532
+ logLevel: fresh.logLevel,
533
+ secret: fresh.secret
534
+ });
535
+ await broker.start();
536
+ console.log("[Agent] Reconnected successfully");
537
+ };
538
+ const loadedAddons = /* @__PURE__ */ new Map();
539
+ const consoleLogger = {
540
+ info: (msg) => console.log(`[Agent] ${msg}`),
541
+ warn: (msg) => console.warn(`[Agent] ${msg}`),
542
+ error: (msg) => console.error(`[Agent] ${msg}`),
543
+ debug: (msg) => console.debug(`[Agent] ${msg}`),
544
+ child: () => consoleLogger,
545
+ withTags: () => consoleLogger
546
+ };
547
+ const capabilityRegistry = new import_kernel.CapabilityRegistry(consoleLogger);
548
+ const agentCapabilities = [
549
+ import_types2.storageCapability,
550
+ import_types2.settingsStoreCapability,
551
+ import_types2.logDestinationCapability,
552
+ import_types2.metricsProviderCapability,
553
+ import_types2.decoderCapability,
554
+ import_types2.motionDetectionCapability,
555
+ import_types2.pipelineExecutorCapability,
556
+ import_types2.pipelineRunnerCapability,
557
+ import_types2.audioAnalyzerCapability,
558
+ import_types2.platformProbeCapability
559
+ ];
560
+ for (const cap of agentCapabilities) {
561
+ capabilityRegistry.declareCapability(cap);
562
+ }
563
+ const loggerFactory = (addonId) => agentLogManager.createLogger().withTags({ addonId });
564
+ const agentService = createAgentService({
565
+ addonsDir: config.addonsDir,
566
+ dataDir: config.dataDir,
567
+ agentName: config.name,
568
+ configPath: config.configPath,
569
+ loadedAddons,
570
+ // Resolve the current metrics-provider cap lazily from the registry.
571
+ // The cap is registered during bootCoreAddons but may not exist in test
572
+ // scenarios — `null` is a documented fallback for both.
573
+ getMetricsProvider: () => capabilityRegistry.getSingleton("metrics-provider"),
574
+ agentVersion: readAgentVersion()
575
+ });
576
+ broker.createService(agentService);
577
+ const processService = (0, import_kernel.createProcessService)(broker.nodeID, config.dataDir);
578
+ broker.createService(processService);
579
+ (0, import_kernel.registerClusterEventsService)(broker);
580
+ broker.createService((0, import_kernel.createHwAccelService)((0, import_kernel.createKernelHwAccel)()));
581
+ await broker.start();
582
+ void startAgentHttpServer(() => broker, {
583
+ port: config.statusPort ?? 4444,
584
+ nodeId: config.nodeId,
585
+ dataDir: config.dataDir,
586
+ configPath: config.configPath,
587
+ onReconnect: reconnect
588
+ });
589
+ await bootCoreAddons(broker, config, capabilityRegistry, loadedAddons, loggerFactory);
590
+ for (const dest of capabilityRegistry.getCollection("log-destination")) {
591
+ agentLogManager.addDestination(dest);
592
+ }
593
+ const storageProvider = capabilityRegistry.getSingleton("storage") ?? void 0;
594
+ await loadClusterCapableAddons(broker, config, capabilityRegistry, loadedAddons);
595
+ await loadDeployedAddons(broker, config.addonsDir, config.dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry);
596
+ let hubReachableFired = false;
597
+ broker.localBus.on("$node.connected", ({ node }) => {
598
+ if (node.id !== "hub") return;
599
+ if (hubReachableFired) return;
600
+ hubReachableFired = true;
601
+ for (const [, entry] of loadedAddons) {
602
+ if (!entry.addon || typeof entry.addon.onHubReachable !== "function") continue;
603
+ Promise.resolve(entry.addon.onHubReachable()).catch((err) => {
604
+ console.error(`[Agent] ${entry.id} onHubReachable() threw:`, err);
605
+ });
606
+ }
607
+ });
608
+ console.log(`[Agent] Node "${config.nodeId}" (name="${config.name}") ready \u2014 ${loadedAddons.size} addon(s) loaded`);
609
+ const shutdown = async () => {
610
+ console.log("[Agent] Shutting down...");
611
+ for (const [, entry] of loadedAddons) {
612
+ if (entry.addon?.shutdown) {
613
+ try {
614
+ await entry.addon.shutdown();
615
+ } catch {
616
+ }
617
+ }
618
+ }
619
+ await broker.stop();
620
+ process.exit(0);
621
+ };
622
+ process.on("SIGTERM", shutdown);
623
+ process.on("SIGINT", shutdown);
624
+ }
625
+ var AGENT_INFRA = import_kernel.INFRA_CAPABILITIES;
626
+ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFactory) {
627
+ const packageDirs = resolveAddonPackageDirs(config.addonsDir);
628
+ if (packageDirs.length === 0) {
629
+ console.warn("[Agent] No addon packages found \u2014 running without infrastructure addons");
630
+ return;
631
+ }
632
+ console.log(`[Agent] Scanning ${packageDirs.length} addon package(s) for infra providers`);
633
+ const loader = new import_kernel.AddonLoader();
634
+ for (const dir of packageDirs) {
635
+ try {
636
+ await loader.loadFromAddonDir(dir);
637
+ } catch (err) {
638
+ console.warn(`[Agent] Failed to scan ${dir}: ${(0, import_types2.errMsg)(err)}`);
639
+ }
640
+ }
641
+ for (const infra of AGENT_INFRA) {
642
+ const candidates = loader.listAddons().filter(
643
+ (a) => a.declaration.capabilities?.some((c) => {
644
+ const capName = typeof c === "string" ? c : c.name;
645
+ return capName === infra.name;
646
+ })
647
+ );
648
+ const addon = infra.name === "log-destination" ? candidates.find((a) => a.declaration.id === "hub-forwarder") ?? candidates[0] : candidates[0];
649
+ if (!addon) {
650
+ if (infra.required) {
651
+ console.error(`[Agent] Required infrastructure addon for "${infra.name}" not found`);
652
+ }
653
+ continue;
654
+ }
655
+ const addonId = addon.declaration.id;
656
+ try {
657
+ const instance = new addon.addonClass();
658
+ const storageProvider = registry.getSingleton("storage") ?? void 0;
659
+ const context = (0, import_kernel.createAddonContext)(broker, addon.declaration, config.dataDir, {
660
+ storageProvider,
661
+ addonConfig: { rootPath: config.dataDir },
662
+ createLogger: loggerFactory,
663
+ capabilityRegistry: registry
664
+ });
665
+ const initResult = (0, import_types.normalizeAddonInitResult)(await instance.initialize(context));
666
+ for (const reg of initResult?.providers ?? []) {
667
+ const capName = reg.capability.name;
668
+ registry.registerProvider(capName, addonId, reg.provider);
669
+ context.registerProvider(capName, reg.provider);
670
+ }
671
+ loadedAddons.set(addonId, {
672
+ id: addonId,
673
+ status: "running",
674
+ version: addon.declaration.version,
675
+ packageName: addon.packageName,
676
+ addon: instance
677
+ });
678
+ console.log(`[Agent] Core addon "${addonId}" initialized`);
679
+ } catch (err) {
680
+ const msg = (0, import_types2.errMsg)(err);
681
+ console.error(`[Agent] Failed to initialize core addon "${addonId}": ${msg}`);
682
+ if (infra.required) {
683
+ throw new Error(`Required infrastructure addon "${addonId}" failed: ${msg}`, { cause: err });
684
+ }
685
+ }
686
+ }
687
+ }
688
+ async function loadClusterCapableAddons(broker, config, capabilityRegistry, loadedAddons) {
689
+ const addonPackageDirs = resolveAddonPackageDirs(config.addonsDir);
690
+ if (addonPackageDirs.length === 0) return;
691
+ const allGroupCandidates = [];
692
+ const dirToLoader = /* @__PURE__ */ new Map();
693
+ for (const dir of addonPackageDirs) {
694
+ const loader = new import_kernel.AddonLoader();
695
+ try {
696
+ await loader.loadFromAddonDir(dir);
697
+ } catch (err) {
698
+ console.warn(`[Agent] Skipping ${dir}: ${(0, import_types2.errMsg)(err)}`);
699
+ continue;
700
+ }
701
+ dirToLoader.set(dir, loader);
702
+ for (const registered of loader.listAddons()) {
703
+ if (loadedAddons.has(registered.declaration.id)) continue;
704
+ if (registered.declaration.execution === void 0) continue;
705
+ const placement = (0, import_types.resolveAddonPlacement)(registered.declaration);
706
+ if (placement === "hub-only") continue;
707
+ allGroupCandidates.push({
708
+ groupId: (0, import_types.resolveAddonGroup)(registered.declaration),
709
+ addonId: registered.declaration.id,
710
+ addonDir: dir,
711
+ version: registered.declaration.version ?? "0.0.0",
712
+ packageName: registered.packageName,
713
+ capabilities: registered.declaration.capabilities ?? []
714
+ });
715
+ }
716
+ }
717
+ if (allGroupCandidates.length > 0) {
718
+ const grouped = /* @__PURE__ */ new Map();
719
+ for (const c of allGroupCandidates) {
720
+ const arr = grouped.get(c.groupId) ?? [];
721
+ arr.push(c);
722
+ grouped.set(c.groupId, arr);
723
+ }
724
+ for (const [groupId, addons] of grouped) {
725
+ try {
726
+ await broker.call("$process.spawnGroup", {
727
+ groupId,
728
+ addons: addons.map((a) => ({ addonId: a.addonId, addonDir: a.addonDir }))
729
+ });
730
+ for (const a of addons) {
731
+ for (const cap of a.capabilities) {
732
+ const capName = typeof cap === "string" ? cap : cap.name;
733
+ const proxy = new Proxy({}, {
734
+ get(_target, prop) {
735
+ if (prop === "then" || typeof prop === "symbol") return void 0;
736
+ return (params) => broker.call(`${a.addonId}.${capName}.${prop}`, params);
737
+ }
738
+ });
739
+ capabilityRegistry.registerProvider(capName, a.addonId, proxy);
740
+ }
741
+ loadedAddons.set(a.addonId, {
742
+ id: a.addonId,
743
+ status: "running",
744
+ version: a.version,
745
+ packageName: a.packageName
746
+ });
747
+ }
748
+ console.log(`[Agent] Group "${groupId}" spawned with ${addons.length} addon(s): ${addons.map((a) => a.addonId).join(", ")}`);
749
+ } catch (err) {
750
+ const msg = err instanceof Error ? err.stack ?? err.message : String(err);
751
+ console.error(`[Agent] Failed to spawn group "${groupId}": ${msg}`);
752
+ for (const a of addons) {
753
+ loadedAddons.set(a.addonId, { id: a.addonId, status: "error" });
754
+ }
755
+ }
756
+ }
757
+ }
758
+ for (const dir of addonPackageDirs) {
759
+ const loader = dirToLoader.get(dir);
760
+ if (!loader) continue;
761
+ for (const registered of loader.listAddons()) {
762
+ const addonId = registered.declaration.id;
763
+ if (loadedAddons.has(addonId)) continue;
764
+ if (!(0, import_types.isDeployableToAgent)(registered.declaration)) continue;
765
+ console.warn(
766
+ `[Agent] Addon "${addonId}" is deployable but missing from any spawned group \u2014 verify package.json execution field`
767
+ );
768
+ }
769
+ }
770
+ }
771
+ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
772
+ if (!fs4.existsSync(addonsDir)) return;
773
+ const loader = new import_kernel.AddonLoader();
774
+ await loader.loadFromDirectory(addonsDir);
775
+ const contextOptions = {
776
+ storageProvider,
777
+ addonConfig: storageProvider ? { modelsDir: storageProvider.resolve({ location: "models", relativePath: "" }) } : {},
778
+ createLogger: loggerFactory,
779
+ capabilityRegistry
780
+ };
781
+ for (const registered of loader.listAddons()) {
782
+ const addonId = registered.declaration.id;
783
+ if (loadedAddons.has(addonId)) continue;
784
+ if (!(0, import_types.isDeployableToAgent)(registered.declaration)) continue;
785
+ try {
786
+ const instance = new registered.addonClass();
787
+ const context = (0, import_kernel.createAddonContext)(broker, registered.declaration, dataDir, contextOptions);
788
+ await instance.initialize(context);
789
+ const service = (0, import_kernel.createAddonService)(instance, registered.declaration);
790
+ broker.createService(service);
791
+ loadedAddons.set(addonId, {
792
+ id: addonId,
793
+ status: "running",
794
+ version: registered.declaration.version,
795
+ packageName: registered.packageName,
796
+ addon: instance
797
+ });
798
+ console.log(`[Agent] Deployed addon "${addonId}" loaded`);
799
+ } catch (err) {
800
+ const msg = (0, import_types2.errMsg)(err);
801
+ console.error(`[Agent] Failed to load deployed addon "${addonId}": ${msg}`);
802
+ loadedAddons.set(addonId, { id: addonId, status: "error" });
803
+ }
804
+ }
805
+ }
806
+ function readAgentVersion() {
807
+ const candidates = [
808
+ path4.resolve(__dirname, "..", "package.json"),
809
+ path4.resolve(__dirname, "..", "..", "package.json")
810
+ ];
811
+ for (const candidate of candidates) {
812
+ try {
813
+ if (!fs4.existsSync(candidate)) continue;
814
+ const raw = JSON.parse(fs4.readFileSync(candidate, "utf-8"));
815
+ if (raw.name === "@camstack/agent" && typeof raw.version === "string") return raw.version;
816
+ } catch {
817
+ }
818
+ }
819
+ return "unknown";
820
+ }
821
+ function isDir(p) {
822
+ try {
823
+ return fs4.statSync(p).isDirectory();
824
+ } catch {
825
+ return false;
826
+ }
827
+ }
828
+ function resolveAddonPackageDirs(addonsDir) {
829
+ const dirs = [];
830
+ if (!fs4.existsSync(addonsDir)) return dirs;
831
+ for (const name of fs4.readdirSync(addonsDir)) {
832
+ const full = path4.join(addonsDir, name);
833
+ if (name.startsWith("@") && isDir(full)) {
834
+ for (const sub of fs4.readdirSync(full)) {
835
+ const subFull = path4.join(full, sub);
836
+ if (isDir(subFull) && fs4.existsSync(path4.join(subFull, "package.json"))) {
837
+ dirs.push(subFull);
838
+ }
839
+ }
840
+ } else if (isDir(full) && fs4.existsSync(path4.join(full, "package.json"))) {
841
+ dirs.push(full);
842
+ }
843
+ }
844
+ return dirs;
845
+ }
846
+ var scriptName = process.argv[1] ?? "";
847
+ if (scriptName.endsWith("agent-bootstrap.js") || scriptName.endsWith("agent-bootstrap.ts")) {
848
+ startAgent().catch((err) => {
849
+ console.error("[Agent] Fatal error:", err);
850
+ process.exit(1);
851
+ });
852
+ }
853
+ // Annotate the CommonJS export names for ESM import in node:
854
+ 0 && (module.exports = {
855
+ createAgentHttpServer,
856
+ createAgentService,
857
+ loadAgentConfig,
858
+ startAgent,
859
+ startAgentHttpServer
860
+ });
861
+ //# sourceMappingURL=index.js.map