@songsid/agend 2.0.8-beta.19 → 2.0.8-beta.20

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.
@@ -163,6 +163,12 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
163
163
  toggleFleetCollab(instanceName: string): boolean;
164
164
  notifyInstanceTopic(instanceName: string, text: string): void;
165
165
  /** Send a "🛑 Cancel" button to the instance's topic/channel after delivery. */
166
+ /**
167
+ * Handle the DC `/save` slash command for both classic AND fleet-topic targets.
168
+ * Picks the backend-appropriate command (kiro → /chat save, claude → /export);
169
+ * unsupported backends get a clear error. Routes via classic paste or fleet IPC.
170
+ */
171
+ private handleSlashSave;
166
172
  sendCancelButton(instanceName: string): Promise<void>;
167
173
  /**
168
174
  * Poll the instance's idle state and retire its cancel button once the work is
@@ -22,7 +22,7 @@ import { processAttachments } from "./channel/attachment-handler.js";
22
22
  import { routeToolCall } from "./channel/tool-router.js";
23
23
  import { Scheduler } from "./scheduler/index.js";
24
24
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
25
- import { TopicCommands, sanitizeInstanceName } from "./topic-commands.js";
25
+ import { TopicCommands, sanitizeInstanceName, saveCommandForBackend, parseSaveFilename, SAVE_FILENAME_RE, SAVE_UNSUPPORTED_MSG } from "./topic-commands.js";
26
26
  import { DailySummary } from "./daily-summary.js";
27
27
  import { WebhookEmitter } from "./webhook-emitter.js";
28
28
  import { TmuxControlClient } from "./tmux-control.js";
@@ -794,7 +794,11 @@ export class FleetManager {
794
794
  timestamp: new Date(),
795
795
  });
796
796
  }
797
- else if (data.command === "save" || data.command === "load") {
797
+ else if (data.command === "save") {
798
+ await this.handleSlashSave(data);
799
+ }
800
+ else if (data.command === "load") {
801
+ // load is kiro-cli/classic only — no claude-code equivalent.
798
802
  if (!this.classicChannels?.isAdmin(data.userId)) {
799
803
  await data.respond("⛔ This command requires admin access.");
800
804
  return;
@@ -804,25 +808,13 @@ export class FleetManager {
804
808
  await data.respond("No active agent in this channel. Use `/start` first.");
805
809
  return;
806
810
  }
807
- let rawCmd;
808
- if (data.command === "save") {
809
- const filename = data.options?.filename;
810
- if (!/^[\w.-]+$/.test(filename)) {
811
- await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
812
- return;
813
- }
814
- rawCmd = data.options?.force ? `/chat save ${filename} -f` : `/chat save ${filename}`;
815
- }
816
- else {
817
- const filename = data.options?.filename;
818
- if (!/^[\w.-]+$/.test(filename)) {
819
- await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
820
- return;
821
- }
822
- rawCmd = `/chat load ${filename}`;
811
+ const filename = data.options?.filename;
812
+ if (!SAVE_FILENAME_RE.test(filename ?? "")) {
813
+ await data.respond("⛔ Invalid filename only letters, numbers, dots, hyphens, underscores allowed.");
814
+ return;
823
815
  }
824
- this.pasteRawToClassicInstance(target.name, rawCmd);
825
- await data.respond(`✅ Sent \`${rawCmd}\` to ${target.name}`);
816
+ this.pasteRawToClassicInstance(target.name, `/chat load ${filename}`);
817
+ await data.respond(`✅ Sent \`/chat load ${filename}\` to ${target.name}`);
826
818
  }
827
819
  else if (data.command === "compact") {
828
820
  const target = this.routing.resolve(data.channelId);
@@ -1064,7 +1056,11 @@ export class FleetManager {
1064
1056
  timestamp: new Date(),
1065
1057
  });
1066
1058
  }
1067
- else if (data.command === "save" || data.command === "load") {
1059
+ else if (data.command === "save") {
1060
+ await this.handleSlashSave(data);
1061
+ }
1062
+ else if (data.command === "load") {
1063
+ // load is kiro-cli/classic only — no claude-code equivalent.
1068
1064
  if (!this.classicChannels?.isAdmin(data.userId)) {
1069
1065
  await data.respond("⛔ This command requires admin access.");
1070
1066
  return;
@@ -1074,25 +1070,13 @@ export class FleetManager {
1074
1070
  await data.respond("No active agent in this channel. Use `/start` first.");
1075
1071
  return;
1076
1072
  }
1077
- let rawCmd;
1078
- if (data.command === "save") {
1079
- const filename = data.options?.filename;
1080
- if (!/^[\w.-]+$/.test(filename)) {
1081
- await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
1082
- return;
1083
- }
1084
- rawCmd = data.options?.force ? `/chat save ${filename} -f` : `/chat save ${filename}`;
1085
- }
1086
- else {
1087
- const filename = data.options?.filename;
1088
- if (!/^[\w.-]+$/.test(filename)) {
1089
- await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
1090
- return;
1091
- }
1092
- rawCmd = `/chat load ${filename}`;
1073
+ const filename = data.options?.filename;
1074
+ if (!SAVE_FILENAME_RE.test(filename ?? "")) {
1075
+ await data.respond("⛔ Invalid filename only letters, numbers, dots, hyphens, underscores allowed.");
1076
+ return;
1093
1077
  }
1094
- this.pasteRawToClassicInstance(target.name, rawCmd);
1095
- await data.respond(`✅ Sent \`${rawCmd}\` to ${target.name}`);
1078
+ this.pasteRawToClassicInstance(target.name, `/chat load ${filename}`);
1079
+ await data.respond(`✅ Sent \`/chat load ${filename}\` to ${target.name}`);
1096
1080
  }
1097
1081
  else if (data.command === "compact") {
1098
1082
  const target = this.routing.resolve(data.channelId);
@@ -1578,6 +1562,36 @@ export class FleetManager {
1578
1562
  await msgAdapter?.sendText(chatId, reply);
1579
1563
  return;
1580
1564
  }
1565
+ // Handle /save command (admin only)
1566
+ if (text === "/save" || text.startsWith("/save ") || text.startsWith("/save@")) {
1567
+ if (!this.classicChannels.isAdmin(msg.userId)) {
1568
+ await msgAdapter?.sendText(chatId, "⛔ /save requires admin access.");
1569
+ return;
1570
+ }
1571
+ const saveTarget = this.routing.resolve(chatId);
1572
+ if (!saveTarget || saveTarget.kind !== "classic") {
1573
+ await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
1574
+ return;
1575
+ }
1576
+ const filename = parseSaveFilename(text);
1577
+ if (!filename) {
1578
+ await msgAdapter?.sendText(chatId, "Usage: /save <filename>");
1579
+ return;
1580
+ }
1581
+ if (!SAVE_FILENAME_RE.test(filename)) {
1582
+ await msgAdapter?.sendText(chatId, "⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
1583
+ return;
1584
+ }
1585
+ const backend = this.classicChannels.getBackendByInstance(saveTarget.name, this.fleetConfig?.defaults?.backend);
1586
+ const cmd = saveCommandForBackend(backend, filename);
1587
+ if (!cmd) {
1588
+ await msgAdapter?.sendText(chatId, SAVE_UNSUPPORTED_MSG);
1589
+ return;
1590
+ }
1591
+ this.pasteRawToClassicInstance(saveTarget.name, cmd);
1592
+ await msgAdapter?.sendText(chatId, `✅ Sent \`${cmd}\` to ${saveTarget.name}`);
1593
+ return;
1594
+ }
1581
1595
  // Route to classic channel if registered
1582
1596
  const target = this.routing.resolve(chatId);
1583
1597
  if (target?.kind === "classic") {
@@ -2525,6 +2539,44 @@ export class FleetManager {
2525
2539
  // Sent after delivering a user message to an instance; clicking it (or
2526
2540
  // /cancel) sends Escape to the instance's pane to interrupt generation.
2527
2541
  /** Send a "🛑 Cancel" button to the instance's topic/channel after delivery. */
