@songsid/agend 0.0.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/README.md +210 -0
- package/README.zh-TW.md +134 -0
- package/dist/access-path.d.ts +10 -0
- package/dist/access-path.js +32 -0
- package/dist/access-path.js.map +1 -0
- package/dist/adapter-world.d.ts +25 -0
- package/dist/adapter-world.js +41 -0
- package/dist/adapter-world.js.map +1 -0
- package/dist/agent-cli-instructions.md +50 -0
- package/dist/agent-cli.d.ts +2 -0
- package/dist/agent-cli.js +200 -0
- package/dist/agent-cli.js.map +1 -0
- package/dist/agent-endpoint.d.ts +25 -0
- package/dist/agent-endpoint.js +162 -0
- package/dist/agent-endpoint.js.map +1 -0
- package/dist/backend/antigravity.d.ts +17 -0
- package/dist/backend/antigravity.js +98 -0
- package/dist/backend/antigravity.js.map +1 -0
- package/dist/backend/claude-code.d.ts +23 -0
- package/dist/backend/claude-code.js +171 -0
- package/dist/backend/claude-code.js.map +1 -0
- package/dist/backend/codex.d.ts +18 -0
- package/dist/backend/codex.js +160 -0
- package/dist/backend/codex.js.map +1 -0
- package/dist/backend/factory.d.ts +2 -0
- package/dist/backend/factory.js +28 -0
- package/dist/backend/factory.js.map +1 -0
- package/dist/backend/gemini-cli.d.ts +17 -0
- package/dist/backend/gemini-cli.js +163 -0
- package/dist/backend/gemini-cli.js.map +1 -0
- package/dist/backend/index.d.ts +7 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/kiro.d.ts +17 -0
- package/dist/backend/kiro.js +147 -0
- package/dist/backend/kiro.js.map +1 -0
- package/dist/backend/marker-utils.d.ts +13 -0
- package/dist/backend/marker-utils.js +64 -0
- package/dist/backend/marker-utils.js.map +1 -0
- package/dist/backend/mock.d.ts +25 -0
- package/dist/backend/mock.js +85 -0
- package/dist/backend/mock.js.map +1 -0
- package/dist/backend/opencode.d.ts +16 -0
- package/dist/backend/opencode.js +136 -0
- package/dist/backend/opencode.js.map +1 -0
- package/dist/backend/types.d.ts +86 -0
- package/dist/backend/types.js +33 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/channel/access-manager.d.ts +18 -0
- package/dist/channel/access-manager.js +153 -0
- package/dist/channel/access-manager.js.map +1 -0
- package/dist/channel/adapters/telegram.d.ts +63 -0
- package/dist/channel/adapters/telegram.js +646 -0
- package/dist/channel/adapters/telegram.js.map +1 -0
- package/dist/channel/attachment-handler.d.ts +15 -0
- package/dist/channel/attachment-handler.js +88 -0
- package/dist/channel/attachment-handler.js.map +1 -0
- package/dist/channel/factory.d.ts +12 -0
- package/dist/channel/factory.js +67 -0
- package/dist/channel/factory.js.map +1 -0
- package/dist/channel/ipc-bridge.d.ts +26 -0
- package/dist/channel/ipc-bridge.js +220 -0
- package/dist/channel/ipc-bridge.js.map +1 -0
- package/dist/channel/mcp-server.d.ts +10 -0
- package/dist/channel/mcp-server.js +288 -0
- package/dist/channel/mcp-server.js.map +1 -0
- package/dist/channel/mcp-tools.d.ts +17 -0
- package/dist/channel/mcp-tools.js +110 -0
- package/dist/channel/mcp-tools.js.map +1 -0
- package/dist/channel/message-bus.d.ts +17 -0
- package/dist/channel/message-bus.js +86 -0
- package/dist/channel/message-bus.js.map +1 -0
- package/dist/channel/message-queue.d.ts +39 -0
- package/dist/channel/message-queue.js +253 -0
- package/dist/channel/message-queue.js.map +1 -0
- package/dist/channel/tool-router.d.ts +6 -0
- package/dist/channel/tool-router.js +75 -0
- package/dist/channel/tool-router.js.map +1 -0
- package/dist/channel/tool-tracker.d.ts +13 -0
- package/dist/channel/tool-tracker.js +58 -0
- package/dist/channel/tool-tracker.js.map +1 -0
- package/dist/channel/types.d.ts +118 -0
- package/dist/channel/types.js +2 -0
- package/dist/channel/types.js.map +1 -0
- package/dist/chat-export.d.ts +4 -0
- package/dist/chat-export.js +91 -0
- package/dist/chat-export.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +59 -0
- package/dist/classic-channel-manager.js +193 -0
- package/dist/classic-channel-manager.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1833 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +118 -0
- package/dist/config.js.map +1 -0
- package/dist/context-guardian.d.ts +26 -0
- package/dist/context-guardian.js +73 -0
- package/dist/context-guardian.js.map +1 -0
- package/dist/cost-guard.d.ts +36 -0
- package/dist/cost-guard.js +147 -0
- package/dist/cost-guard.js.map +1 -0
- package/dist/daemon-entry.d.ts +1 -0
- package/dist/daemon-entry.js +29 -0
- package/dist/daemon-entry.js.map +1 -0
- package/dist/daemon.d.ts +152 -0
- package/dist/daemon.js +1714 -0
- package/dist/daemon.js.map +1 -0
- package/dist/daily-summary.d.ts +13 -0
- package/dist/daily-summary.js +55 -0
- package/dist/daily-summary.js.map +1 -0
- package/dist/event-log.d.ts +36 -0
- package/dist/event-log.js +100 -0
- package/dist/event-log.js.map +1 -0
- package/dist/export-import.d.ts +2 -0
- package/dist/export-import.js +162 -0
- package/dist/export-import.js.map +1 -0
- package/dist/fleet-context.d.ts +61 -0
- package/dist/fleet-context.js +4 -0
- package/dist/fleet-context.js.map +1 -0
- package/dist/fleet-dashboard-html.d.ts +6 -0
- package/dist/fleet-dashboard-html.js +443 -0
- package/dist/fleet-dashboard-html.js.map +1 -0
- package/dist/fleet-health-server.d.ts +35 -0
- package/dist/fleet-health-server.js +290 -0
- package/dist/fleet-health-server.js.map +1 -0
- package/dist/fleet-instructions.d.ts +5 -0
- package/dist/fleet-instructions.js +161 -0
- package/dist/fleet-instructions.js.map +1 -0
- package/dist/fleet-manager.d.ts +212 -0
- package/dist/fleet-manager.js +3655 -0
- package/dist/fleet-manager.js.map +1 -0
- package/dist/fleet-rpc-handlers.d.ts +42 -0
- package/dist/fleet-rpc-handlers.js +356 -0
- package/dist/fleet-rpc-handlers.js.map +1 -0
- package/dist/fleet-system-prompt.d.ts +11 -0
- package/dist/fleet-system-prompt.js +61 -0
- package/dist/fleet-system-prompt.js.map +1 -0
- package/dist/general-knowledge/skills.md +177 -0
- package/dist/hang-detector.d.ts +16 -0
- package/dist/hang-detector.js +53 -0
- package/dist/hang-detector.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/instance-lifecycle.d.ts +90 -0
- package/dist/instance-lifecycle.js +592 -0
- package/dist/instance-lifecycle.js.map +1 -0
- package/dist/instructions.d.ts +15 -0
- package/dist/instructions.js +90 -0
- package/dist/instructions.js.map +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +84 -0
- package/dist/logger.js.map +1 -0
- package/dist/outbound-handlers.d.ts +51 -0
- package/dist/outbound-handlers.js +739 -0
- package/dist/outbound-handlers.js.map +1 -0
- package/dist/outbound-schemas.d.ts +238 -0
- package/dist/outbound-schemas.js +248 -0
- package/dist/outbound-schemas.js.map +1 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +42 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin/agend/.claude-plugin/plugin.json +5 -0
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +595 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/routing-engine.d.ts +22 -0
- package/dist/routing-engine.js +44 -0
- package/dist/routing-engine.js.map +1 -0
- package/dist/safe-async.d.ts +6 -0
- package/dist/safe-async.js +20 -0
- package/dist/safe-async.js.map +1 -0
- package/dist/scheduler/db.d.ts +37 -0
- package/dist/scheduler/db.js +360 -0
- package/dist/scheduler/db.js.map +1 -0
- package/dist/scheduler/db.test.d.ts +1 -0
- package/dist/scheduler/db.test.js +92 -0
- package/dist/scheduler/db.test.js.map +1 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.js +4 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +44 -0
- package/dist/scheduler/scheduler.js +197 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/scheduler.test.d.ts +1 -0
- package/dist/scheduler/scheduler.test.js +119 -0
- package/dist/scheduler/scheduler.test.js.map +1 -0
- package/dist/scheduler/types.d.ts +107 -0
- package/dist/scheduler/types.js +7 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service-installer.d.ts +17 -0
- package/dist/service-installer.js +182 -0
- package/dist/service-installer.js.map +1 -0
- package/dist/setup-wizard.d.ts +48 -0
- package/dist/setup-wizard.js +701 -0
- package/dist/setup-wizard.js.map +1 -0
- package/dist/statusline-watcher.d.ts +34 -0
- package/dist/statusline-watcher.js +73 -0
- package/dist/statusline-watcher.js.map +1 -0
- package/dist/stt.d.ts +10 -0
- package/dist/stt.js +33 -0
- package/dist/stt.js.map +1 -0
- package/dist/tmux-control.d.ts +52 -0
- package/dist/tmux-control.js +207 -0
- package/dist/tmux-control.js.map +1 -0
- package/dist/tmux-manager.d.ts +44 -0
- package/dist/tmux-manager.js +218 -0
- package/dist/tmux-manager.js.map +1 -0
- package/dist/topic-archiver.d.ts +40 -0
- package/dist/topic-archiver.js +103 -0
- package/dist/topic-archiver.js.map +1 -0
- package/dist/topic-commands.d.ts +28 -0
- package/dist/topic-commands.js +359 -0
- package/dist/topic-commands.js.map +1 -0
- package/dist/transcript-monitor.d.ts +23 -0
- package/dist/transcript-monitor.js +164 -0
- package/dist/transcript-monitor.js.map +1 -0
- package/dist/types.d.ts +211 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/dashboard.html +719 -0
- package/dist/web-api.d.ts +101 -0
- package/dist/web-api.js +648 -0
- package/dist/web-api.js.map +1 -0
- package/dist/webhook-emitter.d.ts +15 -0
- package/dist/webhook-emitter.js +41 -0
- package/dist/webhook-emitter.js.map +1 -0
- package/dist/workflow-templates/default.md +35 -0
- package/package.json +76 -0
- package/templates/launchd.plist.ejs +31 -0
- package/templates/systemd.service.ejs +16 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import { resolve as pathResolve, isAbsolute } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { BroadcastArgs, CreateInstanceArgs, CreateTeamArgs, DelegateTaskArgs, DeleteInstanceArgs, DeleteTeamArgs, DeployTemplateArgs, DescribeInstanceArgs, ListDeploymentsArgs, ListInstancesArgs, ListTeamsArgs, ReplaceInstanceArgs, ReportResultArgs, RequestInformationArgs, RestartInstanceArgs, SendToInstanceArgs, StartInstanceArgs, TeardownDeploymentArgs, UpdateTeamArgs, validateArgs, } from "./outbound-schemas.js";
|
|
4
|
+
const HOME_DIR = homedir();
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize an error for inclusion in outbound responses sent to agents.
|
|
7
|
+
* Logs the full error server-side, then returns a redacted message that
|
|
8
|
+
* drops the user's home directory and truncates length. Agents get enough
|
|
9
|
+
* context to react without leaking host layout.
|
|
10
|
+
*/
|
|
11
|
+
function sanitizeError(err, ctx, operation) {
|
|
12
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
13
|
+
ctx.logger.warn({ err: e, operation }, `${operation} failed`);
|
|
14
|
+
let msg = e.message || String(err);
|
|
15
|
+
if (HOME_DIR)
|
|
16
|
+
msg = msg.split(HOME_DIR).join("~");
|
|
17
|
+
if (msg.length > 300)
|
|
18
|
+
msg = msg.slice(0, 297) + "...";
|
|
19
|
+
return msg;
|
|
20
|
+
}
|
|
21
|
+
// ── Handler implementations ─────────────────────────────────────────────
|
|
22
|
+
const sendToInstance = (ctx, rawArgs, respond, meta) => {
|
|
23
|
+
const v = validateArgs(SendToInstanceArgs, rawArgs, "send_to_instance");
|
|
24
|
+
if (!v.ok) {
|
|
25
|
+
respond(null, v.error);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const { instance_name: targetName, message, request_kind: reqKind, requires_reply, task_summary, working_directory, branch, correlation_id: parsedCorrelationId } = v.data;
|
|
29
|
+
const senderLabel = meta.senderSessionName ?? meta.instanceName;
|
|
30
|
+
const isExternalSender = meta.senderSessionName != null && meta.senderSessionName !== meta.instanceName;
|
|
31
|
+
let targetIpc = ctx.instanceIpcClients.get(targetName);
|
|
32
|
+
let targetSession = targetName;
|
|
33
|
+
let targetInstanceName = targetName;
|
|
34
|
+
if (!targetIpc) {
|
|
35
|
+
const hostInstance = ctx.sessionRegistry.get(targetName);
|
|
36
|
+
if (hostInstance) {
|
|
37
|
+
targetIpc = ctx.instanceIpcClients.get(hostInstance);
|
|
38
|
+
targetSession = targetName;
|
|
39
|
+
targetInstanceName = hostInstance;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!targetIpc) {
|
|
43
|
+
const existsInConfig = targetName in (ctx.fleetConfig?.instances ?? {});
|
|
44
|
+
if (existsInConfig) {
|
|
45
|
+
respond(null, `Instance '${targetName}' is stopped. Use start_instance('${targetName}') to start it first.`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
respond(null, `Instance or session not found: ${targetName}`);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const correlationId = parsedCorrelationId || `cid-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
53
|
+
const ipcMeta = {
|
|
54
|
+
chat_id: "",
|
|
55
|
+
message_id: `xmsg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
56
|
+
user: `instance:${senderLabel}`,
|
|
57
|
+
user_id: `instance:${senderLabel}`,
|
|
58
|
+
ts: new Date().toISOString(),
|
|
59
|
+
thread_id: "",
|
|
60
|
+
from_instance: senderLabel,
|
|
61
|
+
correlation_id: correlationId,
|
|
62
|
+
};
|
|
63
|
+
if (reqKind)
|
|
64
|
+
ipcMeta.request_kind = reqKind;
|
|
65
|
+
if (requires_reply != null)
|
|
66
|
+
ipcMeta.requires_reply = String(requires_reply);
|
|
67
|
+
if (task_summary)
|
|
68
|
+
ipcMeta.task_summary = task_summary;
|
|
69
|
+
if (working_directory)
|
|
70
|
+
ipcMeta.working_directory = working_directory;
|
|
71
|
+
if (branch)
|
|
72
|
+
ipcMeta.branch = branch;
|
|
73
|
+
targetIpc.send({ type: "fleet_inbound", targetSession, content: message, meta: ipcMeta });
|
|
74
|
+
// Cross-instance topic notifications for visibility.
|
|
75
|
+
// general_topic instances are always skipped (keep General clean).
|
|
76
|
+
// Target topic: task/query → full message; report/update → silent; other → short summary.
|
|
77
|
+
// Sender topic: always show full outbound message (so users can see what the agent sent).
|
|
78
|
+
const requestKind = ipcMeta.request_kind;
|
|
79
|
+
const groupId = ctx.fleetConfig?.channel?.group_id;
|
|
80
|
+
if (groupId && ctx.adapter) {
|
|
81
|
+
const instances = ctx.fleetConfig?.instances ?? {};
|
|
82
|
+
const notificationLabel = `${senderLabel} → ${targetName}`;
|
|
83
|
+
// ── Target topic notification ──
|
|
84
|
+
const skipTargetNotification = requestKind === "report" || requestKind === "update";
|
|
85
|
+
if (!skipTargetNotification) {
|
|
86
|
+
const targetInstance = instances[targetInstanceName];
|
|
87
|
+
const targetTopicId = targetInstance?.topic_id;
|
|
88
|
+
const targetIsGeneral = targetInstance?.general_topic === true;
|
|
89
|
+
if (targetTopicId && !targetIsGeneral && !ctx.sessionRegistry.has(targetName)) {
|
|
90
|
+
const targetAdapter = ctx.getAdapterForInstance?.(targetInstanceName) ?? ctx.adapter;
|
|
91
|
+
const targetGroupId = ctx.getGroupIdForInstance?.(targetInstanceName) ?? String(groupId);
|
|
92
|
+
const showFull = requestKind === "task" || requestKind === "query";
|
|
93
|
+
const text = showFull
|
|
94
|
+
? `${notificationLabel}:\n${message}`
|
|
95
|
+
: `${notificationLabel}: ${ipcMeta.task_summary ?? `${message.slice(0, 100)}${message.length > 100 ? "…" : ""}`}`;
|
|
96
|
+
targetAdapter.sendText(String(targetGroupId), text, { threadId: String(targetTopicId) })
|
|
97
|
+
.catch(e => ctx.logger.warn({ err: e }, "Failed to post target topic notification"));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ── Sender topic notification ──
|
|
101
|
+
const senderInstance = instances[meta.instanceName];
|
|
102
|
+
const senderTopicId = senderInstance?.topic_id;
|
|
103
|
+
const senderIsGeneral = senderInstance?.general_topic === true;
|
|
104
|
+
if (senderTopicId && !senderIsGeneral) {
|
|
105
|
+
const senderAdapter = ctx.getAdapterForInstance?.(meta.instanceName) ?? ctx.adapter;
|
|
106
|
+
const senderGroupId = ctx.getGroupIdForInstance?.(meta.instanceName) ?? String(groupId);
|
|
107
|
+
senderAdapter.sendText(senderGroupId, `${notificationLabel}:\n${message}`, { threadId: String(senderTopicId) })
|
|
108
|
+
.catch(e => ctx.logger.warn({ err: e }, "Failed to post sender topic notification"));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
ctx.logger.info(`✉ ${senderLabel} → ${targetName}: ${(message ?? "").slice(0, 100)}`);
|
|
112
|
+
const taskSummary = ipcMeta.task_summary || (message ?? "").slice(0, 200);
|
|
113
|
+
ctx.eventLog?.logActivity("message", senderLabel, taskSummary, targetName, ipcMeta.request_kind);
|
|
114
|
+
ctx.queueMirrorMessage?.(`${senderLabel} → ${targetName}: ${(message ?? "").slice(0, 500)}${(message ?? "").length > 500 ? " […]" : ""}`);
|
|
115
|
+
respond({ sent: true, target: targetName, correlation_id: correlationId,
|
|
116
|
+
...(ctx.lifecycle.daemons.get(targetInstanceName)?.isErrorState && {
|
|
117
|
+
warning: `${targetName} is currently in error state (rate-limited or paused). Message delivered but may not be processed.`,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const listInstances = (ctx, rawArgs, respond, meta) => {
|
|
122
|
+
const v = validateArgs(ListInstancesArgs, rawArgs, "list_instances");
|
|
123
|
+
if (!v.ok) {
|
|
124
|
+
respond(null, v.error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const senderLabel = meta.senderSessionName ?? meta.instanceName;
|
|
128
|
+
const filterTags = v.data.tags;
|
|
129
|
+
let allInstances = Object.entries(ctx.fleetConfig?.instances ?? {})
|
|
130
|
+
.filter(([name]) => name !== meta.instanceName && name !== senderLabel)
|
|
131
|
+
.map(([name, config]) => ({
|
|
132
|
+
name,
|
|
133
|
+
type: "instance",
|
|
134
|
+
status: ctx.lifecycle.daemons.has(name) ? "running" : "stopped",
|
|
135
|
+
working_directory: config.working_directory,
|
|
136
|
+
topic_id: config.topic_id ?? null,
|
|
137
|
+
display_name: config.display_name ?? null,
|
|
138
|
+
description: config.description ?? null,
|
|
139
|
+
backend: config.backend ?? "claude-code",
|
|
140
|
+
tags: config.tags ?? [],
|
|
141
|
+
last_activity: ctx.lastActivityMs(name) ? new Date(ctx.lastActivityMs(name)).toISOString() : null,
|
|
142
|
+
}));
|
|
143
|
+
if (filterTags?.length) {
|
|
144
|
+
allInstances = allInstances.filter(i => i.tags.some(t => filterTags.includes(t)));
|
|
145
|
+
}
|
|
146
|
+
// Append classic bot instances
|
|
147
|
+
if (ctx.classicChannels && !filterTags?.length) {
|
|
148
|
+
const fleetNames = new Set(Object.keys(ctx.fleetConfig?.instances ?? {}));
|
|
149
|
+
for (const ch of ctx.classicChannels.getAll()) {
|
|
150
|
+
if (ch.instanceName === meta.instanceName || fleetNames.has(ch.instanceName))
|
|
151
|
+
continue;
|
|
152
|
+
allInstances.push({
|
|
153
|
+
name: ch.instanceName,
|
|
154
|
+
type: "instance",
|
|
155
|
+
status: ctx.lifecycle.daemons.has(ch.instanceName) ? "running" : "stopped",
|
|
156
|
+
working_directory: "",
|
|
157
|
+
topic_id: ch.channelId,
|
|
158
|
+
display_name: `classic: ${ch.name}`,
|
|
159
|
+
description: `ClassicBot channel (${ch.name})`,
|
|
160
|
+
backend: ch.backend ?? "claude-code",
|
|
161
|
+
tags: ["classic"],
|
|
162
|
+
last_activity: ctx.lastActivityMs(ch.instanceName) ? new Date(ctx.lastActivityMs(ch.instanceName)).toISOString() : null,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const externalSessions = [...ctx.sessionRegistry.entries()]
|
|
167
|
+
.filter(([sessName]) => sessName !== senderLabel)
|
|
168
|
+
.map(([sessName, hostInstance]) => ({ name: sessName, type: "session", host: hostInstance }));
|
|
169
|
+
respond({ instances: allInstances, external_sessions: externalSessions });
|
|
170
|
+
};
|
|
171
|
+
const describeInstance = (ctx, rawArgs, respond) => {
|
|
172
|
+
const v = validateArgs(DescribeInstanceArgs, rawArgs, "describe_instance");
|
|
173
|
+
if (!v.ok) {
|
|
174
|
+
respond(null, v.error);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const targetName = v.data.name;
|
|
178
|
+
const config = ctx.fleetConfig?.instances[targetName];
|
|
179
|
+
if (config) {
|
|
180
|
+
respond({
|
|
181
|
+
name: targetName,
|
|
182
|
+
type: "instance",
|
|
183
|
+
description: config.description ?? null,
|
|
184
|
+
tags: config.tags ?? [],
|
|
185
|
+
working_directory: config.working_directory,
|
|
186
|
+
status: ctx.lifecycle.daemons.has(targetName) ? "running" : "stopped",
|
|
187
|
+
topic_id: config.topic_id ?? null,
|
|
188
|
+
backend: config.backend ?? "claude-code",
|
|
189
|
+
model: config.model ?? null,
|
|
190
|
+
last_activity: ctx.lastActivityMs(targetName) ? new Date(ctx.lastActivityMs(targetName)).toISOString() : null,
|
|
191
|
+
worktree_source: config.worktree_source ?? null,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const hostInstance = ctx.sessionRegistry.get(targetName);
|
|
196
|
+
if (hostInstance) {
|
|
197
|
+
respond({ name: targetName, type: "session", host: hostInstance, status: "running" });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
respond(null, `Instance or session '${targetName}' not found`);
|
|
201
|
+
};
|
|
202
|
+
const startInstance = async (ctx, rawArgs, respond) => {
|
|
203
|
+
const v = validateArgs(StartInstanceArgs, rawArgs, "start_instance");
|
|
204
|
+
if (!v.ok) {
|
|
205
|
+
respond(null, v.error);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const targetName = v.data.name;
|
|
209
|
+
if (ctx.lifecycle.daemons.has(targetName)) {
|
|
210
|
+
respond({ success: true, status: "already_running" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const targetConfig = ctx.fleetConfig?.instances[targetName];
|
|
214
|
+
if (!targetConfig) {
|
|
215
|
+
respond(null, `Instance '${targetName}' not found in fleet config`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await ctx.startInstance(targetName, targetConfig, true);
|
|
220
|
+
await ctx.connectIpcToInstance(targetName);
|
|
221
|
+
respond({ success: true, status: "started" });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
respond(null, `Failed to start instance '${targetName}': ${sanitizeError(err, ctx, `start_instance(${targetName})`)}`);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const restartInstance = async (ctx, rawArgs, respond) => {
|
|
228
|
+
const v = validateArgs(RestartInstanceArgs, rawArgs, "restart_instance");
|
|
229
|
+
if (!v.ok) {
|
|
230
|
+
respond(null, v.error);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const targetName = v.data.name;
|
|
234
|
+
try {
|
|
235
|
+
await ctx.restartSingleInstance(targetName);
|
|
236
|
+
respond({ success: true, status: "restarted" });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
respond(null, `Failed to restart instance '${targetName}': ${sanitizeError(err, ctx, `restart_instance(${targetName})`)}`);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
/** Wrap send_to_instance with pre-filled metadata fields. */
|
|
243
|
+
function wrapAsSend(schema, toolName, buildArgs, warnMissing) {
|
|
244
|
+
return (ctx, rawArgs, respond, meta) => {
|
|
245
|
+
const v = validateArgs(schema, rawArgs, toolName);
|
|
246
|
+
if (!v.ok) {
|
|
247
|
+
respond(null, v.error);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (warnMissing)
|
|
251
|
+
warnMissing(ctx, v.data, meta);
|
|
252
|
+
const { targetName, body, kind, reply, summary } = buildArgs(v.data);
|
|
253
|
+
// Forward correlation_id verbatim if the caller supplied one.
|
|
254
|
+
const extra = v.data.correlation_id
|
|
255
|
+
? { correlation_id: v.data.correlation_id }
|
|
256
|
+
: {};
|
|
257
|
+
const newArgs = {
|
|
258
|
+
...extra,
|
|
259
|
+
instance_name: targetName,
|
|
260
|
+
message: body,
|
|
261
|
+
request_kind: kind,
|
|
262
|
+
requires_reply: reply,
|
|
263
|
+
task_summary: summary,
|
|
264
|
+
};
|
|
265
|
+
// Re-dispatch through the handler map
|
|
266
|
+
return sendToInstance(ctx, newArgs, respond, meta);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const requestInformation = wrapAsSend(RequestInformationArgs, "request_information", ({ target_instance, question, context }) => ({
|
|
270
|
+
targetName: target_instance,
|
|
271
|
+
body: context ? `${question}\n\nContext: ${context}` : question,
|
|
272
|
+
kind: "query", reply: true,
|
|
273
|
+
summary: question.slice(0, 120),
|
|
274
|
+
}));
|
|
275
|
+
const delegateTask = wrapAsSend(DelegateTaskArgs, "delegate_task", ({ target_instance, task, success_criteria, context }) => {
|
|
276
|
+
let body = task;
|
|
277
|
+
if (success_criteria)
|
|
278
|
+
body += `\n\nSuccess criteria: ${success_criteria}`;
|
|
279
|
+
if (context)
|
|
280
|
+
body += `\n\nContext: ${context}`;
|
|
281
|
+
return { targetName: target_instance, body, kind: "task", reply: true, summary: task.slice(0, 120) };
|
|
282
|
+
});
|
|
283
|
+
const reportResult = wrapAsSend(ReportResultArgs, "report_result", ({ target_instance, summary, artifacts }) => {
|
|
284
|
+
let body = summary;
|
|
285
|
+
if (artifacts)
|
|
286
|
+
body += `\n\nArtifacts: ${artifacts}`;
|
|
287
|
+
return { targetName: target_instance, body, kind: "report", reply: false, summary: summary.slice(0, 120) };
|
|
288
|
+
}, (ctx, args, meta) => {
|
|
289
|
+
if (!args.correlation_id) {
|
|
290
|
+
ctx.logger.warn({ instanceName: meta.instanceName, targetName: args.target_instance }, "report_result called without correlation_id");
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
const createInstance = async (ctx, rawArgs, respond, meta) => {
|
|
294
|
+
const v = validateArgs(CreateInstanceArgs, rawArgs, "create_instance");
|
|
295
|
+
if (!v.ok) {
|
|
296
|
+
respond(null, v.error);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const callerAdapterId = meta?.instanceName ? ctx.getWorldForInstance?.(meta.instanceName)?.id : undefined;
|
|
300
|
+
await ctx.lifecycle.handleCreate(v.data, respond, callerAdapterId);
|
|
301
|
+
};
|
|
302
|
+
const deleteInstance = async (ctx, rawArgs, respond, meta) => {
|
|
303
|
+
const v = validateArgs(DeleteInstanceArgs, rawArgs, "delete_instance");
|
|
304
|
+
if (!v.ok) {
|
|
305
|
+
respond(null, v.error);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const targetName = v.data.name;
|
|
309
|
+
const caller = meta.instanceName;
|
|
310
|
+
const callerConfig = ctx.fleetConfig?.instances[caller];
|
|
311
|
+
const isSelf = targetName === caller;
|
|
312
|
+
const isCoordinator = callerConfig?.general_topic === true;
|
|
313
|
+
if (!isSelf && !isCoordinator) {
|
|
314
|
+
respond(null, `delete_instance denied: '${caller}' may only delete itself (coordinator instances may delete any)`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await ctx.lifecycle.handleDelete(v.data, respond);
|
|
318
|
+
};
|
|
319
|
+
const replaceInstance = async (ctx, rawArgs, respond) => {
|
|
320
|
+
const v = validateArgs(ReplaceInstanceArgs, rawArgs, "replace_instance");
|
|
321
|
+
if (!v.ok) {
|
|
322
|
+
respond(null, v.error);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await ctx.lifecycle.handleReplace(v.data, respond);
|
|
326
|
+
};
|
|
327
|
+
const broadcast = (ctx, rawArgs, respond, meta) => {
|
|
328
|
+
const v = validateArgs(BroadcastArgs, rawArgs, "broadcast");
|
|
329
|
+
if (!v.ok) {
|
|
330
|
+
respond(null, v.error);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const { message, targets, team: teamName, tags: filterTags, task_summary, request_kind, requires_reply } = v.data;
|
|
334
|
+
const senderLabel = meta.senderSessionName ?? meta.instanceName;
|
|
335
|
+
// Resolve target list: team, explicit targets, tag filter, or all running
|
|
336
|
+
let targetNames;
|
|
337
|
+
if (teamName) {
|
|
338
|
+
const teamDef = ctx.fleetConfig?.teams?.[teamName];
|
|
339
|
+
if (!teamDef) {
|
|
340
|
+
respond(null, `Team not found: ${teamName}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Silently skip members that are not currently running
|
|
344
|
+
targetNames = teamDef.members.filter(n => n !== meta.instanceName && n !== senderLabel && ctx.instanceIpcClients.has(n));
|
|
345
|
+
}
|
|
346
|
+
else if (targets?.length) {
|
|
347
|
+
targetNames = targets;
|
|
348
|
+
}
|
|
349
|
+
else if (filterTags?.length) {
|
|
350
|
+
// Filter by tags from fleet config
|
|
351
|
+
targetNames = Object.entries(ctx.fleetConfig?.instances ?? {})
|
|
352
|
+
.filter(([name, config]) => name !== meta.instanceName && name !== senderLabel
|
|
353
|
+
&& config.tags?.some((t) => filterTags.includes(t)))
|
|
354
|
+
.map(([name]) => name);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
targetNames = [...ctx.instanceIpcClients.keys()].filter(n => n !== meta.instanceName && n !== senderLabel);
|
|
358
|
+
}
|
|
359
|
+
const sentTo = [];
|
|
360
|
+
const failed = [];
|
|
361
|
+
for (const targetName of targetNames) {
|
|
362
|
+
const targetIpc = ctx.instanceIpcClients.get(targetName) ?? ctx.instanceIpcClients.get(ctx.sessionRegistry.get(targetName) ?? "");
|
|
363
|
+
if (!targetIpc) {
|
|
364
|
+
failed.push(targetName);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const correlationId = `bcast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
368
|
+
const ipcMeta = {
|
|
369
|
+
chat_id: "", message_id: `bcast-${Date.now()}`, user: `instance:${senderLabel}`,
|
|
370
|
+
user_id: `instance:${senderLabel}`, ts: new Date().toISOString(), thread_id: "",
|
|
371
|
+
from_instance: senderLabel, correlation_id: correlationId,
|
|
372
|
+
};
|
|
373
|
+
if (request_kind)
|
|
374
|
+
ipcMeta.request_kind = request_kind;
|
|
375
|
+
if (requires_reply != null)
|
|
376
|
+
ipcMeta.requires_reply = String(requires_reply);
|
|
377
|
+
if (task_summary)
|
|
378
|
+
ipcMeta.task_summary = task_summary;
|
|
379
|
+
targetIpc.send({ type: "fleet_inbound", targetSession: targetName, content: message, meta: ipcMeta });
|
|
380
|
+
sentTo.push(targetName);
|
|
381
|
+
}
|
|
382
|
+
ctx.logger.info(`📢 ${senderLabel} broadcast to ${sentTo.length} instances: ${(message).slice(0, 80)}`);
|
|
383
|
+
const summary = task_summary || message.slice(0, 200);
|
|
384
|
+
for (const target of sentTo) {
|
|
385
|
+
ctx.eventLog?.logActivity("message", senderLabel, summary, target);
|
|
386
|
+
}
|
|
387
|
+
ctx.queueMirrorMessage?.(`📢 ${senderLabel} → [${sentTo.join(", ")}]: ${message.slice(0, 500)}${message.length > 500 ? " […]" : ""}`);
|
|
388
|
+
respond({ sent_to: sentTo, failed, count: sentTo.length });
|
|
389
|
+
};
|
|
390
|
+
// ── Teams ────────────────────────────────────────────────────────────────
|
|
391
|
+
const createTeam = (ctx, rawArgs, respond) => {
|
|
392
|
+
const v = validateArgs(CreateTeamArgs, rawArgs, "create_team");
|
|
393
|
+
if (!v.ok) {
|
|
394
|
+
respond(null, v.error);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const { name, members, description } = v.data;
|
|
398
|
+
if (!ctx.fleetConfig) {
|
|
399
|
+
respond(null, "Fleet config not available");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const invalid = members.filter(m => !ctx.fleetConfig.instances[m]);
|
|
403
|
+
if (invalid.length) {
|
|
404
|
+
respond(null, `Invalid instance names: ${invalid.join(", ")}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
ctx.fleetConfig.teams ??= {};
|
|
408
|
+
if (ctx.fleetConfig.teams[name]) {
|
|
409
|
+
respond(null, `Team already exists: ${name}`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
ctx.fleetConfig.teams[name] = { members, ...(description ? { description } : {}) };
|
|
413
|
+
ctx.saveFleetConfig();
|
|
414
|
+
respond({ created: name, members });
|
|
415
|
+
};
|
|
416
|
+
const deleteTeam = (ctx, rawArgs, respond) => {
|
|
417
|
+
const v = validateArgs(DeleteTeamArgs, rawArgs, "delete_team");
|
|
418
|
+
if (!v.ok) {
|
|
419
|
+
respond(null, v.error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const { name } = v.data;
|
|
423
|
+
if (!ctx.fleetConfig?.teams?.[name]) {
|
|
424
|
+
respond(null, `Team not found: ${name}`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
delete ctx.fleetConfig.teams[name];
|
|
428
|
+
ctx.saveFleetConfig();
|
|
429
|
+
respond({ deleted: name });
|
|
430
|
+
};
|
|
431
|
+
const listTeams = (ctx, rawArgs, respond) => {
|
|
432
|
+
const v = validateArgs(ListTeamsArgs, rawArgs, "list_teams");
|
|
433
|
+
if (!v.ok) {
|
|
434
|
+
respond(null, v.error);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const teams = ctx.fleetConfig?.teams ?? {};
|
|
438
|
+
const result = Object.entries(teams).map(([name, def]) => ({
|
|
439
|
+
name,
|
|
440
|
+
description: def.description ?? null,
|
|
441
|
+
members: def.members.map(m => ({
|
|
442
|
+
name: m,
|
|
443
|
+
running: ctx.lifecycle.daemons.has(m),
|
|
444
|
+
})),
|
|
445
|
+
}));
|
|
446
|
+
respond(result);
|
|
447
|
+
};
|
|
448
|
+
const updateTeam = (ctx, rawArgs, respond) => {
|
|
449
|
+
const v = validateArgs(UpdateTeamArgs, rawArgs, "update_team");
|
|
450
|
+
if (!v.ok) {
|
|
451
|
+
respond(null, v.error);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const { name, add, remove } = v.data;
|
|
455
|
+
if (!ctx.fleetConfig?.teams?.[name]) {
|
|
456
|
+
respond(null, `Team not found: ${name}`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const team = ctx.fleetConfig.teams[name];
|
|
460
|
+
if (add?.length) {
|
|
461
|
+
const invalid = add.filter(m => !ctx.fleetConfig.instances[m]);
|
|
462
|
+
if (invalid.length) {
|
|
463
|
+
respond(null, `Invalid instance names: ${invalid.join(", ")}`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
team.members = [...new Set([...team.members, ...add])];
|
|
467
|
+
}
|
|
468
|
+
if (remove?.length) {
|
|
469
|
+
team.members = team.members.filter(m => !remove.includes(m));
|
|
470
|
+
}
|
|
471
|
+
ctx.saveFleetConfig();
|
|
472
|
+
respond({ name, members: team.members });
|
|
473
|
+
};
|
|
474
|
+
// ── Fleet Templates ────────────────────────────────────────────────────
|
|
475
|
+
const deployTemplate = async (ctx, rawArgs, respond) => {
|
|
476
|
+
const v = validateArgs(DeployTemplateArgs, rawArgs, "deploy_template");
|
|
477
|
+
if (!v.ok) {
|
|
478
|
+
respond(null, v.error);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const { template: templateName, directory: rawDirectory, name: deploymentNameArg, branch } = v.data;
|
|
482
|
+
const deploymentName = deploymentNameArg || templateName;
|
|
483
|
+
// Reject relative paths; require absolute or ~-prefixed. Resolve and normalize (collapses `..`).
|
|
484
|
+
const expanded = rawDirectory.replace(/^~/, process.env.HOME || "~");
|
|
485
|
+
if (!isAbsolute(expanded)) {
|
|
486
|
+
respond(null, `deploy_template: directory must be an absolute path (got: ${rawDirectory})`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const directory = pathResolve(expanded);
|
|
490
|
+
if (!ctx.fleetConfig) {
|
|
491
|
+
respond(null, "Fleet config not available");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const template = ctx.fleetConfig.templates?.[templateName];
|
|
495
|
+
if (!template) {
|
|
496
|
+
respond(null, `Template not found: "${templateName}". Available: ${Object.keys(ctx.fleetConfig.templates ?? {}).join(", ") || "(none)"}`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// Check for existing deployment with the same name
|
|
500
|
+
const existingDeployment = Object.values(ctx.fleetConfig.instances)
|
|
501
|
+
.some(inst => inst.tags?.includes(`deployment:${deploymentName}`));
|
|
502
|
+
if (existingDeployment) {
|
|
503
|
+
respond(null, `Deployment "${deploymentName}" already exists. Use a different name or teardown first.`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// Check team name collision early
|
|
507
|
+
if (template.team) {
|
|
508
|
+
ctx.fleetConfig.teams ??= {};
|
|
509
|
+
if (ctx.fleetConfig.teams[deploymentName]) {
|
|
510
|
+
respond(null, `Team "${deploymentName}" already exists`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const createdInstances = [];
|
|
515
|
+
try {
|
|
516
|
+
for (const [role, instanceDef] of Object.entries(template.instances)) {
|
|
517
|
+
// Resolve profile: instance-level fields override profile defaults
|
|
518
|
+
if (instanceDef.profile) {
|
|
519
|
+
const profile = ctx.fleetConfig.profiles?.[instanceDef.profile];
|
|
520
|
+
if (!profile) {
|
|
521
|
+
throw new Error(`Profile "${instanceDef.profile}" not found for role "${role}". Available: ${Object.keys(ctx.fleetConfig.profiles ?? {}).join(", ") || "(none)"}`);
|
|
522
|
+
}
|
|
523
|
+
// Apply profile defaults only for fields not set on the instance
|
|
524
|
+
if (!instanceDef.backend && profile.backend)
|
|
525
|
+
instanceDef.backend = profile.backend;
|
|
526
|
+
if (!instanceDef.model && profile.model)
|
|
527
|
+
instanceDef.model = profile.model;
|
|
528
|
+
if (!instanceDef.model_failover && profile.model_failover)
|
|
529
|
+
instanceDef.model_failover = profile.model_failover;
|
|
530
|
+
if (!instanceDef.tool_set && profile.tool_set)
|
|
531
|
+
instanceDef.tool_set = profile.tool_set;
|
|
532
|
+
if (instanceDef.lightweight == null && profile.lightweight != null)
|
|
533
|
+
instanceDef.lightweight = profile.lightweight;
|
|
534
|
+
}
|
|
535
|
+
const topicName = `${deploymentName}-${role}`;
|
|
536
|
+
const deploymentTags = [
|
|
537
|
+
`deployment:${deploymentName}`,
|
|
538
|
+
`template:${templateName}`,
|
|
539
|
+
`role:${role}`,
|
|
540
|
+
...(instanceDef.tags ?? []),
|
|
541
|
+
];
|
|
542
|
+
const createArgs = {
|
|
543
|
+
directory,
|
|
544
|
+
topic_name: topicName,
|
|
545
|
+
...(instanceDef.description ? { description: instanceDef.description } : {}),
|
|
546
|
+
...(instanceDef.model ? { model: instanceDef.model } : {}),
|
|
547
|
+
...(instanceDef.backend ? { backend: instanceDef.backend } : {}),
|
|
548
|
+
...(instanceDef.model_failover ? { model_failover: instanceDef.model_failover } : {}),
|
|
549
|
+
...(instanceDef.systemPrompt ? { systemPrompt: instanceDef.systemPrompt } : {}),
|
|
550
|
+
...(instanceDef.tool_set ? { tool_set: instanceDef.tool_set } : {}),
|
|
551
|
+
...(instanceDef.skipPermissions != null ? { skipPermissions: instanceDef.skipPermissions } : {}),
|
|
552
|
+
...(instanceDef.lightweight != null ? { lightweight: instanceDef.lightweight } : {}),
|
|
553
|
+
...(instanceDef.workflow !== undefined ? { workflow: instanceDef.workflow } : {}),
|
|
554
|
+
tags: deploymentTags,
|
|
555
|
+
...(branch ? { branch: `${deploymentName}-${role}`, start_point: branch } : {}),
|
|
556
|
+
};
|
|
557
|
+
// Create instance via handleCreate with a promise wrapper
|
|
558
|
+
const result = await new Promise((resolve) => {
|
|
559
|
+
ctx.lifecycle.handleCreate(createArgs, (res, err) => {
|
|
560
|
+
if (err)
|
|
561
|
+
resolve({ success: false, error: err });
|
|
562
|
+
else {
|
|
563
|
+
const r = res;
|
|
564
|
+
resolve({ success: true, name: r.name, status: r.status });
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
if (!result.success) {
|
|
569
|
+
throw new Error(`Failed to create instance for role "${role}": ${result.error}`);
|
|
570
|
+
}
|
|
571
|
+
// Detect duplicate directory collision (handleCreate returns already_exists)
|
|
572
|
+
if (result.status === "already_exists") {
|
|
573
|
+
throw new Error(`Instance for role "${role}" conflicts with existing instance "${result.name}" (same working directory). Use branch parameter for separate worktrees.`);
|
|
574
|
+
}
|
|
575
|
+
const instanceName = result.name;
|
|
576
|
+
const config = ctx.fleetConfig.instances[instanceName];
|
|
577
|
+
createdInstances.push({
|
|
578
|
+
name: instanceName,
|
|
579
|
+
role,
|
|
580
|
+
model: config.model,
|
|
581
|
+
backend: config.backend,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
// Create team if requested
|
|
585
|
+
let teamName;
|
|
586
|
+
if (template.team) {
|
|
587
|
+
teamName = deploymentName;
|
|
588
|
+
ctx.fleetConfig.teams[teamName] = {
|
|
589
|
+
members: createdInstances.map(i => i.name),
|
|
590
|
+
description: template.description,
|
|
591
|
+
};
|
|
592
|
+
ctx.saveFleetConfig();
|
|
593
|
+
}
|
|
594
|
+
respond({
|
|
595
|
+
success: true,
|
|
596
|
+
deployment: deploymentName,
|
|
597
|
+
template: templateName,
|
|
598
|
+
instances: createdInstances,
|
|
599
|
+
...(teamName ? { team: teamName } : {}),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
// Full rollback: delete all created instances (best-effort)
|
|
604
|
+
const rollbackErrors = [];
|
|
605
|
+
for (const inst of createdInstances) {
|
|
606
|
+
try {
|
|
607
|
+
await new Promise((resolve) => {
|
|
608
|
+
ctx.lifecycle.handleDelete({ name: inst.name, delete_topic: true }, (_res, delErr) => {
|
|
609
|
+
if (delErr)
|
|
610
|
+
rollbackErrors.push(`${inst.name}: ${delErr}`);
|
|
611
|
+
resolve();
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
rollbackErrors.push(`${inst.name}: ${sanitizeError(e, ctx, `deploy_template.rollback(${inst.name})`)}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const rollbackNote = rollbackErrors.length
|
|
620
|
+
? ` Rollback errors (manual cleanup needed): ${rollbackErrors.join("; ")}`
|
|
621
|
+
: " All created instances rolled back.";
|
|
622
|
+
respond(null, `${sanitizeError(err, ctx, "deploy_template")}${rollbackNote}`);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const teardownDeployment = async (ctx, rawArgs, respond) => {
|
|
626
|
+
const v = validateArgs(TeardownDeploymentArgs, rawArgs, "teardown_deployment");
|
|
627
|
+
if (!v.ok) {
|
|
628
|
+
respond(null, v.error);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const { name } = v.data;
|
|
632
|
+
if (!ctx.fleetConfig) {
|
|
633
|
+
respond(null, "Fleet config not available");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
// Find instances by deployment tag
|
|
637
|
+
const deploymentTag = `deployment:${name}`;
|
|
638
|
+
const deploymentInstances = Object.entries(ctx.fleetConfig.instances)
|
|
639
|
+
.filter(([_, config]) => config.tags?.includes(deploymentTag))
|
|
640
|
+
.map(([instanceName]) => instanceName);
|
|
641
|
+
if (deploymentInstances.length === 0) {
|
|
642
|
+
respond(null, `No deployment found with name "${name}"`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const deleted = [];
|
|
646
|
+
const errors = [];
|
|
647
|
+
for (const instanceName of deploymentInstances) {
|
|
648
|
+
try {
|
|
649
|
+
await new Promise((resolve) => {
|
|
650
|
+
ctx.lifecycle.handleDelete({ name: instanceName, delete_topic: true }, (_res, err) => {
|
|
651
|
+
if (err)
|
|
652
|
+
errors.push(`${instanceName}: ${err}`);
|
|
653
|
+
else
|
|
654
|
+
deleted.push(instanceName);
|
|
655
|
+
resolve();
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (e) {
|
|
660
|
+
errors.push(`${instanceName}: ${sanitizeError(e, ctx, `teardown_deployment(${instanceName})`)}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Delete team if exists (best-effort)
|
|
664
|
+
let teamDeleted = false;
|
|
665
|
+
if (ctx.fleetConfig.teams?.[name]) {
|
|
666
|
+
delete ctx.fleetConfig.teams[name];
|
|
667
|
+
ctx.saveFleetConfig();
|
|
668
|
+
teamDeleted = true;
|
|
669
|
+
}
|
|
670
|
+
respond({
|
|
671
|
+
success: errors.length === 0,
|
|
672
|
+
deployment: name,
|
|
673
|
+
deleted,
|
|
674
|
+
team_deleted: teamDeleted,
|
|
675
|
+
...(errors.length ? { errors } : {}),
|
|
676
|
+
});
|
|
677
|
+
};
|
|
678
|
+
const listDeployments = (ctx, rawArgs, respond) => {
|
|
679
|
+
const v = validateArgs(ListDeploymentsArgs, rawArgs, "list_deployments");
|
|
680
|
+
if (!v.ok) {
|
|
681
|
+
respond(null, v.error);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (!ctx.fleetConfig) {
|
|
685
|
+
respond(null, "Fleet config not available");
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// Aggregate instances by deployment tag
|
|
689
|
+
const deployments = new Map();
|
|
690
|
+
for (const [name, config] of Object.entries(ctx.fleetConfig.instances)) {
|
|
691
|
+
const deployTag = config.tags?.find(t => t.startsWith("deployment:"));
|
|
692
|
+
if (!deployTag)
|
|
693
|
+
continue;
|
|
694
|
+
const deploymentName = deployTag.slice("deployment:".length);
|
|
695
|
+
if (!deployments.has(deploymentName)) {
|
|
696
|
+
const templateTag = config.tags?.find(t => t.startsWith("template:"));
|
|
697
|
+
deployments.set(deploymentName, {
|
|
698
|
+
template: templateTag ? templateTag.slice("template:".length) : null,
|
|
699
|
+
instances: [],
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
const roleTag = config.tags?.find(t => t.startsWith("role:"));
|
|
703
|
+
deployments.get(deploymentName).instances.push({
|
|
704
|
+
name,
|
|
705
|
+
role: roleTag ? roleTag.slice("role:".length) : null,
|
|
706
|
+
running: ctx.lifecycle.has(name),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const result = [...deployments.entries()].map(([name, data]) => ({
|
|
710
|
+
name,
|
|
711
|
+
template: data.template,
|
|
712
|
+
instances: data.instances,
|
|
713
|
+
team: ctx.fleetConfig?.teams?.[name] ? name : null,
|
|
714
|
+
}));
|
|
715
|
+
respond(result);
|
|
716
|
+
};
|
|
717
|
+
// ── Registry ────────────────────────────────────────────────────────────
|
|
718
|
+
export const outboundHandlers = new Map([
|
|
719
|
+
["send_to_instance", sendToInstance],
|
|
720
|
+
["broadcast", broadcast],
|
|
721
|
+
["list_instances", listInstances],
|
|
722
|
+
["request_information", requestInformation],
|
|
723
|
+
["delegate_task", delegateTask],
|
|
724
|
+
["report_result", reportResult],
|
|
725
|
+
["describe_instance", describeInstance],
|
|
726
|
+
["start_instance", startInstance],
|
|
727
|
+
["restart_instance", restartInstance],
|
|
728
|
+
["create_instance", createInstance],
|
|
729
|
+
["delete_instance", deleteInstance],
|
|
730
|
+
["replace_instance", replaceInstance],
|
|
731
|
+
["create_team", createTeam],
|
|
732
|
+
["delete_team", deleteTeam],
|
|
733
|
+
["list_teams", listTeams],
|
|
734
|
+
["update_team", updateTeam],
|
|
735
|
+
["deploy_template", deployTemplate],
|
|
736
|
+
["teardown_deployment", teardownDeployment],
|
|
737
|
+
["list_deployments", listDeployments],
|
|
738
|
+
]);
|
|
739
|
+
//# sourceMappingURL=outbound-handlers.js.map
|