@circuitwall/jarela 2.3.0 → 2.4.0

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 (208) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +4 -4
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  13. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/v1/access/[identity]/route.js +7 -0
  19. package/.next/standalone/.next/server/app/api/v1/access/route.js +7 -0
  20. package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +7 -0
  21. package/.next/standalone/.next/server/app/api/v1/agents/[id]/display-filters/route.js +17 -5
  22. package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +7 -0
  23. package/.next/standalone/.next/server/app/api/v1/agents/[id]/thread/route.js +17 -5
  24. package/.next/standalone/.next/server/app/api/v1/agents/route.js +7 -0
  25. package/.next/standalone/.next/server/app/api/v1/allowed-sites/[hostname]/route.js +7 -0
  26. package/.next/standalone/.next/server/app/api/v1/allowed-sites/route.js +7 -0
  27. package/.next/standalone/.next/server/app/api/v1/artifacts/lifecycle/route.js +7 -0
  28. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +26 -32
  29. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/[remote_jid]/route.js +18 -7
  31. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/route.js +18 -7
  32. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +26 -32
  33. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +27 -33
  35. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +26 -32
  37. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/[route_id]/route.js +28 -12
  39. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/route.js +28 -12
  40. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +26 -32
  41. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js +9 -2
  43. package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/v1/bridges/route.js +19 -7
  45. package/.next/standalone/.next/server/app/api/v1/bridges/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/v1/dashboard/metrics/route.js +17 -5
  47. package/.next/standalone/.next/server/app/api/v1/dashboard/pricing/route.js +7 -0
  48. package/.next/standalone/.next/server/app/api/v1/documents/search/route.js +7 -0
  49. package/.next/standalone/.next/server/app/api/v1/documents/settings/route.js +7 -0
  50. package/.next/standalone/.next/server/app/api/v1/env-sync/allowlist/route.js +7 -0
  51. package/.next/standalone/.next/server/app/api/v1/env-sync/route.js +7 -0
  52. package/.next/standalone/.next/server/app/api/v1/events/route.js +26 -32
  53. package/.next/standalone/.next/server/app/api/v1/events/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/v1/extension/browser/activate/route.js +7 -0
  55. package/.next/standalone/.next/server/app/api/v1/extension/browser/foreground/route.js +7 -0
  56. package/.next/standalone/.next/server/app/api/v1/extension/browser/history/route.js +7 -0
  57. package/.next/standalone/.next/server/app/api/v1/extension/browser/poll/route.js +7 -0
  58. package/.next/standalone/.next/server/app/api/v1/extension/browser/progress/route.js +7 -0
  59. package/.next/standalone/.next/server/app/api/v1/extension/browser/result/route.js +7 -0
  60. package/.next/standalone/.next/server/app/api/v1/extension/browser/retry/route.js +7 -0
  61. package/.next/standalone/.next/server/app/api/v1/extension/browser/status/route.js +7 -0
  62. package/.next/standalone/.next/server/app/api/v1/extension/browser/tabs/route.js +7 -0
  63. package/.next/standalone/.next/server/app/api/v1/harnesses/[id]/route.js +7 -0
  64. package/.next/standalone/.next/server/app/api/v1/harnesses/default/route.js +7 -0
  65. package/.next/standalone/.next/server/app/api/v1/harnesses/route.js +7 -0
  66. package/.next/standalone/.next/server/app/api/v1/health/route.js +17 -5
  67. package/.next/standalone/.next/server/app/api/v1/integrations/[name]/route.js +7 -0
  68. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/callback/route.js +7 -0
  69. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/start/route.js +7 -0
  70. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/status/route.js +7 -0
  71. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/callback/route.js +7 -0
  72. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/start/route.js +7 -0
  73. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/status/route.js +7 -0
  74. package/.next/standalone/.next/server/app/api/v1/integrations/route.js +7 -0
  75. package/.next/standalone/.next/server/app/api/v1/lifecycle/adoption/route.js +17 -5
  76. package/.next/standalone/.next/server/app/api/v1/maps/embed/route.js +7 -0
  77. package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +17 -5
  78. package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +17 -5
  79. package/.next/standalone/.next/server/app/api/v1/memory/[namespace]/[key]/route.js +7 -0
  80. package/.next/standalone/.next/server/app/api/v1/memory/policy/route.js +7 -0
  81. package/.next/standalone/.next/server/app/api/v1/memory/route.js +7 -0
  82. package/.next/standalone/.next/server/app/api/v1/memory/search/route.js +7 -0
  83. package/.next/standalone/.next/server/app/api/v1/models/[name]/compact-threads/route.js +7 -0
  84. package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +7 -0
  85. package/.next/standalone/.next/server/app/api/v1/models/route.js +7 -0
  86. package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +27 -12
  87. package/.next/standalone/.next/server/app/api/v1/profile/location/route.js +7 -0
  88. package/.next/standalone/.next/server/app/api/v1/profile/route.js +7 -0
  89. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +7 -0
  90. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +7 -0
  91. package/.next/standalone/.next/server/app/api/v1/providers/github-copilot/auth/route.js +7 -0
  92. package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -0
  93. package/.next/standalone/.next/server/app/api/v1/proxy-config/route.js +7 -0
  94. package/.next/standalone/.next/server/app/api/v1/redaction/init/route.js +7 -0
  95. package/.next/standalone/.next/server/app/api/v1/redaction/route.js +7 -0
  96. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/route.js +17 -5
  97. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/route.js +7 -0
  98. package/.next/standalone/.next/server/app/api/v1/security/idle-timeout/route.js +7 -0
  99. package/.next/standalone/.next/server/app/api/v1/security/pin/route.js +7 -0
  100. package/.next/standalone/.next/server/app/api/v1/security/state/route.js +7 -0
  101. package/.next/standalone/.next/server/app/api/v1/security/unlock/route.js +7 -0
  102. package/.next/standalone/.next/server/app/api/v1/security/verify-pin/route.js +7 -0
  103. package/.next/standalone/.next/server/app/api/v1/skills/[id]/route.js +7 -0
  104. package/.next/standalone/.next/server/app/api/v1/skills/repos/[id]/route.js +7 -0
  105. package/.next/standalone/.next/server/app/api/v1/skills/repos/route.js +7 -0
  106. package/.next/standalone/.next/server/app/api/v1/skills/route.js +7 -0
  107. package/.next/standalone/.next/server/app/api/v1/tasks/[agent_id]/route.js +7 -0
  108. package/.next/standalone/.next/server/app/api/v1/tasks/route.js +7 -0
  109. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/activity/route.js +7 -0
  110. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/context-pin/route.js +12 -6
  111. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +7 -0
  112. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +0 -1
  113. package/.next/standalone/.next/server/app/api/v1/threads/route.js +17 -5
  114. package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/api/v1/voice/transcribe/route.js +17 -5
  116. package/.next/standalone/.next/server/app/api/v1/voice/tts/route.js +17 -5
  117. package/.next/standalone/.next/server/app/api/v1/watchers/[id]/route.js +17 -5
  118. package/.next/standalone/.next/server/app/page.js +322 -275
  119. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/setup/page.js +8 -0
  121. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app-paths-manifest.json +4 -4
  123. package/.next/standalone/.next/server/chunks/1388.js +106 -36
  124. package/.next/standalone/.next/server/chunks/2068.js +47 -10
  125. package/.next/standalone/.next/server/chunks/210.js +6 -6
  126. package/.next/standalone/.next/server/chunks/2447.js +10 -5
  127. package/.next/standalone/.next/server/chunks/2471.js +17 -17
  128. package/.next/standalone/.next/server/chunks/3042.js +10 -8
  129. package/.next/standalone/.next/server/chunks/{4540.js → 309.js} +104 -38
  130. package/.next/standalone/.next/server/chunks/{2350.js → 3272.js} +104 -37
  131. package/.next/standalone/.next/server/chunks/3399.js +525 -525
  132. package/.next/standalone/.next/server/chunks/3524.js +150 -67
  133. package/.next/standalone/.next/server/chunks/4189.js +1 -1
  134. package/.next/standalone/.next/server/chunks/4508.js +2 -2
  135. package/.next/standalone/.next/server/chunks/46.js +73 -28
  136. package/.next/standalone/.next/server/chunks/5908.js +63 -42
  137. package/.next/standalone/.next/server/chunks/6142.js +10 -2
  138. package/.next/standalone/.next/server/chunks/6649.js +1 -1
  139. package/.next/standalone/.next/server/chunks/8461.js +10 -8
  140. package/.next/standalone/.next/server/chunks/9032.js +5 -5
  141. package/.next/standalone/.next/server/chunks/9164.js +435 -176
  142. package/.next/standalone/.next/server/chunks/9304.js +51 -12
  143. package/.next/standalone/.next/server/chunks/{423.js → 9590.js} +66 -21
  144. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  145. package/.next/standalone/.next/server/middleware.js +71 -18
  146. package/.next/standalone/.next/server/pages/404.html +2 -2
  147. package/.next/standalone/.next/server/pages/500.html +1 -1
  148. package/.next/standalone/.next/server/preview-props.json +3 -3
  149. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  150. package/.next/standalone/.next/static/chunks/app/{page-213a623b37226452.js → page-04748847c6ff866f.js} +171 -132
  151. package/.next/standalone/.next/static/css/{312542a5725ba0d5.css → 2669e8744cc8c212.css} +2 -2
  152. package/.next/standalone/docs/adoption/changesets.json +49 -0
  153. package/.next/standalone/package.json +1 -1
  154. package/CHANGELOG.md +19 -0
  155. package/app/api/v1/threads/[thread_id]/run/route.ts +0 -1
  156. package/components/chat/MessageBubble.tsx +12 -0
  157. package/components/integrations/NetworkSection.tsx +2 -2
  158. package/lib/agents/citation-checker.test.ts +40 -2
  159. package/lib/agents/citation-checker.ts +56 -11
  160. package/lib/agents/context-boundary.ts +5 -6
  161. package/lib/agents/instruction-edits.test.ts +120 -0
  162. package/lib/agents/instruction-edits.ts +65 -42
  163. package/lib/agents/prepare/request.ts +0 -2
  164. package/lib/agents/prepare/system-prompt.test.ts +10 -0
  165. package/lib/agents/prepare/system-prompt.ts +8 -2
  166. package/lib/agents/proposals.test.ts +76 -0
  167. package/lib/agents/proposals.ts +13 -5
  168. package/lib/agents/run-thread.auto-boundary.test.ts +125 -5
  169. package/lib/agents/run-thread.ts +24 -9
  170. package/lib/agents/sqlite-checkpoint-saver.test.ts +91 -0
  171. package/lib/agents/warm-summary-background.ts +22 -2
  172. package/lib/agents/warm-topics.test.ts +155 -1
  173. package/lib/agents/warm-topics.ts +27 -7
  174. package/lib/bridges/dispatcher.ts +1 -0
  175. package/lib/bridges/message-role.test.ts +81 -0
  176. package/lib/bridges/message-role.ts +23 -10
  177. package/lib/bridges/types.ts +6 -0
  178. package/lib/bridges/whatsapp-link.test.ts +32 -0
  179. package/lib/bridges/whatsapp-link.ts +21 -0
  180. package/lib/bridges/whatsapp.inbound.test.ts +239 -0
  181. package/lib/bridges/whatsapp.sender.test.ts +80 -1
  182. package/lib/bridges/whatsapp.ts +76 -38
  183. package/lib/pricing/snapshot.test.ts +70 -0
  184. package/lib/pricing/snapshot.ts +10 -2
  185. package/lib/proxy/dispatcher.test.ts +119 -1
  186. package/lib/proxy/dispatcher.ts +22 -9
  187. package/lib/stores/agent-configs.ts +10 -5
  188. package/lib/stores/bridges.test.ts +81 -0
  189. package/lib/stores/bridges.ts +9 -8
  190. package/lib/stores/proxy-config.test.ts +120 -0
  191. package/lib/stores/proxy-config.ts +22 -4
  192. package/lib/stores/threads.ts +12 -8
  193. package/lib/tools/communications/calendar.test.ts +43 -2
  194. package/lib/tools/communications/calendar.ts +2 -0
  195. package/lib/tools/communications/gmail.test.ts +51 -1
  196. package/lib/tools/communications/gmail.ts +44 -2
  197. package/lib/tools/communications/outlook-calendar.ts +2 -0
  198. package/lib/tools/communications/outlook.test.ts +33 -2
  199. package/lib/tools/communications/outlook.ts +2 -0
  200. package/lib/tools/filesystem/edit-match.test.ts +133 -0
  201. package/lib/tools/filesystem/edit-match.ts +7 -2
  202. package/lib/tools/support/result-refs.test.ts +131 -0
  203. package/lib/tools/support/result-refs.ts +41 -9
  204. package/lib/utils/source-link.test.ts +51 -0
  205. package/lib/utils/source-link.ts +45 -0
  206. package/package.json +1 -1
  207. /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → hwujW4hR1b-r3k5WdMxZO}/_buildManifest.js +0 -0
  208. /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → hwujW4hR1b-r3k5WdMxZO}/_ssgManifest.js +0 -0