2542
+ /**
2543
+ * Handle the DC `/save` slash command for both classic AND fleet-topic targets.
2544
+ * Picks the backend-appropriate command (kiro → /chat save, claude → /export);
2545
+ * unsupported backends get a clear error. Routes via classic paste or fleet IPC.
2546
+ */
2547
+ async handleSlashSave(data) {
2548
+ if (!this.classicChannels?.isAdmin(data.userId)) {
2549
+ await data.respond("⛔ This command requires admin access.");
2550
+ return;
2551
+ }
2552
+ const target = this.routing.resolve(data.channelId);
2553
+ if (!target) {
2554
+ await data.respond("No active agent in this channel. Use `/start` first.");
2555
+ return;
2556
+ }
2557
+ const filename = data.options?.filename ?? "";
2558
+ if (!SAVE_FILENAME_RE.test(filename)) {
2559
+ await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
2560
+ return;
2561
+ }
2562
+ const backend = target.kind === "classic"
2563
+ ? this.classicChannels.getBackendByInstance(target.name, this.fleetConfig?.defaults?.backend)
2564
+ : (this.fleetConfig?.instances[target.name]?.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code");
2565
+ // force (-f) is only meaningful for kiro/classic /chat save.
2566
+ const force = target.kind === "classic" && !!data.options?.force;
2567
+ const cmd = saveCommandForBackend(backend, filename, force);
2568
+ if (!cmd) {
2569
+ await data.respond(SAVE_UNSUPPORTED_MSG);
2570
+ return;
2571
+ }
2572
+ if (target.kind === "classic") {
2573
+ this.pasteRawToClassicInstance(target.name, cmd);
2574
+ }
2575
+ else {
2576
+ this.instanceIpcClients.get(target.name)?.send({ type: "raw_paste", content: cmd });
2577
+ }
2578
+ await data.respond(`✅ Sent \`${cmd}\` to ${target.name}`);
2579
+ }
2528
2580
  async sendCancelButton(instanceName) {
2529
2581
  // Replace any stale button for this instance first.
2530
2582
  this.clearCancelButton(instanceName);