dsh-aimail 0.1.23 → 0.1.25

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/lib/inbound.d.ts CHANGED
@@ -1,4 +1,7 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
1
2
  import type { Context } from '@deepseek-ai/cordis';
3
+ import { type AgentConfig, type AgentPullHandle, type AgentPullOverrides, type InboundPayload } from '@aimail/mail-core';
4
+ import type { MailService } from './mail-service.js';
2
5
  export declare const name = "mail-inbound";
3
6
  export declare const inject: string[];
4
7
  export interface Config {
@@ -9,5 +12,53 @@ export interface Config {
9
12
  /** Deliver path (default /aimail/inbound). */
10
13
  path?: string;
11
14
  }
15
+ /** The resolved target of one inbound payload (binding + routed address). */
16
+ export interface InboundTarget {
17
+ cfg: AgentConfig;
18
+ agentAddr: string;
19
+ }
20
+ /** Result of the shared inbound chain (routing→deliver). */
21
+ export interface InboundOutcome {
22
+ status: string;
23
+ detail?: string | undefined;
24
+ /**
25
+ * Did the mail reach a dsh session? `false` ⇒ a pulled delivery must NOT be
26
+ * acked (it re-pulls next round) — the pull-side equivalent of the push
27
+ * side's 503.
28
+ */
29
+ ok: boolean;
30
+ }
31
+ /**
32
+ * Recipient routing — the ONE routing step both entries share (push handler +
33
+ * pull loop). The per-delivery target is authoritative: the bridge injects
34
+ * X-AIMail-Email on each single-delivery POST (legacy); payload.to is the
35
+ * FILTERED full list (external recipients first), so to[0] is often an external
36
+ * address. Use the header when present; only iterate toRaw when the header is
37
+ * absent (batch deliveries carry no such header).
38
+ */
39
+ export declare function resolveInboundTarget(mail: MailService, payload: InboundPayload, headers: Record<string, unknown>): Promise<InboundTarget | undefined>;
40
+ /**
41
+ * Post-routing inbound chain: preprocess (13 steps + ping/pong) → deliver to a
42
+ * dsh session. Shared by the push handler and the pull loop.
43
+ *
44
+ * ⚠ The TRUST step is not here: pushed mail is verified by the per-address HMAC
45
+ * (bridge↔endpoint boundary), pulled mail by the gateway's own verification of
46
+ * the agent-scope key. Routing, enrichment and delivery are one chain.
47
+ */
48
+ export declare function deliverInbound(ctx: Context, target: InboundTarget, payload: InboundPayload, headers: Record<string, unknown>): Promise<InboundOutcome>;
49
+ /** The push handler (HTTP route) around the shared chain. */
50
+ export declare function createInboundHandler(ctx: Context, mail: MailService, deliverPath: string): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
51
+ /**
52
+ * Pull entry (agent-scope bindings only) — the missing production wire for the
53
+ * address-code flow. Pulled mail enters `deliverInbound`: the very same chain
54
+ * pushed mail takes (routing → preprocess → session delivery). Never started
55
+ * for system/bridge bindings (push-served): the decision lives in mail-core.
56
+ */
57
+ export declare function startInboundPull(ctx: Context, opts?: {
58
+ log?: (line: string) => void;
59
+ env?: NodeJS.ProcessEnv;
60
+ overrides?: AgentPullOverrides;
61
+ systemId?: string;
62
+ }): Promise<AgentPullHandle[]>;
12
63
  export declare function apply(ctx: Context, config?: Config): () => void;
13
64
  //# sourceMappingURL=inbound.d.ts.map
package/lib/inbound.js CHANGED
@@ -11,6 +11,12 @@
11
11
  * resume) or spawn a fresh disposable session when unbound — context
12
12
  * continuity is aimail's (local meta threading), not the session's.
13
13
  * 200 ack on delivery; 503 on session-create failure (bridge retries).
