@rubytech/create-maxy-code 0.1.279 → 0.1.280
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/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +42 -0
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +60 -15
- package/payload/platform/plugins/admin/mcp/dist/__tests__/e-sign-discoverable.test.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/e-sign-discoverable.test.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/e-sign-discoverable.test.js +54 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/e-sign-discoverable.test.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/content-producer-authoring-skills.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/content-producer-authoring-skills.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/content-producer-authoring-skills.js +19 -0
- package/payload/platform/plugins/admin/mcp/dist/content-producer-authoring-skills.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +2 -5
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
- package/payload/platform/plugins/admin/skills/a4-print-documents/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/access-manager/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/capabilities-here/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/datetime/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/deck-pages/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/file-presentation/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/investigate/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/plainly/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/qr-code/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/session-management/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/skill-builder/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/specialist-management/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/task/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/update-knowledge/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/upgrade/SKILL.md +2 -0
- package/payload/platform/plugins/aeo/skills/structured-answer/SKILL.md +2 -0
- package/payload/platform/plugins/business-assistant/PLUGIN.md +2 -1
- package/payload/platform/plugins/business-assistant/{references/e-sign.md → skills/e-sign/SKILL.md} +9 -2
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +2 -0
- package/payload/platform/plugins/deep-research/skills/academic-verify/SKILL.md +2 -0
- package/payload/platform/plugins/deep-research/skills/book-mirror/SKILL.md +2 -0
- package/payload/platform/plugins/deep-research/skills/data-research/SKILL.md +2 -0
- package/payload/platform/plugins/deep-research/skills/deep-research/SKILL.md +2 -0
- package/payload/platform/plugins/deep-research/skills/strategic-reading/SKILL.md +2 -0
- package/payload/platform/plugins/email/PLUGIN.md +2 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +42 -11
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +123 -57
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-otp-extract.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-otp-extract.js +4 -2
- package/payload/platform/plugins/email/mcp/dist/tools/email-otp-extract.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js +18 -8
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js +18 -8
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js.map +1 -1
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +2 -0
- package/payload/platform/plugins/email/skills/email-ingest/SKILL.md +2 -0
- package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +2 -0
- package/payload/platform/plugins/linkedin-extension/skills/linkedin-extension/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/archive-crawler/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/challenge/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/concept-synthesis/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/connect/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/emerge/SKILL.md +2 -0
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +2 -0
- package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +3 -1
- package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/__tests__/restatement-ladder.test.sh +21 -0
- package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +2 -0
- package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -0
- package/payload/platform/plugins/slides/skills/deck-system/SKILL.md +2 -0
- package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +2 -0
- package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +2 -0
- package/payload/platform/plugins/work/skills/execute-task/SKILL.md +2 -0
- package/payload/platform/plugins/workflows/skills/workflow-manager/SKILL.md +2 -0
- package/payload/platform/scripts/lib/__tests__/admin-skills-bootstrap.test.sh +64 -0
- package/payload/platform/scripts/lib/admin-skills-bootstrap.sh +109 -0
- package/payload/platform/scripts/setup-account.sh +8 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +43 -11
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +74 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +48 -2
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/liveness-divergence.d.ts +19 -0
- package/payload/platform/services/claude-session-manager/dist/liveness-divergence.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/liveness-divergence.js +38 -0
- package/payload/platform/services/claude-session-manager/dist/liveness-divergence.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +8 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +12 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/scope-record.d.ts +11 -0
- package/payload/platform/services/claude-session-manager/dist/scope-record.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/scope-record.js +81 -0
- package/payload/platform/services/claude-session-manager/dist/scope-record.js.map +1 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +10 -7
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +14 -14
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +2 -6
- package/payload/server/public/assets/AdminShell-C6_5h1cC.js +1 -0
- package/payload/server/public/assets/{admin-xRCXEFNW.js → admin-BpnsTEa3.js} +1 -1
- package/payload/server/public/assets/{browser-DT2XfRpL.js → browser-fgBRZ5gK.js} +1 -1
- package/payload/server/public/assets/{data-Dr7XUvAx.js → data-Cuynp6Y6.js} +1 -1
- package/payload/server/public/assets/{graph-DOEjjQHD.js → graph-Ddg24bjy.js} +1 -1
- package/payload/server/public/assets/{whatsapp-cDxgZkbM.js → whatsapp-BE79pNh8.js} +1 -1
- package/payload/server/public/browser.html +2 -2
- package/payload/server/public/data.html +2 -2
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/public/whatsapp.html +2 -2
- package/payload/server/server.js +338 -461
- package/payload/server/public/assets/AdminShell-DnH3kou-.js +0 -1
|
@@ -5,6 +5,8 @@ description: Use when authoring or auditing a page whose primary purpose is to a
|
|
|
5
5
|
|
|
6
6
|
# Structured-answer page template
|
|
7
7
|
|
|
8
|
+
Invoked from `specialists:content-producer`.
|
|
9
|
+
|
|
8
10
|
Answer engines (Claude, ChatGPT, Perplexity, Google AI Overviews, Bing Copilot) lift a single short span when they cite a page. That span is almost always the first paragraph after the H1, when it is short and direct. Every other element on the page either supports the lift or gets ignored.
|
|
9
11
|
|
|
10
12
|
## The shape
|
|
@@ -32,6 +32,8 @@ Enquiries also arrive through the business's **published-site contact form**, wh
|
|
|
32
32
|
|
|
33
33
|
Track status through the pipeline: enquiry → quoted → booked → completed → invoiced → paid.
|
|
34
34
|
|
|
35
|
+
When a built document — a quote, contract, proposal, or set of terms — needs a recordable signature without DocuSign, load the `e-sign` skill (`skill-load skillName="e-sign"`).
|
|
36
|
+
|
|
35
37
|
## Session & Task Continuity
|
|
36
38
|
|
|
37
39
|
Every conversation is a session. At the start of each session:
|
|
@@ -57,7 +59,6 @@ Load the relevant reference when the task requires it:
|
|
|
57
59
|
- **Triage & escalation** → `references/escalation.md` — urgency classification (RED/AMBER/GREEN), customer handoff protocol, voice IVR handling
|
|
58
60
|
- **Scheduling & briefings** → `references/scheduling.md` — booking protocol, geographic clustering, travel time estimation, morning briefings, holiday mode, GPS integration
|
|
59
61
|
- **Invoices & payment** → `references/invoicing.md` — HTML invoice creation, PDF generation via `browser-navigate` + `browser-pdf-save`, sending, payment chase protocol
|
|
60
|
-
- **E-signatures** → `references/e-sign.md` — sign any hosted document (quote, contract, proposal): a Cloudflare Pages signing form captures the acceptance to D1, then the agent sweeps the row and emails both parties a PDF of the signed content
|
|
61
62
|
- **Customer records** → `references/crm.md` — contact record management, `contact_lookup`/`contact_update` usage, pipeline tracking, when to create/update records
|
|
62
63
|
- **Site form submissions** → `references/site-lead-intake.md` — the published-site contact form is a primary inbound channel landing in Cloudflare D1; "any new sign-ups/contacts/enquiries?" reads D1 first, then dedupes each unswept row into the graph and routes it to the recorded front-line owner
|
|
63
64
|
- **Document filing** → `references/document-management.md` — dual-scope filing system, naming conventions, folder structure for customer-facing vs internal documents
|
package/payload/platform/plugins/business-assistant/{references/e-sign.md → skills/e-sign/SKILL.md}
RENAMED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: e-sign
|
|
3
|
+
description: "Put a legally-recordable signature on any hosted document — a quote, contract, proposal, or set of terms — without DocuSign. A Cloudflare Pages signing form captures the acceptance to D1; the agent then sweeps the row, renders the signed content to PDF, and emails both parties. Triggers: 'sign this proposal', 'get this contract signed', 'put a signature on this', 'e-signature', 'send for signature', 'accept a proposal online', 'signature on a document'."
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# E-signatures — signing any hosted document (form → D1 → PDF + email)
|
|
2
7
|
|
|
8
|
+
**Invoked by the business-assistant (or admin) directly** when a built document — a quote, contract, proposal, or set of terms — needs a recordable signature.
|
|
9
|
+
|
|
3
10
|
How to put a legally-recordable signature on **any** document the business hosts — a quote, a contract, a proposal, a set of terms — without DocuSign. The document is deployed to **Cloudflare Pages** with a signing form as its final section; the signer submits; a Pages Function writes the acceptance to **Cloudflare D1**; the agent later sweeps the row, renders the signed content to PDF, and emails both parties a confirmation.
|
|
4
11
|
|
|
5
|
-
This
|
|
12
|
+
This skill owns **only the signature**: the form, the capture, the PDF, and the dispatch. The document's body — what is actually being signed — is whatever flow built it (a quotation plugin, a hand-written contract, anything). This begins at *"a built document needs a signature."*
|
|
6
13
|
|
|
7
14
|
It composes two patterns you must read alongside it:
|
|
8
15
|
|
|
9
|
-
- **`cloudflare/references/d1-data-capture.md`** — the form → Pages Function → D1 → sweep mechanics, and the token-scope breakage (§ 0). Everything about minting the Pages-+-D1 token, `wrangler.toml`, and `wrangler d1 execute --remote` lives there; this
|
|
16
|
+
- **`cloudflare/references/d1-data-capture.md`** — the form → Pages Function → D1 → sweep mechanics, and the token-scope breakage (§ 0). Everything about minting the Pages-+-D1 token, `wrangler.toml`, and `wrangler d1 execute --remote` lives there; this skill does not repeat it.
|
|
10
17
|
- **`business-assistant/references/invoicing.md`** — PDF generation via `browser-navigate` + `browser-pdf-save` (its step 4). The signed-content PDF uses that exact pattern.
|
|
11
18
|
|
|
12
19
|
Auth for every `wrangler`/API call is a minted narrow token per `cloudflare/references/api.md`. Deploy mechanics are `cloudflare/references/hosting-sites.md`.
|
|
@@ -5,6 +5,8 @@ description: Cloudflare operations for the install — tunnel setup/diagnosis/re
|
|
|
5
5
|
|
|
6
6
|
# Cloudflare operations
|
|
7
7
|
|
|
8
|
+
Invoked from `specialists:personal-assistant`.
|
|
9
|
+
|
|
8
10
|
This is the entry point for every Cloudflare task on the install. Pick the operation class, read the matching reference, run the command via Bash, relay the literal output, and prove the outcome with a live signal. There are no Cloudflare MCP tools; the plugin registers none. The agent never browser-automates the dashboard — the operator clicks where a click is genuinely required.
|
|
9
11
|
|
|
10
12
|
| You want to… | Read | Outcome contract |
|
|
@@ -5,6 +5,8 @@ description: "Traces an academic or scientific claim through the publication cha
|
|
|
5
5
|
|
|
6
6
|
# Academic verify
|
|
7
7
|
|
|
8
|
+
Invoked from `specialists:research-assistant`.
|
|
9
|
+
|
|
8
10
|
This skill is for claims that arrive as "studies show", "research suggests", or a named paper title. It traces the claim to the actual paper, the actual journal, and the actual conclusion of the source. It never says "verified" without a primary source URL.
|
|
9
11
|
|
|
10
12
|
## When to run
|
|
@@ -5,6 +5,8 @@ description: "Reads a book the owner provides (PDF, ePub, web link, or text), wa
|
|
|
5
5
|
|
|
6
6
|
# Book mirror
|
|
7
7
|
|
|
8
|
+
Invoked from `specialists:research-assistant`.
|
|
9
|
+
|
|
8
10
|
This skill turns a book into a usable document by walking the chapters one at a time and asking, for each: "what does this say, and how does it apply to this specific owner". The output is a personalised pack the owner can read once and act on. It is grounded in the graph; the right-hand column is always specific to what the graph already knows about the owner.
|
|
9
11
|
|
|
10
12
|
## When to run
|
|
@@ -5,6 +5,8 @@ description: "Recipe-driven structured-data extraction. Takes a named recipe and
|
|
|
5
5
|
|
|
6
6
|
# Data research
|
|
7
7
|
|
|
8
|
+
Invoked from `specialists:research-assistant`.
|
|
9
|
+
|
|
8
10
|
This skill turns unstructured-looking sources (emails, reports, forms, web pages) into structured graph entities. The transformation is always recipe-driven: a named YAML recipe declares what fields to extract, what validations to apply, and what graph nodes to write. The skill runs a named recipe; it never invents a schema on the fly.
|
|
9
11
|
|
|
10
12
|
**Default parent.** Every recipe declares its hierarchy parent via `graph_target.parent` — either `Project` (the run-time recipe argument names which Project elementId to attach to) or `LocalBusiness` (the account root, for cross-project recipes like investor-updates). The skill refuses to run a recipe whose `parent` field is missing or whose named Project does not exist. Per-row writes attach to that parent via the canonical containment edge; the recipe's `relationships:` block describes cross-hierarchy edges (e.g. `ROUND_FOR → Organization`) which are written separately and do not substitute for the parent.
|
|
@@ -20,6 +20,8 @@ config:
|
|
|
20
20
|
|
|
21
21
|
# Deep Research Skill
|
|
22
22
|
|
|
23
|
+
Invoked from `specialists:research-assistant`.
|
|
24
|
+
|
|
23
25
|
You are a research agent. Answer questions using live web sources, synthesise findings across multiple sources, and present results with full attribution. You do not guess. You search, read, extract, and cite.
|
|
24
26
|
|
|
25
27
|
## Workflow
|
|
@@ -5,6 +5,8 @@ description: "Reads a source (article, book, report, document) through a specifi
|
|
|
5
5
|
|
|
6
6
|
# Strategic reading
|
|
7
7
|
|
|
8
|
+
Invoked from `specialists:research-assistant`.
|
|
9
|
+
|
|
8
10
|
This skill reads a document with a specific question in mind. The owner names the lens (a project, a problem, a goal). The skill summarises what the source actually says, then translates each section into how it applies to the named lens, and closes with a playbook of three to seven concrete steps. Both halves are grounded: the left half in the source text, the right half in the graph.
|
|
9
11
|
|
|
10
12
|
## When to run
|
|
@@ -58,6 +58,8 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
|
|
|
58
58
|
|
|
59
59
|
**Identity boundary (operator-identity fix).** This plugin manages the AGENT's email channel: an `:EmailAccount` node with IMAP/SMTP credentials, polling state, and the agent's `agentAddress`. It is NOT the operator-identity store. The operator's own email lives on the OWNS-bound `:Person` (`Person.email`) and is captured via `profile-update` with `personFields`. Confusing the two surfaces (writing the operator's personal email to `EmailAccount.agentAddress`, or vice versa) breaks routing AND identity coverage. See [.docs/agents.md § Coverage-driven elicitation bias] for the operator-identity contract.
|
|
60
60
|
|
|
61
|
+
**Login vs alias — the default recipient set.** A config carries two addresses. `email` is the IMAP login: the address you authenticate as, and an address this mailbox owns. `agentAddress` is an *additional* alias to surface (often a per-agent address on a shared catch-all mailbox). When they differ, `email-read`, `email-search`, and `email-otp-extract` default to surfacing mail addressed to **either** — not the alias alone — because replies commonly arrive at the login address (a proposal sent from `info@` is answered to `info@`, not the `maxy@` alias). When no distinct alias is configured, no recipient filter is applied. Pass an explicit `to=` to narrow to a single address. Whenever the default filter hides messages from the fetched window, `email-read`/`email-search` append a `Recipient filter: kept N, hid M (to ∈ {…}); pass to="…" to change.` line — including when every message was hidden — so "I can't see it" is never indistinguishable from "it isn't there".
|
|
62
|
+
|
|
61
63
|
## Capabilities
|
|
62
64
|
|
|
63
65
|
- **Provider lookup:** `email-provider-info` — given just an email address, returns the inferred IMAP/SMTP hosts and the provider's app-password / trusted-application / IMAP-enable requirement, deterministically from the provider map. Call this BEFORE soliciting a password so the requirement is relayed first; most providers reject the normal login password. Returns "unknown" (operator supplies hosts) or "unsupported" (provider blocks IMAP/SMTP, e.g. Tuta).
|
|
@@ -14,6 +14,8 @@ export interface PaginatedMessages {
|
|
|
14
14
|
messages: EmailMessage[];
|
|
15
15
|
/** Pass as before_uid to get the next page of older messages. Omitted when results are exhausted. */
|
|
16
16
|
nextBeforeUid?: number;
|
|
17
|
+
/** Kept-vs-hidden accounting, present only when a client-side recipient filter ran. */
|
|
18
|
+
recipientFilter?: RecipientFilterStats;
|
|
17
19
|
}
|
|
18
20
|
export declare const MAX_LIMIT = 50;
|
|
19
21
|
/**
|
|
@@ -130,17 +132,46 @@ export declare function extractBodyFromFetch(msg: {
|
|
|
130
132
|
*/
|
|
131
133
|
export declare function testConnection(config: EmailConfig, password: string): Promise<string | null>;
|
|
132
134
|
/**
|
|
133
|
-
* Check whether an IMAP envelope's TO field contains the target
|
|
134
|
-
* Case-insensitive comparison. Strict: if the server didn't
|
|
135
|
-
* addresses, the message is excluded — we cannot confirm it was
|
|
136
|
-
* to
|
|
135
|
+
* Check whether an IMAP envelope's TO field contains any of the target
|
|
136
|
+
* addresses. Case-insensitive comparison. Strict: if the server didn't
|
|
137
|
+
* return TO addresses, the message is excluded — we cannot confirm it was
|
|
138
|
+
* addressed to any target. Only the TO field is inspected (not Cc).
|
|
139
|
+
*
|
|
140
|
+
* Accepts a single address or a set of acceptable addresses; a message
|
|
141
|
+
* matches when its TO contains at least one of them.
|
|
137
142
|
*/
|
|
138
143
|
export declare function matchesRecipient(envelope: {
|
|
139
144
|
to?: Array<{
|
|
140
145
|
name?: string;
|
|
141
146
|
address?: string;
|
|
142
147
|
}>;
|
|
143
|
-
},
|
|
148
|
+
}, target: string | string[]): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the implicit recipient-filter set for read/search when no explicit
|
|
151
|
+
* `to` is supplied. The mailbox always owns its IMAP login (`config.email`),
|
|
152
|
+
* and `config.agentAddress` is an additional alias to surface. When an alias
|
|
153
|
+
* is configured (truthy and different from the login) both addresses are
|
|
154
|
+
* returned, so replies that arrive at the login address are not hidden.
|
|
155
|
+
*
|
|
156
|
+
* When no distinct alias is configured, returns `undefined` — no recipient
|
|
157
|
+
* filter is applied at all, preserving the pre-existing behaviour for plain
|
|
158
|
+
* (non-alias) mailboxes.
|
|
159
|
+
*/
|
|
160
|
+
export declare function resolveDefaultRecipients(config: {
|
|
161
|
+
email: string;
|
|
162
|
+
agentAddress?: string;
|
|
163
|
+
}): string[] | undefined;
|
|
164
|
+
/** Kept-vs-hidden accounting for a client-side recipient filter pass. */
|
|
165
|
+
export interface RecipientFilterStats {
|
|
166
|
+
kept: number;
|
|
167
|
+
hidden: number;
|
|
168
|
+
recipients: string[];
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Render the operator-facing recipient-filter signal line. Returns null when
|
|
172
|
+
* nothing was hidden, so a fully-matched window emits no misleading line.
|
|
173
|
+
*/
|
|
174
|
+
export declare function formatRecipientFilterLine(stats: RecipientFilterStats | undefined): string | null;
|
|
144
175
|
/**
|
|
145
176
|
* Build IMAP SEARCH criteria for fetchRecent.
|
|
146
177
|
* Never includes TO — that filter is applied client-side to avoid
|
|
@@ -150,11 +181,11 @@ export declare function buildFetchRecentCriteria(options: {
|
|
|
150
181
|
sender?: string;
|
|
151
182
|
since?: Date;
|
|
152
183
|
before?: Date;
|
|
153
|
-
to?: string;
|
|
184
|
+
to?: string | string[];
|
|
154
185
|
limit?: number;
|
|
155
186
|
}): {
|
|
156
187
|
criteria: Record<string, unknown>;
|
|
157
|
-
|
|
188
|
+
clientSideRecipients: string[] | undefined;
|
|
158
189
|
fetchLimit: number;
|
|
159
190
|
};
|
|
160
191
|
/**
|
|
@@ -169,10 +200,10 @@ export declare function buildSearchCriteria(options: {
|
|
|
169
200
|
body?: string;
|
|
170
201
|
since?: Date;
|
|
171
202
|
before?: Date;
|
|
172
|
-
to?: string;
|
|
203
|
+
to?: string | string[];
|
|
173
204
|
}): {
|
|
174
205
|
criteria: Record<string, unknown>;
|
|
175
|
-
|
|
206
|
+
clientSideRecipients: string[] | undefined;
|
|
176
207
|
};
|
|
177
208
|
/**
|
|
178
209
|
* Fetch recent messages from a mailbox folder.
|
|
@@ -187,7 +218,7 @@ export declare function fetchRecent(config: EmailConfig, password: string, optio
|
|
|
187
218
|
subjectPattern?: string;
|
|
188
219
|
since?: Date;
|
|
189
220
|
before?: Date;
|
|
190
|
-
to?: string;
|
|
221
|
+
to?: string | string[];
|
|
191
222
|
folder?: "inbox" | "sent";
|
|
192
223
|
beforeUid?: number;
|
|
193
224
|
}): Promise<PaginatedMessages>;
|
|
@@ -201,7 +232,7 @@ export declare function searchMessages(config: EmailConfig, password: string, qu
|
|
|
201
232
|
since?: Date;
|
|
202
233
|
before?: Date;
|
|
203
234
|
limit?: number;
|
|
204
|
-
to?: string;
|
|
235
|
+
to?: string | string[];
|
|
205
236
|
folder?: "inbox" | "sent";
|
|
206
237
|
beforeUid?: number;
|
|
207
238
|
}): Promise<PaginatedMessages>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imap.d.ts","sourceRoot":"","sources":["../../src/lib/imap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAA2B,MAAM,UAAU,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAuBpD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,cAAc,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"imap.d.ts","sourceRoot":"","sources":["../../src/lib/imap.ts"],"names":[],"mappings":"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"}
|
|
@@ -320,16 +320,48 @@ const TO_OVERFETCH_MULTIPLIER = 3;
|
|
|
320
320
|
// ─── Default since floor for unbounded alias searches ───────────────
|
|
321
321
|
const DEFAULT_SINCE_DAYS = 7;
|
|
322
322
|
/**
|
|
323
|
-
* Check whether an IMAP envelope's TO field contains the target
|
|
324
|
-
* Case-insensitive comparison. Strict: if the server didn't
|
|
325
|
-
* addresses, the message is excluded — we cannot confirm it was
|
|
326
|
-
* to
|
|
323
|
+
* Check whether an IMAP envelope's TO field contains any of the target
|
|
324
|
+
* addresses. Case-insensitive comparison. Strict: if the server didn't
|
|
325
|
+
* return TO addresses, the message is excluded — we cannot confirm it was
|
|
326
|
+
* addressed to any target. Only the TO field is inspected (not Cc).
|
|
327
|
+
*
|
|
328
|
+
* Accepts a single address or a set of acceptable addresses; a message
|
|
329
|
+
* matches when its TO contains at least one of them.
|
|
327
330
|
*/
|
|
328
|
-
export function matchesRecipient(envelope,
|
|
331
|
+
export function matchesRecipient(envelope, target) {
|
|
329
332
|
if (!envelope.to || envelope.to.length === 0)
|
|
330
333
|
return false;
|
|
331
|
-
const
|
|
332
|
-
return envelope.to.some((addr) =>
|
|
334
|
+
const targets = (Array.isArray(target) ? target : [target]).map((t) => t.toLowerCase());
|
|
335
|
+
return envelope.to.some((addr) => {
|
|
336
|
+
const a = addr.address?.toLowerCase();
|
|
337
|
+
return a !== undefined && targets.includes(a);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve the implicit recipient-filter set for read/search when no explicit
|
|
342
|
+
* `to` is supplied. The mailbox always owns its IMAP login (`config.email`),
|
|
343
|
+
* and `config.agentAddress` is an additional alias to surface. When an alias
|
|
344
|
+
* is configured (truthy and different from the login) both addresses are
|
|
345
|
+
* returned, so replies that arrive at the login address are not hidden.
|
|
346
|
+
*
|
|
347
|
+
* When no distinct alias is configured, returns `undefined` — no recipient
|
|
348
|
+
* filter is applied at all, preserving the pre-existing behaviour for plain
|
|
349
|
+
* (non-alias) mailboxes.
|
|
350
|
+
*/
|
|
351
|
+
export function resolveDefaultRecipients(config) {
|
|
352
|
+
if (config.agentAddress && config.agentAddress !== config.email) {
|
|
353
|
+
return [config.email, config.agentAddress];
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Render the operator-facing recipient-filter signal line. Returns null when
|
|
359
|
+
* nothing was hidden, so a fully-matched window emits no misleading line.
|
|
360
|
+
*/
|
|
361
|
+
export function formatRecipientFilterLine(stats) {
|
|
362
|
+
if (!stats || stats.hidden <= 0)
|
|
363
|
+
return null;
|
|
364
|
+
return `Recipient filter: kept ${stats.kept}, hid ${stats.hidden} (to ∈ {${stats.recipients.join(", ")}}); pass to="…" to change.`;
|
|
333
365
|
}
|
|
334
366
|
/**
|
|
335
367
|
* Build IMAP SEARCH criteria for fetchRecent.
|
|
@@ -338,6 +370,7 @@ export function matchesRecipient(envelope, targetAddress) {
|
|
|
338
370
|
*/
|
|
339
371
|
export function buildFetchRecentCriteria(options) {
|
|
340
372
|
const { sender, since, before, to, limit = 20 } = options;
|
|
373
|
+
const recipients = to ? (Array.isArray(to) ? to : [to]) : undefined;
|
|
341
374
|
const criteria = {};
|
|
342
375
|
if (sender)
|
|
343
376
|
criteria.from = sender;
|
|
@@ -351,8 +384,8 @@ export function buildFetchRecentCriteria(options) {
|
|
|
351
384
|
}
|
|
352
385
|
return {
|
|
353
386
|
criteria,
|
|
354
|
-
|
|
355
|
-
fetchLimit:
|
|
387
|
+
clientSideRecipients: recipients,
|
|
388
|
+
fetchLimit: recipients ? limit * TO_OVERFETCH_MULTIPLIER : limit,
|
|
356
389
|
};
|
|
357
390
|
}
|
|
358
391
|
/**
|
|
@@ -363,6 +396,7 @@ export function buildFetchRecentCriteria(options) {
|
|
|
363
396
|
*/
|
|
364
397
|
export function buildSearchCriteria(options) {
|
|
365
398
|
const { sender, subject, body, since, before, to } = options;
|
|
399
|
+
const recipients = to ? (Array.isArray(to) ? to : [to]) : undefined;
|
|
366
400
|
const criteria = {};
|
|
367
401
|
if (sender)
|
|
368
402
|
criteria.from = sender;
|
|
@@ -375,9 +409,9 @@ export function buildSearchCriteria(options) {
|
|
|
375
409
|
if (before)
|
|
376
410
|
criteria.before = before;
|
|
377
411
|
// TO is intentionally excluded — filtered client-side after fetch
|
|
378
|
-
// When
|
|
379
|
-
// inject a date floor to prevent unbounded full-mailbox scans
|
|
380
|
-
if (
|
|
412
|
+
// When recipient filtering was the only effective filter (nothing else
|
|
413
|
+
// bounds the search), inject a date floor to prevent unbounded full-mailbox scans
|
|
414
|
+
if (recipients && Object.keys(criteria).length === 0) {
|
|
381
415
|
const sinceFloor = new Date();
|
|
382
416
|
sinceFloor.setDate(sinceFloor.getDate() - DEFAULT_SINCE_DAYS);
|
|
383
417
|
criteria.since = sinceFloor;
|
|
@@ -387,7 +421,7 @@ export function buildSearchCriteria(options) {
|
|
|
387
421
|
}
|
|
388
422
|
return {
|
|
389
423
|
criteria,
|
|
390
|
-
|
|
424
|
+
clientSideRecipients: recipients,
|
|
391
425
|
};
|
|
392
426
|
}
|
|
393
427
|
/**
|
|
@@ -408,11 +442,11 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
408
442
|
}
|
|
409
443
|
catch { /* invalid — ignore */ }
|
|
410
444
|
}
|
|
411
|
-
const { criteria,
|
|
445
|
+
const { criteria, clientSideRecipients, fetchLimit } = buildFetchRecentCriteria({
|
|
412
446
|
sender, since, before, to, limit,
|
|
413
447
|
});
|
|
414
448
|
const t0 = Date.now();
|
|
415
|
-
diag("fetchRecent", "start", `folder=${folder} criteria=${JSON.stringify(criteria)} fetchLimit=${fetchLimit}
|
|
449
|
+
diag("fetchRecent", "start", `folder=${folder} criteria=${JSON.stringify(criteria)} fetchLimit=${fetchLimit} clientSideRecipients=${clientSideRecipients ? `[${clientSideRecipients.join(",")}]` : "none"} beforeUid=${beforeUid ?? "none"}`);
|
|
416
450
|
try {
|
|
417
451
|
await client.connect();
|
|
418
452
|
diag("fetchRecent", "connected", `${Date.now() - t0}ms`);
|
|
@@ -441,6 +475,8 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
441
475
|
const uidList = recentUids.map(String).join(",");
|
|
442
476
|
diag("fetchRecent", "fetching envelopes", `UIDs=[${uidList}]`);
|
|
443
477
|
let envelopeCount = 0;
|
|
478
|
+
let recipientKept = 0;
|
|
479
|
+
let recipientHidden = 0;
|
|
444
480
|
for await (const msg of client.fetch(uidList, {
|
|
445
481
|
envelope: true,
|
|
446
482
|
uid: true,
|
|
@@ -449,9 +485,14 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
449
485
|
}, { uid: true })) {
|
|
450
486
|
envelopeCount++;
|
|
451
487
|
diag("fetchRecent", `envelope #${envelopeCount}`, `uid=${msg.uid} from=${msg.envelope?.from?.[0]?.address ?? "?"} at ${Date.now() - t2}ms`);
|
|
452
|
-
// Client-side
|
|
453
|
-
if (
|
|
454
|
-
|
|
488
|
+
// Client-side recipient filter (replaces IMAP SEARCH TO), with kept-vs-hidden accounting
|
|
489
|
+
if (clientSideRecipients) {
|
|
490
|
+
if (!matchesRecipient(msg.envelope ?? {}, clientSideRecipients)) {
|
|
491
|
+
recipientHidden++;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
recipientKept++;
|
|
495
|
+
}
|
|
455
496
|
const fromAddr = msg.envelope?.from?.[0]?.address ?? "(unknown)";
|
|
456
497
|
const subject = msg.envelope?.subject ?? "(no subject)";
|
|
457
498
|
const date = msg.envelope?.date?.toISOString() ?? "(unknown date)";
|
|
@@ -459,30 +500,39 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
459
500
|
const inReplyTo = msg.envelope?.inReplyTo ?? null;
|
|
460
501
|
if (subjectRegex && !subjectRegex.test(subject))
|
|
461
502
|
continue;
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
503
|
+
// Collect up to `limit` messages for display, but keep iterating past
|
|
504
|
+
// that point so the recipient kept/hidden accounting covers the whole
|
|
505
|
+
// fetched window — the envelopes are already fetched, so the extra
|
|
506
|
+
// iterations cost nothing on the wire and we skip body extraction.
|
|
507
|
+
if (messages.length < limit) {
|
|
508
|
+
const bodyPreview = extractBodyFromFetch(msg);
|
|
509
|
+
diag("fetchRecent", `body #${envelopeCount}`, `uid=${msg.uid} preview=${bodyPreview.length}chars type=${msg.bodyStructure?.type ?? "?"}`);
|
|
510
|
+
const attachments = walkAttachmentMetadata(msg.bodyStructure);
|
|
511
|
+
messages.push({
|
|
512
|
+
uid: msg.uid,
|
|
513
|
+
messageId,
|
|
514
|
+
inReplyTo,
|
|
515
|
+
from: fromAddr,
|
|
516
|
+
subject,
|
|
517
|
+
date,
|
|
518
|
+
bodyPreview,
|
|
519
|
+
attachments,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
478
522
|
}
|
|
479
523
|
diag("fetchRecent", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
524
|
+
if (clientSideRecipients) {
|
|
525
|
+
diag("fetchRecent", "filter applied", `kept=${recipientKept} hidden=${recipientHidden} recipients=[${clientSideRecipients.join(",")}]`);
|
|
526
|
+
}
|
|
480
527
|
// Return newest first
|
|
481
528
|
messages.reverse();
|
|
482
529
|
// Calculate pagination cursor: use the oldest UID in the *fetched* set
|
|
483
530
|
// (not the filtered set) to avoid skipping messages between filtered-out
|
|
484
|
-
// and kept messages when client-side
|
|
531
|
+
// and kept messages when client-side recipient/subject filtering is active
|
|
485
532
|
const result = { messages };
|
|
533
|
+
if (clientSideRecipients) {
|
|
534
|
+
result.recipientFilter = { kept: recipientKept, hidden: recipientHidden, recipients: clientSideRecipients };
|
|
535
|
+
}
|
|
486
536
|
if (totalAfterCursor > fetchLimit && recentUids.length > 0) {
|
|
487
537
|
const oldestFetchedUid = recentUids[0]; // UIDs are sorted ascending
|
|
488
538
|
result.nextBeforeUid = oldestFetchedUid;
|
|
@@ -511,13 +561,13 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
511
561
|
export async function searchMessages(config, password, query) {
|
|
512
562
|
const { sender, subject, body, since, before, limit = 20, to, folder = "inbox", beforeUid } = query;
|
|
513
563
|
const client = createClient(config, password);
|
|
514
|
-
|
|
515
|
-
const fetchLimit = to ? limit * TO_OVERFETCH_MULTIPLIER : limit;
|
|
516
|
-
const { criteria, clientSideTo } = buildSearchCriteria({
|
|
564
|
+
const { criteria, clientSideRecipients } = buildSearchCriteria({
|
|
517
565
|
sender, subject, body, since, before, to,
|
|
518
566
|
});
|
|
567
|
+
// When client-side recipient filtering is active, overfetch to compensate for non-matching messages
|
|
568
|
+
const fetchLimit = clientSideRecipients ? limit * TO_OVERFETCH_MULTIPLIER : limit;
|
|
519
569
|
const t0 = Date.now();
|
|
520
|
-
diag("searchMessages", "start", `folder=${folder} criteria=${JSON.stringify(criteria)}
|
|
570
|
+
diag("searchMessages", "start", `folder=${folder} criteria=${JSON.stringify(criteria)} clientSideRecipients=${clientSideRecipients ? `[${clientSideRecipients.join(",")}]` : "none"} beforeUid=${beforeUid ?? "none"}`);
|
|
521
571
|
try {
|
|
522
572
|
await client.connect();
|
|
523
573
|
diag("searchMessages", "connected", `${Date.now() - t0}ms`);
|
|
@@ -542,6 +592,8 @@ export async function searchMessages(config, password, query) {
|
|
|
542
592
|
const messages = [];
|
|
543
593
|
const t2 = Date.now();
|
|
544
594
|
let envelopeCount = 0;
|
|
595
|
+
let recipientKept = 0;
|
|
596
|
+
let recipientHidden = 0;
|
|
545
597
|
for await (const msg of client.fetch(recentUids.map(String).join(","), {
|
|
546
598
|
envelope: true,
|
|
547
599
|
uid: true,
|
|
@@ -549,33 +601,47 @@ export async function searchMessages(config, password, query) {
|
|
|
549
601
|
bodyParts: [{ key: "1", start: 0, maxLength: BODY_MAX_BYTES }],
|
|
550
602
|
}, { uid: true })) {
|
|
551
603
|
envelopeCount++;
|
|
552
|
-
// Client-side
|
|
553
|
-
if (
|
|
554
|
-
|
|
604
|
+
// Client-side recipient filter (replaces IMAP SEARCH TO), with kept-vs-hidden accounting
|
|
605
|
+
if (clientSideRecipients) {
|
|
606
|
+
if (!matchesRecipient(msg.envelope ?? {}, clientSideRecipients)) {
|
|
607
|
+
recipientHidden++;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
recipientKept++;
|
|
611
|
+
}
|
|
555
612
|
const fromAddr = msg.envelope?.from?.[0]?.address ?? "(unknown)";
|
|
556
613
|
const subj = msg.envelope?.subject ?? "(no subject)";
|
|
557
614
|
const date = msg.envelope?.date?.toISOString() ?? "(unknown date)";
|
|
558
615
|
const messageId = msg.envelope?.messageId ?? null;
|
|
559
616
|
const inReplyTo = msg.envelope?.inReplyTo ?? null;
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
617
|
+
// Collect up to `limit` messages for display, but keep iterating past
|
|
618
|
+
// that point so the recipient kept/hidden accounting covers the whole
|
|
619
|
+
// fetched window — the envelopes are already fetched, so the extra
|
|
620
|
+
// iterations cost nothing on the wire and we skip body extraction.
|
|
621
|
+
if (messages.length < limit) {
|
|
622
|
+
const bodyPreview = extractBodyFromFetch(msg);
|
|
623
|
+
const attachments = walkAttachmentMetadata(msg.bodyStructure);
|
|
624
|
+
messages.push({
|
|
625
|
+
uid: msg.uid,
|
|
626
|
+
messageId,
|
|
627
|
+
inReplyTo,
|
|
628
|
+
from: fromAddr,
|
|
629
|
+
subject: subj,
|
|
630
|
+
date,
|
|
631
|
+
bodyPreview,
|
|
632
|
+
attachments,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
575
635
|
}
|
|
576
636
|
diag("searchMessages", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
637
|
+
if (clientSideRecipients) {
|
|
638
|
+
diag("searchMessages", "filter applied", `kept=${recipientKept} hidden=${recipientHidden} recipients=[${clientSideRecipients.join(",")}]`);
|
|
639
|
+
}
|
|
577
640
|
messages.reverse();
|
|
578
641
|
const result = { messages };
|
|
642
|
+
if (clientSideRecipients) {
|
|
643
|
+
result.recipientFilter = { kept: recipientKept, hidden: recipientHidden, recipients: clientSideRecipients };
|
|
644
|
+
}
|
|
579
645
|
if (totalAfterCursor > fetchLimit && recentUids.length > 0) {
|
|
580
646
|
const oldestFetchedUid = recentUids[0]; // UIDs are sorted ascending
|
|
581
647
|
result.nextBeforeUid = oldestFetchedUid;
|