@songsid/agend 2.3.0-beta.16 → 2.3.0-beta.18

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 (122) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/backend/claude-code.js +13 -4
  4. package/dist/backend/claude-code.js.map +1 -1
  5. package/dist/backend/codex.js +15 -2
  6. package/dist/backend/codex.js.map +1 -1
  7. package/dist/backend-command.d.ts +2 -0
  8. package/dist/backend-command.js +3 -1
  9. package/dist/backend-command.js.map +1 -1
  10. package/dist/bug-report-cli.d.ts +30 -0
  11. package/dist/bug-report-cli.js +138 -0
  12. package/dist/bug-report-cli.js.map +1 -0
  13. package/dist/bug-report.d.ts +47 -0
  14. package/dist/bug-report.js +136 -0
  15. package/dist/bug-report.js.map +1 -0
  16. package/dist/channel/mcp-tools.js +1 -1
  17. package/dist/channel/mcp-tools.js.map +1 -1
  18. package/dist/cli.js +93 -8
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config-validator.d.ts +6 -0
  21. package/dist/config-validator.js +13 -0
  22. package/dist/config-validator.js.map +1 -1
  23. package/dist/daemon.d.ts +14 -1
  24. package/dist/daemon.js +59 -3
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/diagnostic-redaction.d.ts +10 -0
  27. package/dist/diagnostic-redaction.js +167 -0
  28. package/dist/diagnostic-redaction.js.map +1 -0
  29. package/dist/fleet-manager.d.ts +18 -0
  30. package/dist/fleet-manager.js +109 -8
  31. package/dist/fleet-manager.js.map +1 -1
  32. package/dist/general-knowledge/skills/bug-report/SKILL.md +31 -0
  33. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +22 -1
  34. package/dist/instance-lifecycle.d.ts +8 -1
  35. package/dist/instance-lifecycle.js +68 -12
  36. package/dist/instance-lifecycle.js.map +1 -1
  37. package/dist/locale.js +55 -2
  38. package/dist/locale.js.map +1 -1
  39. package/dist/login-controller.d.ts +45 -0
  40. package/dist/login-controller.js +66 -3
  41. package/dist/login-controller.js.map +1 -1
  42. package/dist/ls-attach-resolve.d.ts +3 -2
  43. package/dist/ls-attach-resolve.js +10 -4
  44. package/dist/ls-attach-resolve.js.map +1 -1
  45. package/dist/ls-rows.d.ts +12 -0
  46. package/dist/ls-rows.js +82 -3
  47. package/dist/ls-rows.js.map +1 -1
  48. package/dist/needs-you.d.ts +10 -0
  49. package/dist/needs-you.js +2 -1
  50. package/dist/needs-you.js.map +1 -1
  51. package/dist/outbound-handlers.js +31 -5
  52. package/dist/outbound-handlers.js.map +1 -1
  53. package/dist/quickstart-api.d.ts +9 -0
  54. package/dist/quickstart-api.js +82 -0
  55. package/dist/quickstart-api.js.map +1 -1
  56. package/dist/service-installer.js +12 -0
  57. package/dist/service-installer.js.map +1 -1
  58. package/dist/settings-effect.js +15 -1
  59. package/dist/settings-effect.js.map +1 -1
  60. package/dist/setup-form.d.ts +1 -1
  61. package/dist/setup-form.js +23 -7
  62. package/dist/setup-form.js.map +1 -1
  63. package/dist/ui/backend-signin.js +297 -0
  64. package/dist/ui/chat-msg-actions.js +176 -0
  65. package/dist/ui/chat-store.js +33 -3
  66. package/dist/ui/chat-strings.js +8 -2
  67. package/dist/ui/chat-thread.js +44 -2
  68. package/dist/ui/panel-chat.js +37 -0
  69. package/dist/ui/panel-needs.js +4 -2
  70. package/dist/ui/panel-settings.js +6 -3
  71. package/dist/ui/shared/app-shell.js +2 -1
  72. package/dist/ui/shared/app-stream.js +2 -1
  73. package/dist/ui/shared/app.css +42 -0
  74. package/dist/ui/shared/app.js +2 -0
  75. package/dist/ui/shared/usage-dialog.js +26 -3
  76. package/dist/ui/shared/view-strings.js +10 -0
  77. package/dist/ui/web-terminal/terminal.html +1 -1
  78. package/dist/update-launch.d.ts +9 -0
  79. package/dist/update-launch.js +38 -12
  80. package/dist/update-launch.js.map +1 -1
  81. package/dist/usage/format-rich.js +6 -3
  82. package/dist/usage/format-rich.js.map +1 -1
  83. package/dist/usage/i18n-keys.d.ts +1 -1
  84. package/dist/usage/i18n-keys.js +2 -1
  85. package/dist/usage/i18n-keys.js.map +1 -1
  86. package/dist/usage/i18n.d.ts +13 -1
  87. package/dist/usage/i18n.js +29 -0
  88. package/dist/usage/i18n.js.map +1 -1
  89. package/dist/usage/providers.d.ts +43 -8
  90. package/dist/usage/providers.js +58 -20
  91. package/dist/usage/providers.js.map +1 -1
  92. package/dist/usage/usage-api.d.ts +2 -0
  93. package/dist/usage/usage-api.js +180 -55
  94. package/dist/usage/usage-api.js.map +1 -1
  95. package/dist/user-systemd.d.ts +24 -0
  96. package/dist/user-systemd.js +75 -0
  97. package/dist/user-systemd.js.map +1 -0
  98. package/dist/web-api.d.ts +10 -1
  99. package/dist/web-api.js +134 -5
  100. package/dist/web-api.js.map +1 -1
  101. package/dist/web-backend-login.d.ts +130 -0
  102. package/dist/web-backend-login.js +225 -0
  103. package/dist/web-backend-login.js.map +1 -0
  104. package/dist/web-chat-history.d.ts +31 -0
  105. package/dist/web-chat-history.js +49 -1
  106. package/dist/web-chat-history.js.map +1 -1
  107. package/dist/web-commands.d.ts +5 -1
  108. package/dist/web-commands.js +1 -1
  109. package/dist/web-commands.js.map +1 -1
  110. package/dist/web-guest-access.d.ts +3 -2
  111. package/dist/web-guest-access.js +10 -6
  112. package/dist/web-guest-access.js.map +1 -1
  113. package/dist/windows-shortcuts.d.ts +78 -0
  114. package/dist/windows-shortcuts.js +203 -0
  115. package/dist/windows-shortcuts.js.map +1 -0
  116. package/launcher/agend-install.cjs +548 -0
  117. package/launcher/install-admission.cjs +4 -39
  118. package/launcher/postinstall.cjs +14 -12
  119. package/package.json +3 -2
  120. package/scripts/preinstall-guard.cjs +2 -5
  121. package/templates/windows-shortcuts.ps1 +181 -0
  122. package/launcher/old-updater-note.cjs +0 -48
