@rubytech/create-maxy-code 0.1.485 → 0.1.487
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 +5 -3
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +23 -2
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +30 -8
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +22 -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-index-push.test.ts +23 -0
- 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__/schema-exposed-dirs.test.ts +43 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +54 -5
- 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 +28 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +195 -6
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +193 -10
- 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/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +12 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
- 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 +315 -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 +231 -103
|
@@ -29,11 +29,11 @@ body {
|
|
|
29
29
|
padding: 2rem 1rem;
|
|
30
30
|
background: var(--dp-paper);
|
|
31
31
|
color: var(--dp-ink);
|
|
32
|
-
font:
|
|
32
|
+
font: 14px/1.5 system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.dp-card {
|
|
36
|
-
max-width:
|
|
36
|
+
max-width: 56rem;
|
|
37
37
|
margin: 0 auto;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -98,16 +98,45 @@ body {
|
|
|
98
98
|
border-bottom: 1px solid var(--dp-rule);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/* One line per name, never wrapped. The name splits into a stem that
|
|
102
|
+
ellipsis-truncates and an extension that never shrinks, so `.pdf` vs `.html`
|
|
103
|
+
on two same-named orders stays visible. flex:1 + min-width:0 lets the name
|
|
104
|
+
give up width to the Delete button beside it. */
|
|
101
105
|
.dp-file-name {
|
|
102
106
|
color: var(--dp-accent);
|
|
103
107
|
text-decoration: none;
|
|
104
|
-
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 0.5rem;
|
|
111
|
+
flex: 1;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
overflow: hidden;
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
.dp-file-name:hover {
|
|
108
117
|
text-decoration: underline;
|
|
109
118
|
}
|
|
110
119
|
|
|
120
|
+
/* The two-span name block. No gap between stem and extension so they read as
|
|
121
|
+
one filename; the stem clips with an ellipsis, the extension is pinned. */
|
|
122
|
+
.dp-name {
|
|
123
|
+
display: flex;
|
|
124
|
+
min-width: 0;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.dp-name-main {
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
text-overflow: ellipsis;
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
min-width: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.dp-name-ext {
|
|
136
|
+
flex: none;
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
.dp-del {
|
|
112
141
|
border: 1px solid var(--dp-rule);
|
|
113
142
|
border-radius: 0.375rem;
|
|
@@ -140,10 +169,15 @@ body {
|
|
|
140
169
|
color: var(--dp-ink);
|
|
141
170
|
}
|
|
142
171
|
|
|
172
|
+
/* In the pinned header the crumbs are the flexible middle: they take the space
|
|
173
|
+
the up/upload buttons leave and clip on one line when a path runs deep. */
|
|
143
174
|
.dp-crumbs {
|
|
144
175
|
font-size: 13px;
|
|
145
176
|
color: var(--dp-muted);
|
|
146
|
-
|
|
177
|
+
flex: 1;
|
|
178
|
+
min-width: 0;
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
white-space: nowrap;
|
|
147
181
|
}
|
|
148
182
|
|
|
149
183
|
/* Crumbs and folder rows are buttons (navigation is a fetch, not a page load),
|
|
@@ -166,10 +200,19 @@ body {
|
|
|
166
200
|
|
|
167
201
|
.dp-folder-name {
|
|
168
202
|
font-weight: 600;
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
gap: 0.5rem;
|
|
206
|
+
flex: 1;
|
|
207
|
+
min-width: 0;
|
|
208
|
+
overflow: hidden;
|
|
169
209
|
}
|
|
170
210
|
|
|
171
|
-
/* The file rows inside the tree are buttons too — same reset
|
|
172
|
-
|
|
211
|
+
/* The file rows inside the tree are buttons too — same reset. The layout (flex,
|
|
212
|
+
flex:1, min-width:0, overflow) comes from the base .dp-file-name, so these
|
|
213
|
+
truncate the same way; only the button chrome and left-align are added here.
|
|
214
|
+
The row-kind icon plus its colour (below) is what tells a folder from a file,
|
|
215
|
+
so these no longer match .dp-folder-name's weight. */
|
|
173
216
|
.dp-shared .dp-file-name {
|
|
174
217
|
background: none;
|
|
175
218
|
border: 0;
|
|
@@ -179,6 +222,30 @@ body {
|
|
|
179
222
|
text-align: left;
|
|
180
223
|
}
|
|
181
224
|
|
|
225
|
+
/* Row-kind icon: sized to the text, never shrinks, stroke follows the span's
|
|
226
|
+
colour. Folder takes the accent, file the muted grey — both existing vars, so
|
|
227
|
+
the :root rewrite in portal-brand-css.mjs leaves the distinction intact. */
|
|
228
|
+
.dp-icon {
|
|
229
|
+
flex: none;
|
|
230
|
+
display: inline-flex;
|
|
231
|
+
width: 1em;
|
|
232
|
+
height: 1em;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.dp-icon svg {
|
|
236
|
+
width: 1em;
|
|
237
|
+
height: 1em;
|
|
238
|
+
display: block;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.dp-icon-folder {
|
|
242
|
+
color: var(--dp-accent);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.dp-icon-file {
|
|
246
|
+
color: var(--dp-muted);
|
|
247
|
+
}
|
|
248
|
+
|
|
182
249
|
/* Passcode field with its reveal control. The passcode is transcribed out of
|
|
183
250
|
band, so being able to check what was typed is the point of the toggle.
|
|
184
251
|
Reuses existing variables and declares no new :root value, which
|
|
@@ -203,3 +270,125 @@ body {
|
|
|
203
270
|
cursor: pointer;
|
|
204
271
|
white-space: nowrap;
|
|
205
272
|
}
|
|
273
|
+
|
|
274
|
+
/* Right-click Download menu for a shared file. Reuses existing variables and
|
|
275
|
+
declares no new :root value, which portal-brand-css.mjs would drop. */
|
|
276
|
+
.dp-menu {
|
|
277
|
+
position: fixed;
|
|
278
|
+
z-index: 10;
|
|
279
|
+
background: var(--dp-paper);
|
|
280
|
+
border: 1px solid var(--dp-rule);
|
|
281
|
+
border-radius: 0.375rem;
|
|
282
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
|
283
|
+
padding: 0.25rem;
|
|
284
|
+
min-width: 8.75rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.dp-menu-item {
|
|
288
|
+
display: block;
|
|
289
|
+
width: 100%;
|
|
290
|
+
text-align: left;
|
|
291
|
+
padding: 0.375rem 0.625rem;
|
|
292
|
+
border: 0;
|
|
293
|
+
background: transparent;
|
|
294
|
+
border-radius: 0.25rem;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
font: inherit;
|
|
297
|
+
color: var(--dp-ink);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.dp-menu-item:hover,
|
|
301
|
+
.dp-menu-item:focus {
|
|
302
|
+
background: var(--dp-rule);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* The upload area while a file is dragged over it. */
|
|
306
|
+
.dp-upload.dp-dropping {
|
|
307
|
+
outline: 2px dashed var(--dp-accent);
|
|
308
|
+
outline-offset: 3px;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* Pinned nav row. position:sticky keeps it in flow, so it reserves its own
|
|
312
|
+
height and never overlaps the first file row (which position:fixed would). An
|
|
313
|
+
opaque paper background hides the rows that scroll under it. Reuses existing
|
|
314
|
+
variables and declares no new :root value, which portal-brand-css.mjs drops. */
|
|
315
|
+
.dp-header {
|
|
316
|
+
position: sticky;
|
|
317
|
+
top: 0;
|
|
318
|
+
z-index: 5;
|
|
319
|
+
display: flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
gap: 0.5rem;
|
|
322
|
+
padding: 0.5rem 0;
|
|
323
|
+
margin-bottom: 0.5rem;
|
|
324
|
+
background: var(--dp-paper);
|
|
325
|
+
border-bottom: 1px solid var(--dp-rule);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* The up and upload controls that flank the crumbs. Square, muted chrome so the
|
|
329
|
+
filenames stay the loudest thing on the row. */
|
|
330
|
+
.dp-iconbtn {
|
|
331
|
+
flex: none;
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
width: 2rem;
|
|
336
|
+
height: 2rem;
|
|
337
|
+
padding: 0.375rem;
|
|
338
|
+
border: 1px solid var(--dp-rule);
|
|
339
|
+
border-radius: 0.375rem;
|
|
340
|
+
background: transparent;
|
|
341
|
+
color: var(--dp-muted);
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.dp-iconbtn svg {
|
|
346
|
+
width: 1.1em;
|
|
347
|
+
height: 1.1em;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Disabled up control at the tree root: dimmed and inert, so it reads as "no
|
|
351
|
+
parent from here" rather than a dead button. */
|
|
352
|
+
.dp-iconbtn:disabled {
|
|
353
|
+
opacity: 0.4;
|
|
354
|
+
cursor: default;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Upload modal. [hidden]{display:none!important} keeps this off screen until the
|
|
358
|
+
header icon clears the attribute; display:flex then centres the panel. The
|
|
359
|
+
backdrop is a non-themed literal, same as .dp-menu's shadow. */
|
|
360
|
+
.dp-modal {
|
|
361
|
+
position: fixed;
|
|
362
|
+
inset: 0;
|
|
363
|
+
z-index: 20;
|
|
364
|
+
display: flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
justify-content: center;
|
|
367
|
+
padding: 1rem;
|
|
368
|
+
background: rgba(0, 0, 0, 0.4);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.dp-modal-panel {
|
|
372
|
+
position: relative;
|
|
373
|
+
width: 100%;
|
|
374
|
+
max-width: 24rem;
|
|
375
|
+
padding: 1.5rem;
|
|
376
|
+
background: var(--dp-paper);
|
|
377
|
+
border: 1px solid var(--dp-rule);
|
|
378
|
+
border-radius: 0.5rem;
|
|
379
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.dp-modal-close {
|
|
383
|
+
position: absolute;
|
|
384
|
+
top: 0.5rem;
|
|
385
|
+
right: 0.5rem;
|
|
386
|
+
padding: 0.25rem 0.5rem;
|
|
387
|
+
border: 0;
|
|
388
|
+
background: transparent;
|
|
389
|
+
color: var(--dp-muted);
|
|
390
|
+
font: inherit;
|
|
391
|
+
font-size: 1.25rem;
|
|
392
|
+
line-height: 1;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
}
|
|
@@ -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
|
});
|
|
@@ -244,24 +360,91 @@
|
|
|
244
360
|
revealEl.textContent = revealed ? 'Show passcode' : 'Hide passcode';
|
|
245
361
|
});
|
|
246
362
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
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) {
|
|
251
401
|
var fd = new FormData();
|
|
252
|
-
fd.append('file',
|
|
402
|
+
fd.append('file', file);
|
|
253
403
|
say('Uploading…');
|
|
254
|
-
api('/api/upload', { method: 'POST', body: fd })
|
|
404
|
+
return api('/api/upload', { method: 'POST', body: fd })
|
|
255
405
|
.then(function (r) {
|
|
256
406
|
return r.json();
|
|
257
407
|
})
|
|
258
408
|
.then(function (d) {
|
|
259
409
|
say(d.ok ? 'Uploaded.' : 'Upload failed.');
|
|
260
|
-
|
|
261
|
-
return refresh();
|
|
410
|
+
return refresh().then(function () { return { responded: true, ok: !!d.ok }; });
|
|
262
411
|
})
|
|
263
412
|
.catch(function () {
|
|
264
413
|
say('Upload failed.');
|
|
414
|
+
return { responded: false, ok: false };
|
|
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(); }
|
|
265
447
|
});
|
|
448
|
+
}
|
|
266
449
|
});
|
|
267
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":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-text.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attachment-text.test.ts"],"names":[],"mappings":""}
|