@rubytech/create-maxy-code 0.1.484 → 0.1.486
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/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +124 -0
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +107 -0
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +205 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts +4 -1
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +109 -1
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/caption-coverage.test.ts +117 -0
- package/payload/platform/lib/graph-style/src/index.ts +81 -1
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh +159 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +18 -6
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +141 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-pre.sh +66 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +33 -1
- package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +3 -3
- package/payload/platform/plugins/cloudflare/PLUGIN.md +2 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +23 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +49 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +57 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +44 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +67 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +26 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +42 -3
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +10 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +19 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +19 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +8 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +44 -13
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +220 -6
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +216 -17
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +4 -3
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +5 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js +50 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js +51 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js +89 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +28 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts +22 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js +55 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +32 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +75 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js +49 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +7 -1
- package/payload/platform/plugins/memory/references/schema-construction.md +18 -12
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +11 -6
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +75 -6
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +111 -22
- package/payload/platform/scripts/lib/provision-account-dir.sh +8 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +39 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +299 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +15 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/account-schema/SCHEMA.md +17 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-XGNRSM57.js → chunk-HN4HFTXE.js} +6 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{admin-rScqRq-R.js → admin-BoVCP4Mw.js} +1 -1
- package/payload/server/public/assets/{data-TOWuhMFb.js → data-CtOxa94P.js} +1 -1
- package/payload/server/public/assets/{graph-labels-Axa04w8A.js → graph-labels-C8-sIKuv.js} +1 -1
- package/payload/server/public/assets/{graph-BEGs_P2m.js → graph-mJ9ky5dc.js} +1 -1
- package/payload/server/public/assets/{page-6CZ8R57f.js → page-Cv0qUwMG.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +3 -3
- package/payload/server/server.js +125 -34
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
var sharedEl = document.getElementById('dp-shared');
|
|
20
20
|
var crumbsEl = document.getElementById('dp-crumbs');
|
|
21
21
|
var entriesEl = document.getElementById('dp-entries');
|
|
22
|
+
var upEl = document.getElementById('dp-up');
|
|
23
|
+
var uploadOpenEl = document.getElementById('dp-upload-open');
|
|
24
|
+
var modalEl = document.getElementById('dp-modal');
|
|
25
|
+
var modalCloseEl = document.getElementById('dp-modal-close');
|
|
22
26
|
|
|
23
27
|
/* The folder currently open in the "shared with you" tree; '' is its root. */
|
|
24
28
|
var prefix = '';
|
|
@@ -38,6 +42,55 @@
|
|
|
38
42
|
return li;
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
/* Lucide folder/file icons, inlined as trusted literals — no network request,
|
|
46
|
+
no npm dependency. innerHTML is safe here because the markup is fixed and
|
|
47
|
+
carries no filename or device data; every name still goes through
|
|
48
|
+
textContent. The stroke is currentColor, so each icon takes its span's
|
|
49
|
+
colour (folder accent, file muted; see portal.css). */
|
|
50
|
+
var FOLDER_SVG =
|
|
51
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>';
|
|
52
|
+
var FILE_SVG =
|
|
53
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>';
|
|
54
|
+
|
|
55
|
+
function icon(kind) {
|
|
56
|
+
var s = document.createElement('span');
|
|
57
|
+
var dir = kind === 'directory';
|
|
58
|
+
s.className = dir ? 'dp-icon dp-icon-folder' : 'dp-icon dp-icon-file';
|
|
59
|
+
s.innerHTML = dir ? FOLDER_SVG : FILE_SVG;
|
|
60
|
+
return s;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Split a filename into a stem and its extension. The stem is everything up to
|
|
64
|
+
the last dot; the extension is the dot and what follows. A name with no dot,
|
|
65
|
+
or only a leading dot, is all stem. The two render as separate spans so the
|
|
66
|
+
stem can ellipsis-truncate while the extension stays pinned and visible. */
|
|
67
|
+
function nameParts(name) {
|
|
68
|
+
var dot = name.lastIndexOf('.');
|
|
69
|
+
if (dot > 0) {
|
|
70
|
+
return { main: name.slice(0, dot), ext: name.slice(dot) };
|
|
71
|
+
}
|
|
72
|
+
return { main: name, ext: '' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The two-span name block used by both lists. Every value goes through
|
|
76
|
+
textContent, never markup — the same trust rule the callers already follow.
|
|
77
|
+
An empty ext adds no second span. */
|
|
78
|
+
function nameNode(main, ext) {
|
|
79
|
+
var wrap = document.createElement('span');
|
|
80
|
+
wrap.className = 'dp-name';
|
|
81
|
+
var m = document.createElement('span');
|
|
82
|
+
m.className = 'dp-name-main';
|
|
83
|
+
m.textContent = main;
|
|
84
|
+
wrap.appendChild(m);
|
|
85
|
+
if (ext) {
|
|
86
|
+
var e = document.createElement('span');
|
|
87
|
+
e.className = 'dp-name-ext';
|
|
88
|
+
e.textContent = ext;
|
|
89
|
+
wrap.appendChild(e);
|
|
90
|
+
}
|
|
91
|
+
return wrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
41
94
|
function render(files) {
|
|
42
95
|
// replaceChildren, not innerHTML: every filename below is set with
|
|
43
96
|
// textContent so a name like '<img onerror=…>' renders as text, and
|
|
@@ -54,7 +107,10 @@
|
|
|
54
107
|
var a = document.createElement('a');
|
|
55
108
|
a.className = 'dp-file-name';
|
|
56
109
|
a.href = API + '/api/download?key=' + encodeURIComponent(f.objectKey);
|
|
57
|
-
a
|
|
110
|
+
// Full name on hover, since the middle of a long name is clipped away.
|
|
111
|
+
a.title = f.filename;
|
|
112
|
+
var p = nameParts(f.filename);
|
|
113
|
+
a.appendChild(nameNode(p.main, p.ext));
|
|
58
114
|
li.appendChild(a);
|
|
59
115
|
|
|
60
116
|
var del = document.createElement('button');
|
|
@@ -92,6 +148,46 @@
|
|
|
92
148
|
crumbsEl.appendChild(document.createTextNode(' / '));
|
|
93
149
|
crumbsEl.appendChild(crumb(part, parts.slice(0, i + 1).join('/')));
|
|
94
150
|
});
|
|
151
|
+
// The up control has no parent to reach from the root, so it is inert there.
|
|
152
|
+
upEl.disabled = prefix === '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* One right-click menu at a time. A single element is moved and re-labelled
|
|
156
|
+
rather than spawning one per row, so dismissal has one thing to hide. */
|
|
157
|
+
var menuEl = null;
|
|
158
|
+
function hideMenu() {
|
|
159
|
+
if (menuEl) { menuEl.hidden = true; }
|
|
160
|
+
}
|
|
161
|
+
function showFileMenu(x, y, relPath) {
|
|
162
|
+
if (!menuEl) {
|
|
163
|
+
menuEl = document.createElement('div');
|
|
164
|
+
menuEl.className = 'dp-menu';
|
|
165
|
+
menuEl.hidden = true;
|
|
166
|
+
document.body.appendChild(menuEl);
|
|
167
|
+
}
|
|
168
|
+
menuEl.replaceChildren();
|
|
169
|
+
var b = document.createElement('button');
|
|
170
|
+
b.type = 'button';
|
|
171
|
+
b.className = 'dp-menu-item';
|
|
172
|
+
b.textContent = 'Download';
|
|
173
|
+
b.addEventListener('click', function () {
|
|
174
|
+
hideMenu();
|
|
175
|
+
openShared(relPath);
|
|
176
|
+
});
|
|
177
|
+
menuEl.appendChild(b);
|
|
178
|
+
menuEl.style.left = x + 'px';
|
|
179
|
+
menuEl.style.top = y + 'px';
|
|
180
|
+
menuEl.hidden = false;
|
|
181
|
+
}
|
|
182
|
+
document.addEventListener('click', hideMenu);
|
|
183
|
+
window.addEventListener('blur', hideMenu);
|
|
184
|
+
|
|
185
|
+
/* The endpoint a drag-out points at. redirect=1 makes it 302 to the signed
|
|
186
|
+
link, so the browser downloads the file rather than a JSON body. Resolved to
|
|
187
|
+
an absolute URL (DownloadURL requires one) against the page, so it is correct
|
|
188
|
+
whether api-base is empty, a path, or a full origin. */
|
|
189
|
+
function downloadUrl(relPath) {
|
|
190
|
+
return new URL(API + '/api/download?path=' + encodeURIComponent(relPath) + '&redirect=1', location.href).href;
|
|
95
191
|
}
|
|
96
192
|
|
|
97
193
|
/* Every label goes through textContent, same rule as the upload list: these
|
|
@@ -110,7 +206,13 @@
|
|
|
110
206
|
var b = document.createElement('button');
|
|
111
207
|
b.className = e.kind === 'directory' ? 'dp-folder-name' : 'dp-file-name';
|
|
112
208
|
b.type = 'button';
|
|
113
|
-
|
|
209
|
+
// Full name on hover, since the middle of a long name is clipped away.
|
|
210
|
+
b.title = e.name;
|
|
211
|
+
// A folder's tail is its trailing slash; a file's is its extension. Both
|
|
212
|
+
// stay pinned while the stem truncates.
|
|
213
|
+
var np = e.kind === 'directory' ? { main: e.name, ext: '/' } : nameParts(e.name);
|
|
214
|
+
b.appendChild(icon(e.kind));
|
|
215
|
+
b.appendChild(nameNode(np.main, np.ext));
|
|
114
216
|
b.addEventListener('click', function () {
|
|
115
217
|
if (e.kind === 'directory') {
|
|
116
218
|
prefix = e.relPath;
|
|
@@ -119,6 +221,20 @@
|
|
|
119
221
|
openShared(e.relPath);
|
|
120
222
|
}
|
|
121
223
|
});
|
|
224
|
+
if (e.kind !== 'directory') {
|
|
225
|
+
b.draggable = true;
|
|
226
|
+
b.addEventListener('dragstart', function (ev) {
|
|
227
|
+
var url = downloadUrl(e.relPath);
|
|
228
|
+
// DownloadURL is "mime:filename:absolute-url"; octet-stream keeps the
|
|
229
|
+
// server's content-disposition filename authoritative.
|
|
230
|
+
ev.dataTransfer.setData('DownloadURL', 'application/octet-stream:' + e.name + ':' + url);
|
|
231
|
+
ev.dataTransfer.setData('text/uri-list', url);
|
|
232
|
+
});
|
|
233
|
+
b.addEventListener('contextmenu', function (ev) {
|
|
234
|
+
ev.preventDefault();
|
|
235
|
+
showFileMenu(ev.clientX, ev.clientY, e.relPath);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
122
238
|
li.appendChild(b);
|
|
123
239
|
entriesEl.appendChild(li);
|
|
124
240
|
});
|
|
@@ -192,6 +308,13 @@
|
|
|
192
308
|
});
|
|
193
309
|
}
|
|
194
310
|
|
|
311
|
+
function authFailureMessage(status) {
|
|
312
|
+
if (status === 429) return 'Too many attempts. Try again later.';
|
|
313
|
+
if (status === 400) return 'Enter both your name and your passcode.';
|
|
314
|
+
if (status === 401) return 'That name and passcode did not match.';
|
|
315
|
+
return 'Something went wrong. Please try again in a moment.';
|
|
316
|
+
}
|
|
317
|
+
|
|
195
318
|
loginEl.addEventListener('submit', function (e) {
|
|
196
319
|
e.preventDefault();
|
|
197
320
|
say('Signing in…');
|
|
@@ -209,12 +332,12 @@
|
|
|
209
332
|
});
|
|
210
333
|
})
|
|
211
334
|
.then(function (res) {
|
|
212
|
-
if (res.s === 429) {
|
|
213
|
-
say('Too many attempts. Try again later.');
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
335
|
if (!res.d.ok) {
|
|
217
|
-
|
|
336
|
+
// The server returns the same denial for a wrong passcode and an
|
|
337
|
+
// unknown owner on purpose, so the message stays single and the
|
|
338
|
+
// console carries only the status class, never the reason.
|
|
339
|
+
console.error('[data-portal] auth failed', { status: res.s, error: res.d.error });
|
|
340
|
+
say(authFailureMessage(res.s));
|
|
218
341
|
return;
|
|
219
342
|
}
|
|
220
343
|
document.getElementById('dp-passcode').value = '';
|
|
@@ -223,29 +346,105 @@
|
|
|
223
346
|
say('');
|
|
224
347
|
return refresh();
|
|
225
348
|
})
|
|
226
|
-
.catch(function () {
|
|
227
|
-
|
|
349
|
+
.catch(function (err) {
|
|
350
|
+
console.error('[data-portal] auth failed', { status: 'network', error: err });
|
|
351
|
+
say('Could not reach the server. Check your connection and try again.');
|
|
228
352
|
});
|
|
229
353
|
});
|
|
230
354
|
|
|
231
|
-
document.getElementById('dp-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
355
|
+
var revealEl = document.getElementById('dp-reveal');
|
|
356
|
+
var passcodeInput = document.getElementById('dp-passcode');
|
|
357
|
+
revealEl.addEventListener('click', function () {
|
|
358
|
+
var revealed = passcodeInput.type === 'text';
|
|
359
|
+
passcodeInput.type = revealed ? 'password' : 'text';
|
|
360
|
+
revealEl.textContent = revealed ? 'Show passcode' : 'Hide passcode';
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
/* Up one level: drop the last path segment and re-list. Inert at the root,
|
|
364
|
+
where the button is disabled, so this guard is belt-and-braces. */
|
|
365
|
+
upEl.addEventListener('click', function () {
|
|
366
|
+
if (prefix === '') return;
|
|
367
|
+
var parts = prefix.split('/');
|
|
368
|
+
parts.pop();
|
|
369
|
+
prefix = parts.join('/');
|
|
370
|
+
refresh();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
/* The upload modal. Hidden via the `hidden` attribute so
|
|
374
|
+
[hidden]{display:none!important} stays authoritative over .dp-modal's
|
|
375
|
+
display:flex. It dismisses three ways: the close button, a click on the
|
|
376
|
+
backdrop (never the panel), and Escape while open. */
|
|
377
|
+
function openModal() {
|
|
378
|
+
modalEl.hidden = false;
|
|
379
|
+
document.getElementById('dp-file').focus();
|
|
380
|
+
}
|
|
381
|
+
function closeModal() {
|
|
382
|
+
modalEl.hidden = true;
|
|
383
|
+
}
|
|
384
|
+
uploadOpenEl.addEventListener('click', openModal);
|
|
385
|
+
modalCloseEl.addEventListener('click', closeModal);
|
|
386
|
+
modalEl.addEventListener('click', function (e) {
|
|
387
|
+
if (e.target === modalEl) { closeModal(); }
|
|
388
|
+
});
|
|
389
|
+
document.addEventListener('keydown', function (e) {
|
|
390
|
+
if (e.key === 'Escape' && !modalEl.hidden) { closeModal(); }
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
var uploadForm = document.getElementById('dp-upload');
|
|
394
|
+
|
|
395
|
+
/* One upload path for Browse-and-Upload and for a dropped file. Resolves to
|
|
396
|
+
{ responded, ok }: `responded` is true when the server answered (kept or
|
|
397
|
+
not), so the picker is cleared only when a retry with the same selection is
|
|
398
|
+
pointless; `ok` is true only on a kept upload, which is what closes the
|
|
399
|
+
modal. A network error is { responded:false, ok:false }. */
|
|
400
|
+
function uploadFile(file) {
|
|
235
401
|
var fd = new FormData();
|
|
236
|
-
fd.append('file',
|
|
402
|
+
fd.append('file', file);
|
|
237
403
|
say('Uploading…');
|
|
238
|
-
api('/api/upload', { method: 'POST', body: fd })
|
|
404
|
+
return api('/api/upload', { method: 'POST', body: fd })
|
|
239
405
|
.then(function (r) {
|
|
240
406
|
return r.json();
|
|
241
407
|
})
|
|
242
408
|
.then(function (d) {
|
|
243
409
|
say(d.ok ? 'Uploaded.' : 'Upload failed.');
|
|
244
|
-
|
|
245
|
-
return refresh();
|
|
410
|
+
return refresh().then(function () { return { responded: true, ok: !!d.ok }; });
|
|
246
411
|
})
|
|
247
412
|
.catch(function () {
|
|
248
413
|
say('Upload failed.');
|
|
414
|
+
return { responded: false, ok: false };
|
|
249
415
|
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
uploadForm.addEventListener('submit', function (e) {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
var input = document.getElementById('dp-file');
|
|
421
|
+
if (!input.files || !input.files[0]) return;
|
|
422
|
+
uploadFile(input.files[0]).then(function (res) {
|
|
423
|
+
if (res.responded) { input.value = ''; }
|
|
424
|
+
if (res.ok) { closeModal(); }
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
uploadForm.addEventListener('dragover', function (e) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
uploadForm.classList.add('dp-dropping');
|
|
431
|
+
});
|
|
432
|
+
uploadForm.addEventListener('dragleave', function (e) {
|
|
433
|
+
// Only clear when the pointer leaves the form, not when it crosses onto a
|
|
434
|
+
// child (input, button), which would flicker the outline mid-drag.
|
|
435
|
+
if (!uploadForm.contains(e.relatedTarget)) {
|
|
436
|
+
uploadForm.classList.remove('dp-dropping');
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
uploadForm.addEventListener('drop', function (e) {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
uploadForm.classList.remove('dp-dropping');
|
|
442
|
+
var dt = e.dataTransfer;
|
|
443
|
+
if (dt && dt.files && dt.files[0]) {
|
|
444
|
+
// The drop target now lives inside the modal, so a kept drop closes it too.
|
|
445
|
+
uploadFile(dt.files[0]).then(function (res) {
|
|
446
|
+
if (res.ok) { closeModal(); }
|
|
447
|
+
});
|
|
448
|
+
}
|
|
250
449
|
});
|
|
251
450
|
})();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# __D1_DATABASE_NAME__ the D1 database name (<accountId>-<purpose>)
|
|
5
5
|
# __D1_DATABASE_ID__ the id printed by `wrangler d1 create`
|
|
6
6
|
# __R2_BUCKET_NAME__ the R2 bucket name (<accountId>-<purpose>)
|
|
7
|
-
# __PORTAL_INSTALL_ORIGIN__ the install's
|
|
7
|
+
# __PORTAL_INSTALL_ORIGIN__ the install's PUBLIC origin (https://<public-tunnel-hostname>), NOT the admin/operator host
|
|
8
8
|
name = "__PROJECT_NAME__"
|
|
9
9
|
pages_build_output_dir = "."
|
|
10
10
|
compatibility_date = "2024-01-01"
|
|
@@ -22,8 +22,9 @@ binding = "BUCKET"
|
|
|
22
22
|
bucket_name = "__R2_BUCKET_NAME__"
|
|
23
23
|
|
|
24
24
|
# Where the install answers, so a download of a device-held file can be signed
|
|
25
|
-
# and sent there. Public information
|
|
26
|
-
# ends up at.
|
|
25
|
+
# and sent there. Public information: it is the hostname a client's browser
|
|
26
|
+
# ends up at. Use the PUBLIC host: the admin host is remote-auth-gated and the
|
|
27
|
+
# edge's signed HEAD probe 401s there, which 503s every download.
|
|
27
28
|
#
|
|
28
29
|
# Its partner, PORTAL_FETCH_SECRET, is deliberately NOT here. This file is
|
|
29
30
|
# committed to the account's assembled tree and the deploy publishes the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "email",
|
|
3
|
-
"description": "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
|
|
3
|
+
"description": "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: email
|
|
3
|
-
description: "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
|
|
3
|
+
description: "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
|
|
4
4
|
tools:
|
|
5
5
|
- name: email-provider-info
|
|
6
6
|
publicAllowlist: false
|
|
@@ -14,6 +14,9 @@ tools:
|
|
|
14
14
|
- name: email-fetch-body
|
|
15
15
|
publicAllowlist: false
|
|
16
16
|
adminAllowlist: false
|
|
17
|
+
- name: email-fetch-attachment
|
|
18
|
+
publicAllowlist: false
|
|
19
|
+
adminAllowlist: false
|
|
17
20
|
- name: email-send
|
|
18
21
|
publicAllowlist: false
|
|
19
22
|
adminAllowlist: false
|
|
@@ -84,6 +87,7 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
|
|
|
84
87
|
- **Read inbox:** `email-read` — metadata only (UID, sender, subject, date), plus a best-effort `Archive`/`Archive-Section` hint when the IMAP message's `receivedAt` falls inside a stored `:ConversationArchive {source:'email'}` section, plus an `Attachments (N): filename (mime, size); …` footer when the message carries enclosures. Supports pagination (`before_uid`), folders (`inbox`/`sent`), filtering by sender/date/subject. Metadata is derived from IMAP `bodyStructure` — no bytes downloaded.
|
|
85
88
|
- **Search inbox:** `email-search` — live IMAP search by sender, subject, body keyword, date range. Same per-archive hint and attachment footer as `email-read`.
|
|
86
89
|
- **Delete (move to Trash):** `email-delete` — move the messages named by `uids` (from a prior `email-read`/`email-search`, scoped to a source `folder` of `inbox` or `sent`) into the mailbox's Trash folder. **Recoverable, never an expunge** — the message leaves the inbox and permanent removal is left to the operator's own mail client or the provider's Trash auto-purge; this tool never permanently deletes. Trash is resolved per provider: the `\Trash` special-use folder if advertised, else the first existing of `Trash`, `[Gmail]/Trash`, `Deleted Items`, `Deleted Messages`. `Junk` is never treated as Trash. A mailbox with no resolvable Trash yields a typed error and moves nothing — there is no silent expunge fallback. A `uid` that is already gone is counted not-moved without failing the rest; the result reports `Moved N of M message(s) to <Trash>`. It deletes only the UIDs you name, never by search criteria.
|
|
90
|
+
- **Read one attachment's text:** `email-fetch-attachment` — the extracted text of a single inbox attachment, addressed by `uid` plus an optional `index` (1-based, matching the `Attachments (N): …` footer order) or `filename`. With one attachment and no selector, that attachment is read; with more than one and no selector, or an ambiguous filename, the call is refused with a numbered listing. `application/pdf` is extracted via `pdftotext`, `text/*` by decode; any other type reports that it cannot be extracted, and a PDF with no text layer reports it is a scanned image (no OCR), so "no data in the attachment" never reads the same as "the attachment could not be read". Bytes are downloaded once through the 25 MB-capped fetch path; inbox only. Use this when actionable data (a meeting date, an amount) may sit inside a PDF rather than the email body.
|
|
87
91
|
- **Read one whole message:** `email-fetch-body` — the COMPLETE decoded body of a single message addressed by `uid` (with optional `folder`) or `message_id` (resolved across inbox and sent). No 500-char preview cap. Downloads the full RFC822 source and decodes it with `mailparser`, so every MIME shape — flat text, single-part HTML, `multipart/alternative`, and nested `multipart/related`/`multipart/mixed` (Outlook's inline-image house style) — yields a body; the preview tools above intentionally stay preview-only. Use this when the operator wants to read or act on a message's actual content.
|
|
88
92
|
- **Send:** `email-send` — new outbound email. Pass `cc`/`bcc` (each an array of email addresses) to copy or blind-copy recipients — CC appears in the message header, BCC recipients receive the message without appearing in any header. Pass `attachments` (absolute file paths under the account directory, ≤25 MB each) to attach files as SMTP attachments; an invalid path fails the send with no email dispatched. The success message names the CC recipients and reports the BCC count without naming them.
|
|
89
93
|
- **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Resolves the original envelope via IMAP `SEARCH HEADER Message-ID` (INBOX then `\Sent`), so threading works for any message still present on the server even if the graph never ingested it. Supports `replyAll`, `cc`/`bcc`, and `attachments` (same path rules as `email-send`). Explicit `cc`/`bcc` are added in addition to whatever `replyAll` places in `To`, passed through as given (no overlap de-duplication against `To`).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-select.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attachment-select.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveAttachmentSelector } from "../lib/imap.js";
|
|
3
|
+
const A = { partKey: "2", filename: "a.pdf", mimeType: "application/pdf", sizeBytes: 10, encoding: "base64" };
|
|
4
|
+
const B = { partKey: "3", filename: "b.pdf", mimeType: "application/pdf", sizeBytes: 20, encoding: "base64" };
|
|
5
|
+
describe("resolveAttachmentSelector", () => {
|
|
6
|
+
it("single attachment, no selector → that one", () => {
|
|
7
|
+
expect(resolveAttachmentSelector([A], {})).toEqual({ ok: true, meta: A });
|
|
8
|
+
});
|
|
9
|
+
it("multiple attachments, no selector → error listing them", () => {
|
|
10
|
+
const r = resolveAttachmentSelector([A, B], {});
|
|
11
|
+
expect(r.ok).toBe(false);
|
|
12
|
+
if (!r.ok) {
|
|
13
|
+
expect(r.error).toMatch(/1\. a\.pdf/);
|
|
14
|
+
expect(r.error).toMatch(/2\. b\.pdf/);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
it("index selects the right attachment", () => {
|
|
18
|
+
expect(resolveAttachmentSelector([A, B], { index: 2 })).toEqual({ ok: true, meta: B });
|
|
19
|
+
});
|
|
20
|
+
it("index out of range → error listing", () => {
|
|
21
|
+
const r = resolveAttachmentSelector([A], { index: 2 });
|
|
22
|
+
expect(r.ok).toBe(false);
|
|
23
|
+
if (!r.ok)
|
|
24
|
+
expect(r.error).toMatch(/index 2/);
|
|
25
|
+
});
|
|
26
|
+
it("filename match is case-insensitive", () => {
|
|
27
|
+
expect(resolveAttachmentSelector([A, B], { filename: "B.PDF" })).toEqual({ ok: true, meta: B });
|
|
28
|
+
});
|
|
29
|
+
it("filename with no match → error", () => {
|
|
30
|
+
const r = resolveAttachmentSelector([A, B], { filename: "c.pdf" });
|
|
31
|
+
expect(r.ok).toBe(false);
|
|
32
|
+
if (!r.ok)
|
|
33
|
+
expect(r.error).toMatch(/no attachment named/i);
|
|
34
|
+
});
|
|
35
|
+
it("filename matching two parts → ambiguous error", () => {
|
|
36
|
+
const dup = { ...B, filename: "a.pdf" };
|
|
37
|
+
const r = resolveAttachmentSelector([A, dup], { filename: "a.pdf" });
|
|
38
|
+
expect(r.ok).toBe(false);
|
|
39
|
+
if (!r.ok)
|
|
40
|
+
expect(r.error).toMatch(/more than one/i);
|
|
41
|
+
});
|
|
42
|
+
it("no attachments → error", () => {
|
|
43
|
+
const r = resolveAttachmentSelector([], {});
|
|
44
|
+
expect(r.ok).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("index takes precedence over filename when both given", () => {
|
|
47
|
+
expect(resolveAttachmentSelector([A, B], { index: 1, filename: "b.pdf" })).toEqual({ ok: true, meta: A });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=attachment-select.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-select.test.js","sourceRoot":"","sources":["../../src/__tests__/attachment-select.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,MAAM,CAAC,GAAmB,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAC9H,MAAM,CAAC,GAAmB,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAE9H,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,GAAG,yBAAyB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACtC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACxC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,GAAG,yBAAyB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC,CAAC,EAAE;YAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,GAAG,yBAAyB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;QACnE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC,CAAC,EAAE;YAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,GAAG,GAAmB,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;QACxD,MAAM,CAAC,GAAG,yBAAyB,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC,CAAC,EAAE;YAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,GAAG,yBAAyB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5G,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-text.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attachment-text.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// child_process is mocked so the PDF path is testable without a real pdftotext
|
|
3
|
+
// binary or a real PDF. text/* needs no mock.
|
|
4
|
+
// execFileMock returns either { stdout } (success) or { error } (spawn failure).
|
|
5
|
+
// The mocked execFile routes it through promisify's node-style callback, so no
|
|
6
|
+
// synchronous throw ever escapes into the test runner.
|
|
7
|
+
const execFileMock = vi.fn();
|
|
8
|
+
vi.mock("node:child_process", () => ({
|
|
9
|
+
execFile: (...args) => {
|
|
10
|
+
const cb = args[args.length - 1];
|
|
11
|
+
const outcome = execFileMock(args[0], args[1]);
|
|
12
|
+
if (outcome.error)
|
|
13
|
+
cb(outcome.error);
|
|
14
|
+
else
|
|
15
|
+
cb(null, { stdout: outcome.stdout ?? "" });
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
import { extractAttachmentText } from "../lib/attachment-text.js";
|
|
19
|
+
describe("extractAttachmentText", () => {
|
|
20
|
+
beforeEach(() => execFileMock.mockReset());
|
|
21
|
+
it("decodes text/plain", async () => {
|
|
22
|
+
const r = await extractAttachmentText(Buffer.from(" meeting at 3pm "), "text/plain");
|
|
23
|
+
expect(r.outcome).toBe("extracted");
|
|
24
|
+
expect(r.text).toBe("meeting at 3pm");
|
|
25
|
+
expect(r.mimeType).toBe("text/plain");
|
|
26
|
+
});
|
|
27
|
+
it("returns unsupported-mime for images", async () => {
|
|
28
|
+
const r = await extractAttachmentText(Buffer.from([0x89, 0x50]), "image/png");
|
|
29
|
+
expect(r.outcome).toBe("unsupported-mime");
|
|
30
|
+
expect(r.text).toBe("");
|
|
31
|
+
});
|
|
32
|
+
it("extracts a PDF via pdftotext", async () => {
|
|
33
|
+
execFileMock.mockReturnValue({ stdout: "Tuesday Sep 8, 2026 10:30am" });
|
|
34
|
+
const r = await extractAttachmentText(Buffer.from("%PDF-1.4"), "application/pdf");
|
|
35
|
+
expect(execFileMock).toHaveBeenCalledOnce();
|
|
36
|
+
expect(execFileMock.mock.calls[0][0]).toBe("pdftotext");
|
|
37
|
+
expect(r.outcome).toBe("extracted");
|
|
38
|
+
expect(r.text).toContain("Sep 8, 2026");
|
|
39
|
+
});
|
|
40
|
+
it("reports no-text-layer when a PDF yields no text", async () => {
|
|
41
|
+
execFileMock.mockReturnValue({ stdout: " " });
|
|
42
|
+
const r = await extractAttachmentText(Buffer.from("%PDF-1.4"), "application/pdf");
|
|
43
|
+
expect(r.outcome).toBe("no-text-layer");
|
|
44
|
+
expect(r.text).toBe("");
|
|
45
|
+
});
|
|
46
|
+
it("surfaces a poppler-install hint when pdftotext is missing", async () => {
|
|
47
|
+
execFileMock.mockReturnValue({ error: new Error("spawn pdftotext ENOENT") });
|
|
48
|
+
await expect(extractAttachmentText(Buffer.from("%PDF-1.4"), "application/pdf")).rejects.toThrow(/poppler-utils/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=attachment-text.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-text.test.js","sourceRoot":"","sources":["../../src/__tests__/attachment-text.test.ts"],"names":[],"mappings":"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"}
|
package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-fetch-attachment.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-fetch-attachment.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const loadCredentials = vi.fn();
|
|
3
|
+
const fetchMessageAttachments = vi.fn();
|
|
4
|
+
const fetchAttachmentBytesForMessages = vi.fn();
|
|
5
|
+
const extractAttachmentText = vi.fn();
|
|
6
|
+
vi.mock("../lib/credentials.js", () => ({
|
|
7
|
+
loadCredentials: (...a) => loadCredentials(...a),
|
|
8
|
+
}));
|
|
9
|
+
// Keep the real resolveAttachmentSelector (pure, already tested); override only
|
|
10
|
+
// the two network-bound functions.
|
|
11
|
+
vi.mock("../lib/imap.js", async (importActual) => {
|
|
12
|
+
const actual = await importActual();
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
fetchMessageAttachments: (...a) => fetchMessageAttachments(...a),
|
|
16
|
+
fetchAttachmentBytesForMessages: (...a) => fetchAttachmentBytesForMessages(...a),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
vi.mock("../lib/attachment-text.js", () => ({
|
|
20
|
+
extractAttachmentText: (...a) => extractAttachmentText(...a),
|
|
21
|
+
}));
|
|
22
|
+
import { emailFetchAttachment } from "../tools/email-fetch-attachment.js";
|
|
23
|
+
const PDF = { partKey: "2", filename: "proposal.pdf", mimeType: "application/pdf", sizeBytes: 12345, encoding: "base64" };
|
|
24
|
+
const PDF2 = { partKey: "3", filename: "terms.pdf", mimeType: "application/pdf", sizeBytes: 6789, encoding: "base64" };
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
loadCredentials.mockReset();
|
|
27
|
+
fetchMessageAttachments.mockReset();
|
|
28
|
+
fetchAttachmentBytesForMessages.mockReset();
|
|
29
|
+
extractAttachmentText.mockReset();
|
|
30
|
+
loadCredentials.mockResolvedValue({ credentials: { config: { email: "info@x.com" }, password: "pw" } });
|
|
31
|
+
});
|
|
32
|
+
describe("emailFetchAttachment", () => {
|
|
33
|
+
it("returns the extracted text with a header for a PDF", async () => {
|
|
34
|
+
fetchMessageAttachments.mockResolvedValue({ found: true, attachments: [PDF] });
|
|
35
|
+
fetchAttachmentBytesForMessages.mockResolvedValue([{ uid: 99, partKey: "2", bytes: Buffer.from("x"), failureReason: null }]);
|
|
36
|
+
extractAttachmentText.mockResolvedValue({ outcome: "extracted", text: "Meeting: Tue Sep 8 10:30", mimeType: "application/pdf" });
|
|
37
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 99 });
|
|
38
|
+
expect(out).toMatch(/Attachment: proposal\.pdf/);
|
|
39
|
+
expect(out).toMatch(/part 2/);
|
|
40
|
+
expect(out).toMatch(/Meeting: Tue Sep 8 10:30/);
|
|
41
|
+
});
|
|
42
|
+
it("refuses with a listing when two attachments and no selector", async () => {
|
|
43
|
+
fetchMessageAttachments.mockResolvedValue({ found: true, attachments: [PDF, PDF2] });
|
|
44
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 99 });
|
|
45
|
+
expect(out).toMatch(/pass index= or filename=/);
|
|
46
|
+
expect(out).toMatch(/1\. proposal\.pdf/);
|
|
47
|
+
expect(fetchAttachmentBytesForMessages).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
it("reports a scanned PDF with no text layer", async () => {
|
|
50
|
+
fetchMessageAttachments.mockResolvedValue({ found: true, attachments: [PDF] });
|
|
51
|
+
fetchAttachmentBytesForMessages.mockResolvedValue([{ uid: 99, partKey: "2", bytes: Buffer.from("x"), failureReason: null }]);
|
|
52
|
+
extractAttachmentText.mockResolvedValue({ outcome: "no-text-layer", text: "", mimeType: "application/pdf" });
|
|
53
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 99 });
|
|
54
|
+
expect(out).toMatch(/no extractable text layer/);
|
|
55
|
+
expect(out).toMatch(/OCR is not available/);
|
|
56
|
+
});
|
|
57
|
+
it("reports unsupported mime", async () => {
|
|
58
|
+
const docx = { partKey: "2", filename: "x.docx", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", sizeBytes: 10, encoding: "base64" };
|
|
59
|
+
fetchMessageAttachments.mockResolvedValue({ found: true, attachments: [docx] });
|
|
60
|
+
fetchAttachmentBytesForMessages.mockResolvedValue([{ uid: 99, partKey: "2", bytes: Buffer.from("x"), failureReason: null }]);
|
|
61
|
+
extractAttachmentText.mockResolvedValue({ outcome: "unsupported-mime", text: "", mimeType: docx.mimeType });
|
|
62
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 99 });
|
|
63
|
+
expect(out).toMatch(/cannot be extracted from this type/i);
|
|
64
|
+
});
|
|
65
|
+
it("reports an oversize attachment", async () => {
|
|
66
|
+
fetchMessageAttachments.mockResolvedValue({ found: true, attachments: [PDF] });
|
|
67
|
+
fetchAttachmentBytesForMessages.mockResolvedValue([{ uid: 99, partKey: "2", bytes: null, failureReason: "oversize" }]);
|
|
68
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 99 });
|
|
69
|
+
expect(out).toMatch(/25 MB/);
|
|
70
|
+
expect(extractAttachmentText).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
it("reports a download failure", async () => {
|
|
73
|
+
fetchMessageAttachments.mockResolvedValue({ found: true, attachments: [PDF] });
|
|
74
|
+
fetchAttachmentBytesForMessages.mockResolvedValue([{ uid: 99, partKey: "2", bytes: null, failureReason: "fetch-error" }]);
|
|
75
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 99 });
|
|
76
|
+
expect(out).toMatch(/Could not download/);
|
|
77
|
+
});
|
|
78
|
+
it("reports message not found", async () => {
|
|
79
|
+
fetchMessageAttachments.mockResolvedValue({ found: false, attachments: [] });
|
|
80
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 404 });
|
|
81
|
+
expect(out).toMatch(/No message found for UID 404/);
|
|
82
|
+
});
|
|
83
|
+
it("refuses when credentials are missing", async () => {
|
|
84
|
+
loadCredentials.mockResolvedValue({ error: "Email not configured." });
|
|
85
|
+
const out = await emailFetchAttachment({ accountId: "acc", uid: 99 });
|
|
86
|
+
expect(out).toMatch(/Email not configured/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=email-fetch-attachment.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-fetch-attachment.test.js","sourceRoot":"","sources":["../../src/__tests__/email-fetch-attachment.test.ts"],"names":[],"mappings":"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"}
|