14
+ *
15
+ * Pull entry (2026-09-27): an address activated by an activation CODE has no
16
+ * push path (the gateway stores webhook_url=NULL for it), so this plugin also
17
+ * polls its own mailbox on a timer and delivers through the SAME chain above
18
+ * (`deliverInbound`). Enabled only for agent-scope bindings — the decision and
19
+ * the loop live in mail-core (`startAgentPullEntries` / `startPolling`).
14
20
  */
15
21
  import * as fs from 'node:fs';
16
22
  import * as os from 'node:os';
@@ -19,7 +25,7 @@ import { fileURLToPath } from 'node:url';
19
25
  import { createServer } from 'node:http';
20
26
  import { randomUUID } from 'node:crypto';
21
27
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
22
- import { processInboundMail, verifySignature, routeAddressFromHeaders, updateAgentConfig, loadAgentConfig, saveAgentConfig } from '@aimail/mail-core';
28
+ import { processInboundMail, verifySignature, routeAddressFromHeaders, updateAgentConfig, loadAgentConfig, saveAgentConfig, startAgentPullEntries, INBOUND_PATH, INBOUND_PORTS } from '@aimail/mail-core';
23
29
  import { ensureBridgeRoutesForSystem, formatBridgeRouteLine, isBridgeRouteWarning } from '@aimail/mail-core';
24
30
  export const name = 'mail-inbound';
25
31
  export const inject = ['mail', 'agents'];
@@ -36,44 +42,123 @@ function readBody(req) {
36
42
  req.on('error', reject);
37
43
  });
38
44
  }