package/dist/web-api.js CHANGED
@@ -34,7 +34,7 @@ import { readStatuslineModel, resolveInstanceContext } from "./topic-commands.js
34
34
  import { z } from "zod";
35
35
  import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
36
36
  import { dashboardOriginFor } from "./web-preview.js";
37
- import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
37
+ import { newWebMessageId, parseLastEventId, webChatReplyRef, webChatReplyText } from "./web-chat-history.js";
38
38
  import { attachmentDelivery, createPartFile, displayName, fileTooLargeText, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, UPLOAD_LIMITS, UPLOAD_MAX_MS, UPLOAD_STALL_MS, UploadSniffer, uploadLimits, wellFormed } from "./web-upload.js";
39
39
  import { getAgendHome } from "./paths.js";
40
40
  import { OPERATOR, isOwnerEffectKey } from "./web-prefs.js";
@@ -91,6 +91,8 @@ const SendMessageSchema = z.object({
91
91
  message: z.string().max(MAX_TEXT),
92
92
  /** Ids from POST /ui/upload, for this instance, not yet sent. */
93
93
  attachments: z.array(z.string().regex(/^[0-9a-f]{32}$/)).max(UPLOAD_LIMITS.maxFiles).optional(),
94
+ /** #2272: the message of this chat this one replies to — its row only; the server reads what it quotes. */
95
+ replyTo: z.object({ boot: z.string().regex(/^[0-9a-f]{1,32}$/), id: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER) }).strict().optional(),
94
96
  }).strict().refine(v => v.message.trim().length > 0 || (v.attachments?.length ?? 0) > 0, { message: "a message or at least one file", path: ["message"] });
