@songsid/agend 2.2.0-beta.3 → 2.2.0-beta.4

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.
Files changed (96) hide show
  1. package/dist/agent-cli.js +6 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/backend/claude-code.d.ts +39 -2
  4. package/dist/backend/claude-code.js +115 -31
  5. package/dist/backend/claude-code.js.map +1 -1
  6. package/dist/backend/credential-profile.d.ts +33 -3
  7. package/dist/backend/credential-profile.js +107 -5
  8. package/dist/backend/credential-profile.js.map +1 -1
  9. package/dist/backend/types.d.ts +1 -1
  10. package/dist/cache-service.d.ts +2 -0
  11. package/dist/cache-service.js +2 -1
  12. package/dist/cache-service.js.map +1 -1
  13. package/dist/channel/adapters/discord.d.ts +13 -0
  14. package/dist/channel/adapters/discord.js +42 -0
  15. package/dist/channel/adapters/discord.js.map +1 -1
  16. package/dist/channel/adapters/telegram.d.ts +6 -1
  17. package/dist/channel/adapters/telegram.js +27 -2
  18. package/dist/channel/adapters/telegram.js.map +1 -1
  19. package/dist/channel/types.d.ts +22 -1
  20. package/dist/config-validator.js +4 -0
  21. package/dist/config-validator.js.map +1 -1
  22. package/dist/daemon.d.ts +96 -0
  23. package/dist/daemon.js +339 -16
  24. package/dist/daemon.js.map +1 -1
  25. package/dist/delivery-outbox.d.ts +18 -5
  26. package/dist/delivery-outbox.js +121 -14
  27. package/dist/delivery-outbox.js.map +1 -1
  28. package/dist/fleet-context.d.ts +1 -0
  29. package/dist/fleet-manager.d.ts +59 -3
  30. package/dist/fleet-manager.js +357 -71
  31. package/dist/fleet-manager.js.map +1 -1
  32. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +43 -10
  33. package/dist/host-memory.d.ts +10 -0
  34. package/dist/host-memory.js +33 -1
  35. package/dist/host-memory.js.map +1 -1
  36. package/dist/instance-lifecycle.d.ts +25 -6
  37. package/dist/instance-lifecycle.js +73 -26
  38. package/dist/instance-lifecycle.js.map +1 -1
  39. package/dist/locale.js +44 -18
  40. package/dist/locale.js.map +1 -1
  41. package/dist/login-controller.d.ts +20 -3
  42. package/dist/login-controller.js +71 -19
  43. package/dist/login-controller.js.map +1 -1
  44. package/dist/login-flows.d.ts +5 -2
  45. package/dist/login-flows.js +5 -4
  46. package/dist/login-flows.js.map +1 -1
  47. package/dist/memory-pressure.d.ts +13 -1
  48. package/dist/memory-pressure.js +50 -10
  49. package/dist/memory-pressure.js.map +1 -1
  50. package/dist/outbound-handlers.d.ts +61 -0
  51. package/dist/outbound-handlers.js +100 -35
  52. package/dist/outbound-handlers.js.map +1 -1
  53. package/dist/outbound-schemas.d.ts +1 -0
  54. package/dist/outbound-schemas.js +2 -0
  55. package/dist/outbound-schemas.js.map +1 -1
  56. package/dist/public-web-gateway.js +4 -3
  57. package/dist/public-web-gateway.js.map +1 -1
  58. package/dist/settings-api.js +10 -4
  59. package/dist/settings-api.js.map +1 -1
  60. package/dist/spawn-gate.js +5 -1
  61. package/dist/spawn-gate.js.map +1 -1
  62. package/dist/spawn-hold-reply.d.ts +12 -0
  63. package/dist/spawn-hold-reply.js +39 -0
  64. package/dist/spawn-hold-reply.js.map +1 -0
  65. package/dist/topic-commands.js +9 -2
  66. package/dist/topic-commands.js.map +1 -1
  67. package/dist/turn-resume.d.ts +2 -0
  68. package/dist/turn-resume.js +1 -1
  69. package/dist/turn-resume.js.map +1 -1
  70. package/dist/types.d.ts +2 -0
  71. package/dist/ui/chat-mentions.js +63 -0
  72. package/dist/ui/chat-render.js +25 -0
  73. package/dist/ui/chat-thread.js +26 -8
  74. package/dist/ui/panel-chat.js +17 -9
  75. package/dist/ui/panel-details.js +2 -0
  76. package/dist/ui/panel-fleet.js +51 -22
  77. package/dist/ui/panel-settings.js +3 -1
  78. package/dist/ui/preview-panel.js +48 -14
  79. package/dist/ui/preview.js +5 -1
  80. package/dist/ui/settings-token.js +1 -1
  81. package/dist/ui/shared/app-store.js +3 -1
  82. package/dist/ui/shared/app.css +11 -7
  83. package/dist/ui/shared/instance-nav.js +32 -7
  84. package/dist/ui/shared/view-strings.js +6 -0
  85. package/dist/unrecognised-prompt.d.ts +23 -0
  86. package/dist/unrecognised-prompt.js +74 -0
  87. package/dist/unrecognised-prompt.js.map +1 -0
  88. package/dist/web-api.d.ts +3 -0
  89. package/dist/web-api.js +115 -17
  90. package/dist/web-api.js.map +1 -1
  91. package/dist/web-auth.js +1 -1
  92. package/dist/web-auth.js.map +1 -1
  93. package/dist/web-mentions.d.ts +44 -0
  94. package/dist/web-mentions.js +75 -0
  95. package/dist/web-mentions.js.map +1 -0
  96. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ /**
2
+ * #1691: the shape of a prompt a CLI is waiting on, read from a pane — for prompts no backend table knows (a new CLI
3
+ * version's dialog, #1690's "Switch model?"). Pure: the daemon decides when a shape counts (no known dialog, no idle
4
+ * composer, unchanged for a minute) and never answers it.
5
+ *
6
+ * A shape is bottom-anchored: the prompt is the last thing on screen apart from a short footer, so an agent's reply
7
+ * that merely contains a numbered list (or "[y/n]") above the CLI's own prompt rows is not one.
8
+ */
9
+ export interface PromptShape {
10
+ /** "numbered": a cursor glyph on a numbered option, with a neighbouring option; "yes-no": a [y/n]-style question. */
11
+ kind: "numbered" | "yes-no";
12
+ /** Numbered options seen (0 for yes-no). */
13
+ options: number;
14
+ /** The prompt's question/title line, sanitised and bounded (see questionLine); null when there is none. */
15
+ question: string | null;
16
+ /** The prompt's rows, from the question to the last option — its identity (the same block is the same prompt). */
17
+ block: string;
18
+ }
19
+ export declare const QUESTION_MAX = 80;
20
+ /** One short line for a notice: escapes and control characters out, whitespace collapsed, at most QUESTION_MAX. */
21
+ export declare function questionLine(raw: string): string;
22
+ /** The prompt the pane is showing at its bottom, or null. */
23
+ export declare function unrecognisedPromptShape(pane: string): PromptShape | null;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * #1691: the shape of a prompt a CLI is waiting on, read from a pane — for prompts no backend table knows (a new CLI
3
+ * version's dialog, #1690's "Switch model?"). Pure: the daemon decides when a shape counts (no known dialog, no idle
4
+ * composer, unchanged for a minute) and never answers it.
5
+ *
6
+ * A shape is bottom-anchored: the prompt is the last thing on screen apart from a short footer, so an agent's reply
7
+ * that merely contains a numbered list (or "[y/n]") above the CLI's own prompt rows is not one.
8
+ */
9
+ /** Rows looked at, counted from the bottom (non-empty rows only). */
10
+ const BOTTOM_ROWS = 20;
11
+ /** Rows a prompt may have below its last option or question (a hint footer: "Enter to confirm · Esc to cancel"). */
12
+ const FOOTER_ROWS = 3;
13
+ /** How far above the first option the question is looked for. */
14
+ const QUESTION_ROWS = 8;
15
+ export const QUESTION_MAX = 80;
16
+ const CURSOR_OPTION = /^[ \t]*[❯›>][ \t]*(\d{1,2})[.)][ \t]+\S/;
17
+ const OPTION = /^[ \t]*(?:[❯›>][ \t]*)?(\d{1,2})[.)][ \t]+\S/;
18
+ const YES_NO = /[[(][ \t]*[yY][ \t]*\/[ \t]*[nN][ \t]*[\])][ \t]*[:?]?[ \t]*$/;
19
+ /** A rule or border row (box drawing, block elements, dashes): the top of a dialog. */
20
+ const RULE = /^[ \t]*[─━═╌┄▔▁_\-=]{8,}[ \t]*$/;
21
+ const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
22
+ /** One short line for a notice: escapes and control characters out, whitespace collapsed, at most QUESTION_MAX. */
23
+ export function questionLine(raw) {
24
+ const text = raw.replace(ANSI, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/\s+/g, " ").trim();
25
+ return text.length > QUESTION_MAX ? `${text.slice(0, QUESTION_MAX - 1)}…` : text;
26
+ }
27
+ /** The prompt the pane is showing at its bottom, or null. */
28
+ export function unrecognisedPromptShape(pane) {
29
+ const rows = pane.replace(ANSI, "").replace(/\r/g, "").split("\n").map(r => r.trimEnd());
30
+ const filled = [];
31
+ for (let i = rows.length - 1; i >= 0 && filled.length < BOTTOM_ROWS; i--)
32
+ if (rows[i].trim() !== "")
33
+ filled.unshift(i);
34
+ if (filled.length === 0)
35
+ return null;
36
+ const after = (row) => filled.filter(i => i > row).length;
37
+ // A cursor on a numbered option, with its neighbours: the contiguous run of option rows around it.
38
+ for (let k = filled.length - 1; k >= 0; k--) {
39
+ const at = filled[k];
40
+ const cursor = CURSOR_OPTION.exec(rows[at]);
41
+ if (!cursor)
42
+ continue;
43
+ let first = at, last = at;
44
+ while (first - 1 >= 0 && OPTION.test(rows[first - 1]))
45
+ first--;
46
+ while (last + 1 < rows.length && OPTION.test(rows[last + 1]))
47
+ last++;
48
+ const numbers = rows.slice(first, last + 1).map(r => Number(OPTION.exec(r)[1]));
49
+ const consecutive = numbers.every((n, i) => i === 0 || n === numbers[i - 1] + 1);
50
+ if (numbers.length < 2 || !consecutive || after(last) > FOOTER_ROWS)
51
+ return null;
52
+ // The question: the top of the dialog — the first row under a rule/border, else the topmost filled row — within
53
+ // QUESTION_ROWS above the options.
54
+ let top = -1;
55
+ for (let i = first - 1; i >= 0 && first - i <= QUESTION_ROWS; i--) {
56
+ if (RULE.test(rows[i]))
57
+ break;
58
+ if (rows[i].trim() !== "")
59
+ top = i;
60
+ }
61
+ const question = top >= 0 ? questionLine(rows[top]) : null;
62
+ const block = rows.slice(top >= 0 ? top : first, last + 1).join("\n");
63
+ return { kind: "numbered", options: numbers.length, question: question || null, block };
64
+ }
65
+ // A [y/n] question at the bottom.
66
+ for (let k = filled.length - 1; k >= 0 && filled.length - 1 - k <= FOOTER_ROWS; k--) {
67
+ const at = filled[k];
68
+ if (!YES_NO.test(rows[at]))
69
+ continue;
70
+ return { kind: "yes-no", options: 0, question: questionLine(rows[at]) || null, block: rows[at].trim() };
71
+ }
72
+ return null;
73
+ }
74
+ //# sourceMappingURL=unrecognised-prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unrecognised-prompt.js","sourceRoot":"","sources":["../src/unrecognised-prompt.ts"],"names":[],"mappings":"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"}
package/dist/web-api.d.ts CHANGED
@@ -9,6 +9,7 @@ import { type WebChatHistory } from "./web-chat-history.js";
9
9
  import { type WebFileLedger } from "./web-upload.js";
