@ganju/utils 0.0.1

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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.md +23 -0
  4. package/dist/apiError.d.ts +3 -0
  5. package/dist/apiError.d.ts.map +1 -0
  6. package/dist/apiError.js +36 -0
  7. package/dist/attachment.d.ts +33 -0
  8. package/dist/attachment.d.ts.map +1 -0
  9. package/dist/attachment.js +59 -0
  10. package/dist/base64.d.ts +10 -0
  11. package/dist/base64.d.ts.map +1 -0
  12. package/dist/base64.js +50 -0
  13. package/dist/channelDebounce.d.ts +26 -0
  14. package/dist/channelDebounce.d.ts.map +1 -0
  15. package/dist/channelDebounce.js +35 -0
  16. package/dist/channelNotifier.d.ts +9 -0
  17. package/dist/channelNotifier.d.ts.map +1 -0
  18. package/dist/channelNotifier.js +31 -0
  19. package/dist/chunking.d.ts +28 -0
  20. package/dist/chunking.d.ts.map +1 -0
  21. package/dist/chunking.js +97 -0
  22. package/dist/cliConstants.d.ts +9 -0
  23. package/dist/cliConstants.d.ts.map +1 -0
  24. package/dist/cliConstants.js +47 -0
  25. package/dist/coerce.d.ts +2 -0
  26. package/dist/coerce.d.ts.map +1 -0
  27. package/dist/coerce.js +13 -0
  28. package/dist/constants.d.ts +624 -0
  29. package/dist/constants.d.ts.map +1 -0
  30. package/dist/constants.js +2220 -0
  31. package/dist/crypto.d.ts +14 -0
  32. package/dist/crypto.d.ts.map +1 -0
  33. package/dist/crypto.js +93 -0
  34. package/dist/customCodeProject.d.ts +91 -0
  35. package/dist/customCodeProject.d.ts.map +1 -0
  36. package/dist/customCodeProject.js +154 -0
  37. package/dist/customCodeToken.d.ts +41 -0
  38. package/dist/customCodeToken.d.ts.map +1 -0
  39. package/dist/customCodeToken.js +115 -0
  40. package/dist/deadline.d.ts +6 -0
  41. package/dist/deadline.d.ts.map +1 -0
  42. package/dist/deadline.js +45 -0
  43. package/dist/discordSend.d.ts +25 -0
  44. package/dist/discordSend.d.ts.map +1 -0
  45. package/dist/discordSend.js +8 -0
  46. package/dist/embeddable.d.ts +2 -0
  47. package/dist/embeddable.d.ts.map +1 -0
  48. package/dist/embeddable.js +6 -0
  49. package/dist/exposedResource.d.ts +6 -0
  50. package/dist/exposedResource.d.ts.map +1 -0
  51. package/dist/exposedResource.js +18 -0
  52. package/dist/extractedDocument.d.ts +70 -0
  53. package/dist/extractedDocument.d.ts.map +1 -0
  54. package/dist/extractedDocument.js +2 -0
  55. package/dist/fetcher.d.ts +8 -0
  56. package/dist/fetcher.d.ts.map +1 -0
  57. package/dist/fetcher.js +68 -0
  58. package/dist/formatFilename.d.ts +2 -0
  59. package/dist/formatFilename.d.ts.map +1 -0
  60. package/dist/formatFilename.js +19 -0
  61. package/dist/formatRelative.d.ts +12 -0
  62. package/dist/formatRelative.d.ts.map +1 -0
  63. package/dist/formatRelative.js +28 -0
  64. package/dist/formatRelativeTime.d.ts +2 -0
  65. package/dist/formatRelativeTime.d.ts.map +1 -0
  66. package/dist/formatRelativeTime.js +49 -0
  67. package/dist/getEnv.d.ts +5 -0
  68. package/dist/getEnv.d.ts.map +1 -0
  69. package/dist/getEnv.js +11 -0
  70. package/dist/gmailSend.d.ts +24 -0
  71. package/dist/gmailSend.d.ts.map +1 -0
  72. package/dist/gmailSend.js +9 -0
  73. package/dist/index.d.ts +2839 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +155 -0
  76. package/dist/jsonSchemaToZodShape.d.ts +38 -0
  77. package/dist/jsonSchemaToZodShape.d.ts.map +1 -0
  78. package/dist/jsonSchemaToZodShape.js +117 -0
  79. package/dist/l2Normalize.d.ts +2 -0
  80. package/dist/l2Normalize.d.ts.map +1 -0
  81. package/dist/l2Normalize.js +25 -0
  82. package/dist/languageCookieDomain.d.ts +21 -0
  83. package/dist/languageCookieDomain.d.ts.map +1 -0
  84. package/dist/languageCookieDomain.js +39 -0
  85. package/dist/localizeZodIssue.d.ts +43 -0
  86. package/dist/localizeZodIssue.d.ts.map +1 -0
  87. package/dist/localizeZodIssue.js +90 -0
  88. package/dist/mailHeaders.d.ts +4 -0
  89. package/dist/mailHeaders.d.ts.map +1 -0
  90. package/dist/mailHeaders.js +11 -0
  91. package/dist/mcpProxy.d.ts +2 -0
  92. package/dist/mcpProxy.d.ts.map +1 -0
  93. package/dist/mcpProxy.js +21 -0
  94. package/dist/mimeMessage.d.ts +21 -0
  95. package/dist/mimeMessage.d.ts.map +1 -0
  96. package/dist/mimeMessage.js +58 -0
  97. package/dist/oauth.d.ts +21 -0
  98. package/dist/oauth.d.ts.map +1 -0
  99. package/dist/oauth.js +75 -0
  100. package/dist/oauthProviders.d.ts +12 -0
  101. package/dist/oauthProviders.d.ts.map +1 -0
  102. package/dist/oauthProviders.js +99 -0
  103. package/dist/outlookSend.d.ts +19 -0
  104. package/dist/outlookSend.d.ts.map +1 -0
  105. package/dist/outlookSend.js +9 -0
  106. package/dist/parseHttpError.d.ts +2 -0
  107. package/dist/parseHttpError.d.ts.map +1 -0
  108. package/dist/parseHttpError.js +23 -0
  109. package/dist/planLimitError.d.ts +18 -0
  110. package/dist/planLimitError.d.ts.map +1 -0
  111. package/dist/planLimitError.js +35 -0
  112. package/dist/processQueueBatch.d.ts +17 -0
  113. package/dist/processQueueBatch.d.ts.map +1 -0
  114. package/dist/processQueueBatch.js +32 -0
  115. package/dist/reservedToolName.d.ts +16 -0
  116. package/dist/reservedToolName.d.ts.map +1 -0
  117. package/dist/reservedToolName.js +25 -0
  118. package/dist/resourceUri.d.ts +2 -0
  119. package/dist/resourceUri.d.ts.map +1 -0
  120. package/dist/resourceUri.js +19 -0
  121. package/dist/retry.d.ts +10 -0
  122. package/dist/retry.d.ts.map +1 -0
  123. package/dist/retry.js +37 -0
  124. package/dist/sanitize.d.ts +7 -0
  125. package/dist/sanitize.d.ts.map +1 -0
  126. package/dist/sanitize.js +69 -0
  127. package/dist/schema.d.ts +2175 -0
  128. package/dist/schema.d.ts.map +1 -0
  129. package/dist/schema.js +1382 -0
  130. package/dist/sdkConstants.d.ts +15 -0
  131. package/dist/sdkConstants.d.ts.map +1 -0
  132. package/dist/sdkConstants.js +43 -0
  133. package/dist/slackSend.d.ts +38 -0
  134. package/dist/slackSend.d.ts.map +1 -0
  135. package/dist/slackSend.js +14 -0
  136. package/dist/slug.d.ts +5 -0
  137. package/dist/slug.d.ts.map +1 -0
  138. package/dist/slug.js +34 -0
  139. package/dist/slugifyTitle.d.ts +2 -0
  140. package/dist/slugifyTitle.d.ts.map +1 -0
  141. package/dist/slugifyTitle.js +11 -0
  142. package/dist/sources.d.ts +43 -0
  143. package/dist/sources.d.ts.map +1 -0
  144. package/dist/sources.js +73 -0
  145. package/dist/ssrf.d.ts +3 -0
  146. package/dist/ssrf.d.ts.map +1 -0
  147. package/dist/ssrf.js +46 -0
  148. package/dist/tallyUsageKinds.d.ts +9 -0
  149. package/dist/tallyUsageKinds.d.ts.map +1 -0
  150. package/dist/tallyUsageKinds.js +20 -0
  151. package/dist/telegramSend.d.ts +28 -0
  152. package/dist/telegramSend.d.ts.map +1 -0
  153. package/dist/telegramSend.js +9 -0
  154. package/dist/toolCatalog.d.ts +452 -0
  155. package/dist/toolCatalog.d.ts.map +1 -0
  156. package/dist/toolCatalog.js +552 -0
  157. package/dist/validateMessageVariables.d.ts +7 -0
  158. package/dist/validateMessageVariables.d.ts.map +1 -0
  159. package/dist/validateMessageVariables.js +16 -0
  160. package/dist/vendorError.d.ts +2 -0
  161. package/dist/vendorError.d.ts.map +1 -0
  162. package/dist/vendorError.js +25 -0
  163. package/dist/whatsappSend.d.ts +31 -0
  164. package/dist/whatsappSend.d.ts.map +1 -0
  165. package/dist/whatsappSend.js +11 -0
  166. package/package.json +76 -0
  167. package/src/apiError.ts +40 -0
  168. package/src/attachment.ts +97 -0
  169. package/src/base64.ts +60 -0
  170. package/src/channelDebounce.ts +76 -0
  171. package/src/channelNotifier.ts +39 -0
  172. package/src/chunking.ts +131 -0
  173. package/src/cliConstants.ts +50 -0
  174. package/src/coerce.ts +9 -0
  175. package/src/constants.ts +2542 -0
  176. package/src/crypto.ts +108 -0
  177. package/src/customCodeProject.ts +182 -0
  178. package/src/customCodeToken.ts +198 -0
  179. package/src/deadline.ts +43 -0
  180. package/src/discordSend.ts +41 -0
  181. package/src/embeddable.ts +4 -0
  182. package/src/exposedResource.ts +40 -0
  183. package/src/extractedDocument.ts +62 -0
  184. package/src/fetcher.ts +82 -0
  185. package/src/formatFilename.ts +19 -0
  186. package/src/formatRelative.ts +36 -0
  187. package/src/formatRelativeTime.ts +45 -0
  188. package/src/getEnv.ts +13 -0
  189. package/src/gmailSend.ts +34 -0
  190. package/src/index.ts +397 -0
  191. package/src/jsonSchemaToZodShape.ts +111 -0
  192. package/src/l2Normalize.ts +19 -0
  193. package/src/languageCookieDomain.ts +37 -0
  194. package/src/localizeZodIssue.ts +124 -0
  195. package/src/mailHeaders.ts +12 -0
  196. package/src/mcpProxy.ts +20 -0
  197. package/src/mimeMessage.ts +82 -0
  198. package/src/oauth.ts +103 -0
  199. package/src/oauthProviders.ts +118 -0
  200. package/src/outlookSend.ts +44 -0
  201. package/src/parseHttpError.ts +16 -0
  202. package/src/planLimitError.ts +50 -0
  203. package/src/processQueueBatch.ts +46 -0
  204. package/src/reservedToolName.ts +27 -0
  205. package/src/resourceUri.ts +17 -0
  206. package/src/retry.ts +43 -0
  207. package/src/sanitize.ts +67 -0
  208. package/src/schema.ts +1624 -0
  209. package/src/sdkConstants.ts +44 -0
  210. package/src/slackSend.ts +65 -0
  211. package/src/slug.ts +37 -0
  212. package/src/slugifyTitle.ts +8 -0
  213. package/src/sources.ts +117 -0
  214. package/src/ssrf.ts +34 -0
  215. package/src/tallyUsageKinds.ts +20 -0
  216. package/src/telegramSend.ts +39 -0
  217. package/src/toolCatalog.ts +635 -0
  218. package/src/validateMessageVariables.ts +18 -0
  219. package/src/vendorError.ts +19 -0
  220. package/src/whatsappSend.ts +47 -0
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@ganju/utils",
3
+ "version": "0.0.1",
4
+ "description": "Shared constants, schemas and helpers for Ganju",
5
+ "license": "Apache-2.0",
6
+ "author": "MontoyaAndres <andresmontoyafcb@gmail.com>",
7
+ "homepage": "https://ganju.ai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MontoyaAndres/ganju.git",
11
+ "directory": "packages/utils"
12
+ },
13
+ "keywords": [
14
+ "ganju",
15
+ "mcp",
16
+ "model-context-protocol"
17
+ ],
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE",
23
+ "NOTICE"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "main": "dist/index.js",
29
+ "module": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.js"
35
+ },
36
+ "./constants": {
37
+ "types": "./dist/constants.d.ts",
38
+ "import": "./dist/constants.js",
39
+ "require": "./dist/constants.js"
40
+ },
41
+ "./base64": {
42
+ "types": "./dist/base64.d.ts",
43
+ "import": "./dist/base64.js",
44
+ "require": "./dist/base64.js"
45
+ },
46
+ "./toolCatalog": {
47
+ "types": "./dist/toolCatalog.d.ts",
48
+ "import": "./dist/toolCatalog.js",
49
+ "require": "./dist/toolCatalog.js"
50
+ },
51
+ "./cliConstants": {
52
+ "types": "./dist/cliConstants.d.ts",
53
+ "import": "./dist/cliConstants.js",
54
+ "require": "./dist/cliConstants.js"
55
+ },
56
+ "./sdkConstants": {
57
+ "types": "./dist/sdkConstants.d.ts",
58
+ "import": "./dist/sdkConstants.js",
59
+ "require": "./dist/sdkConstants.js"
60
+ }
61
+ },
62
+ "scripts": {
63
+ "build": "tsc",
64
+ "prepublishOnly": "npm run build",
65
+ "clean": "rm -rf dist && rm -rf node_modules"
66
+ },
67
+ "dependencies": {
68
+ "@noble/ciphers": "^2.2.0",
69
+ "dayjs": "^1.11.21",
70
+ "zod": "^4.4.3"
71
+ },
72
+ "devDependencies": {
73
+ "@ganju/tsconfig": "*",
74
+ "typescript": "^6.0.3"
75
+ }
76
+ }
@@ -0,0 +1,40 @@
1
+ // API error responses come in two shapes: Zod validation failures arrive as
2
+ // { errors: [{ path, message }] }; everything else as { error: 'message' }.
3
+ // These helpers normalize both so callers don't have to branch every time.
4
+
5
+ interface ApiErrorBody {
6
+ error?: unknown;
7
+ errors?: unknown;
8
+ }
9
+
10
+ export const isApiError = (data: unknown): boolean => {
11
+ if (!data || typeof data !== 'object') return false;
12
+ const body = data as ApiErrorBody;
13
+ return body.error != null || body.errors != null;
14
+ };
15
+
16
+ export const getApiErrorMessage = (data: unknown, fallback: string): string => {
17
+ if (data && typeof data === 'object') {
18
+ const body = data as ApiErrorBody;
19
+
20
+ if (Array.isArray(body.errors)) {
21
+ const messages = body.errors
22
+ .map(issue =>
23
+ issue && typeof issue === 'object'
24
+ ? (issue as { message?: string }).message
25
+ : null
26
+ )
27
+ .filter((message): message is string => !!message);
28
+ if (messages.length > 0) return messages.join(', ');
29
+ }
30
+
31
+ if (typeof body.error === 'string') return body.error;
32
+
33
+ if (body.error && typeof body.error === 'object') {
34
+ const message = (body.error as { message?: string }).message;
35
+ if (message) return message;
36
+ }
37
+ }
38
+
39
+ return fallback;
40
+ };
@@ -0,0 +1,97 @@
1
+ import { constants } from './constants';
2
+
3
+ // Turning one artifact_resource into the three things a multipart `attachment`
4
+ // field needs — bytes, a mime type, and a filename — is the same branch in every
5
+ // path that delivers a file: the native gmail, outlook and slack handlers, and
6
+ // the custom-code broker's sendFile. A resource is either an object in R2
7
+ // (fileKey) or inline text (content), and the filename fallbacks differ between
8
+ // the two.
9
+ //
10
+ // Size caps deliberately stay at the call sites. Each destination has its own
11
+ // (Gmail caps the combined raw size, Outlook caps per file AND combined, Slack
12
+ // caps per file), and folding them in here would mean a helper that has to know
13
+ // which vendor it is serving.
14
+
15
+ export interface AttachmentResource {
16
+ uri: string;
17
+ fileKey?: string | null;
18
+ content?: string | null;
19
+ mimeType?: string | null;
20
+ fileName?: string | null;
21
+ title?: string | null;
22
+ }
23
+
24
+ export interface ResolvedAttachment {
25
+ bytes: ArrayBuffer;
26
+ mimeType: string;
27
+ filename: string;
28
+ }
29
+
30
+ export type ResolveAttachmentResult =
31
+ { ok: true; attachment: ResolvedAttachment } | { ok: false; error: string };
32
+
33
+ /**
34
+ * Resolve one resource's attachment bytes.
35
+ *
36
+ * `readObject` rather than an R2 bucket so this stays free of a workers-types
37
+ * dependency — @ganju/utils is bundled into every uploaded user script, and the
38
+ * callers each already hold their own bucket binding.
39
+ *
40
+ * `verb` only shapes the error text: Slack "uploads" a file where the two mail
41
+ * providers "attach" one, and the message is read by a model deciding what to do
42
+ * next.
43
+ */
44
+ export const resolveAttachment = async (
45
+ resource: AttachmentResource,
46
+ readObject: (key: string) => Promise<ArrayBuffer | null>,
47
+ verb: 'attach' | 'upload' = 'attach'
48
+ ): Promise<ResolveAttachmentResult> => {
49
+ if (resource.fileKey) {
50
+ const bytes = await readObject(resource.fileKey);
51
+ if (!bytes) {
52
+ return {
53
+ ok: false,
54
+ error: `Resource bytes missing in storage for ${resource.uri} (fileKey: ${resource.fileKey})`
55
+ };
56
+ }
57
+ return {
58
+ ok: true,
59
+ attachment: {
60
+ bytes,
61
+ mimeType:
62
+ resource.mimeType || constants.MIMETYPE_APPLICATION_OCTET_STREAM,
63
+ filename:
64
+ resource.fileName ||
65
+ resource.title ||
66
+ resource.uri.split('/').pop() ||
67
+ 'attachment'
68
+ }
69
+ };
70
+ }
71
+
72
+ if (resource.content !== null && resource.content !== undefined) {
73
+ const encoded = new TextEncoder().encode(resource.content);
74
+ // Sliced to its own buffer: TextEncoder may hand back a view into a larger
75
+ // allocation, and a Blob built from the whole buffer would carry the slack.
76
+ const bytes = encoded.buffer.slice(
77
+ encoded.byteOffset,
78
+ encoded.byteOffset + encoded.byteLength
79
+ ) as ArrayBuffer;
80
+ const base = resource.fileName || resource.title || 'attachment';
81
+ return {
82
+ ok: true,
83
+ attachment: {
84
+ bytes,
85
+ mimeType: resource.mimeType || 'text/plain',
86
+ // Inline resources are text with no extension of their own, and a file
87
+ // named "Q3 Report" arrives unopenable on most clients.
88
+ filename: /\.[a-z0-9]+$/i.test(base) ? base : `${base}.txt`
89
+ }
90
+ };
91
+ }
92
+
93
+ return {
94
+ ok: false,
95
+ error: `Resource ${resource.uri} has no inline content and no file in storage; cannot ${verb}.`
96
+ };
97
+ };
package/src/base64.ts ADDED
@@ -0,0 +1,60 @@
1
+ // Both Cloudflare Workers (with nodejs_compat) and the Node container expose
2
+ // the global Buffer; only pure browsers don't. When available it's an order
3
+ // of magnitude faster than the btoa/charCode loop, which matters once payloads
4
+ // reach tens of megabytes (e.g. a Gmail attachment).
5
+ declare const Buffer:
6
+ | {
7
+ from(
8
+ input: Uint8Array | string,
9
+ encoding?: string
10
+ ): { toString(encoding: string): string };
11
+ }
12
+ | undefined;
13
+
14
+ const HAS_BUFFER = typeof Buffer !== 'undefined';
15
+
16
+ export const bytesToBase64 = (bytes: Uint8Array): string => {
17
+ if (HAS_BUFFER) return Buffer!.from(bytes).toString('base64');
18
+ let binary = '';
19
+ for (let i = 0; i < bytes.length; i++)
20
+ binary += String.fromCharCode(bytes[i]);
21
+ return btoa(binary);
22
+ };
23
+
24
+ export const base64ToBytes = (b64: string): Uint8Array => {
25
+ const binary = atob(b64);
26
+ const out = new Uint8Array(binary.length);
27
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
28
+ return out;
29
+ };
30
+
31
+ export const hexToBytes = (hex: string): Uint8Array => {
32
+ if (hex.length % 2 !== 0) return new Uint8Array(0);
33
+ const out = new Uint8Array(hex.length / 2);
34
+ for (let i = 0; i < out.length; i++) {
35
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
36
+ }
37
+ return out;
38
+ };
39
+
40
+ export const utf8ToBase64 = (value: string): string => {
41
+ if (HAS_BUFFER) return Buffer!.from(value, 'utf8').toString('base64');
42
+ return bytesToBase64(new TextEncoder().encode(value));
43
+ };
44
+
45
+ export const base64ToUtf8 = (b64: string): string =>
46
+ new TextDecoder().decode(base64ToBytes(b64));
47
+
48
+ export const toBase64Url = (b64: string): string =>
49
+ b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
50
+
51
+ export const fromBase64Url = (b64url: string): string => {
52
+ const standard = b64url.replace(/-/g, '+').replace(/_/g, '/');
53
+ return standard.padEnd(Math.ceil(standard.length / 4) * 4, '=');
54
+ };
55
+
56
+ export const utf8ToBase64Url = (value: string): string =>
57
+ toBase64Url(utf8ToBase64(value));
58
+
59
+ export const base64UrlToUtf8 = (b64url: string): string =>
60
+ base64ToUtf8(fromBase64Url(b64url));
@@ -0,0 +1,76 @@
1
+ import { constants } from './constants';
2
+
3
+ // How a burst of messages from one participant is coalesced into a single turn.
4
+ // The buffer lives in a Durable Object keyed by (channel, conversation,
5
+ // participant) — these helpers are the pieces both sides of that boundary share:
6
+ // the webhook that pushes into the buffer and the DO that flushes it.
7
+
8
+ // A single inbound message held in the buffer. `externalMessageId` is the
9
+ // platform's own id (Telegram message_id, Slack ts, wamid, Discord message id) —
10
+ // it dedupes platform webhook resends and is recorded on the channel_message row.
11
+ export interface BufferedChannelMessage {
12
+ text: string;
13
+ externalMessageId: string | null;
14
+ receivedAt: number;
15
+ }
16
+
17
+ // Everything the flush needs to run the turn and deliver the reply, minus the
18
+ // message texts themselves. The newest push wins: a display name or thread id
19
+ // can change mid-burst, and the freshest is the one to answer against.
20
+ // `delivery` is opaque here — each platform stores what its own send path needs
21
+ // (chat id + reply-to, Slack channel + thread_ts, …).
22
+ export interface ChannelBufferEnvelope {
23
+ channelId: string;
24
+ platform: string;
25
+ externalConversationId: string;
26
+ conversationTitle: string | null;
27
+ conversationScope: string;
28
+ externalParticipantId: string;
29
+ participantDisplayName: string | null;
30
+ participantMetadata: Record<string, unknown> | null;
31
+ delivery: Record<string, unknown>;
32
+ }
33
+
34
+ // The payload the DO hands back to the worker when a batch is ready.
35
+ export interface ChannelBufferFlush {
36
+ envelope: ChannelBufferEnvelope;
37
+ messages: BufferedChannelMessage[];
38
+ }
39
+
40
+ // Read the channel's debounce window off its `config`, clamped to the supported
41
+ // range. An unset value takes the default; an explicit 0 disables buffering, so
42
+ // the channel answers every message as it arrives. Anything malformed (a string,
43
+ // a negative, a value past the cap) falls back to the default rather than
44
+ // throwing — a bad config row must not take a live bot down.
45
+ export const resolveDebounceMs = (config: unknown): number => {
46
+ const raw = (config as { debounceMs?: unknown } | null)?.debounceMs;
47
+ if (typeof raw !== 'number' || !Number.isFinite(raw)) {
48
+ return constants.CHANNEL_DEBOUNCE_DEFAULT_MS;
49
+ }
50
+ if (raw === constants.CHANNEL_DEBOUNCE_DISABLED) {
51
+ return constants.CHANNEL_DEBOUNCE_DISABLED;
52
+ }
53
+ if (raw < constants.CHANNEL_DEBOUNCE_MIN_MS) {
54
+ return constants.CHANNEL_DEBOUNCE_DEFAULT_MS;
55
+ }
56
+ return Math.min(raw, constants.CHANNEL_DEBOUNCE_MAX_MS);
57
+ };
58
+
59
+ // The DO instance name for one participant's burst. Keyed by participant, not
60
+ // just conversation: in a group chat two people asking at once are two separate
61
+ // questions, and merging them into one turn would answer neither well.
62
+ export const channelBufferKey = (
63
+ channelId: string,
64
+ externalConversationId: string,
65
+ externalParticipantId: string
66
+ ): string => `${channelId}:${externalConversationId}:${externalParticipantId}`;
67
+
68
+ // Collapse buffered messages into the single user turn the model sees. Blank
69
+ // fragments are dropped (a bare @mention among real text carries nothing).
70
+ export const joinBufferedMessages = (
71
+ messages: Array<{ text: string }>
72
+ ): string =>
73
+ messages
74
+ .map(message => message.text.trim())
75
+ .filter(Boolean)
76
+ .join(constants.CHANNEL_DEBOUNCE_JOIN);
@@ -0,0 +1,39 @@
1
+ import { constants } from './constants';
2
+
3
+ export interface ToolStatusEvent {
4
+ toolName: string;
5
+ arguments: Record<string, unknown>;
6
+ }
7
+
8
+ export interface ChannelNotifier {
9
+ toolStarted: (event: ToolStatusEvent) => Promise<void>;
10
+ }
11
+
12
+ const TOOL_STATUS_COPY: Record<string, string> = {
13
+ [constants.RESOURCE_TOOL_KEY_SEND_RESOURCE]: 'Preparing the file to send…',
14
+ 'gmail-send-email': 'Sending the email…',
15
+ 'gmail-reply-email': 'Sending the reply…',
16
+ 'gmail-forward-email': 'Forwarding the email…',
17
+ 'gmail-send-draft': 'Sending the draft…',
18
+ 'gmail-create-draft': 'Saving the draft…',
19
+ 'gmail-update-draft': 'Updating the draft…',
20
+ 'gmail-delete-draft': 'Deleting the draft…',
21
+ 'gmail-trash-email': 'Moving the email to trash…',
22
+ 'gmail-modify-labels': 'Updating email labels…',
23
+ 'gmail-batch-modify-labels': 'Updating email labels…',
24
+ 'outlook-send-email': 'Sending the email…',
25
+ 'outlook-reply-email': 'Sending the reply…',
26
+ 'outlook-forward-email': 'Forwarding the email…',
27
+ 'outlook-send-draft': 'Sending the draft…',
28
+ 'outlook-create-draft': 'Saving the draft…',
29
+ 'outlook-update-draft': 'Updating the draft…',
30
+ 'outlook-delete-draft': 'Deleting the draft…',
31
+ 'outlook-trash-email': 'Moving the email to trash…',
32
+ 'outlook-move-message': 'Moving the email…',
33
+ 'outlook-batch-move-messages': 'Moving the emails…',
34
+ 'web-search': 'Searching the web…',
35
+ 'web-extract': 'Reading the page…'
36
+ };
37
+
38
+ export const getToolStatusMessage = (toolName: string): string | null =>
39
+ TOOL_STATUS_COPY[toolName] ?? null;
@@ -0,0 +1,131 @@
1
+ import { constants } from './constants';
2
+
3
+ import type {
4
+ ExtractedDocument,
5
+ ExtractedDocumentMetadata
6
+ } from './extractedDocument';
7
+
8
+ export type Separator = (typeof constants.CHUNK_SEPARATORS)[number];
9
+
10
+ const trySplit = (text: string, sep: string | RegExp): string[] | null => {
11
+ if (typeof sep === 'string') {
12
+ return text.includes(sep) ? text.split(sep) : null;
13
+ }
14
+ return sep.test(text) ? text.split(sep) : null;
15
+ };
16
+
17
+ export const splitRecursive = (
18
+ text: string,
19
+ target: number,
20
+ separators: readonly Separator[] = constants.CHUNK_SEPARATORS
21
+ ): string[] => {
22
+ if (text.length <= target) return [text];
23
+
24
+ for (const { split, join } of separators) {
25
+ const parts = trySplit(text, split);
26
+ if (!parts) continue;
27
+ const chunks: string[] = [];
28
+ let current = '';
29
+ for (const part of parts) {
30
+ const piece = current ? current + join + part : part;
31
+ if (piece.length > target && current) {
32
+ chunks.push(current);
33
+ current = part;
34
+ } else {
35
+ current = piece;
36
+ }
37
+ }
38
+ if (current) chunks.push(current);
39
+ return chunks.flatMap(chunk =>
40
+ chunk.length > target ? splitRecursive(chunk, target, separators) : [chunk]
41
+ );
42
+ }
43
+
44
+ const chunks: string[] = [];
45
+ for (let i = 0; i < text.length; i += target) {
46
+ chunks.push(text.slice(i, i + target));
47
+ }
48
+ return chunks;
49
+ };
50
+
51
+ export const chunkText = (
52
+ text: string,
53
+ options?: { targetChars?: number; overlapChars?: number }
54
+ ): string[] => {
55
+ const target = options?.targetChars ?? constants.CHUNK_TARGET_CHARS;
56
+ const overlap = options?.overlapChars ?? constants.CHUNK_OVERLAP_CHARS;
57
+
58
+ const trimmed = text.trim();
59
+ if (!trimmed) return [];
60
+ if (trimmed.length <= target) return [trimmed];
61
+
62
+ const rawChunks = splitRecursive(trimmed, target);
63
+ const withOverlap: string[] = [];
64
+ for (let i = 0; i < rawChunks.length; i++) {
65
+ const prev = i > 0 ? rawChunks[i - 1].slice(-overlap) : '';
66
+ withOverlap.push((prev ? prev + '\n' : '') + rawChunks[i]);
67
+ }
68
+ return withOverlap.filter(c => c.trim().length > 0);
69
+ };
70
+
71
+ export const buildHeader = (input: {
72
+ title?: string | null;
73
+ description?: string | null;
74
+ uri?: string | null;
75
+ mimeType?: string | null;
76
+ fileName?: string | null;
77
+ }): string =>
78
+ [
79
+ input.title ? `Title: ${input.title}` : null,
80
+ input.description ? `Description: ${input.description}` : null,
81
+ input.uri ? `URI: ${input.uri}` : null,
82
+ input.mimeType ? `MimeType: ${input.mimeType}` : null,
83
+ input.fileName ? `FileName: ${input.fileName}` : null
84
+ ]
85
+ .filter(Boolean)
86
+ .join('\n');
87
+
88
+ export interface ChunkMetadata extends ExtractedDocumentMetadata {
89
+ chunkIndexInPage: number;
90
+ chunksInPage: number;
91
+ }
92
+
93
+ export interface PreparedChunk {
94
+ content: string;
95
+ metadata: ChunkMetadata | null;
96
+ }
97
+
98
+ export const prepareChunks = (
99
+ header: string,
100
+ documents: ExtractedDocument[] | null,
101
+ fallbackContent: string | null,
102
+ options?: { targetChars?: number; overlapChars?: number }
103
+ ): PreparedChunk[] => {
104
+ if (documents && documents.length > 0) {
105
+ const out: PreparedChunk[] = [];
106
+ documents.forEach((doc, docIndex) => {
107
+ const body = (doc.pageContent || '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim();
108
+ if (!body) return;
109
+ const text = docIndex === 0 && header ? `${header}\n\n${body}` : body;
110
+ const pieces = chunkText(text, options);
111
+ pieces.forEach((piece, pieceIndex) => {
112
+ out.push({
113
+ content: piece,
114
+ metadata: {
115
+ ...doc.metadata,
116
+ chunkIndexInPage: pieceIndex,
117
+ chunksInPage: pieces.length
118
+ }
119
+ });
120
+ });
121
+ });
122
+ return out;
123
+ }
124
+
125
+ const body = (fallbackContent || '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim();
126
+ const fullText = [header, body].filter(Boolean).join('\n\n');
127
+ return chunkText(fullText, options).map(content => ({
128
+ content,
129
+ metadata: null
130
+ }));
131
+ };
@@ -0,0 +1,50 @@
1
+ // The handful of constants that get bundled into the published `ganju` CLI,
2
+ // split out of the main constants module for exactly that reason.
3
+ //
4
+ // The same problem `sdkConstants.ts` solves, with a sharper edge. `constants.ts`
5
+ // is one large object literal, so a bundler cannot drop the keys a consumer
6
+ // doesn't touch — and the CLI ships to npm, where those keys would be plan
7
+ // limits, pricing, vendor scopes and internal URLs sitting readable inside a
8
+ // public package. Eight values live here instead, and the main module
9
+ // re-exports them so there is still one definition of each.
10
+ //
11
+ // The rule for this file: nothing lands here unless the CLI reads it, and it
12
+ // imports nothing.
13
+
14
+ // How `ganju login` identifies itself, and where it listens for the redirect.
15
+ //
16
+ // A fixed port would fail whenever something else already held it, so the CLI
17
+ // binds the first one here that is free. All of them are registered up front
18
+ // rather than only the one that gets used: the provider does implement RFC 8252
19
+ // loopback matching, where the port is ignored for a 127.0.0.0/8 redirect, but
20
+ // that is one library's behaviour and this is a login that has already opened
21
+ // someone's browser by the time it would fail.
22
+ export const CLI_OAUTH_CLIENT_NAME = 'Ganju CLI';
23
+ export const CLI_OAUTH_REDIRECT_PATH = '/callback';
24
+ export const CLI_OAUTH_REDIRECT_PORTS = [8976, 8977, 8978, 8979, 8980];
25
+
26
+ // `offline_access` is what mints the refresh token; without it a CLI session
27
+ // would end an hour after login.
28
+ export const CLI_OAUTH_SCOPES = [
29
+ 'openid',
30
+ 'profile',
31
+ 'email',
32
+ 'offline_access'
33
+ ];
34
+
35
+ // Refresh this far before the access token actually expires, so a long command
36
+ // can't have its token die mid-request.
37
+ export const CLI_TOKEN_REFRESH_SKEW_SECONDS = 60;
38
+
39
+ // What `ganju build` rewrites the SDK import to. The deployed script imports the
40
+ // sibling module the publish pipeline attaches, whatever the author typed.
41
+ export const CUSTOM_CODE_SDK_SPECIFIER = './ganju-sdk.js';
42
+
43
+ // Set well below Cloudflare's own script-size ceiling so a rejection happens
44
+ // with a legible error rather than at deploy time.
45
+ export const CUSTOM_CODE_MAX_BUNDLE_BYTES = 3 * 1024 * 1024;
46
+
47
+ // The provider `ganju secret` writes under. custom-code secrets are the
48
+ // `ctx.secret(name)` values a user script reads at runtime, addressed by the
49
+ // label in each row's metadata.
50
+ export const CREDENTIAL_PROVIDER_CUSTOM_CODE = 'custom-code' as 'custom-code';
package/src/coerce.ts ADDED
@@ -0,0 +1,9 @@
1
+ export const toStringArray = (input: unknown): string[] => {
2
+ if (Array.isArray(input)) {
3
+ return input.map(s => String(s).trim()).filter(Boolean);
4
+ }
5
+ if (typeof input === 'string' && input.trim()) {
6
+ return input.split(',').map(s => s.trim()).filter(Boolean);
7
+ }
8
+ return [];
9
+ };