95
97
  /** An instance name that is safe as one path segment (no separator, no NUL, not a dot name). */
96
98
  const safeInstanceSegment = (name) => /^[^\\/\x00]+$/.test(name) && name !== "." && name !== "..";
@@ -521,6 +523,104 @@ function handleEffectTrials(req, res, url, ctx, path, method) {
521
523
  catch { /* response ended */ } });
522
524
  return true;
523
525
  }
526
+ /**
527
+ * #2233: signing a backend in from the dashboard (web-backend-login.ts). A signed-in admin session on the local surface
528
+ * only — never the CLI's header token (an agent's), a guest, a read session or the public link (refused before the body
529
+ * is read, and again after it). Every answer is no-store: the starter's view carries the terminal's access code.
530
+ */
531
+ function handleBackendLogin(req, res, url, ctx, path, method) {
532
+ res.setHeader("Cache-Control", "private, no-store");
533
+ res.setHeader("X-Content-Type-Options", "nosniff");
534
+ const reading = method === "GET";
535
+ if (gatewayRequestContext(req)) {
536
+ json(res, reading ? 404 : 403, { error: "sign_in_not_over_public_link" });
537
+ return true;
538
+ }
539
+ const who = () => {
540
+ const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
541
+ if (v.kind !== "allow")
542
+ return { error: WEB_TOKEN_INVALID_MESSAGE, status: 401 };
543
+ if (v.via !== "session" || v.session.tier !== "admin" || v.session.surface !== "local")
544
+ return { error: "sign_in_needs_a_local_admin", status: 403 };
545
+ return { handle: v.session.handle, label: v.session.label };
546
+ };
547
+ const first = who();
548
+ if ("error" in first) {
549
+ json(res, first.status, { error: first.error });
550
+ return true;
551
+ }
552
+ const logins = ctx.webBackendLogins;
553
+ if (!logins) {
554
+ json(res, 404, { error: "sign_in_unavailable" });
555
+ return true;
556
+ }
557
+ const action = /^\/ui\/backend-login\/(check|start|cancel)$/.exec(path)?.[1];
558
+ if (reading && path === "/ui/backend-login" && [...url.searchParams.keys()].length === 0) {
559
+ json(res, 200, { login: logins.view(first.handle), targets: ctx.backendSignInTargets?.() ?? [] });
560
+ return true;
561
+ }
562
+ if (method !== "POST" || !action) {
563
+ json(res, 404, { error: "not_found" });
564
+ return true;
565
+ }
566
+ (async () => {
567
+ let bytes;
568
+ try {
569
+ bytes = await readBoundedWebBody(req, 4 * 1024);
570
+ }
571
+ catch {
572
+ json(res, 413, { error: "sign_in_body_bound" });
573
+ return;
574
+ }
575
+ if (!permitWebContinuation(req, res, ctx))
576
+ return;
577
+ const now = who();
578
+ if ("error" in now) {
579
+ json(res, now.status, { error: now.error });
580
+ return;
581
+ }
582
+ if (now.handle !== first.handle) {
583
+ json(res, 409, { error: "sign_in_session_changed" });
584
+ return;
585
+ }
586
+ let body;
587
+ try {
588
+ body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
589
+ }
590
+ catch {
591
+ json(res, 400, { error: "sign_in_json" });
592
+ return;
593
+ }
594
+ if (action === "check") {
595
+ const parsed = z.strictObject({ backend: z.string().regex(/^[a-z][a-z0-9-]{0,31}$/i), profile: z.string().min(1).max(64).optional() }).safeParse(body);
596
+ if (!parsed.success) {
597
+ json(res, 400, { error: "sign_in_args" });
598
+ return;
599
+ }
600
+ json(res, 200, await logins.check(now, parsed.data.backend, parsed.data.profile));
601
+ }
602
+ else if (action === "start") {
603
+ const parsed = z.strictObject({ confirm: z.string().regex(/^[0-9a-f]{32}$/), tunnel: z.boolean().optional() }).safeParse(body);
604
+ if (!parsed.success) {
605
+ json(res, 400, { error: "sign_in_args" });
606
+ return;
607
+ }
608
+ const result = await logins.begin(now, parsed.data.confirm, parsed.data.tunnel === true);
609
+ json(res, "error" in result ? 409 : 200, result);
610
+ }
611
+ else {
612
+ if (!z.strictObject({}).safeParse(body).success) {
613
+ json(res, 400, { error: "sign_in_args" });
614
+ return;
615
+ }
616
+ json(res, 200, { message: await logins.cancel() });
617
+ }
618
+ })().catch(() => { try {
619
+ json(res, 500, { error: "sign_in_request_failed" });
620
+ }
621
+ catch { /* response ended */ } });
622
+ return true;
623
+ }
524
624
  /** #1629 B2: effect routes require a human session; route bodies never supply an actor or reference holder. */