@@ -93,23 +93,27 @@ export async function readToolResultRef(args: {
93
93
  }
94
94
  const offset = Math.max(0, Math.floor(args.offset ?? 0));
95
95
  const limit = Math.min(MAX_READ_BYTES, Math.max(1, Math.floor(args.limit ?? DEFAULT_PREVIEW_BYTES)));
96
- let buf: Buffer;
97
96
  try {
98
97
  const file = await fsp.open(abs, "r");
99
98
  try {
100
99
  const stat = await file.stat();
101
- const readLength = Math.max(0, Math.min(limit, stat.size - offset));
102
- buf = Buffer.alloc(readLength);
103
- if (readLength > 0) await file.read(buf, 0, readLength, offset);
104
- const nextOffset = offset + readLength;
100
+ // Read a few bytes either side so the window can be moved onto UTF-8
101
+ // character boundaries; paging must never split a multi-byte character.
102
+ const windowStart = Math.min(Math.max(0, offset - 3), stat.size);
103
+ const windowLength = Math.max(0, Math.min(stat.size - windowStart, offset - windowStart + limit + 3));
104
+ const buf = Buffer.alloc(windowLength);
105
+ if (windowLength > 0) await file.read(buf, 0, windowLength, windowStart);
106
+ const { start, end } = utf8Window(buf, Math.min(offset, stat.size) - windowStart, limit);
107
+ const actualOffset = windowStart + start;
108
+ const nextOffset = windowStart + end;
105
109
  return {
106
110
  ok: true,
107
111
  status: "done",
108
112
  name: args.name,
109
- offset,
113
+ offset: actualOffset,
110
114
  limit,
111
115
  bytes: stat.size,
112
- result: buf.toString("utf8"),
116
+ result: buf.subarray(start, end).toString("utf8"),
113
117
  next_offset: nextOffset < stat.size ? nextOffset : null,
114
118
  done: nextOffset >= stat.size,
115
119
  };
@@ -126,6 +130,25 @@ export async function readToolResultRef(args: {
126
130
  }
127
131
  }
128
132
 
133
+ const isUtf8Continuation = (byte: number | undefined) => byte !== undefined && (byte & 0xc0) === 0x80;
134
+
135
+ // Moves `[start, start + limit)` inside `buf` onto character boundaries: the
136
+ // start backs up to its lead byte, and the end backs up so it does not cut a
137
+ // character — unless that character alone exceeds `limit`, in which case the
138
+ // whole character is returned so paging always makes progress.
139
+ function utf8Window(buf: Buffer, start: number, limit: number): { start: number; end: number } {
140
+ let s = Math.max(0, Math.min(start, buf.length));
141
+ while (s > 0 && isUtf8Continuation(buf[s])) s--;
142
+ let e = Math.min(buf.length, s + limit);
143
+ if (e < buf.length && isUtf8Continuation(buf[e])) {
144
+ let lead = e;
145
+ while (lead > s && isUtf8Continuation(buf[lead])) lead--;
146
+ if (lead > s) e = lead;
147
+ else while (e < buf.length && isUtf8Continuation(buf[e])) e++;
148
+ }
149
+ return { start: s, end: e };
150
+ }
151
+
129
152
  export function serializeToolResult(result: unknown): string {
130
153
  if (typeof result === "string") return result;
131
154
  return JSON.stringify(result) ?? String(result);
@@ -145,8 +168,15 @@ function buildPreview(value: string, mimeType: string): { value: unknown; bytes:
145
168
  try {
146
169
  const parsed = JSON.parse(value) as unknown;
147
170
  if (Array.isArray(parsed)) {
148
- const preview = parsed.slice(0, JSON_ARRAY_PREVIEW_ITEMS);
149
- return { value: preview, bytes: Buffer.byteLength(JSON.stringify(preview)), total_count: parsed.length };
171
+ // Cap by bytes as well as items: a few huge rows must not smuggle the
172
+ // whole result back inline.
173
+ for (let n = Math.min(parsed.length, JSON_ARRAY_PREVIEW_ITEMS); n > 0; n--) {
174
+ const preview = parsed.slice(0, n);
175
+ const bytes = Buffer.byteLength(JSON.stringify(preview));
176
+ if (bytes <= DEFAULT_PREVIEW_BYTES) return { value: preview, bytes, total_count: parsed.length };
177
+ }
178
+ const text = takeUtf8Prefix(value, DEFAULT_PREVIEW_BYTES);
179
+ return { value: text, bytes: Buffer.byteLength(text), total_count: parsed.length };
150
180
  }
151
181
  } catch {
152
182
  // Fall through to byte preview.
@@ -165,6 +195,8 @@ function takeUtf8Prefix(value: string, maxBytes: number): string {
165
195
  if (Buffer.byteLength(value.slice(0, mid)) <= maxBytes) lo = mid;
166
196
  else hi = mid - 1;
167
197
  }
198
+ const code = value.charCodeAt(lo - 1);
199
+ if (lo > 0 && code >= 0xd800 && code <= 0xdbff) lo--;
168
200
  return value.slice(0, lo);
169
201
  }
170
202
 
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { citeAs, httpsUrlOrNull } from "./source-link";
3
+
4
+ describe("httpsUrlOrNull", () => {
5
+ it("accepts absolute https URLs", () => {
6
+ expect(httpsUrlOrNull("https://outlook.office365.com/owa/?ItemID=AAk%3D&exvsurl=1")).toBe(
7
+ "https://outlook.office365.com/owa/?ItemID=AAk%3D&exvsurl=1",
8
+ );
9
+ });
10
+
11
+ it("rejects missing, relative, and non-https URLs", () => {
12
+ for (const url of [null, undefined, "", "/mail/123", "http://example.com", "javascript:alert(1)", "message://%3cid%3e", "not a url"]) {
13
+ expect(httpsUrlOrNull(url)).toBeNull();
14
+ }
15
+ });
16
+ });
17
+
18
+ describe("citeAs", () => {
19
+ it("builds a markdown link from a label and URL", () => {
20
+ expect(citeAs("Quarterly report", "https://www.google.com/calendar/event?eid=abc")).toBe(
21
+ "[Quarterly report](https://www.google.com/calendar/event?eid=abc)",
22
+ );
23
+ });
24
+
25
+ it("returns null without a usable URL", () => {
26
+ expect(citeAs("Subject", null)).toBeNull();
27
+ expect(citeAs("Subject", "http://insecure.example")).toBeNull();
28
+ });
29
+
30
+ it("escapes brackets and flattens whitespace in the label", () => {
31
+ expect(citeAs("Re: [EXT]\n budget\\plan", "https://x.test/a")).toBe("[Re: \\[EXT\\] budget\\\\plan](https://x.test/a)");
32
+ });
33
+
34
+ it("falls back to a default label for an empty subject", () => {
35
+ expect(citeAs(" ", "https://x.test/a", "Outlook message")).toBe("[Outlook message](https://x.test/a)");
36
+ expect(citeAs(null, "https://x.test/a")).toBe("[source](https://x.test/a)");
37
+ });
38
+
39
+ it("caps long labels", () => {
40
+ const link = citeAs("x".repeat(200), "https://x.test/a")!;
41
+ const label = link.slice(1, link.indexOf("]("));
42
+ expect(label.length).toBe(80);
43
+ expect(label.endsWith("…")).toBe(true);
44
+ });
45
+
46
+ it("encodes characters that would end the markdown destination", () => {
47
+ const link = citeAs("Event", "https://x.test/path(1)?q=a b")!;
48
+ expect(link).toBe("[Event](https://x.test/path%281%29?q=a%20b)");
49
+ expect(/^\[[^\]]*\]\([^()\s]+\)$/.test(link)).toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Source-link fields for tool results, so an agent can link a reply back to
3
+ * the email, calendar event, or chat it came from without building URLs.
4
+ *
5
+ * `cite_as` is a ready-made markdown link the system prompt tells agents to
6
+ * paste verbatim. Building it in code keeps the label and URL well-formed:
7
+ * a stray `]` in a subject or `)` in a URL would otherwise break the link.
8
+ */
9
+
10
+ const MAX_LABEL_CHARS = 80;
11
+
12
+ /** Returns `url` when it is an absolute https URL, otherwise null. */
13
+ export function httpsUrlOrNull(url: string | null | undefined): string | null {
14
+ if (!url) return null;
15
+ try {
16
+ const parsed = new URL(url);
17
+ return parsed.protocol === "https:" ? parsed.href : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function markdownLabel(label: string | null | undefined, fallback: string): string {
24
+ const flat = (label ?? "").replace(/\s+/g, " ").trim();
25
+ const capped = flat.length > MAX_LABEL_CHARS ? `${flat.slice(0, MAX_LABEL_CHARS - 1).trimEnd()}…` : flat;
26
+ return (capped || fallback).replace(/[\\[\]]/g, (c) => `\\${c}`);
27
+ }
28
+
29
+ function markdownDestination(url: string): string {
30
+ return url.replace(/[()\s<>]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
31
+ }
32
+
33
+ /**
34
+ * Pre-built markdown link for `cite_as`, or null when there is no usable URL.
35
+ * Only https URLs qualify: the chat renderer drops other schemes.
36
+ */
37
+ export function citeAs(
38
+ label: string | null | undefined,
39
+ url: string | null | undefined,
40
+ fallbackLabel = "source",
41
+ ): string | null {
42
+ const safe = httpsUrlOrNull(url);
43
+ if (!safe) return null;
44
+ return `[${markdownLabel(label, fallbackLabel)}](${markdownDestination(safe)})`;
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circuitwall/jarela",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Jarela — local chat interface for LangGraph agents (multi-provider, single-process, SQLite-backed).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Andrew Ge Wu",