dsh-aimail 0.1.0-rc.9 → 0.1.7
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/README.md +60 -0
- package/cordis.patch.yml +27 -15
- package/lib/inbound.d.ts +13 -0
- package/lib/inbound.js +179 -0
- package/lib/index.d.ts +12 -5
- package/lib/index.js +12 -5
- package/lib/mail-service.d.ts +39 -0
- package/lib/mail-service.js +139 -0
- package/lib/tools.d.ts +16 -0
- package/lib/tools.js +76 -0
- package/package.json +38 -13
- package/resources/board/role_prompt_en/common.md +33 -0
- package/resources/board/role_prompt_en/orchestrator.md +43 -0
- package/resources/board/role_prompt_en/role_calibrator.md +27 -0
- package/resources/board/role_prompt_en/verifier.md +47 -0
- package/resources/board/role_prompt_en/whoami.md +24 -0
- package/resources/board/role_prompt_en/worker.md +48 -0
- package/resources/board/role_prompt_zh/common.md +33 -0
- package/resources/board/role_prompt_zh/orchestrator.md +43 -0
- package/resources/board/role_prompt_zh/role_calibrator.md +27 -0
- package/resources/board/role_prompt_zh/verifier.md +47 -0
- package/resources/board/role_prompt_zh/whoami.md +24 -0
- package/resources/board/role_prompt_zh/worker.md +48 -0
- package/resources/board/role_soul_en/Orchestrator.md +24 -0
- package/resources/board/role_soul_en/Owner.md +23 -0
- package/resources/board/role_soul_en/Verifier.md +23 -0
- package/resources/board/role_soul_en/Worker.md +23 -0
- package/resources/board/role_soul_zh/Orchestrator.md +24 -0
- package/resources/board/role_soul_zh/Owner.md +25 -0
- package/resources/board/role_soul_zh/Verifier.md +23 -0
- package/resources/board/role_soul_zh/Worker.md +23 -0
- package/resources/skills/DESCRIPTION.md +3 -0
- package/resources/skills/SKILL.md +231 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# dsh-aimail
|
|
2
|
+
|
|
3
|
+
AIMail plugin for dsh (deepseek-harness). It gives a dsh agent a mailbox on
|
|
4
|
+
AIMail: inbound email is delivered into the agent's session, and the agent
|
|
5
|
+
can send mail, manage contacts, keep thread notes, and work on A2A boards
|
|
6
|
+
through 13 plain tools.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Prerequisites:
|
|
11
|
+
|
|
12
|
+
- dsh (deepseek-harness) with the web profile
|
|
13
|
+
- an AIMail binding for the dsh session (`aimail install` from the
|
|
14
|
+
aimail repo sets up `aimail_gateway.json` and per-address
|
|
15
|
+
`agentmail.json`)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# install (idempotent)
|
|
19
|
+
dsh plugin --profile web add dsh-aimail
|
|
20
|
+
|
|
21
|
+
# uninstall (idempotent)
|
|
22
|
+
dsh plugin --profile web remove dsh-aimail
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What it does
|
|
26
|
+
|
|
27
|
+
**Tools** — the same 13 bare-name tools as every other adapter:
|
|
28
|
+
`send_mail`, `manage_contacts`, `contact_profile`, `set_contact_profile`,
|
|
29
|
+
`email_summary`, `set_email_summary`, `search_mail`,
|
|
30
|
+
`board_status`, `board_task_list`, `board_task_show`, `board_heartbeat`,
|
|
31
|
+
`board_members`, `set_public_whoami`.
|
|
32
|
+
Identity comes from the session id resolved through `@aimail/mail`
|
|
33
|
+
(`agentmail.json` is the sole identity source); unbound sessions fail loud.
|
|
34
|
+
|
|
35
|
+
**Inbound mail** — a profile-scoped HTTP endpoint (`POST /aimail/inbound`,
|
|
36
|
+
default port `9099`, override with `AIMAIL_INBOUND_PORT`). Each bridge
|
|
37
|
+
delivery is HMAC verified against the per-agent secret, enriched by the
|
|
38
|
+
shared preprocess chain, and
|
|
39
|
+
delivered to the agent's session (live followup, cold resume, or a fresh
|
|
40
|
+
session bound for the turn).
|
|
41
|
+
|
|
42
|
+
**Persona** — mounts an email-agent persona that teaches the model the mail
|
|
43
|
+
workflow (reply-all semantics, tool selection, thread continuity).
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Other adapters
|
|
47
|
+
|
|
48
|
+
The same tool surface and inbound contract, bound to other agent platforms:
|
|
49
|
+
|
|
50
|
+
- [openclaw-aimail](https://www.npmjs.com/package/openclaw-aimail) — AIMail plugin for OpenClaw — definePluginEntry: 13 tools, in-gateway HTTP route, register/status commands.
|
|
51
|
+
- [pi-aimail](https://www.npmjs.com/package/pi-aimail) — AIMail extension for pi (earendil-works/pi) — registerTool tools + local inbound listener bridged via sendUserMessage.
|
|
52
|
+
|
|
53
|
+
## Related repositories
|
|
54
|
+
|
|
55
|
+
- [metercai/aimail](https://github.com/metercai/aimail) — the AIMail monorepo:
|
|
56
|
+
CLI (`cli/`), Python SDK (`pysdk/`), TypeScript SDK (`tssdk/`), bridge
|
|
57
|
+
distributions.
|
|
58
|
+
- [metercai/aimail-gateway](https://github.com/metercai/aimail-gateway) — the
|
|
59
|
+
AIMail gateway: SMTP/HTTP mail service, address & activation APIs, and the
|
|
60
|
+
board endpoints the tools talk to.
|
package/cordis.patch.yml
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
1
|
+
# dsh-aimail bundle layer — mounted via `dsh plugin --profile web add dsh-aimail`.
|
|
2
|
+
# Self-mounting: all entries resolve to subpaths of this very package
|
|
3
|
+
# (package.json "exports"), so the bundle needs no sibling @aimail/* packages.
|
|
4
|
+
#
|
|
5
|
+
# Patch dialect (dsh-app-boot): rows that exist in an earlier layer (dsh-base)
|
|
6
|
+
# are targeted by id and their whole `config` is replaced; rows new to this
|
|
7
|
+
# bundle are added under `insert:`. Targeting an absent row only warns.
|
|
3
8
|
|
|
4
9
|
# ── identity ────────────────────────────────────────────────────────────────
|
|
10
|
+
# Base owns the `system-prompt` row (config.persona defaults to ''). We replace
|
|
11
|
+
# it to set this deployment's persona. (dsh-persona is a scope-only row for
|
|
12
|
+
# agent presets — mounting it globally fails loud, so it is not used here.)
|
|
5
13
|
|
|
6
|
-
- id:
|
|
7
|
-
name: '@deepseek-ai/dsh-persona'
|
|
14
|
+
- id: system-prompt
|
|
8
15
|
config:
|
|
9
|
-
|
|
10
|
-
You are an email-capable agent on
|
|
16
|
+
persona: >-
|
|
17
|
+
You are an email-capable agent on AIMail. Use send_mail to reply to
|
|
11
18
|
inbound mail; manage_contacts for your whitelist; contact_profile /
|
|
12
19
|
set_contact_profile for contact context; email_summary /
|
|
13
20
|
set_email_summary for thread notes; board_* tools for A2A board work.
|
|
14
21
|
|
|
15
|
-
# ──
|
|
22
|
+
# ── rows new to this bundle ─────────────────────────────────────────────────
|
|
16
23
|
|
|
17
|
-
-
|
|
18
|
-
|
|
24
|
+
- insert:
|
|
25
|
+
# AIMail host service (ctx.mail)
|
|
26
|
+
- id: mail
|
|
27
|
+
name: 'dsh-aimail/mail-service'
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
# AIMail inbound endpoint (node:http + session delivery). Port is fixed:
|
|
30
|
+
# single consumer (this profile); the local bridge routes here.
|
|
31
|
+
- id: mail-inbound
|
|
32
|
+
name: 'dsh-aimail/inbound'
|
|
33
|
+
config:
|
|
34
|
+
port: 9099
|
|
22
35
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
name: '@meterwei/tool-mail'
|
|
36
|
+
# AIMail tools (12 bare names, semantics from @aimail/mail-core)
|
|
37
|
+
- id: tool-mail
|
|
38
|
+
name: 'dsh-aimail/tools'
|
package/lib/inbound.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
export declare const name = "mail-inbound";
|
|
3
|
+
export declare const inject: string[];
|
|
4
|
+
export interface Config {
|
|
5
|
+
/** Listen host (default 127.0.0.1). */
|
|
6
|
+
host?: string;
|
|
7
|
+
/** Listen port (default AIMAIL_INBOUND_PORT or 9099). */
|
|
8
|
+
port?: number;
|
|
9
|
+
/** Deliver path (default /aimail/inbound). */
|
|
10
|
+
path?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function apply(ctx: Context, config?: Config): () => void;
|
|
13
|
+
//# sourceMappingURL=inbound.d.ts.map
|
package/lib/inbound.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-aimail inbound — AIMail inbound endpoint for this profile.
|
|
3
|
+
*
|
|
4
|
+
* node:http listener (headless-friendly). Receives bridge-forwarded raw
|
|
5
|
+
* webhook bodies at POST {path} (default /aimail/inbound):
|
|
6
|
+
* recipient routing (resolveByRecipient: exact → persona-strip fallback)
|
|
7
|
+
* → HMAC verify (X-Webhook-Signature vs webhook_secret from agentmail.json)
|
|
8
|
+
* → TS preprocess chain (mail-core, DSH-PREPROCESS-CONTRACT.md)
|
|
9
|
+
* → ping/pong intercept (three-stage logs, swallowed)
|
|
10
|
+
* → un-intercepted: deliver to the bound dsh session (live followup, cold
|
|
11
|
+
* resume) or spawn a fresh disposable session when unbound — context
|
|
12
|
+
* continuity is aimail's (local meta threading), not the session's.
|
|
13
|
+
* 200 ack on delivery; 503 on session-create failure (bridge retries).
|
|
14
|
+
*/
|
|
15
|
+
import { createServer } from 'node:http';
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
18
|
+
import { processInboundMail, verifySignature, routeAddressFromHeaders, updateAgentConfig, loadAgentConfig, saveAgentConfig } from '@aimail/mail-core';
|
|
19
|
+
export const name = 'mail-inbound';
|
|
20
|
+
export const inject = ['mail', 'agents'];
|
|
21
|
+
function writeJson(res, code, body) {
|
|
22
|
+
const text = JSON.stringify(body);
|
|
23
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
24
|
+
res.end(text);
|
|
25
|
+
}
|
|
26
|
+
function readBody(req) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
req.on('data', (c) => chunks.push(c));
|
|
30
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
31
|
+
req.on('error', reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function apply(ctx, config = {}) {
|
|
35
|
+
const mail = ctx.get('mail');
|
|
36
|
+
if (mail === undefined) {
|
|
37
|
+
throw new Error('mail-inbound requires the mail service: mount dsh-aimail/mail-service first');
|
|
38
|
+
}
|
|
39
|
+
const host = config.host ?? '127.0.0.1';
|
|
40
|
+
const port = config.port ?? Number(process.env.AIMAIL_INBOUND_PORT ?? 9099);
|
|
41
|
+
const deliverPath = config.path ?? '/aimail/inbound';
|
|
42
|
+
const server = createServer(async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
if (req.method !== 'POST' || (req.url ?? '').split('?')[0] !== deliverPath) {
|
|
45
|
+
writeJson(res, 404, { status: 'not_found' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const rawBody = await readBody(req);
|
|
49
|
+
let payload;
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(rawBody.toString('utf-8'));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
writeJson(res, 400, { status: 'bad_json' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Inbound routing (Q3 — mirror Python bridge routing): the per-delivery
|
|
58
|
+
// target is authoritative. The bridge injects X-AIMail-Email (legacy
|
|
59
|
+
// X-Amail-Email fallback) on each single-delivery POST; payload.to is
|
|
60
|
+
// the FILTERED full list (external recipients first), so to[0] is often
|
|
61
|
+
// an external address. Use the header when present; only iterate toRaw
|
|
62
|
+
// when the header is absent (batch deliveries carry no such header).
|
|
63
|
+
const headers = {
|
|
64
|
+
...req.headers,
|
|
65
|
+
...(payload.headers ?? {}),
|
|
66
|
+
};
|
|
67
|
+
const routeAddr = routeAddressFromHeaders(headers);
|
|
68
|
+
const toRaw = Array.isArray(payload.to) ? payload.to : typeof payload.to === 'string' ? [payload.to] : [];
|
|
69
|
+
const routeCandidates = routeAddr ? [routeAddr] : toRaw;
|
|
70
|
+
let cfg;
|
|
71
|
+
let agentAddr = '';
|
|
72
|
+
for (const t of routeCandidates) {
|
|
73
|
+
const addr = String(t).trim();
|
|
74
|
+
if (!addr.includes('@'))
|
|
75
|
+
continue;
|
|
76
|
+
const c = await mail.resolveByRecipient(addr);
|
|
77
|
+
if (c) {
|
|
78
|
+
cfg = c;
|
|
79
|
+
agentAddr = addr;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!cfg) {
|
|
84
|
+
writeJson(res, 200, { status: 'no_agent', detail: `no binding for ${routeAddr || toRaw.join(',')}` });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// HMAC verify (per-address webhook_secret)
|
|
88
|
+
const sig = req.headers['x-webhook-signature'] ?? '';
|
|
89
|
+
if (!verifySignature(rawBody, sig, cfg.webhook_secret ?? '')) {
|
|
90
|
+
writeJson(res, 401, { status: 'bad_signature' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// TS preprocess chain (13 steps) + ping/pong intercept
|
|
94
|
+
const result = await processInboundMail(payload, headers, {
|
|
95
|
+
systemId: cfg.system_id,
|
|
96
|
+
email: cfg.email,
|
|
97
|
+
});
|
|
98
|
+
if (result === null) {
|
|
99
|
+
writeJson(res, 200, { status: 'intercepted' });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Deliver to a dsh session:
|
|
103
|
+
// - cfg.session_id set + live → followup that session (UI continuity)
|
|
104
|
+
// - cfg.session_id set + cold → resume it, else fall through
|
|
105
|
+
// - unbound (or resume failed) → spawn a FRESH session. Context
|
|
106
|
+
// continuity is aimail's job (local meta threading + email_summary),
|
|
107
|
+
// not the session's — per the deployment decision each inbound email
|
|
108
|
+
// gets its own disposable session.
|
|
109
|
+
const agents = ctx.get('agents');
|
|
110
|
+
if (agents === undefined) {
|
|
111
|
+
writeJson(res, 200, { status: 'no_agents_service', detail: 'dsh-agent not mounted' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Model route: the deployment's default selection (base bundle's
|
|
115
|
+
// `agent-default-model` row, e.g. deepseek-official/deepseek-v4-flash)
|
|
116
|
+
// — same source the web UI's api-proxy uses for agents.create().
|
|
117
|
+
// Without it the turn dies with "no provider/model".
|
|
118
|
+
const agentOptions = ctx.get('agentDefaultModel')
|
|
119
|
+
?.currentSelection();
|
|
120
|
+
const boundId = cfg.session_id ?? '';
|
|
121
|
+
const message = createUserMessage({
|
|
122
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, to: agentAddr }) }],
|
|
123
|
+
source: { kind: 'user' },
|
|
124
|
+
});
|
|
125
|
+
const live = boundId ? agents.get(boundId) : undefined;
|
|
126
|
+
if (live) {
|
|
127
|
+
live.followup(message);
|
|
128
|
+
writeJson(res, 200, { status: 'delivered', detail: 'followup queued' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (boundId) {
|
|
132
|
+
try {
|
|
133
|
+
const handle = await agents.resume({ resumeSessionId: boundId, agentOptions });
|
|
134
|
+
handle.agent.followup(message);
|
|
135
|
+
writeJson(res, 200, { status: 'resumed', detail: 'cold session resumed + followup queued' });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// resume failed (no persistence, stale id) — fall through to a fresh session
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Fresh disposable session for this email.
|
|
143
|
+
const sessionId = randomUUID();
|
|
144
|
+
try {
|
|
145
|
+
// Bind this session into the agent's config so the mail tools can
|
|
146
|
+
// resolve credentials (resolveBySessionId matches agentmail.json's
|
|
147
|
+
// session_id). Unbind again once the turn settles — but only if the
|
|
148
|
+
// binding is still OURS (a concurrent email may have re-bound).
|
|
149
|
+
await updateAgentConfig(cfg.system_id, cfg.email, { session_id: sessionId });
|
|
150
|
+
const handle = await agents.create({ sessionId, meta: { cwd: process.cwd() }, agentOptions });
|
|
151
|
+
handle.agent.followup(message);
|
|
152
|
+
void handle.agent.whenIdle()
|
|
153
|
+
.then(async () => {
|
|
154
|
+
const cur = await loadAgentConfig(cfg.system_id, cfg.email);
|
|
155
|
+
if (cur && cur.session_id === sessionId) {
|
|
156
|
+
const { session_id: _drop, ...rest } = cur;
|
|
157
|
+
await saveAgentConfig(rest, cfg.system_id);
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.then(() => handle.dispose())
|
|
161
|
+
.catch(() => { });
|
|
162
|
+
writeJson(res, 200, { status: 'delivered', detail: `fresh session ${sessionId}` });
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
// 503 (not 2xx) so the bridge does NOT ack and will retry; a 200 here
|
|
166
|
+
// would silently swallow the email.
|
|
167
|
+
writeJson(res, 503, { status: 'session_create_failed', detail: e instanceof Error ? e.message : String(e) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
writeJson(res, 500, { status: 'error', detail: e instanceof Error ? e.message : String(e) });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
server.listen(port, host);
|
|
175
|
+
return () => {
|
|
176
|
+
server.close();
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=inbound.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* dsh
|
|
2
|
+
* dsh-aimail — AIMail plugin for dsh (single self-contained bundle).
|
|
3
|
+
*
|
|
4
|
+
* The bundle patch (cordis.patch.yml) self-mounts three subpath entries:
|
|
5
|
+
* dsh-aimail/mail-service → ctx.mail (config resolution binding)
|
|
6
|
+
* dsh-aimail/tools → 12 AIMail bare tools
|
|
7
|
+
* dsh-aimail/inbound → node:http inbound endpoint + delivery
|
|
8
|
+
*
|
|
9
|
+
* Installed via: dsh plugin --profile web add dsh-aimail
|
|
10
|
+
* This entry re-exports the shared core API for programmatic use.
|
|
6
11
|
*/
|
|
7
|
-
export * from '@
|
|
12
|
+
export * from '@aimail/mail-core';
|
|
13
|
+
export * from '@aimail/mail';
|
|
14
|
+
export { type MailService } from './mail-service.js';
|
|
8
15
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* dsh
|
|
2
|
+
* dsh-aimail — AIMail plugin for dsh (single self-contained bundle).
|
|
3
|
+
*
|
|
4
|
+
* The bundle patch (cordis.patch.yml) self-mounts three subpath entries:
|
|
5
|
+
* dsh-aimail/mail-service → ctx.mail (config resolution binding)
|
|
6
|
+
* dsh-aimail/tools → 12 AIMail bare tools
|
|
7
|
+
* dsh-aimail/inbound → node:http inbound endpoint + delivery
|
|
8
|
+
*
|
|
9
|
+
* Installed via: dsh plugin --profile web add dsh-aimail
|
|
10
|
+
* This entry re-exports the shared core API for programmatic use.
|
|
6
11
|
*/
|
|
7
|
-
export * from '@
|
|
12
|
+
export * from '@aimail/mail-core';
|
|
13
|
+
export * from '@aimail/mail';
|
|
14
|
+
export {} from './mail-service.js';
|
|
8
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-aimail mail service — provides ctx.mail to this bundle's tools/inbound
|
|
3
|
+
* entries. Thin dsh binding over the platform-neutral @aimail/mail resolvers
|
|
4
|
+
* (sessionId/email/recipient → agentmail.json → AgentConfig).
|
|
5
|
+
*
|
|
6
|
+
* Identity = agentmail.json only; the AIMAIL_SYSTEM_ID env narrows scope.
|
|
7
|
+
*
|
|
8
|
+
* Auto-bind (SDK auto-binding): a session resolution that finds no binding
|
|
9
|
+
* triggers one auto-bind attempt per (system, session) — register chain +
|
|
10
|
+
* agentmail.json + bridge route via mail-core autoBind, deriving the address
|
|
11
|
+
* `agent-<session8>.<system_name>@domain` — and retries the resolution. The
|
|
12
|
+
* once-guard means a failed attempt (gateway unreachable, no system config)
|
|
13
|
+
* never hammers the network on every tool call; the original unbound error
|
|
14
|
+
* is rethrown for the caller to handle.
|
|
15
|
+
*/
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
17
|
+
import { type MailToolCtx } from '@aimail/mail';
|
|
18
|
+
import { type AgentConfig } from '@aimail/mail-core';
|
|
19
|
+
export declare const name = "mail";
|
|
20
|
+
export declare const inject: never[];
|
|
21
|
+
/** The ctx.mail service surface (consumed by the tools + inbound entries). */
|
|
22
|
+
export interface MailService {
|
|
23
|
+
/** Optional explicit system scope (AIMAIL_SYSTEM_ID); empty = scan all. */
|
|
24
|
+
readonly systemId: string;
|
|
25
|
+
/** Resolve config for a dsh session id (uuid). Throws when unbound. */
|
|
26
|
+
resolveConfig(sessionId: string): Promise<AgentConfig>;
|
|
27
|
+
/** Resolve a tool context for a session id. Throws when unbound. */
|
|
28
|
+
resolveCtx(sessionId: string): Promise<MailToolCtx>;
|
|
29
|
+
/** Resolve config by the agent's registered email. Throws when unbound. */
|
|
30
|
+
resolveByEmail(email: string): Promise<AgentConfig>;
|
|
31
|
+
/** Inbound recipient routing: exact match → persona-strip fallback. */
|
|
32
|
+
resolveByRecipient(email: string): Promise<AgentConfig | undefined>;
|
|
33
|
+
}
|
|
34
|
+
/** Reset the once-guard (test hook / after an operator fixed the env). */
|
|
35
|
+
export declare function resetAutoBindOnce(): void;
|
|
36
|
+
export declare function apply(ctx: Context, config?: {
|
|
37
|
+
systemId?: string;
|
|
38
|
+
}): void;
|
|
39
|
+
//# sourceMappingURL=mail-service.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { resolveByRecipient, resolveByEmail, resolveBySessionId, } from '@aimail/mail';
|
|
2
|
+
import { autoBind, emailForAgent, ensureSystem, hasAnySystem, listSystemDirs, readSystemConfig, releaseAllSystems, } from '@aimail/mail-core';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
export const name = 'mail';
|
|
7
|
+
export const inject = [];
|
|
8
|
+
/** Local inbound path the dsh-aimail/inbound entry listens on (default). */
|
|
9
|
+
const INBOUND_PATH = '/aimail/inbound';
|
|
10
|
+
/** The local receive endpoint registered as this session's webhook_url. */
|
|
11
|
+
function inboundWebhookUrl() {
|
|
12
|
+
const fromEnv = (process.env.AIMAIL_INBOUND_URL ?? '').trim();
|
|
13
|
+
if (fromEnv)
|
|
14
|
+
return fromEnv.replace(/\/+$/, '') + INBOUND_PATH;
|
|
15
|
+
const port = Number(process.env.AIMAIL_INBOUND_PORT ?? 9099);
|
|
16
|
+
const p = Number.isInteger(port) && port > 0 ? port : 9099;
|
|
17
|
+
return `http://127.0.0.1:${p}${INBOUND_PATH}`;
|
|
18
|
+
}
|
|
19
|
+
/** Process once-guard per (system, session): at most one auto-bind attempt. */
|
|
20
|
+
const _autoBindAttempted = new Set();
|
|
21
|
+
/** Reset the once-guard (test hook / after an operator fixed the env). */
|
|
22
|
+
export function resetAutoBindOnce() {
|
|
23
|
+
_autoBindAttempted.clear();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One-shot per-session auto-bind. Never throws — failures warn and fall
|
|
27
|
+
* through to the caller's original unbound error.
|
|
28
|
+
*/
|
|
29
|
+
async function tryAutoBindSession(systemId, sessionId) {
|
|
30
|
+
const key = `${systemId}:${sessionId}`;
|
|
31
|
+
if (_autoBindAttempted.has(key))
|
|
32
|
+
return undefined;
|
|
33
|
+
_autoBindAttempted.add(key);
|
|
34
|
+
try {
|
|
35
|
+
const gw = await readSystemConfig(systemId);
|
|
36
|
+
if (!gw.domain)
|
|
37
|
+
return undefined;
|
|
38
|
+
const short = sessionId.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8) || 'session';
|
|
39
|
+
const email = emailForAgent(`agent-${short}`, gw.domain, gw.system_name ?? '');
|
|
40
|
+
const res = await autoBind({
|
|
41
|
+
systemId,
|
|
42
|
+
email,
|
|
43
|
+
webhookUrl: inboundWebhookUrl(),
|
|
44
|
+
extraFields: {
|
|
45
|
+
session_id: sessionId,
|
|
46
|
+
preset: process.env.AIMAIL_PRESET ?? 'mail',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
if (!(res.registered || res.exists))
|
|
50
|
+
return undefined;
|
|
51
|
+
// Re-resolve: the binding now carries this session_id.
|
|
52
|
+
return await resolveBySessionId(sessionId, { systemId });
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
console.warn(`[dsh-aimail] session auto-bind failed for ${sessionId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function apply(ctx, config = {}) {
|
|
60
|
+
const systemId = config.systemId ?? process.env.AIMAIL_SYSTEM_ID ?? '';
|
|
61
|
+
// SDK-shipped board resources (role prompts/souls) → local config dir,
|
|
62
|
+
// so a dsh-only machine (no Python SDK/CLI) still gets them. Idempotent;
|
|
63
|
+
// never overwrites user-personalized files.
|
|
64
|
+
try {
|
|
65
|
+
releaseAllSystems(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'board'));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// non-fatal: resources are a seed; explicit release can re-run later
|
|
69
|
+
}
|
|
70
|
+
// install readiness: a dsh-only machine ensures its system through the CLI
|
|
71
|
+
// reverse-call ABI (`aimail ensure-system`, L1 only — never platform wiring,
|
|
72
|
+
// which is how the install↔plugin call loop stays acyclic). UNCONDITIONAL
|
|
73
|
+
// reverse-call (ownership short-circuit lives inside ensureSystem): a
|
|
74
|
+
// multi-platform machine with only ANOTHER platform's systems must still
|
|
75
|
+
// reach the CLI so this dsh profile binds its own system — gating on "any
|
|
76
|
+
// system exists" regressed that (AUDIT-1 P1-7). CLI missing → actionable
|
|
77
|
+
// bootstrap hint on stderr.
|
|
78
|
+
try {
|
|
79
|
+
const platformHome = process.env.AIMAIL_SYSTEM_HOME?.trim() ||
|
|
80
|
+
process.env.DSH_HOME?.trim() ||
|
|
81
|
+
path.join(os.homedir(), '.dsh');
|
|
82
|
+
void ensureSystem({ systemHome: platformHome })
|
|
83
|
+
.then((r) => {
|
|
84
|
+
if (r.ok) {
|
|
85
|
+
if (r.activated) {
|
|
86
|
+
console.log(`[dsh-aimail] system activated: ${r.systemId}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const hint = r.hint ? ` (${r.hint})` : '';
|
|
91
|
+
console.warn(`[dsh-aimail] no aimail system yet — ${r.error ?? 'unknown'}` + hint);
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
.catch((e) => {
|
|
95
|
+
console.warn(`[dsh-aimail] system ensure failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// non-fatal
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a session config; on an unbound miss with a machine system
|
|
103
|
+
* config present, auto-bind that session once and retry.
|
|
104
|
+
*/
|
|
105
|
+
const resolveOrAutoBindSession = async (sessionId) => {
|
|
106
|
+
if (!sessionId)
|
|
107
|
+
throw new Error('no session id to resolve aimail config');
|
|
108
|
+
try {
|
|
109
|
+
return await resolveBySessionId(sessionId, { systemId });
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
if (!hasAnySystem())
|
|
113
|
+
throw e;
|
|
114
|
+
let target = systemId || process.env.AIMAIL_SYSTEM_ID || '';
|
|
115
|
+
if (!target) {
|
|
116
|
+
const sids = await listSystemDirs();
|
|
117
|
+
if (sids.length !== 1)
|
|
118
|
+
throw e; // ambiguous scope — caller's error stands
|
|
119
|
+
target = sids[0];
|
|
120
|
+
}
|
|
121
|
+
const cfg = await tryAutoBindSession(target, sessionId);
|
|
122
|
+
if (cfg)
|
|
123
|
+
return cfg;
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const service = {
|
|
128
|
+
systemId,
|
|
129
|
+
resolveConfig: (sessionId) => resolveOrAutoBindSession(sessionId),
|
|
130
|
+
resolveCtx: async (sessionId) => {
|
|
131
|
+
const cfg = await resolveOrAutoBindSession(sessionId);
|
|
132
|
+
return { systemId: cfg.system_id, email: cfg.email };
|
|
133
|
+
},
|
|
134
|
+
resolveByEmail: (email) => resolveByEmail(email),
|
|
135
|
+
resolveByRecipient: (email) => resolveByRecipient(email),
|
|
136
|
+
};
|
|
137
|
+
ctx.provide('mail', service);
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=mail-service.js.map
|
package/lib/tools.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-aimail tools — registers the 12 AIMail bare tools for this profile.
|
|
3
|
+
*
|
|
4
|
+
* Semantic text (names, descriptions, parameter descriptions) comes from
|
|
5
|
+
* the shared MAIL_TOOLS registry in @aimail/mail-core (single source of
|
|
6
|
+
* truth, parity-tested against amail_mcp_server.py). This adapter only:
|
|
7
|
+
* - iterates MAIL_TOOLS, translating each entry to a dsh defineTool
|
|
8
|
+
* - binds execution: exec.agent.id (dsh session uuid) → ctx.mail.resolveCtx
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
export declare const name = "tool-mail";
|
|
12
|
+
export declare const inject: string[];
|
|
13
|
+
export declare function apply(ctx: Context, config?: {
|
|
14
|
+
identity?: string;
|
|
15
|
+
}): void;
|
|
16
|
+
//# sourceMappingURL=tools.d.ts.map
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { MAIL_TOOLS, setAgentIdentity, setAgentModel, } from '@aimail/mail-core';
|
|
3
|
+
export const name = 'tool-mail';
|
|
4
|
+
export const inject = ['tools', 'mail'];
|
|
5
|
+
function textRender(_args, value) {
|
|
6
|
+
return [{ type: 'text', text: JSON.stringify(value) }];
|
|
7
|
+
}
|
|
8
|
+
const jsonOutput = {
|
|
9
|
+
schema: { type: 'json' },
|
|
10
|
+
render: textRender,
|
|
11
|
+
};
|
|
12
|
+
/** ToolResult → JsonValue (output.schema contract). */
|
|
13
|
+
const run = (p) => p;
|
|
14
|
+
/**
|
|
15
|
+
* Translate the neutral MailToolParam into a dsh ParameterPropertySpec.
|
|
16
|
+
* dsh requires `required?: true` (never false) and per-type literal shapes,
|
|
17
|
+
* so optional fields are omitted rather than set to undefined/false.
|
|
18
|
+
*/
|
|
19
|
+
function toDshParam(p) {
|
|
20
|
+
const base = {};
|
|
21
|
+
if (p.type === 'string') {
|
|
22
|
+
base.type = 'string';
|
|
23
|
+
if (p.enum !== undefined)
|
|
24
|
+
base.enum = p.enum;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
base.type = 'array';
|
|
28
|
+
if (p.items !== undefined)
|
|
29
|
+
base.items = { type: p.items.type };
|
|
30
|
+
}
|
|
31
|
+
if (p.description !== undefined)
|
|
32
|
+
base.description = p.description;
|
|
33
|
+
if (p.required === true)
|
|
34
|
+
base.required = true;
|
|
35
|
+
return base;
|
|
36
|
+
}
|
|
37
|
+
export function apply(ctx, config = {}) {
|
|
38
|
+
const mail = ctx.get('mail');
|
|
39
|
+
if (mail === undefined) {
|
|
40
|
+
throw new Error('tool-mail requires the mail service: mount dsh-aimail/mail-service first');
|
|
41
|
+
}
|
|
42
|
+
if (config.identity)
|
|
43
|
+
setAgentIdentity(config.identity);
|
|
44
|
+
// Primary model: same deployment default the inbound router uses for
|
|
45
|
+
// agents.create() (cordis 'agentDefaultModel' service).
|
|
46
|
+
const adm = ctx.get('agentDefaultModel');
|
|
47
|
+
const sel = adm?.currentSelection?.();
|
|
48
|
+
const modelId = typeof sel === 'string'
|
|
49
|
+
? sel
|
|
50
|
+
: sel?.model ?? sel?.id;
|
|
51
|
+
if (modelId)
|
|
52
|
+
setAgentModel(modelId);
|
|
53
|
+
const resolve = async (exec) => {
|
|
54
|
+
const sessionId = String(exec.agent?.id ?? '');
|
|
55
|
+
return mail.resolveCtx(sessionId);
|
|
56
|
+
};
|
|
57
|
+
for (const tool of MAIL_TOOLS) {
|
|
58
|
+
const { handler: _handler, ...semantic } = tool;
|
|
59
|
+
void _handler;
|
|
60
|
+
// Translate the neutral parameter schema into dsh's spec shape.
|
|
61
|
+
const parameters = {};
|
|
62
|
+
for (const [key, p] of Object.entries(semantic.parameters)) {
|
|
63
|
+
parameters[key] = toDshParam(p);
|
|
64
|
+
}
|
|
65
|
+
ctx.tools.register(defineTool({
|
|
66
|
+
name: semantic.name,
|
|
67
|
+
description: semantic.description,
|
|
68
|
+
parameters,
|
|
69
|
+
output: jsonOutput,
|
|
70
|
+
async execute(args, exec) {
|
|
71
|
+
return run(tool.handler(await resolve(exec), args));
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=tools.js.map
|