525
625
  function handleEffects(req, res, url, ctx, path, method) {
526
626
  res.setHeader("Cache-Control", "private, no-store");
@@ -1834,6 +1934,9 @@ export function handleWebRequest(req, res, url, ctx) {
1834
1934
  // Canvas effects are absent from the public gateway manifest and independently refused above.
1835
1935
  if (path === "/ui/effect-trials" || path.startsWith("/ui/effect-trials/"))
1836
1936
  return handleEffectTrials(req, res, url, ctx, path, method);
1937
+ // #2233: backend sign-in — absent from the public manifest and the guest table, and refused here too.
1938
+ if (path === "/ui/backend-login" || path.startsWith("/ui/backend-login/"))
1939
+ return handleBackendLogin(req, res, url, ctx, path, method);
1837
1940
  if (path === "/ui/effects" || path.startsWith("/ui/effects/"))
1838
1941
  return handleEffects(req, res, url, ctx, path, method);
1839
1942
  // #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
@@ -2647,6 +2750,17 @@ export function handleWebRequest(req, res, url, ctx) {
2647
2750
  json(res, 404, { error: "not found" });
2648
2751
  return true;
2649
2752
  }
2753
+ /**
2754
+ * #2272: the line a web reply's channel copy opens with — the quoted sender and first words when that message reached
2755
+ * the channel (its members saw it); a web-only one is only named, never copied out to the channel.
2756
+ */
2757
+ export function webReplyEchoLine(quoted) {
2758
+ if (!quoted.channel)
2759
+ return t("web.echo_reply_web_only");
2760
+ const ref = webChatReplyRef(quoted);
2761
+ const words = ref.text.slice(0, 120) || (ref.media === "photo" ? "[photo]" : ref.media ? "[file]" : "");
2762
+ return t("web.echo_reply", quoted.sender.slice(0, 80), words);
2763
+ }
2650
2764
  /** Keep the echo in a single platform message, including its attachment names. */
2651
2765
  function webEchoPreview(message, names) {
2652
2766
  const text = wellFormed(message.slice(0, 500));
@@ -2701,7 +2815,7 @@ function handleSendMessage(req, res, ctx) {
2701
2815
  json(res, 400, { error: `${issue.path.join(".") || "body"}: ${issue.message}` });
2702
2816
  return;
2703
2817
  }
2704
- const { instance, message: typed, attachments = [] } = parsed.data;
2818
+ const { instance, message: typed, attachments = [], replyTo } = parsed.data;
2705
2819
  // #1920 step 2b: a guest writes only to an in-scope instance, while guests may chat, and never a command.
2706
2820
  const guest = guestRequestOf(req);
2707
2821
  const refused = guestChatRefusal(guest, instance);
@@ -2713,6 +2827,16 @@ function handleSendMessage(req, res, ctx) {
2713
2827
  json(res, 403, { error: "guest_no_commands" });
2714
2828
  return;
2715
2829
  }
2830
+ // #2272: a reply names a message this chat still keeps — and, for a guest, one it may see. What it quotes is read
2831
+ // from that row here, never taken from the page.
2832
+ let quoted;
2833
+ if (replyTo) {
2834
+ quoted = ctx.webChatHistory?.find(instance, replyTo.boot, replyTo.id);
2835
+ if (!quoted || quoted.effectTrial || (guest && !guestSeesMessage(quoted, guest, guest.scope()))) {
2836
+ json(res, 409, { error: t("web.reply_gone"), reason: "reply_gone" });
2837
+ return;
2838
+ }
2839
+ }
2716
2840
  const registered = registeredChatInstance(ctx, instance);
2717
2841
  if (!registered && !ctx.instanceIpcClients.has(instance)) {
2718
2842
  json(res, 404, { error: `Instance not found: ${instance}` });
@@ -2766,8 +2890,10 @@ function handleSendMessage(req, res, ctx) {
2766
2890
  const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
2767
2891
  const canEcho = ctx.fleetConfig?.web?.echo_to_channel !== false
2768
2892
  && !ctx.isClassicInstance?.(instance) && syncAdapter && groupId && topicId != null;
2769
- // Add no runtime credentials or local attachment paths to the display copy.
2770
- const preview = webEchoPreview(message, files.map(f => f.name));
2893
+ // Add no runtime credentials or local attachment paths to the display copy. A reply opens with the line it
2894
+ // answers (#2272) — quoted only if that message reached the channel; a web-only one is named, never copied out.
2895
+ const quoteLine = quoted ? webReplyEchoLine(quoted) : "";
2896
+ const preview = (quoteLine ? `${quoteLine}\n` : "") + webEchoPreview(message, files.map(f => f.name));
2771
2897
  // ClassicBot echo (#1320 part B): same ordering lane, each room's setting (#2054: on unless it says false, or
2772
2898
  // the fleet-wide switch is off for a room that is unset) resolved at send time inside sendClassicWebEcho.
2773
2899
  const canClassicEcho = ctx.isClassicInstance?.(instance) === true && !!ctx.sendClassicWebEcho;
@@ -2805,6 +2931,8 @@ function handleSendMessage(req, res, ctx) {
2805
2931
  ...(guest ? { web_guest: "1" } : {}), // #1920: "[user:… via web (guest), id:…]" (authority none)
2806
2932
  adapter_id: syncAdapter?.id,
2807
2933
  ...delivery.meta,
2934
+ // #2272: `(reply_to: "…")`, as a Discord or Telegram reply gives the agent.
2935
+ ...(quoted ? { reply_to_text: webChatReplyText(quoted) } : {}),
2808
2936
  },
2809
2937
  });
2810
2938
  }
@@ -2840,7 +2968,8 @@ function handleSendMessage(req, res, ctx) {
2840
2968
  ctx.lastInboundUser.set(instance, person.name);
2841
2969
  ctx.eventLog?.logActivity("message", person.name, (message || `[${files.length} file(s)]`).slice(0, 200), instance);
2842
2970
  // #1920: the account that wrote it (a guest sees its own); channel-visible once its copy was sent (above).
2843
- ctx.emitSseEvent("message", { instance, sender: person.name, role: "user", text: message, ts, attachments: files.map(publicAttachment), messageId, ...(person.bound ? { from: person.voterKey } : {}) });
2971
+ ctx.emitSseEvent("message", { instance, sender: person.name, role: "user", text: message, ts, attachments: files.map(publicAttachment), messageId, ...(person.bound ? { from: person.voterKey } : {}),
2972
+ ...(quoted ? { replyTo: webChatReplyRef(quoted) } : {}) });
2844
2973
  json(res, 200, { sent: true, messageId });
2845
2974
  }
2846
2975
  catch {