10
10
  import type { WebSessionStore } from "./web-session.js";
11
11
  import { type WebPrefsStore } from "./web-prefs.js";
12
+ import { type MentionNames, type MentionQuery } from "./web-mentions.js";
12
13
  import type { ExplicitInstanceRemoval } from "./instance-removal.js";
13
14
  /**
14
15
  * Push a single SSE frame to every client. If a client throws (closed socket
@@ -121,6 +122,8 @@ export interface WebApiContext {
121
122
  webImage?(kind: "bot" | "avatar", key: string): CachedImage | null;
122
123
  /** #1630: one Discord server emoji of the fleet's bots, from the host cache (fetched once); null for any other id. */
123
124
  webEmoji?(id: string): Promise<CachedImage | null>;
125
+ /** #1682: names for mention ids (only those asked; from memory, never a request); absent: no names. */
126
+ webMentionNames?(query: MentionQuery): MentionNames;
124
127
  /** #1630: the custom emoji of the servers the fleet's Discord bots are in. */
125
128
  webEmojiList?(): Promise<Array<{
126
129
  id: string;
package/dist/web-api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { withSpawnHoldReply } from "./spawn-hold-reply.js";
1
2
  import { binaryProbe } from "./binary-probe.js";
2
3
  import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
3
4
  import { readBoundedWebBody } from "./web-body.js";
@@ -25,6 +26,8 @@ import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
25
26
  import { attachmentDelivery, displayName, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, sniffUpload, UPLOAD_LIMITS, wellFormed } from "./web-upload.js";
26
27
  import { getAgendHome } from "./paths.js";
27
28
  import { OPERATOR } from "./web-prefs.js";
29
+ import { commitTeamCreate, commitTeamUpdate } from "./outbound-handlers.js";
30
+ import { parseMentionQuery } from "./web-mentions.js";
28
31
  import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
29
32
  import { measureSyncWork } from "./sync-work-attribution.js";
30
33
  // ── Strict public-facing schemas ────────────────────────────────────────────
@@ -58,6 +61,12 @@ const TeamCreateSchema = z.object({
58
61
  members: z.array(z.string().min(1).max(256)).min(1).max(256),
59
62
  description: z.string().max(MAX_TEXT).optional(),
60
63
  }).strict();
64
+ // #1689: an edit of a team — its members as they should be, and its description ("" clears it). No name: a team is
65
+ // not renamed.
66
+ const TeamEditSchema = z.object({
67
+ members: z.array(z.string().min(1).max(256)).min(1).max(256),
68
+ description: z.string().max(MAX_TEXT).optional(),
69
+ }).strict();
61
70
  const ConfigUpdateSchema = z.object({
62
71
  channel: z.object({
63
72
  group_id: z.union([z.number(), z.string()]).optional(),
@@ -420,6 +429,24 @@ export function handleWebRequest(req, res, url, ctx) {
420
429
  (ctx.webEmojiList ? ctx.webEmojiList() : Promise.resolve([])).then(answer, () => answer([]));
421
430
  return true;
422
431
  }
432
+ // #1682: the names of the mention ids a page found in what it rendered (at most MAX_MENTION_IDS, digits only) — only
433
+ // those, so no page (the public link included) ever gets a directory; from memory, never a request.
434
+ if (method === "GET" && path === "/ui/mentions") {
435
+ const parsed = parseMentionQuery(url.searchParams);
436
+ if (!parsed.ok) {
437
+ json(res, 400, { error: parsed.error });
438
+ return true;
439
+ }
440
+ let names = { u: {}, r: {}, c: {} };
441
+ try {
442
+ names = ctx.webMentionNames?.(parsed.query) ?? names;
443
+ }
444
+ catch (err) {
445
+ ctx.logger.debug({ err: err.message }, "mention names unavailable");
446
+ }
447
+ json(res, 200, names);
448
+ return true;
449
+ }
423
450
  // ── Fleet prompts (C4): the hang / exit / interactive-prompt buttons, answerable here too ──
424
451
  if (method === "GET" && path === "/ui/prompts") {
425
452
  json(res, 200, { prompts: ctx.listWebPrompts?.() ?? [] });
@@ -720,13 +747,18 @@ export function handleWebRequest(req, res, url, ctx) {
720
747
  }
721
748
  const topicMode = ctx.fleetConfig?.channel?.mode === "topic";
722
749
  (async () => {
750
+ let held = false;
723
751
  try {
724
- await ctx.startInstance(name, config, topicMode ?? false);
752
+ await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false));
725
753
  ctx.emitSseEvent("status", ctx.getUiStatus());
726
- json(res, 200, { started: name });
754
+ if (!held)
755
+ json(res, 200, { started: name });
727
756
  }
728
757
  catch (err) {
729
- json(res, 500, { error: err.message });
758
+ if (!held)
759
+ json(res, 500, { error: err.message });
760
+ else
761
+ ctx.logger.error({ err, name }, "Held instance action failed");
730
762
  }
731
763
  })();
732
764
  return true;
@@ -839,14 +871,19 @@ export function handleWebRequest(req, res, url, ctx) {
839
871
  if (method === "POST" && restartMatch) {
840
872
  const name = decodeURIComponent(restartMatch[1]);
841
873
  (async () => {
874
+ let held = false;
842
875
  try {
843
- await ctx.restartSingleInstance(name, { explicit: true });
876
+ await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.restartSingleInstance(name, { explicit: true }));
844
877
  ctx.emitSseEvent("status", ctx.getUiStatus());
845
- json(res, 200, { restarted: name });
878
+ if (!held)
879
+ json(res, 200, { restarted: name });
846
880
  }
847
881
  catch (err) {
848
882
  const status = err.message.includes("not found") ? 404 : 500;
849
- json(res, status, { error: err.message });
883
+ if (!held)
884
+ json(res, status, { error: err.message });
885
+ else
886
+ ctx.logger.error({ err, name }, "Held instance action failed");
850
887
  }
851
888
  })();
852
889
  return true;
@@ -1027,26 +1064,82 @@ export function handleWebRequest(req, res, url, ctx) {
1027
1064
  return true;
1028
1065
  }
1029
1066
  if (method === "POST" && path === "/ui/teams") {
1067
+ // #1689: not over the public link (absent from its manifest too), like editing and deleting a team.
1068
+ if (gatewayRequestContext(req)) {
1069
+ json(res, 403, { error: "teams_not_over_public_link" });
1070
+ return true;
1071
+ }
1030
1072
  (async () => {
1031
1073
  try {
1032
1074
  const body = await parseBody(req);
1033
1075
  if (!permitWebContinuation(req, res, ctx))
1034
1076
  return;
1077
+ // #1692: who may write, asked again once the body is in (as the edit does).
1078
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1079
+ if (verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin")) {
1080
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1081
+ return;
1082
+ }
1035
1083
  const parsed = parseOrReject(TeamCreateSchema, body, res);
1036
1084
  if (!parsed)
1037
1085
  return;
1038
- if (!ctx.fleetConfig) {
1039
- json(res, 500, { error: "No fleet config" });
1086
+ // #1692: the create_team path — members checked, an existing team never overwritten, a failed save undone.
1087
+ const r = commitTeamCreate(ctx.fleetConfig, parsed, () => ctx.saveFleetConfig());
1088
+ if (!r.ok) {
1089
+ json(res, r.status, { error: r.error });
1040
1090
  return;
1041
1091
  }
1042
- if (!ctx.fleetConfig.teams)
1043
- ctx.fleetConfig.teams = {};
1044
- ctx.fleetConfig.teams[parsed.name] = {
1045
- members: parsed.members,
1046
- description: parsed.description,
1047
- };
1048
- ctx.saveFleetConfig();
1049
- json(res, 200, { created: parsed.name });
1092
+ json(res, 200, r.result);
1093
+ }
1094
+ catch (err) {
1095
+ json(res, 400, { error: err.message });
1096
+ }
1097
+ })();
1098
+ return true;
1099
+ }
1100
+ // #1689: edit a team — the same update as the update_team tool (applyTeamUpdate), from the members the page sends:
1101
+ // what to add and remove is worked out against the team as it is now. Never over the public link.
1102
+ const teamEditMatch = path.match(/^\/ui\/teams\/(.+)$/);
1103
+ if ((method === "PATCH" || method === "PUT") && teamEditMatch) {
1104
+ if (gatewayRequestContext(req)) {
1105
+ json(res, 403, { error: "teams_not_over_public_link" });
1106
+ return true;
1107
+ }
1108
+ const name = decodeURIComponent(teamEditMatch[1]);
1109
+ // Who may write, asked again once the body is in (#1693 review): a session revoked or a token rotated meanwhile
1110
+ // writes nothing.
1111
+ const writerNow = () => {
1112
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1113
+ return verdict.kind === "allow" && !(verdict.via === "session" && verdict.session?.tier !== "admin");
1114
+ };
1115
+ (async () => {
1116
+ try {
1117
+ const body = await parseBody(req);
1118
+ if (!permitWebContinuation(req, res, ctx))
1119
+ return;
1120
+ if (!writerNow()) {
1121
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1122
+ return;
1123
+ }
1124
+ const parsed = parseOrReject(TeamEditSchema, body, res);
1125
+ if (!parsed)
1126
+ return;
1127
+ const teams = ctx.fleetConfig?.teams;
1128
+ const current = teams && Object.hasOwn(teams, name) ? teams[name].members : null;
1129
+ if (!current) {
1130
+ json(res, 404, { error: `Team not found: ${name}` });
1131
+ return;
1132
+ }
1133
+ const want = [...new Set(parsed.members)];
1134
+ // The update and its save as one: a failed save leaves the team as it was (500).
1135
+ const r = commitTeamUpdate(ctx.fleetConfig, {
1136
+ name, add: want.filter(m => !current.includes(m)), remove: current.filter(m => !want.includes(m)), description: parsed.description,
1137
+ }, () => ctx.saveFleetConfig());
1138
+ if (!r.ok) {
1139
+ json(res, r.status, { error: r.error });
1140
+ return;
1141
+ }
1142
+ json(res, 200, { updated: r.result });
1050
1143
  }
1051
1144
  catch (err) {
1052
1145
  json(res, 400, { error: err.message });
@@ -1056,8 +1149,13 @@ export function handleWebRequest(req, res, url, ctx) {
1056
1149
  }
1057
1150
  const teamDelMatch = path.match(/^\/ui\/teams\/(.+)$/);
1058
1151
  if (method === "DELETE" && teamDelMatch) {
1152
+ if (gatewayRequestContext(req)) {
1153
+ json(res, 403, { error: "teams_not_over_public_link" });
1154
+ return true;
1155
+ }
1059
1156
  const name = decodeURIComponent(teamDelMatch[1]);
1060
- if (!ctx.fleetConfig?.teams?.[name]) {
1157
+ // Own keys (#1689): a team is one fleet.yaml defines — "__proto__" or "constructor" is not found.
1158
+ if (!ctx.fleetConfig?.teams || !Object.hasOwn(ctx.fleetConfig.teams, name)) {
1061
1159
  json(res, 404, { error: `Team not found: ${name}` });
1062
1160
  return true;
1063
1161
  }