@aimail/mail 0.1.0-rc.8 → 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 ADDED
@@ -0,0 +1,49 @@
1
+ # @aimail/mail
2
+
3
+ Platform-neutral AIMail config resolution for TypeScript: session id / email
4
+ / recipient → `agentmail.json` → `AgentConfig`. Pure functions — no framework
5
+ imports, no host wiring.
6
+
7
+ [![npm](https://img.shields.io/npm/v/@aimail/mail)](https://www.npmjs.com/package/@aimail/mail)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @aimail/mail
13
+ ```
14
+
15
+ ## What it does
16
+
17
+ Every adapter needs the same three lookups, sourced from the per-address
18
+ `agentmail.json` bindings under `$AIMAIL_HOME` (default `~/.aimail`):
19
+
20
+ - `resolveBySessionId(sessionId)` — session-file match first, then agent_id
21
+ field match. For dsh session uuids and OpenClaw/pi agent ids.
22
+ - `resolveByEmail(email)` — exact registered-address match. Throws when
23
+ unbound.
24
+ - `resolveByRecipient(recipient)` — exact match first, then persona-strip
25
+ fallback (recipient local part ends with `.<registered local part>`),
26
+ mirroring the Python `route_agent_for_email` semantics. Inbound routing for
27
+ single-in-multi-out platforms (one gateway hosts many agents; mail can
28
+ arrive at role aliases).
29
+ - `resolveConfig()`-style composition is left to each adapter: the identity
30
+ source (pointer file / factory ctx / env) is platform-specific by design.
31
+
32
+ ## Scope narrowing
33
+
34
+ `ResolveOptions.systemId` narrows the scan to one system; when omitted the
35
+ `AIMAIL_SYSTEM_ID` env is used; when that is also empty, all systems under
36
+ `$AIMAIL_HOME/systems/` are scanned (two-level traversal:
37
+ `systems/{system_id}/{address_dir}/agentmail.json`).
38
+
39
+ Unbound resolutions throw loudly (`no aimail binding for …`) — callers
40
+ surface that to the model/user instead of guessing an identity.
41
+
42
+ ## Related repositories
43
+
44
+ - [metercai/aimail](https://github.com/metercai/aimail) — the AIMail monorepo:
45
+ CLI (`cli/`), Python SDK (`pysdk/`), TypeScript SDK (`tssdk/`, you are here),
46
+ bridge distributions.
47
+ - [metercai/aimail-gateway](https://github.com/metercai/aimail-gateway) — the
48
+ AIMail gateway: SMTP/HTTP mail service and the address & activation APIs
49
+ that create the bindings being resolved here.
package/lib/index.d.ts CHANGED
@@ -1,34 +1,31 @@
1
- /**
2
- * @aimail/mail — host-layer AgentMail service (ctx.mail).
3
- *
4
- * Provides per-session agentmail config resolution for tool-mail / mail-inbound:
5
- * session_id (exec.agent.id) → agentmail.json → ToolCtx {systemId, email}.
6
- * Config identity = agentmail.json only (single source of truth); the optional
7
- * AMAIL_SYSTEM_ID env narrows the scan scope.
8
- */
9
- import type { Context } from '@deepseek-ai/cordis';
10
1
  import { type AgentConfig } from '@aimail/mail-core';
11
- export declare const name = "mail";
12
- export declare const inject: never[];
13
- /** Session-level tool context handed to mail-core tool functions. */
2
+ /** Tool context handed to mail-core tool functions. */
14
3
  export interface MailToolCtx {
15
4
  systemId: string;
16
5
  email?: string;
17
6
  }
18
- /** The ctx.mail service surface. */
19
- export interface MailService {
20
- /** Optional explicit system scope (AMAIL_SYSTEM_ID); empty = scan all. */
21
- readonly systemId: string;
22
- /** Resolve config for a dsh session id (uuid). Throws when unbound. */
23
- resolveConfig(sessionId: string): Promise<AgentConfig>;
24
- /** Resolve config by agent email address (inbound routing). Throws when unbound. */
25
- resolveByEmail(email: string): Promise<AgentConfig>;
26
- /** Resolve a tool context for a session id. Throws when unbound. */
27
- resolveCtx(sessionId: string): Promise<MailToolCtx>;
28
- }
29
- /** Service identity used by consumers (ctx.get('mail')). */
30
- export declare const MAIL_SERVICE = "mail";
31
- export declare function apply(ctx: Context, config?: {
7
+ /** Resolution options shared by every resolver. */
8
+ export interface ResolveOptions {
9
+ /** Explicit system scope; defaults to the AIMAIL_SYSTEM_ID env (empty = scan all). */
32
10
  systemId?: string;
33
- }): void;
11
+ }
12
+ /**
13
+ * Resolve config for a platform session/agent id (dsh session uuid, or an
14
+ * openclaw agentId). Session-file match first, then agent_id field match.
15
+ * Throws when unbound.
16
+ */
17
+ export declare function resolveBySessionId(sessionId: string, opts?: ResolveOptions): Promise<AgentConfig>;
18
+ /** Resolve config by the agent's registered email address. Throws when unbound. */
19
+ export declare function resolveByEmail(email: string, opts?: ResolveOptions): Promise<AgentConfig>;
20
+ /**
21
+ * Inbound recipient routing (mirrors Python route_agent_for_email):
22
+ * 1. exact registered-address match
23
+ * 2. persona-prefix fallback: `persona.profile@…` → `profile@…`
24
+ * (single-in-multi-out platforms; PERSONA_SUPPORTED=false semantics)
25
+ * Returns the bound AgentConfig, or undefined when the recipient matches
26
+ * no agent (caller decides — typically `no_agent` intercept).
27
+ */
28
+ export declare function resolveByRecipient(email: string, opts?: ResolveOptions): Promise<AgentConfig | undefined>;
29
+ /** Tool context for a session id: {systemId, email}. Throws when unbound. */
30
+ export declare function resolveCtx(sessionId: string, opts?: ResolveOptions): Promise<MailToolCtx>;
34
31
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -1,40 +1,117 @@
1
- import { loadConfigByAgentId, loadConfigByEmail, loadConfigBySessionId, } from '@aimail/mail-core';
2
- export const name = 'mail';
3
- export const inject = [];
4
- /** Service identity used by consumers (ctx.get('mail')). */
5
- export const MAIL_SERVICE = 'mail';
6
- export function apply(ctx, config = {}) {
7
- const systemId = config.systemId ?? process.env.AMAIL_SYSTEM_ID ?? '';
8
- const service = {
9
- systemId,
10
- async resolveConfig(sessionId) {
11
- if (!sessionId)
12
- throw new Error('no session id to resolve agentmail config');
13
- let cfg = systemId
14
- ? await loadConfigBySessionId(systemId, sessionId)
15
- : await loadConfigBySessionId('', sessionId);
16
- if (!cfg && systemId) {
17
- cfg = await loadConfigByAgentId(systemId, sessionId);
18
- }
19
- if (!cfg) {
20
- throw new Error(`no agentmail binding for session ${sessionId} — run bind_agent.py first`);
1
+ /**
2
+ * @aimail/mail — platform-neutral AIMail config resolution.
3
+ *
4
+ * Pure functions: sessionId / email / agentId → agentmail.json → AgentConfig.
5
+ * No framework imports (no cordis, no dsh-sdk); each platform adapter
6
+ * (dsh-aimail, openclaw-aimail) binds these to its own identity source.
7
+ *
8
+ * Identity = agentmail.json only (single source of truth); the optional
9
+ * AIMAIL_SYSTEM_ID env narrows the scan scope. Unbound resolutions throw.
10
+ */
11
+ import { promises as fs } from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { cleanAddr, loadConfigByAgentId, loadConfigByEmail, loadConfigBySessionId, systemDir, } from '@aimail/mail-core';
14
+ function systemIdFrom(opts) {
15
+ return opts.systemId ?? process.env.AIMAIL_SYSTEM_ID ?? '';
16
+ }
17
+ function unbound(what) {
18
+ return new Error(`no aimail binding for ${what} — run bind_agent.py first`);
19
+ }
20
+ /** Scan one system dir (or all systems) for every bound AgentConfig. */
21
+ async function scanAllConfigs(systemId) {
22
+ const root = systemId ? systemDir(systemId) : systemDir('');
23
+ let names;
24
+ try {
25
+ const entries = await fs.readdir(root, { withFileTypes: true });
26
+ names = entries.filter(e => e.isDirectory()).map(e => e.name);
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ const out = [];
32
+ for (const name of names) {
33
+ const dir = systemId ? root : path.join(root, name);
34
+ let agentDirs;
35
+ try {
36
+ agentDirs = await fs.readdir(dir, { withFileTypes: true });
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ for (const ent of agentDirs) {
42
+ if (!ent.isDirectory())
43
+ continue;
44
+ const p = path.join(dir, ent.name, 'agentmail.json');
45
+ try {
46
+ out.push(JSON.parse(await fs.readFile(p, 'utf-8')));
21
47
  }
22
- return cfg;
23
- },
24
- async resolveCtx(sessionId) {
25
- const cfg = await service.resolveConfig(sessionId);
26
- return { systemId: cfg.system_id, email: cfg.email };
27
- },
28
- async resolveByEmail(email) {
29
- const cfg = systemId
30
- ? await loadConfigByEmail(email, systemId)
31
- : await loadConfigByEmail(email);
32
- if (!cfg) {
33
- throw new Error(`no agentmail binding for ${email} — run bind_agent.py first`);
48
+ catch {
49
+ /* skip unreadable */
34
50
  }
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ /**
56
+ * Resolve config for a platform session/agent id (dsh session uuid, or an
57
+ * openclaw agentId). Session-file match first, then agent_id field match.
58
+ * Throws when unbound.
59
+ */
60
+ export async function resolveBySessionId(sessionId, opts = {}) {
61
+ if (!sessionId)
62
+ throw new Error('no session id to resolve aimail config');
63
+ const systemId = systemIdFrom(opts);
64
+ let cfg = await loadConfigBySessionId(systemId, sessionId);
65
+ if (!cfg && systemId)
66
+ cfg = await loadConfigByAgentId(systemId, sessionId);
67
+ if (!cfg)
68
+ throw unbound(`session ${sessionId}`);
69
+ return cfg;
70
+ }
71
+ /** Resolve config by the agent's registered email address. Throws when unbound. */
72
+ export async function resolveByEmail(email, opts = {}) {
73
+ const systemId = systemIdFrom(opts);
74
+ const cfg = await loadConfigByEmail(email, systemId);
75
+ if (!cfg)
76
+ throw unbound(email);
77
+ return cfg;
78
+ }
79
+ /**
80
+ * Inbound recipient routing (mirrors Python route_agent_for_email):
81
+ * 1. exact registered-address match
82
+ * 2. persona-prefix fallback: `persona.profile@…` → `profile@…`
83
+ * (single-in-multi-out platforms; PERSONA_SUPPORTED=false semantics)
84
+ * Returns the bound AgentConfig, or undefined when the recipient matches
85
+ * no agent (caller decides — typically `no_agent` intercept).
86
+ */
87
+ export async function resolveByRecipient(email, opts = {}) {
88
+ const addr = cleanAddr(email);
89
+ if (!addr)
90
+ return undefined;
91
+ // local part from the ORIGINAL address (cleanAddr maps '@' → '_')
92
+ const local = email.includes('@') ? email.split('@')[0] ?? '' : '';
93
+ const systemId = systemIdFrom(opts);
94
+ const cfgs = await scanAllConfigs(systemId);
95
+ if (!cfgs.length)
96
+ return undefined;
97
+ const exact = cfgs.find(c => cleanAddr(c.email ?? '') === addr);
98
+ if (exact)
99
+ return exact;
100
+ // Persona strip: recipient local part ends with ".<registered local part>".
101
+ if (!local)
102
+ return undefined;
103
+ for (const cfg of cfgs) {
104
+ const baseLocal = (cfg.email ?? '').includes('@')
105
+ ? cfg.email.split('@')[0] ?? ''
106
+ : '';
107
+ if (baseLocal && local.endsWith(`.${baseLocal}`))
35
108
  return cfg;
36
- },
37
- };
38
- ctx.provide(MAIL_SERVICE, service);
109
+ }
110
+ return undefined;
111
+ }
112
+ /** Tool context for a session id: {systemId, email}. Throws when unbound. */
113
+ export async function resolveCtx(sessionId, opts = {}) {
114
+ const cfg = await resolveBySessionId(sessionId, opts);
115
+ return { systemId: cfg.system_id, email: cfg.email };
39
116
  }
40
117
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@aimail/mail",
3
- "description": "AgentMail host service: config resolution + gateway client provisioning for dsh sessions.",
4
- "version": "0.1.0-rc.8",
3
+ "description": "AIMail platform-neutral config resolution: sessionId/email/recipient → agentmail.json → AgentConfig (pure functions, no framework deps).",
4
+ "version": "0.1.7",
5
5
  "publishConfig": {
6
- "access": "public"
6
+ "access": "public",
7
+ "provenance": true
7
8
  },
8
9
  "repository": {
9
10
  "type": "git",
10
- "url": "git+https://github.com/metercai/dsh-aimail.git",
11
- "directory": "packages/mail"
11
+ "url": "https://github.com/metercai/aimail"
12
12
  },
13
13
  "type": "module",
14
14
  "main": "lib/index.js",
@@ -25,13 +25,16 @@
25
25
  "lib/**/*.d.ts"
26
26
  ],
27
27
  "license": "MIT",
28
- "peerDependencies": {
29
- "@deepseek-ai/cordis": "^4.0.1"
30
- },
28
+ "peerDependencies": {},
31
29
  "dependencies": {
32
- "@aimail/mail-core": "^0.1.0-rc.8"
30
+ "@aimail/mail-core": "^0.1.7"
33
31
  },
34
- "devDependencies": {
35
- "@deepseek-ai/cordis": "^4.0.1"
36
- }
37
- }
32
+ "devDependencies": {},
33
+ "keywords": [
34
+ "aimail",
35
+ "agent-mail",
36
+ "config",
37
+ "resolution",
38
+ "binding"
39
+ ]
40
+ }