@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.
Files changed (105) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +124 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +107 -0
  4. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +205 -0
  5. package/payload/platform/lib/graph-style/dist/index.d.ts +4 -1
  6. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  7. package/payload/platform/lib/graph-style/dist/index.js +109 -1
  8. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  9. package/payload/platform/lib/graph-style/src/__tests__/caption-coverage.test.ts +117 -0
  10. package/payload/platform/lib/graph-style/src/index.ts +81 -1
  11. package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh +159 -0
  12. package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +18 -6
  13. package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +141 -0
  14. package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-pre.sh +66 -0
  15. package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +33 -1
  16. package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +3 -3
  17. package/payload/platform/plugins/cloudflare/PLUGIN.md +5 -3
  18. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +23 -2
  19. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +30 -8
  20. package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +22 -2
  21. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
  22. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +49 -2
  23. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +57 -3
  24. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +23 -0
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +44 -2
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +67 -0
  27. package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +43 -0
  28. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +54 -5
  29. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +10 -1
  30. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +19 -5
  31. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +19 -2
  32. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +8 -2
  33. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +28 -5
  34. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +195 -6
  35. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +193 -10
  36. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +4 -3
  37. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  38. package/payload/platform/plugins/email/PLUGIN.md +5 -1
  39. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts +2 -0
  40. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts.map +1 -0
  41. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js +50 -0
  42. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js.map +1 -0
  43. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts +2 -0
  44. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js +51 -0
  46. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js.map +1 -0
  47. package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts +2 -0
  48. package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts.map +1 -0
  49. package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js +89 -0
  50. package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js.map +1 -0
  51. package/payload/platform/plugins/email/mcp/dist/index.js +28 -0
  52. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  53. package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts +22 -0
  54. package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts.map +1 -0
  55. package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js +55 -0
  56. package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js.map +1 -0
  57. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +32 -0
  58. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  59. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +75 -1
  60. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  61. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts +19 -0
  62. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts.map +1 -0
  63. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js +49 -0
  64. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js.map +1 -0
  65. package/payload/platform/plugins/email/references/email-reference.md +7 -1
  66. package/payload/platform/plugins/memory/references/schema-construction.md +18 -12
  67. package/payload/platform/plugins/memory/references/schema-estate-agent.md +11 -6
  68. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts +2 -0
  69. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts.map +1 -0
  70. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js +12 -0
  71. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js.map +1 -0
  72. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts +8 -0
  73. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts.map +1 -0
  74. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js +12 -0
  75. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js.map +1 -0
  76. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +12 -2
  77. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
  78. package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +75 -6
  79. package/payload/platform/scripts/lib/account-schema-owned-dirs.py +111 -22
  80. package/payload/platform/scripts/lib/provision-account-dir.sh +8 -1
  81. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +39 -0
  82. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -0
  83. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +315 -0
  84. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -0
  85. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  86. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  87. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  88. package/payload/platform/services/claude-session-manager/dist/index.js +15 -0
  89. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  90. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  91. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +4 -0
  92. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  93. package/payload/platform/templates/account-schema/SCHEMA.md +17 -0
  94. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  95. package/payload/server/{chunk-XGNRSM57.js → chunk-HN4HFTXE.js} +6 -0
  96. package/payload/server/maxy-edge.js +1 -1
  97. package/payload/server/public/assets/{admin-rScqRq-R.js → admin-BoVCP4Mw.js} +1 -1
  98. package/payload/server/public/assets/{data-TOWuhMFb.js → data-CtOxa94P.js} +1 -1
  99. package/payload/server/public/assets/{graph-labels-Axa04w8A.js → graph-labels-C8-sIKuv.js} +1 -1
  100. package/payload/server/public/assets/{graph-BEGs_P2m.js → graph-mJ9ky5dc.js} +1 -1
  101. package/payload/server/public/assets/{page-6CZ8R57f.js → page-Cv0qUwMG.js} +1 -1
  102. package/payload/server/public/data.html +3 -3
  103. package/payload/server/public/graph.html +2 -2
  104. package/payload/server/public/index.html +3 -3
  105. 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: 16px/1.5 system-ui, -apple-system, 'Segoe UI', sans-serif;
32
+ font: 14px/1.5 system-ui, -apple-system, 'Segoe UI', sans-serif;
33
33
  }
34
34
 
35
35
  .dp-card {
36
- max-width: 34rem;
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
- word-break: break-all;
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
- margin-bottom: 10px;
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, but they must
172
- match .dp-file-name's weight so the two lists read as one surface. */
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.textContent = f.filename;
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
- b.textContent = e.kind === 'directory' ? e.name + '/' : e.name;
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
- document.getElementById('dp-upload').addEventListener('submit', function (e) {
248
- e.preventDefault();
249
- var input = document.getElementById('dp-file');
250
- if (!input.files || !input.files[0]) return;
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', input.files[0]);
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
- input.value = '';
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 public origin (https://<tunnel-hostname>)
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 it is the hostname a client's browser
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=attachment-select.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=attachment-text.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-text.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attachment-text.test.ts"],"names":[],"mappings":""}