@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.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +23 -0
- package/dist/apiError.d.ts +3 -0
- package/dist/apiError.d.ts.map +1 -0
- package/dist/apiError.js +36 -0
- package/dist/attachment.d.ts +33 -0
- package/dist/attachment.d.ts.map +1 -0
- package/dist/attachment.js +59 -0
- package/dist/base64.d.ts +10 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +50 -0
- package/dist/channelDebounce.d.ts +26 -0
- package/dist/channelDebounce.d.ts.map +1 -0
- package/dist/channelDebounce.js +35 -0
- package/dist/channelNotifier.d.ts +9 -0
- package/dist/channelNotifier.d.ts.map +1 -0
- package/dist/channelNotifier.js +31 -0
- package/dist/chunking.d.ts +28 -0
- package/dist/chunking.d.ts.map +1 -0
- package/dist/chunking.js +97 -0
- package/dist/cliConstants.d.ts +9 -0
- package/dist/cliConstants.d.ts.map +1 -0
- package/dist/cliConstants.js +47 -0
- package/dist/coerce.d.ts +2 -0
- package/dist/coerce.d.ts.map +1 -0
- package/dist/coerce.js +13 -0
- package/dist/constants.d.ts +624 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2220 -0
- package/dist/crypto.d.ts +14 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +93 -0
- package/dist/customCodeProject.d.ts +91 -0
- package/dist/customCodeProject.d.ts.map +1 -0
- package/dist/customCodeProject.js +154 -0
- package/dist/customCodeToken.d.ts +41 -0
- package/dist/customCodeToken.d.ts.map +1 -0
- package/dist/customCodeToken.js +115 -0
- package/dist/deadline.d.ts +6 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +45 -0
- package/dist/discordSend.d.ts +25 -0
- package/dist/discordSend.d.ts.map +1 -0
- package/dist/discordSend.js +8 -0
- package/dist/embeddable.d.ts +2 -0
- package/dist/embeddable.d.ts.map +1 -0
- package/dist/embeddable.js +6 -0
- package/dist/exposedResource.d.ts +6 -0
- package/dist/exposedResource.d.ts.map +1 -0
- package/dist/exposedResource.js +18 -0
- package/dist/extractedDocument.d.ts +70 -0
- package/dist/extractedDocument.d.ts.map +1 -0
- package/dist/extractedDocument.js +2 -0
- package/dist/fetcher.d.ts +8 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +68 -0
- package/dist/formatFilename.d.ts +2 -0
- package/dist/formatFilename.d.ts.map +1 -0
- package/dist/formatFilename.js +19 -0
- package/dist/formatRelative.d.ts +12 -0
- package/dist/formatRelative.d.ts.map +1 -0
- package/dist/formatRelative.js +28 -0
- package/dist/formatRelativeTime.d.ts +2 -0
- package/dist/formatRelativeTime.d.ts.map +1 -0
- package/dist/formatRelativeTime.js +49 -0
- package/dist/getEnv.d.ts +5 -0
- package/dist/getEnv.d.ts.map +1 -0
- package/dist/getEnv.js +11 -0
- package/dist/gmailSend.d.ts +24 -0
- package/dist/gmailSend.d.ts.map +1 -0
- package/dist/gmailSend.js +9 -0
- package/dist/index.d.ts +2839 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +155 -0
- package/dist/jsonSchemaToZodShape.d.ts +38 -0
- package/dist/jsonSchemaToZodShape.d.ts.map +1 -0
- package/dist/jsonSchemaToZodShape.js +117 -0
- package/dist/l2Normalize.d.ts +2 -0
- package/dist/l2Normalize.d.ts.map +1 -0
- package/dist/l2Normalize.js +25 -0
- package/dist/languageCookieDomain.d.ts +21 -0
- package/dist/languageCookieDomain.d.ts.map +1 -0
- package/dist/languageCookieDomain.js +39 -0
- package/dist/localizeZodIssue.d.ts +43 -0
- package/dist/localizeZodIssue.d.ts.map +1 -0
- package/dist/localizeZodIssue.js +90 -0
- package/dist/mailHeaders.d.ts +4 -0
- package/dist/mailHeaders.d.ts.map +1 -0
- package/dist/mailHeaders.js +11 -0
- package/dist/mcpProxy.d.ts +2 -0
- package/dist/mcpProxy.d.ts.map +1 -0
- package/dist/mcpProxy.js +21 -0
- package/dist/mimeMessage.d.ts +21 -0
- package/dist/mimeMessage.d.ts.map +1 -0
- package/dist/mimeMessage.js +58 -0
- package/dist/oauth.d.ts +21 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +75 -0
- package/dist/oauthProviders.d.ts +12 -0
- package/dist/oauthProviders.d.ts.map +1 -0
- package/dist/oauthProviders.js +99 -0
- package/dist/outlookSend.d.ts +19 -0
- package/dist/outlookSend.d.ts.map +1 -0
- package/dist/outlookSend.js +9 -0
- package/dist/parseHttpError.d.ts +2 -0
- package/dist/parseHttpError.d.ts.map +1 -0
- package/dist/parseHttpError.js +23 -0
- package/dist/planLimitError.d.ts +18 -0
- package/dist/planLimitError.d.ts.map +1 -0
- package/dist/planLimitError.js +35 -0
- package/dist/processQueueBatch.d.ts +17 -0
- package/dist/processQueueBatch.d.ts.map +1 -0
- package/dist/processQueueBatch.js +32 -0
- package/dist/reservedToolName.d.ts +16 -0
- package/dist/reservedToolName.d.ts.map +1 -0
- package/dist/reservedToolName.js +25 -0
- package/dist/resourceUri.d.ts +2 -0
- package/dist/resourceUri.d.ts.map +1 -0
- package/dist/resourceUri.js +19 -0
- package/dist/retry.d.ts +10 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +37 -0
- package/dist/sanitize.d.ts +7 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +69 -0
- package/dist/schema.d.ts +2175 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +1382 -0
- package/dist/sdkConstants.d.ts +15 -0
- package/dist/sdkConstants.d.ts.map +1 -0
- package/dist/sdkConstants.js +43 -0
- package/dist/slackSend.d.ts +38 -0
- package/dist/slackSend.d.ts.map +1 -0
- package/dist/slackSend.js +14 -0
- package/dist/slug.d.ts +5 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +34 -0
- package/dist/slugifyTitle.d.ts +2 -0
- package/dist/slugifyTitle.d.ts.map +1 -0
- package/dist/slugifyTitle.js +11 -0
- package/dist/sources.d.ts +43 -0
- package/dist/sources.d.ts.map +1 -0
- package/dist/sources.js +73 -0
- package/dist/ssrf.d.ts +3 -0
- package/dist/ssrf.d.ts.map +1 -0
- package/dist/ssrf.js +46 -0
- package/dist/tallyUsageKinds.d.ts +9 -0
- package/dist/tallyUsageKinds.d.ts.map +1 -0
- package/dist/tallyUsageKinds.js +20 -0
- package/dist/telegramSend.d.ts +28 -0
- package/dist/telegramSend.d.ts.map +1 -0
- package/dist/telegramSend.js +9 -0
- package/dist/toolCatalog.d.ts +452 -0
- package/dist/toolCatalog.d.ts.map +1 -0
- package/dist/toolCatalog.js +552 -0
- package/dist/validateMessageVariables.d.ts +7 -0
- package/dist/validateMessageVariables.d.ts.map +1 -0
- package/dist/validateMessageVariables.js +16 -0
- package/dist/vendorError.d.ts +2 -0
- package/dist/vendorError.d.ts.map +1 -0
- package/dist/vendorError.js +25 -0
- package/dist/whatsappSend.d.ts +31 -0
- package/dist/whatsappSend.d.ts.map +1 -0
- package/dist/whatsappSend.js +11 -0
- package/package.json +76 -0
- package/src/apiError.ts +40 -0
- package/src/attachment.ts +97 -0
- package/src/base64.ts +60 -0
- package/src/channelDebounce.ts +76 -0
- package/src/channelNotifier.ts +39 -0
- package/src/chunking.ts +131 -0
- package/src/cliConstants.ts +50 -0
- package/src/coerce.ts +9 -0
- package/src/constants.ts +2542 -0
- package/src/crypto.ts +108 -0
- package/src/customCodeProject.ts +182 -0
- package/src/customCodeToken.ts +198 -0
- package/src/deadline.ts +43 -0
- package/src/discordSend.ts +41 -0
- package/src/embeddable.ts +4 -0
- package/src/exposedResource.ts +40 -0
- package/src/extractedDocument.ts +62 -0
- package/src/fetcher.ts +82 -0
- package/src/formatFilename.ts +19 -0
- package/src/formatRelative.ts +36 -0
- package/src/formatRelativeTime.ts +45 -0
- package/src/getEnv.ts +13 -0
- package/src/gmailSend.ts +34 -0
- package/src/index.ts +397 -0
- package/src/jsonSchemaToZodShape.ts +111 -0
- package/src/l2Normalize.ts +19 -0
- package/src/languageCookieDomain.ts +37 -0
- package/src/localizeZodIssue.ts +124 -0
- package/src/mailHeaders.ts +12 -0
- package/src/mcpProxy.ts +20 -0
- package/src/mimeMessage.ts +82 -0
- package/src/oauth.ts +103 -0
- package/src/oauthProviders.ts +118 -0
- package/src/outlookSend.ts +44 -0
- package/src/parseHttpError.ts +16 -0
- package/src/planLimitError.ts +50 -0
- package/src/processQueueBatch.ts +46 -0
- package/src/reservedToolName.ts +27 -0
- package/src/resourceUri.ts +17 -0
- package/src/retry.ts +43 -0
- package/src/sanitize.ts +67 -0
- package/src/schema.ts +1624 -0
- package/src/sdkConstants.ts +44 -0
- package/src/slackSend.ts +65 -0
- package/src/slug.ts +37 -0
- package/src/slugifyTitle.ts +8 -0
- package/src/sources.ts +117 -0
- package/src/ssrf.ts +34 -0
- package/src/tallyUsageKinds.ts +20 -0
- package/src/telegramSend.ts +39 -0
- package/src/toolCatalog.ts +635 -0
- package/src/validateMessageVariables.ts +18 -0
- package/src/vendorError.ts +19 -0
- 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
|
+
}
|
package/src/apiError.ts
ADDED
|
@@ -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;
|
package/src/chunking.ts
ADDED
|
@@ -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
|
+
};
|