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.
Files changed (33) hide show
  1. package/README.md +60 -0
  2. package/cordis.patch.yml +27 -15
  3. package/lib/inbound.d.ts +13 -0
  4. package/lib/inbound.js +179 -0
  5. package/lib/index.d.ts +12 -5
  6. package/lib/index.js +12 -5
  7. package/lib/mail-service.d.ts +39 -0
  8. package/lib/mail-service.js +139 -0
  9. package/lib/tools.d.ts +16 -0
  10. package/lib/tools.js +76 -0
  11. package/package.json +38 -13
  12. package/resources/board/role_prompt_en/common.md +33 -0
  13. package/resources/board/role_prompt_en/orchestrator.md +43 -0
  14. package/resources/board/role_prompt_en/role_calibrator.md +27 -0
  15. package/resources/board/role_prompt_en/verifier.md +47 -0
  16. package/resources/board/role_prompt_en/whoami.md +24 -0
  17. package/resources/board/role_prompt_en/worker.md +48 -0
  18. package/resources/board/role_prompt_zh/common.md +33 -0
  19. package/resources/board/role_prompt_zh/orchestrator.md +43 -0
  20. package/resources/board/role_prompt_zh/role_calibrator.md +27 -0
  21. package/resources/board/role_prompt_zh/verifier.md +47 -0
  22. package/resources/board/role_prompt_zh/whoami.md +24 -0
  23. package/resources/board/role_prompt_zh/worker.md +48 -0
  24. package/resources/board/role_soul_en/Orchestrator.md +24 -0
  25. package/resources/board/role_soul_en/Owner.md +23 -0
  26. package/resources/board/role_soul_en/Verifier.md +23 -0
  27. package/resources/board/role_soul_en/Worker.md +23 -0
  28. package/resources/board/role_soul_zh/Orchestrator.md +24 -0
  29. package/resources/board/role_soul_zh/Owner.md +25 -0
  30. package/resources/board/role_soul_zh/Verifier.md +23 -0
  31. package/resources/board/role_soul_zh/Worker.md +23 -0
  32. package/resources/skills/DESCRIPTION.md +3 -0
  33. 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
- # agentmail bundle layer — mounted via `dsh plugin --profile web add agentmail`.
2
- # Entry list identical to the mail preset: persona + host services + tools.
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: persona
7
- name: '@deepseek-ai/dsh-persona'
14
+ - id: system-prompt
8
15
  config:
9
- text: >-
10
- You are an email-capable agent on AgentMail. Use send_mail to reply to
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
- # ── AgentMail host services ─────────────────────────────────────────────────
22
+ # ── rows new to this bundle ─────────────────────────────────────────────────
16
23
 
17
- - id: mail
18
- name: '@meterwei/mail'
24
+ - insert:
25
+ # AIMail host service (ctx.mail)
26
+ - id: mail
27
+ name: 'dsh-aimail/mail-service'
19
28
 
20
- - id: mail-inbound
21
- name: '@meterwei/mail-inbound'
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
- # ── AgentMail tools (bare names) ────────────────────────────────────────────
24
-
25
- - id: tool-mail
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'
@@ -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
- * agentmail — dsh bundle entry. The bundle patch (cordis.patch.yml) mounts
3
- * mail / mail-inbound / tool-mail + persona; this entry exposes the shared
4
- * core API for programmatic use. Installed via:
5
- * dsh plugin --profile web add agentmail
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 '@meterwei/mail-core';
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
- * agentmail — dsh bundle entry. The bundle patch (cordis.patch.yml) mounts
3
- * mail / mail-inbound / tool-mail + persona; this entry exposes the shared
4
- * core API for programmatic use. Installed via:
5
- * dsh plugin --profile web add agentmail
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 '@meterwei/mail-core';
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