39
- export function apply(ctx, config = {}) {
40
- const mail = ctx.get('mail');
41
- if (mail === undefined) {
42
- throw new Error('mail-inbound requires the mail service: mount dsh-aimail/mail-service first');
45
+ /**
46
+ * Recipient routing — the ONE routing step both entries share (push handler +
47
+ * pull loop). The per-delivery target is authoritative: the bridge injects
48
+ * X-AIMail-Email on each single-delivery POST (legacy); payload.to is the
49
+ * FILTERED full list (external recipients first), so to[0] is often an external
50
+ * address. Use the header when present; only iterate toRaw when the header is
51
+ * absent (batch deliveries carry no such header).
52
+ */
53
+ export async function resolveInboundTarget(mail, payload, headers) {
54
+ const routeAddr = routeAddressFromHeaders(headers);
55
+ const toRaw = Array.isArray(payload.to) ? payload.to : typeof payload.to === 'string' ? [payload.to] : [];
56
+ const routeCandidates = routeAddr ? [routeAddr] : toRaw;
57
+ let cfg;
58
+ let agentAddr = '';
59
+ for (const t of routeCandidates) {
60
+ const addr = String(t).trim();
61
+ if (!addr.includes('@'))
62
+ continue;
63
+ const c = await mail.resolveByRecipient(addr);
64
+ if (c) {
65
+ cfg = c;
66
+ agentAddr = addr;
67
+ break;
68
+ }
43
69
  }
44
- // SDK-shipped skill → <dshHome>/skills/agentmail/ (idempotent;
45
- // identical-content skip; dshHome resolution mirrors mail-service).
46
- // SKILL.md owns the inbound-message protocol (6-step flow) — a
47
- // different category from tool registration (tool usage). Symmetric
48
- // across openclaw/dsh/pi.
70
+ return cfg ? { cfg, agentAddr } : undefined;
71
+ }
72
+ /**
73
+ * Post-routing inbound chain: preprocess (13 steps + ping/pong) → deliver to a
74
+ * dsh session. Shared by the push handler and the pull loop.
75
+ *
76
+ * ⚠ The TRUST step is not here: pushed mail is verified by the per-address HMAC
77
+ * (bridge↔endpoint boundary), pulled mail by the gateway's own verification of
78
+ * the agent-scope key. Routing, enrichment and delivery are one chain.
79
+ */
80
+ export async function deliverInbound(ctx, target, payload, headers) {
81
+ const { cfg, agentAddr } = target;
82
+ // TS preprocess chain (13 steps) + ping/pong intercept
83
+ const result = await processInboundMail(payload, headers, {
84
+ systemId: cfg.system_id,
85
+ email: cfg.email,
86
+ });
87
+ if (result === null) {
88
+ return { status: 'intercepted', ok: true };
89
+ }
90
+ // Deliver to a dsh session:
91
+ // - cfg.session_id set + live → followup that session (UI continuity)
92
+ // - cfg.session_id set + cold → resume it, else fall through
93
+ // - unbound (or resume failed) → spawn a FRESH session. Context
94
+ // continuity is aimail's job (local meta threading + email_summary),
95
+ // not the session's — per the deployment decision each inbound email
96
+ // gets its own disposable session.
97
+ const agents = ctx.get('agents');
98
+ if (agents === undefined) {
99
+ return { status: 'no_agents_service', detail: 'dsh-agent not mounted', ok: false };
100
+ }
101
+ // Model route: the deployment's default selection (base bundle's
102
+ // `agent-default-model` row, e.g. deepseek-official/deepseek-v4-flash)
103
+ // — same source the web UI's api-proxy uses for agents.create().
104
+ // Without it the turn dies with "no provider/model".
105
+ const agentOptions = ctx.get('agentDefaultModel')
106
+ ?.currentSelection();
107
+ const boundId = cfg.session_id ?? '';
108
+ const message = createUserMessage({
109
+ content: [{ type: 'text', text: JSON.stringify({ ...result, to: agentAddr }) }],
110
+ source: { kind: 'user' },
111
+ });
112
+ const live = boundId ? agents.get(boundId) : undefined;
113
+ if (live) {
114
+ live.followup(message);
115
+ return { status: 'delivered', detail: 'followup queued', ok: true };
116
+ }
117
+ if (boundId) {
118
+ try {
119
+ const handle = await agents.resume({ resumeSessionId: boundId, agentOptions });
120
+ handle.agent.followup(message);
121
+ return { status: 'resumed', detail: 'cold session resumed + followup queued', ok: true };
122
+ }
123
+ catch {
124
+ // resume failed (no persistence, stale id) — fall through to a fresh session
125
+ }
126
+ }
127
+ // Fresh disposable session for this email.
128
+ const sessionId = randomUUID();
49
129
  try {
50
- const dshHome = process.env.AIMAIL_SYSTEM_HOME?.trim() ||
51
- process.env.DSH_HOME?.trim() ||
52
- path.join(os.homedir(), '.dsh');
53
- const skillSrc = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'skills');
54
- const skillDst = path.join(dshHome, 'skills', 'agentmail');
55
- fs.mkdirSync(skillDst, { recursive: true });
56
- for (const f of ['SKILL.md', 'DESCRIPTION.md']) {
57
- const from = path.join(skillSrc, f);
58
- const to = path.join(skillDst, f);
59
- if (!fs.existsSync(from)) {
60
- // missing package resources = packaging/materialize defect: loud but must not block inbound handling
61
- console.error(`[dsh-aimail] skill resource missing: ${from} ` +
62
- '(repo: run scripts/materialize-resources.sh; installed: reinstall the package)');
63
- continue;
130
+ // Bind this session into the agent's config so the mail tools can
131
+ // resolve credentials (resolveBySessionId matches agentmail.json's
132
+ // session_id). Unbind again once the turn settles — but only if the
133
+ // binding is still OURS (a concurrent email may have re-bound).
134
+ await updateAgentConfig(cfg.system_id, cfg.email, { session_id: sessionId });
135
+ const handle = await agents.create({ sessionId, meta: { cwd: process.cwd() }, agentOptions });
136
+ handle.agent.followup(message);
137
+ void handle.agent.whenIdle()
138
+ .then(async () => {
139
+ const cur = await loadAgentConfig(cfg.system_id, cfg.email);
140
+ if (cur && cur.session_id === sessionId) {
141
+ const { session_id: _drop, ...rest } = cur;
142
+ await saveAgentConfig(rest, cfg.system_id);
64
143
  }
65
- if (fs.existsSync(to) && fs.readFileSync(from).equals(fs.readFileSync(to)))
66
- continue;
67
- fs.copyFileSync(from, to);
68
- }
144
+ })
145
+ .then(() => handle.dispose())
146
+ .catch(() => { });
147
+ return { status: 'delivered', detail: `fresh session ${sessionId}`, ok: true };
69
148
  }
70
- catch {
71
- // non-fatal; retried on next plugin start
149
+ catch (e) {
150
+ // push side: 503 (not 2xx) so the bridge does NOT ack and will retry; a 200
151
+ // here would silently swallow the email. Pull side: !ok ⇒ not acked.
152
+ return {
153
+ status: 'session_create_failed',
154
+ detail: e instanceof Error ? e.message : String(e),
155
+ ok: false,
156
+ };
72
157
  }
73
- const host = config.host ?? '127.0.0.1';
74
- const port = config.port ?? Number(process.env.AIMAIL_INBOUND_PORT ?? 9099);
75
- const deliverPath = config.path ?? '/aimail/inbound';
76
- const server = createServer(async (req, res) => {
158
+ }
159
+ /** The push handler (HTTP route) around the shared chain. */
160
+ export function createInboundHandler(ctx, mail, deliverPath) {
161
+ return async (req, res) => {
77
162
  try {
78
163
  if (req.method !== 'POST' || (req.url ?? '').split('?')[0] !== deliverPath) {
79
164
  writeJson(res, 404, { status: 'not_found' });
@@ -88,123 +173,114 @@ export function apply(ctx, config = {}) {
88
173
  writeJson(res, 400, { status: 'bad_json' });
89
174
  return;
90
175
  }
91
- // Inbound routing (Q3 — mirror Python bridge routing): the per-delivery
92
- // target is authoritative. The bridge injects X-AIMail-Email (legacy
93
- // on each single-delivery POST; payload.to is
94
- // the FILTERED full list (external recipients first), so to[0] is often
95
- // an external address. Use the header when present; only iterate toRaw
96
- // when the header is absent (batch deliveries carry no such header).
97
176
  const headers = {
98
177
  ...req.headers,
99
178
  ...(payload.headers ?? {}),
100
179
  };
101
- const routeAddr = routeAddressFromHeaders(headers);
102
- const toRaw = Array.isArray(payload.to) ? payload.to : typeof payload.to === 'string' ? [payload.to] : [];
103
- const routeCandidates = routeAddr ? [routeAddr] : toRaw;
104
- let cfg;
105
- let agentAddr = '';
106
- for (const t of routeCandidates) {
107
- const addr = String(t).trim();
108
- if (!addr.includes('@'))
109
- continue;
110
- const c = await mail.resolveByRecipient(addr);
111
- if (c) {
112
- cfg = c;
113
- agentAddr = addr;
114
- break;
115
- }
116
- }
117
- if (!cfg) {
180
+ const target = await resolveInboundTarget(mail, payload, headers);
181
+ if (!target) {
118
182
  writeJson(res, 200, { status: 'no_agent', detail: 'no binding' });
119
183
  return;
120
184
  }
121
- // HMAC verify (per-address webhook_secret)
185
+ // HMAC verify (per-address webhook_secret) — push-only trust step; pulled
186
+ // mail is authenticated by the gateway on the agent-scope key.
122
187
  const sig = req.headers['x-webhook-signature'] ?? '';
123
- if (!verifySignature(rawBody, sig, cfg.webhook_secret ?? '')) {
188
+ if (!verifySignature(rawBody, sig, target.cfg.webhook_secret ?? '')) {
124
189
  writeJson(res, 401, { status: 'bad_signature' });
125
190
  return;
126
191
  }
127
- // TS preprocess chain (13 steps) + ping/pong intercept
128
- const result = await processInboundMail(payload, headers, {
129
- systemId: cfg.system_id,
130
- email: cfg.email,
131
- });
132
- if (result === null) {
133
- writeJson(res, 200, { status: 'intercepted' });
134
- return;
135
- }
136
- // Deliver to a dsh session:
137
- // - cfg.session_id set + live → followup that session (UI continuity)
138
- // - cfg.session_id set + cold → resume it, else fall through
139
- // - unbound (or resume failed) → spawn a FRESH session. Context
140
- // continuity is aimail's job (local meta threading + email_summary),
141
- // not the session's — per the deployment decision each inbound email
142
- // gets its own disposable session.
143
- const agents = ctx.get('agents');
144
- if (agents === undefined) {
145
- writeJson(res, 200, { status: 'no_agents_service', detail: 'dsh-agent not mounted' });
146
- return;
147
- }
148
- // Model route: the deployment's default selection (base bundle's
149
- // `agent-default-model` row, e.g. deepseek-official/deepseek-v4-flash)
150
- // — same source the web UI's api-proxy uses for agents.create().
151
- // Without it the turn dies with "no provider/model".
152
- const agentOptions = ctx.get('agentDefaultModel')
153
- ?.currentSelection();
154
- const boundId = cfg.session_id ?? '';
155
- const message = createUserMessage({
156
- content: [{ type: 'text', text: JSON.stringify({ ...result, to: agentAddr }) }],
157
- source: { kind: 'user' },
158
- });
159
- const live = boundId ? agents.get(boundId) : undefined;
160
- if (live) {
161
- live.followup(message);
162
- writeJson(res, 200, { status: 'delivered', detail: 'followup queued' });
163
- return;
164
- }
165
- if (boundId) {
166
- try {
167
- const handle = await agents.resume({ resumeSessionId: boundId, agentOptions });
168
- handle.agent.followup(message);
169
- writeJson(res, 200, { status: 'resumed', detail: 'cold session resumed + followup queued' });
170
- return;
171
- }
172
- catch {
173
- // resume failed (no persistence, stale id) — fall through to a fresh session
174
- }
175
- }
176
- // Fresh disposable session for this email.
177
- const sessionId = randomUUID();
178
- try {
179
- // Bind this session into the agent's config so the mail tools can
180
- // resolve credentials (resolveBySessionId matches agentmail.json's
181
- // session_id). Unbind again once the turn settles — but only if the
182
- // binding is still OURS (a concurrent email may have re-bound).
183
- await updateAgentConfig(cfg.system_id, cfg.email, { session_id: sessionId });
184
- const handle = await agents.create({ sessionId, meta: { cwd: process.cwd() }, agentOptions });
185
- handle.agent.followup(message);
186
- void handle.agent.whenIdle()
187
- .then(async () => {
188
- const cur = await loadAgentConfig(cfg.system_id, cfg.email);
189
- if (cur && cur.session_id === sessionId) {
190
- const { session_id: _drop, ...rest } = cur;
191
- await saveAgentConfig(rest, cfg.system_id);
192
- }
193
- })
194
- .then(() => handle.dispose())
195
- .catch(() => { });
196
- writeJson(res, 200, { status: 'delivered', detail: `fresh session ${sessionId}` });
197
- }
198
- catch (e) {
199
- // 503 (not 2xx) so the bridge does NOT ack and will retry; a 200 here
200
- // would silently swallow the email.
201
- writeJson(res, 503, { status: 'session_create_failed', detail: e instanceof Error ? e.message : String(e) });
202
- }
192
+ const out = await deliverInbound(ctx, target, payload, headers);
193
+ // Non-2xx on delivery failure so the bridge retries (unchanged semantics).
194
+ // `detail` is omitted when absent (exactOptionalPropertyTypes: an explicit
195
+ // `undefined` would not satisfy DeliveryOutcome's optional string).
196
+ const body = out.detail === undefined ? { status: out.status } : { status: out.status, detail: out.detail };
197
+ writeJson(res, out.ok ? 200 : 503, body);
203
198
  }
204
199
  catch (e) {
205
200
  writeJson(res, 500, { status: 'error', detail: e instanceof Error ? e.message : String(e) });
206
201
  }
202
+ };
203
+ }
204
+ /**
205
+ * Pull entry (agent-scope bindings only) — the missing production wire for the
206
+ * address-code flow. Pulled mail enters `deliverInbound`: the very same chain
207
+ * pushed mail takes (routing → preprocess → session delivery). Never started
208
+ * for system/bridge bindings (push-served): the decision lives in mail-core.
209
+ */
210
+ export async function startInboundPull(ctx, opts = {}) {
211
+ const mail = ctx.get('mail');
212
+ if (mail === undefined) {
213
+ throw new Error('mail-inbound requires the mail service: mount dsh-aimail/mail-service first');
214
+ }
215
+ return await startAgentPullEntries({
216
+ // Scope = the plugin's own system when it has one, else every system
217
+ // (the per-address binding file is the authority — single source).
218
+ systemId: opts.systemId ?? mail.systemId,
219
+ ...(opts.log ? { log: opts.log } : {}),
220
+ ...(opts.env ? { env: opts.env } : {}),
221
+ ...(opts.overrides ? { overrides: opts.overrides } : {}),
222
+ onEmail: async (cfg, pulled) => {
223
+ if (typeof pulled.body !== 'object' || pulled.body === null) {
224
+ throw new Error(`pulled delivery ${pulled.id} for ${cfg.email} carries no JSON payload — not acking`);
225
+ }
226
+ // The delivery's own address is authoritative for routing (it is what the
227
+ // agent-scope key was verified against server-side).
228
+ const headers = {
229
+ ...(pulled.headers ?? {}),
230
+ 'x-aimail-email': pulled.email,
231
+ };
232
+ const target = await resolveInboundTarget(mail, pulled.body, headers);
233
+ if (!target) {
234
+ throw new Error(`pulled delivery ${pulled.id}: no binding for ${pulled.email} — not acking`);
235
+ }
236
+ const out = await deliverInbound(ctx, target, pulled.body, headers);
237
+ if (!out.ok) {
238
+ throw new Error(`pulled delivery ${pulled.id}: inbound chain did not deliver (${out.status}: ${out.detail ?? ''}) — not acking`);
239
+ }
240
+ },
207
241
  });
242
+ }
243
+ export function apply(ctx, config = {}) {
244
+ const mail = ctx.get('mail');
245
+ if (mail === undefined) {
246
+ throw new Error('mail-inbound requires the mail service: mount dsh-aimail/mail-service first');
247
+ }
248
+ // SDK-shipped skill → <dshHome>/skills/agentmail/ (idempotent;
249
+ // identical-content skip; dshHome resolution mirrors mail-service).
250
+ // SKILL.md owns the inbound-message protocol (6-step flow) — a
251
+ // different category from tool registration (tool usage). Symmetric
252
+ // across openclaw/dsh/pi.
253
+ try {
254
+ const dshHome = process.env.AIMAIL_SYSTEM_HOME?.trim() ||
255
+ process.env.DSH_HOME?.trim() ||
256
+ path.join(os.homedir(), '.dsh');
257
+ const skillSrc = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'skills');
258
+ const skillDst = path.join(dshHome, 'skills', 'agentmail');
259
+ fs.mkdirSync(skillDst, { recursive: true });
260
+ for (const f of ['SKILL.md', 'DESCRIPTION.md']) {
261
+ const from = path.join(skillSrc, f);
262
+ const to = path.join(skillDst, f);
263
+ if (!fs.existsSync(from)) {
264
+ // missing package resources = packaging/materialize defect: loud but must not block inbound handling
265
+ console.error(`[dsh-aimail] skill resource missing: ${from} ` +
266
+ '(repo: run scripts/materialize-resources.sh; installed: reinstall the package)');
267
+ continue;
268
+ }
269
+ if (fs.existsSync(to) && fs.readFileSync(from).equals(fs.readFileSync(to)))
270
+ continue;
271
+ fs.copyFileSync(from, to);
272
+ }
273
+ }
274
+ catch {
275
+ // non-fatal; retried on next plugin start
276
+ }
277
+ const host = config.host ?? '127.0.0.1';
278
+ const port = config.port ?? Number(process.env.AIMAIL_INBOUND_PORT ?? INBOUND_PORTS.dsh);
279
+ const deliverPath = config.path ?? INBOUND_PATH;
280
+ const server = createServer(createInboundHandler(ctx, mail, deliverPath));
281
+ // Pull loops (agent-scope bindings only). Held here so the fiber's dispose
282
+ // stops them with the listener — the host lifecycle owns both.
283
+ let pullHandles = [];
208
284
  server.listen(port, host, () => {
209
285
  // Route side (owner ruling 2026-09-27): the listener is up, so this is the
210
286
  // moment to (re-)pair every address of this system. The bridge deletes
@@ -226,8 +302,20 @@ export function apply(ctx, config = {}) {
226
302
  .catch((e) => {
227
303
  console.warn(`[dsh-aimail] route ensure failed: ${e instanceof Error ? e.message : String(e)}`);
228
304
  });
305
+ // Pull entry: inbound is live ⇒ the pulled mail can enter the same chain.
306
+ // Only agent-scope (address-code) bindings arm a loop (mail-core decides).
307
+ void startInboundPull(ctx, { log: (line) => console.log(line) })
308
+ .then((handles) => {
309
+ pullHandles = handles;
310
+ })
311
+ .catch((e) => {
312
+ console.warn(`[dsh-aimail] pull entry failed to start: ${e instanceof Error ? e.message : String(e)}`);
313
+ });
229
314
  });
230
315
  return () => {
316
+ for (const h of pullHandles)
317
+ h.stop();
318
+ pullHandles = [];
231
319
  server.close();
232
320
  };
233
321
  }
@@ -1,19 +1,18 @@
1
1
  import { resolveByRecipient, resolveByEmail, resolveBySessionId, } from '@aimail/mail';
2
- import { autoBind, emailForAgent, ensureSystem, detectSystemForHome, hasAnySystem, listSystemDirs, readSystemConfig, releaseAllSystems, } from '@aimail/mail-core';
2
+ import { autoBind, emailForAgent, ensureSystem, detectSystemForHome, hasAnySystem, INBOUND_PATH, INBOUND_PORTS, listSystemDirs, readSystemConfig, releaseAllSystems, } from '@aimail/mail-core';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  export const name = 'mail';
7
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. */
8
+ /** The local receive endpoint registered as this session's webhook_url.
9
+ * 路径/端口契约常量唯一副本 = @aimail/mail-core(contract/aimail-contract.json)。 */
11
10
  function inboundWebhookUrl() {
12
11
  const fromEnv = (process.env.AIMAIL_INBOUND_URL ?? '').trim();
13
12
  if (fromEnv)
14
13
  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;
14
+ const port = Number(process.env.AIMAIL_INBOUND_PORT ?? INBOUND_PORTS.dsh);
15
+ const p = Number.isInteger(port) && port > 0 ? port : INBOUND_PORTS.dsh;
17
16
  return `http://127.0.0.1:${p}${INBOUND_PATH}`;
18
17
  }
19
18
  /** Process once-guard per (system, session): at most one auto-bind attempt. */
@@ -21,7 +21,7 @@
21
21
  * [--force]
22
22
  */
23
23
  import { randomUUID } from 'node:crypto';
24
- import { autoBind, emailForAgent, listSystemDirs, loadAgentConfig, readSystemConfig, registerAddress, registerBridgeRoute, resolveRegisterWebhook, saveBinding, } from '@aimail/mail-core';
24
+ import { autoBind, emailForAgent, inboundUrl, INBOUND_PORTS, listSystemDirs, loadAgentConfig, readSystemConfig, registerAddress, registerBridgeRoute, resolveRegisterWebhook, saveBinding, } from '@aimail/mail-core';
25
25
  function arg(argv, name) {
26
26
  const i = argv.indexOf(name);
27
27
  return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
@@ -36,7 +36,7 @@ async function main() {
36
36
  const manager = arg(argv, '--manager');
37
37
  const sessionId = arg(argv, '--session-id');
38
38
  const preset = arg(argv, '--preset') || 'mail';
39
- const localWebhook = arg(argv, '--local-webhook') || 'http://127.0.0.1:9099/aimail/inbound';
39
+ const localWebhook = arg(argv, '--local-webhook') || inboundUrl(INBOUND_PORTS.dsh);
40
40
  const force = flag(argv, '--force');
41
41
  const systemId = systemIdArg ||
42
42
  process.env.AIMAIL_SYSTEM_ID ||
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-aimail",
3
3
  "description": "AIMail plugin for dsh — install via: dsh plugin --profile web add dsh-aimail",
4
- "version": "0.1.23",
4
+ "version": "0.1.25",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": true
@@ -51,8 +51,8 @@
51
51
  "@deepseek-ai/dsh-tools": "^0.0.1-rc.1"
52
52
  },
53
53
  "dependencies": {
54
- "@aimail/mail-core": "^0.1.23",
55
- "@aimail/mail": "^0.1.23"
54
+ "@aimail/mail-core": "^0.1.25",
55
+ "@aimail/mail": "^0.1.25"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@deepseek-ai/cordis": "^4.0.1",
@@ -1,18 +1,18 @@
1
1
  ---
2
- name: aimail
2
+ name: agentmail
3
3
  description: "Send outbound emails for reporting deliverables, updating project status, requesting decisions/approvals, or engaging in A2A collaboration. Also reply to or forward inbound emails from other agents or humans."
4
4
  version: 1.0.0
5
5
  author: MeterCai
6
6
  license: GPL-3.0
7
7
  metadata:
8
8
  hermes:
9
- tags: [email, mail, aimail, conversation]
10
- toolset: aimail
9
+ tags: [email, mail, agentmail, conversation]
10
+ toolset: agentmail
11
11
  ---
12
12
 
13
- # aimail — Email Conversation Agent
13
+ # agentmail — Email Conversation Agent
14
14
 
15
- Your email: **{profile_name}@{domain}**. You conduct conversations via email — replying or forwarding to incoming messages to continue the dialogue, and proactively sending outbound ones for deliverables, status updates, approval requests, or A2A collaboration. The aimail toolset handles delivery, contacts, and summaries; you focus on understanding, deciding, and composing.
15
+ Your email: **{profile_name}@{domain}**. You conduct conversations via email — replying or forwarding to incoming messages to continue the dialogue, and proactively sending outbound ones for deliverables, status updates, approval requests, or A2A collaboration. The agentmail toolset handles delivery, contacts, and summaries; you focus on understanding, deciding, and composing.
16
16
 
17
17
  ---
18
18
 
@@ -221,7 +221,7 @@ set_contact_profile(address="alice@example.com",
221
221
 
222
222
  ## Tools
223
223
 
224
- The aimail tools are registered with full schemas — parameter names, types, and descriptions are visible to you automatically. This table is a quick reference:
224
+ The agentmail tools are registered with full schemas — parameter names, types, and descriptions are visible to you automatically. This table is a quick reference:
225
225
 
226
226
  | Tool | Use |
227
227
  |------|-----|