@songsid/agend 2.1.0-beta.23 → 2.1.0-beta.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 +1 -1
- package/dist/backend/claude-code.js +4 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/factory.js +0 -1
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/grok.d.ts +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/channel/adapters/discord.d.ts +1 -1
- package/dist/channel/adapters/discord.js +19 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/classic-channel-manager.js +1 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +29 -4
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +11 -4
- package/dist/daemon.js +54 -14
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +3 -0
- package/dist/fleet-manager.d.ts +8 -0
- package/dist/fleet-manager.js +136 -24
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -1
- package/dist/instance-lifecycle.d.ts +8 -0
- package/dist/instance-lifecycle.js +23 -13
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +22 -2
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +14 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/setup-wizard.js +1 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/topic-commands.d.ts +5 -0
- package/dist/topic-commands.js +43 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/settings.html +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -24,13 +24,13 @@ import { processAttachments } from "./channel/attachment-handler.js";
|
|
|
24
24
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
25
25
|
import { Scheduler } from "./scheduler/index.js";
|
|
26
26
|
import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
|
|
27
|
-
import { TopicCommands, saveCommandForBackend, parseSaveFilename, SAVE_FILENAME_RE, SAVE_UNSUPPORTED_MSG } from "./topic-commands.js";
|
|
27
|
+
import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, SAVE_FILENAME_RE, SAVE_UNSUPPORTED_MSG } from "./topic-commands.js";
|
|
28
28
|
import { DailySummary } from "./daily-summary.js";
|
|
29
29
|
import { WebhookEmitter } from "./webhook-emitter.js";
|
|
30
30
|
import { TmuxControlClient } from "./tmux-control.js";
|
|
31
31
|
import { safeHandler } from "./safe-async.js";
|
|
32
32
|
import { RoutingEngine } from "./routing-engine.js";
|
|
33
|
-
import { InstanceLifecycle } from "./instance-lifecycle.js";
|
|
33
|
+
import { InstanceLifecycle, BACKEND_INSTALLATION_INFO, checkBinaryInstalled, } from "./instance-lifecycle.js";
|
|
34
34
|
import { TopicArchiver } from "./topic-archiver.js";
|
|
35
35
|
import { StatuslineWatcher } from "./statusline-watcher.js";
|
|
36
36
|
import { outboundHandlers } from "./outbound-handlers.js";
|
|
@@ -40,6 +40,7 @@ import { handleSettingsRequest } from "./settings-api.js";
|
|
|
40
40
|
import { setLocale, detectLocale, t } from "./locale.js";
|
|
41
41
|
import { handleAgentRequest } from "./agent-endpoint.js";
|
|
42
42
|
import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBackend } from "./classic-channel-manager.js";
|
|
43
|
+
import { readLastInboundAt } from "./daemon.js";
|
|
43
44
|
import { getTmuxSession } from "./config.js";
|
|
44
45
|
export function resolveReplyThreadId(argsThreadId, instanceConfig) {
|
|
45
46
|
if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
|
|
@@ -251,6 +252,41 @@ export class FleetManager {
|
|
|
251
252
|
return this.classicChannels?.getInstanceByChannel(channelId, adapterId)
|
|
252
253
|
?? this.routing.resolve(channelId)?.name;
|
|
253
254
|
}
|
|
255
|
+
async handlePauseWakeSlash(data, adapterId) {
|
|
256
|
+
const action = data.command;
|
|
257
|
+
const classicName = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
258
|
+
if (classicName) {
|
|
259
|
+
if (!this.classicChannels?.isAdmin(data.userId)) {
|
|
260
|
+
await data.respond(t("permission.denied"));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
await data.respond(await this.topicCommands.runPauseWake(classicName, action));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!this.isFleetAdmin(data.userId, adapterId)) {
|
|
267
|
+
await data.respond(t("permission.denied"));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const route = this.routing.resolve(data.channelId);
|
|
271
|
+
if (!route) {
|
|
272
|
+
await data.respond(t("classic.no_agent"));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
let target = route.name;
|
|
276
|
+
if (route.kind === "general") {
|
|
277
|
+
const requested = typeof data.options?.instance === "string" ? data.options.instance : undefined;
|
|
278
|
+
if (!requested) {
|
|
279
|
+
await data.respond(t(`${action}.usage`));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (!this.fleetConfig?.instances[requested]) {
|
|
283
|
+
await data.respond(t("instance.not_found", requested));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
target = requested;
|
|
287
|
+
}
|
|
288
|
+
await data.respond(await this.topicCommands.runPauseWake(target, action));
|
|
289
|
+
}
|
|
254
290
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
255
291
|
getAdapterForInstance(name) {
|
|
256
292
|
const worldId = this.instanceWorldBinding.get(name);
|
|
@@ -345,6 +381,19 @@ export class FleetManager {
|
|
|
345
381
|
throw new Error(`Instance '${instanceName}' IPC is unavailable`);
|
|
346
382
|
ipc.send(payload);
|
|
347
383
|
}
|
|
384
|
+
/** Fleet admin is an explicit config allowlist entry, not merely an open/paired user. */
|
|
385
|
+
isFleetAdmin(userId, adapterId) {
|
|
386
|
+
const allowed = this.getChannelConfig(adapterId)?.access?.allowed_users ?? [];
|
|
387
|
+
return allowed.some(entry => String(entry) === String(userId));
|
|
388
|
+
}
|
|
389
|
+
async changeInstancePauseState(name, action) {
|
|
390
|
+
if (action === "wake") {
|
|
391
|
+
await this.lifecycle.wake(name, 30_000);
|
|
392
|
+
return "awake";
|
|
393
|
+
}
|
|
394
|
+
await this.lifecycle.pause(name);
|
|
395
|
+
return this.lifecycle.isPaused(name) ? "paused" : "not_idle";
|
|
396
|
+
}
|
|
348
397
|
async startInstance(name, config, topicMode) {
|
|
349
398
|
if (config.general_topic) {
|
|
350
399
|
this.ensureGeneralInstructions(config.working_directory, config.backend);
|
|
@@ -1038,19 +1087,15 @@ export class FleetManager {
|
|
|
1038
1087
|
// Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
|
|
1039
1088
|
this.adapter.on("slash_command", safeHandler(async (data) => {
|
|
1040
1089
|
if (data.command === "start") {
|
|
1041
|
-
|
|
1042
|
-
if (requestedBackend) {
|
|
1043
|
-
const reply = await this.handleClassicStart(data.channelId, data.channelName, data.userId, data.guildId, adapterId, requestedBackend);
|
|
1044
|
-
await data.respond(reply);
|
|
1045
|
-
}
|
|
1046
|
-
else {
|
|
1047
|
-
await this.beginClassicBackendSelection(data, this.adapter);
|
|
1048
|
-
}
|
|
1090
|
+
await this.handleClassicStartSlash(data, adapterId, this.adapter);
|
|
1049
1091
|
}
|
|
1050
1092
|
else if (data.command === "stop") {
|
|
1051
1093
|
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
1052
1094
|
await data.respond(reply);
|
|
1053
1095
|
}
|
|
1096
|
+
else if (data.command === "pause" || data.command === "wake") {
|
|
1097
|
+
await this.handlePauseWakeSlash(data, adapterId);
|
|
1098
|
+
}
|
|
1054
1099
|
else if (data.command === "chat") {
|
|
1055
1100
|
const text = data.text ?? "";
|
|
1056
1101
|
if (!text) {
|
|
@@ -1325,19 +1370,15 @@ export class FleetManager {
|
|
|
1325
1370
|
// Slash commands: classic bot + admin commands
|
|
1326
1371
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
1327
1372
|
if (data.command === "start") {
|
|
1328
|
-
|
|
1329
|
-
if (requestedBackend) {
|
|
1330
|
-
const reply = await this.handleClassicStart(data.channelId, data.channelName, data.userId, data.guildId, adapterId, requestedBackend);
|
|
1331
|
-
await data.respond(reply);
|
|
1332
|
-
}
|
|
1333
|
-
else {
|
|
1334
|
-
await this.beginClassicBackendSelection(data, adapter);
|
|
1335
|
-
}
|
|
1373
|
+
await this.handleClassicStartSlash(data, adapterId, adapter);
|
|
1336
1374
|
}
|
|
1337
1375
|
else if (data.command === "stop") {
|
|
1338
1376
|
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
1339
1377
|
await data.respond(reply);
|
|
1340
1378
|
}
|
|
1379
|
+
else if (data.command === "pause" || data.command === "wake") {
|
|
1380
|
+
await this.handlePauseWakeSlash(data, adapterId);
|
|
1381
|
+
}
|
|
1341
1382
|
else if (data.command === "chat") {
|
|
1342
1383
|
const text = data.text ?? "";
|
|
1343
1384
|
if (!text) {
|
|
@@ -1897,6 +1938,20 @@ export class FleetManager {
|
|
|
1897
1938
|
await msgAdapter?.sendText(chatId, reply);
|
|
1898
1939
|
return;
|
|
1899
1940
|
}
|
|
1941
|
+
const pauseWake = parsePauseWakeCommand(text);
|
|
1942
|
+
if (pauseWake) {
|
|
1943
|
+
if (!this.classicChannels.isAdmin(msg.userId)) {
|
|
1944
|
+
await msgAdapter?.sendText(chatId, t("permission.denied"));
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const name = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1948
|
+
if (!name) {
|
|
1949
|
+
await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
await msgAdapter?.sendText(chatId, await this.topicCommands.runPauseWake(name, pauseWake.action));
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1900
1955
|
// Handle /compact command (admin only)
|
|
1901
1956
|
if (text === "/compact" || text.startsWith("/compact@")) {
|
|
1902
1957
|
if (!this.classicChannels.isAdmin(msg.userId)) {
|
|
@@ -3829,6 +3884,33 @@ When users create specialized instances, suggest these configurations:
|
|
|
3829
3884
|
return t("classic.topic_bound");
|
|
3830
3885
|
return undefined;
|
|
3831
3886
|
}
|
|
3887
|
+
isBackendInstalled(backend) {
|
|
3888
|
+
const installation = BACKEND_INSTALLATION_INFO[backend];
|
|
3889
|
+
return !!installation && checkBinaryInstalled(installation.binary);
|
|
3890
|
+
}
|
|
3891
|
+
getMissingBackendWarning(backend) {
|
|
3892
|
+
if (!backend)
|
|
3893
|
+
return undefined;
|
|
3894
|
+
const installation = BACKEND_INSTALLATION_INFO[backend];
|
|
3895
|
+
if (!installation || this.isBackendInstalled(backend))
|
|
3896
|
+
return undefined;
|
|
3897
|
+
return t("classic.backend_not_installed", backend, installation.binary, installation.install);
|
|
3898
|
+
}
|
|
3899
|
+
/** Handle Discord's optional static slash choice, warning before a likely startup failure. */
|
|
3900
|
+
async handleClassicStartSlash(data, adapterId, adapter) {
|
|
3901
|
+
const requestedBackend = typeof data.options?.backend === "string" ? data.options.backend : undefined;
|
|
3902
|
+
if (!requestedBackend) {
|
|
3903
|
+
await this.beginClassicBackendSelection(data, adapter);
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
const warning = this.getMissingBackendWarning(requestedBackend);
|
|
3907
|
+
// Keep the deferred ephemeral response useful even if daemon startup later
|
|
3908
|
+
// fails because the executable is absent. This is advisory, not a gate.
|
|
3909
|
+
if (warning)
|
|
3910
|
+
await data.respond(warning);
|
|
3911
|
+
const reply = await this.handleClassicStart(data.channelId, data.channelName, data.userId, data.guildId, adapterId, requestedBackend);
|
|
3912
|
+
await data.respond(warning ? `${warning}\n\n${reply}` : reply);
|
|
3913
|
+
}
|
|
3832
3914
|
/** Present platform-native backend choices, then start on selection or timeout. */
|
|
3833
3915
|
async beginClassicBackendSelection(data, adapter) {
|
|
3834
3916
|
const adapterId = adapter.id;
|
|
@@ -3840,7 +3922,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
3840
3922
|
const nonce = randomBytes(6).toString("hex");
|
|
3841
3923
|
const choices = getClassicBackendChoices().map(choice => ({
|
|
3842
3924
|
id: `${CLASSIC_BACKEND_CALLBACK_PREFIX}${nonce}:${choice.id}`,
|
|
3843
|
-
label: choice.label
|
|
3925
|
+
label: `${this.isBackendInstalled(choice.id) ? "✅" : "❌"} ${choice.label}`,
|
|
3844
3926
|
}));
|
|
3845
3927
|
const complete = data.respondChoices
|
|
3846
3928
|
? async (text) => { await data.respond(text); }
|
|
@@ -3908,8 +3990,17 @@ When users create specialized instances, suggest these configurations:
|
|
|
3908
3990
|
this.pendingClassicStarts.delete(nonce);
|
|
3909
3991
|
clearTimeout(pending.timer);
|
|
3910
3992
|
const selectedBackend = isSelectableClassicBackend(backend) ? backend : undefined;
|
|
3993
|
+
const effectiveBackend = selectedBackend
|
|
3994
|
+
?? this.classicChannels?.getDefaults().backend
|
|
3995
|
+
?? this.fleetConfig?.defaults?.backend
|
|
3996
|
+
?? "claude-code";
|
|
3997
|
+
const warning = this.getMissingBackendWarning(effectiveBackend);
|
|
3998
|
+
// Show the warning before starting so it survives a missing-binary startup
|
|
3999
|
+
// failure. The selected backend is still attempted as requested.
|
|
4000
|
+
if (warning)
|
|
4001
|
+
await pending.complete(warning, pending.messageId);
|
|
3911
4002
|
const reply = await this.handleClassicStart(pending.channelId, pending.channelName, pending.userId, pending.guildId, pending.adapterId, selectedBackend);
|
|
3912
|
-
await pending.complete(reply, pending.messageId);
|
|
4003
|
+
await pending.complete(warning ? `${warning}\n\n${reply}` : reply, pending.messageId);
|
|
3913
4004
|
}
|
|
3914
4005
|
/** Start a classic channel instance with lightweight config */
|
|
3915
4006
|
async startClassicInstance(instanceName, backend, preTaskCommand, model) {
|
|
@@ -4533,8 +4624,27 @@ When users create specialized instances, suggest these configurations:
|
|
|
4533
4624
|
// Fleet API (enriched for agent board)
|
|
4534
4625
|
if (req.method === "GET" && req.url === "/api/fleet") {
|
|
4535
4626
|
const sysInfo = this.getSysInfo();
|
|
4536
|
-
const
|
|
4627
|
+
const fleetInstances = sysInfo.instances.map(inst => ({ ...inst, classic: false }));
|
|
4628
|
+
const fleetNames = new Set(fleetInstances.map(inst => inst.name));
|
|
4629
|
+
const classicInstances = (this.classicChannels?.getAll() ?? [])
|
|
4630
|
+
.filter(channel => !fleetNames.has(channel.instanceName))
|
|
4631
|
+
.map(channel => ({
|
|
4632
|
+
name: channel.instanceName,
|
|
4633
|
+
status: this.getInstanceStatus(channel.instanceName),
|
|
4634
|
+
state: this.getInstanceExecutionState(channel.instanceName),
|
|
4635
|
+
ipc: this.instanceIpcClients.has(channel.instanceName),
|
|
4636
|
+
costCents: this.costGuard?.getDailyCostCents(channel.instanceName) ?? 0,
|
|
4637
|
+
rateLimits: this.statuslineWatcher.getRateLimits(channel.instanceName) ?? null,
|
|
4638
|
+
classic: true,
|
|
4639
|
+
classicName: channel.name,
|
|
4640
|
+
channelId: channel.channelId,
|
|
4641
|
+
adapterId: channel.adapterId ?? null,
|
|
4642
|
+
}));
|
|
4643
|
+
const enriched = [...fleetInstances, ...classicInstances].map(inst => {
|
|
4537
4644
|
const config = this.fleetConfig?.instances[inst.name];
|
|
4645
|
+
const backend = inst.classic
|
|
4646
|
+
? this.classicChannels?.getBackendByInstance(inst.name, this.fleetConfig?.defaults.backend) ?? "claude-code"
|
|
4647
|
+
: config?.backend ?? "claude-code";
|
|
4538
4648
|
// Find claimed tasks for this instance
|
|
4539
4649
|
let currentTask = null;
|
|
4540
4650
|
try {
|
|
@@ -4547,11 +4657,13 @@ When users create specialized instances, suggest these configurations:
|
|
|
4547
4657
|
}
|
|
4548
4658
|
return {
|
|
4549
4659
|
...inst,
|
|
4550
|
-
description: config?.description ?? null,
|
|
4551
|
-
backend
|
|
4660
|
+
description: config?.description ?? ("classicName" in inst ? inst.classicName : null),
|
|
4661
|
+
backend,
|
|
4552
4662
|
tool_set: config?.tool_set ?? "full",
|
|
4553
4663
|
general_topic: config?.general_topic ?? false,
|
|
4554
|
-
|
|
4664
|
+
// User activity is persisted by the daemon, so both the board and
|
|
4665
|
+
// auto-pause retain an accurate age across fleet restarts.
|
|
4666
|
+
lastActivity: (readLastInboundAt(this.getInstanceDir(inst.name)) ?? this.lastActivityMs(inst.name)) || null,
|
|
4555
4667
|
currentTask,
|
|
4556
4668
|
idle: this.getInstanceIdle(inst.name),
|
|
4557
4669
|
state: this.getInstanceExecutionState(inst.name),
|