@atomicmail/agent-skill 0.1.0 → 0.2.1

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 (64) hide show
  1. package/README.md +91 -0
  2. package/SKILL.md +53 -193
  3. package/esm/_dnt.polyfills.d.ts +101 -0
  4. package/esm/_dnt.polyfills.d.ts.map +1 -0
  5. package/esm/_dnt.polyfills.js +127 -0
  6. package/esm/{skill/scripts/lib/auth.d.ts → lib/agent/auth/agent-auth-http.d.ts} +1 -17
  7. package/esm/lib/agent/auth/agent-auth-http.d.ts.map +1 -0
  8. package/esm/lib/agent/auth/agent-auth-http.js +76 -0
  9. package/esm/lib/agent/auth/agent-jwt.d.ts +14 -0
  10. package/esm/lib/agent/auth/agent-jwt.d.ts.map +1 -0
  11. package/esm/lib/agent/auth/agent-jwt.js +29 -0
  12. package/esm/lib/agent/auth/agent-pow.d.ts +5 -0
  13. package/esm/lib/agent/auth/agent-pow.d.ts.map +1 -0
  14. package/esm/lib/agent/auth/agent-pow.js +49 -0
  15. package/esm/lib/agent/jmap/agent-help-content.d.ts +4 -0
  16. package/esm/lib/agent/jmap/agent-help-content.d.ts.map +1 -0
  17. package/esm/lib/agent/jmap/agent-help-content.js +244 -0
  18. package/esm/lib/agent/jmap/agent-jmap.d.ts +49 -0
  19. package/esm/lib/agent/jmap/agent-jmap.d.ts.map +1 -0
  20. package/esm/lib/agent/jmap/agent-jmap.js +174 -0
  21. package/esm/lib/agent/jmap/agent-vars.d.ts +23 -0
  22. package/esm/lib/agent/jmap/agent-vars.d.ts.map +1 -0
  23. package/esm/lib/agent/jmap/agent-vars.js +65 -0
  24. package/esm/{skill/scripts/lib/credentials.d.ts → lib/agent/session/agent-credentials-store.d.ts} +4 -1
  25. package/esm/lib/agent/session/agent-credentials-store.d.ts.map +1 -0
  26. package/esm/{skill/scripts/lib/credentials.js → lib/agent/session/agent-credentials-store.js} +28 -8
  27. package/esm/lib/agent/session/agent-resolve-config.d.ts +24 -0
  28. package/esm/lib/agent/session/agent-resolve-config.d.ts.map +1 -0
  29. package/esm/lib/agent/session/agent-resolve-config.js +70 -0
  30. package/esm/lib/agent/session/agent-session.d.ts +62 -0
  31. package/esm/lib/agent/session/agent-session.d.ts.map +1 -0
  32. package/esm/lib/agent/session/agent-session.js +206 -0
  33. package/esm/lib/core/consts.d.ts.map +1 -0
  34. package/esm/lib/core/types.d.ts +2 -0
  35. package/esm/lib/core/types.d.ts.map +1 -0
  36. package/esm/lib/core/types.js +1 -0
  37. package/esm/lib/core/utils.d.ts +10 -0
  38. package/esm/lib/core/utils.d.ts.map +1 -0
  39. package/esm/lib/core/utils.js +28 -0
  40. package/esm/lib/mod.d.ts +14 -0
  41. package/esm/lib/mod.d.ts.map +1 -0
  42. package/esm/lib/mod.js +13 -0
  43. package/esm/lib/network/auth-client.d.ts +57 -0
  44. package/esm/lib/network/auth-client.d.ts.map +1 -0
  45. package/esm/lib/network/auth-client.js +188 -0
  46. package/esm/skill/cli.d.ts +3 -0
  47. package/esm/skill/cli.d.ts.map +1 -0
  48. package/esm/skill/cli.js +306 -0
  49. package/package.json +5 -6
  50. package/presets/list_inbox.json +39 -0
  51. package/presets/reply.json +75 -0
  52. package/presets/send_mail.json +42 -0
  53. package/esm/lib/src/consts.d.ts.map +0 -1
  54. package/esm/skill/scripts/jmap_request.d.ts +0 -3
  55. package/esm/skill/scripts/jmap_request.d.ts.map +0 -1
  56. package/esm/skill/scripts/jmap_request.js +0 -265
  57. package/esm/skill/scripts/lib/auth.d.ts.map +0 -1
  58. package/esm/skill/scripts/lib/auth.js +0 -163
  59. package/esm/skill/scripts/lib/credentials.d.ts.map +0 -1
  60. package/esm/skill/scripts/signup.d.ts +0 -3
  61. package/esm/skill/scripts/signup.d.ts.map +0 -1
  62. package/esm/skill/scripts/signup.js +0 -170
  63. /package/esm/lib/{src → core}/consts.d.ts +0 -0
  64. /package/esm/lib/{src → core}/consts.js +0 -0
@@ -0,0 +1,57 @@
1
+ export interface AuthClientOptions {
2
+ /** Base URL of auth-service, e.g. "http://localhost:8000". Trailing slashes are stripped. */
3
+ baseUrl: string;
4
+ /**
5
+ * PoW scrypt salt (hex string). When omitted, {@link DEFAULT_POW_SCRYPT_SALT_HEX}
6
+ * is used so clients match the bundled auth-service.
7
+ */
8
+ scryptSaltHex?: string;
9
+ }
10
+ export interface SignupResult {
11
+ /** Freshly minted API key. The server only returns it once — persist it. */
12
+ apiKey: string;
13
+ sessionJWT: string;
14
+ }
15
+ export interface LoginResult {
16
+ sessionJWT: string;
17
+ }
18
+ export interface RenewResult {
19
+ capabilityJWT: string;
20
+ }
21
+ /** Thrown for any non-2xx HTTP response or malformed payload. */
22
+ export declare class AuthClientError extends Error {
23
+ status: number;
24
+ bodyText: string;
25
+ constructor(status: number, bodyText: string, message: string);
26
+ }
27
+ export declare class AuthClient {
28
+ private readonly baseUrl;
29
+ private readonly scryptSaltHex;
30
+ constructor(options: AuthClientOptions);
31
+ /**
32
+ * Register a new inbox under `username`. Returns the freshly minted API key
33
+ * (the server only ever returns it once — the caller MUST persist it) and
34
+ * a session JWT.
35
+ */
36
+ signup(username: string): Promise<SignupResult>;
37
+ /** Exchange an existing API key for a fresh session JWT. */
38
+ login(apiKey: string): Promise<LoginResult>;
39
+ /**
40
+ * Exchange a session JWT for a short-lived capability JWT (audience:
41
+ * api-service).
42
+ */
43
+ renew(sessionJWT: string): Promise<RenewResult>;
44
+ private fetchChallenge;
45
+ private postSession;
46
+ private parseJsonOrThrow;
47
+ /**
48
+ * Brute-force a PoW nonce. Mirrors `generatePow` in
49
+ * services/auth-service/src/crypto.ts: scrypt(`${challenge}:${nonce}`, salt,
50
+ * 64) until `difficulty` leading bits of the digest are zero.
51
+ *
52
+ * Expected work at the server's POW_DIFFICULTY=6 is ~2^6 = 64 attempts; well
53
+ * within the challenge JWT's 3-minute TTL.
54
+ */
55
+ private solvePoW;
56
+ }
57
+ //# sourceMappingURL=auth-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../../src/lib/network/auth-client.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,iEAAiE;AACjE,qBAAa,eAAgB,SAAQ,KAAK;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;gBAEL,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM9D;AAOD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,iBAAiB;IAKtC;;;;OAIG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAwBrD,4DAA4D;IACtD,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAqBjD;;;OAGG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;YAgBvC,cAAc;YAkCd,WAAW;YAeX,gBAAgB;IAuB9B;;;;;;;OAOG;YACW,QAAQ;CAgBvB"}
@@ -0,0 +1,188 @@
1
+ // auth-client
2
+ //
3
+ // Thin HTTP client for services/auth-service. Encapsulates the full PoW
4
+ // challenge → session → capability flow so callers (integration tests, the
5
+ // future agent skill, etc.) don't have to reimplement scrypt grinding.
6
+ //
7
+ // The PoW digest is scrypt-based and uses the SAME salt the auth-service
8
+ // uses on the verify path (see services/auth-service/src/crypto.ts). The
9
+ // client must therefore be configured with that salt — there is no public
10
+ // hash function here, the salt is part of the protocol.
11
+ import { scrypt } from "node:crypto";
12
+ import { DEFAULT_POW_SCRYPT_SALT_HEX } from "../core/consts.js";
13
+ // Mirror services/auth-service/src/crypto.ts exactly. Changing any of these
14
+ // constants on either side breaks PoW interop.
15
+ const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
16
+ const POW_HASH_BYTES = 64;
17
+ /** Thrown for any non-2xx HTTP response or malformed payload. */
18
+ export class AuthClientError extends Error {
19
+ status;
20
+ bodyText;
21
+ constructor(status, bodyText, message) {
22
+ super(message);
23
+ this.name = "AuthClientError";
24
+ this.status = status;
25
+ this.bodyText = bodyText;
26
+ }
27
+ }
28
+ export class AuthClient {
29
+ baseUrl;
30
+ scryptSaltHex;
31
+ constructor(options) {
32
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
33
+ this.scryptSaltHex = options.scryptSaltHex ?? DEFAULT_POW_SCRYPT_SALT_HEX;
34
+ }
35
+ /**
36
+ * Register a new inbox under `username`. Returns the freshly minted API key
37
+ * (the server only ever returns it once — the caller MUST persist it) and
38
+ * a session JWT.
39
+ */
40
+ async signup(username) {
41
+ const { challengeJWT, challenge, difficulty } = await this.fetchChallenge();
42
+ const { powHex, nonce } = await this.solvePoW(challenge, difficulty);
43
+ const data = await this.postSession({
44
+ challengeJWT,
45
+ powHex,
46
+ nonce: nonce.toString(),
47
+ username,
48
+ });
49
+ if (typeof data.apiKey !== "string" ||
50
+ typeof data.sessionJWT !== "string") {
51
+ throw new AuthClientError(200, JSON.stringify(data), "Signup response missing apiKey or sessionJWT.");
52
+ }
53
+ return { apiKey: data.apiKey, sessionJWT: data.sessionJWT };
54
+ }
55
+ /** Exchange an existing API key for a fresh session JWT. */
56
+ async login(apiKey) {
57
+ const { challengeJWT, challenge, difficulty } = await this.fetchChallenge();
58
+ const { powHex, nonce } = await this.solvePoW(challenge, difficulty);
59
+ const data = await this.postSession({
60
+ challengeJWT,
61
+ powHex,
62
+ nonce: nonce.toString(),
63
+ apiKey,
64
+ });
65
+ if (typeof data.sessionJWT !== "string") {
66
+ throw new AuthClientError(200, JSON.stringify(data), "Login response missing sessionJWT.");
67
+ }
68
+ return { sessionJWT: data.sessionJWT };
69
+ }
70
+ /**
71
+ * Exchange a session JWT for a short-lived capability JWT (audience:
72
+ * api-service).
73
+ */
74
+ async renew(sessionJWT) {
75
+ const res = await fetch(`${this.baseUrl}/api/v1/capability`, {
76
+ method: "POST",
77
+ headers: { Authorization: `Bearer ${sessionJWT}` },
78
+ });
79
+ const data = await this.parseJsonOrThrow(res, "capability");
80
+ if (typeof data.capabilityJWT !== "string") {
81
+ throw new AuthClientError(res.status, JSON.stringify(data), "Capability response missing capabilityJWT.");
82
+ }
83
+ return { capabilityJWT: data.capabilityJWT };
84
+ }
85
+ async fetchChallenge() {
86
+ const res = await fetch(`${this.baseUrl}/api/v1/challenge`, {
87
+ method: "POST",
88
+ });
89
+ const data = await this.parseJsonOrThrow(res, "challenge");
90
+ if (typeof data.challengeJWT !== "string") {
91
+ throw new AuthClientError(res.status, JSON.stringify(data), "Challenge response missing challengeJWT.");
92
+ }
93
+ const payload = decodeJwtPayload(data.challengeJWT);
94
+ if (typeof payload.jti !== "string" ||
95
+ typeof payload.difficulty !== "number") {
96
+ throw new AuthClientError(res.status, data.challengeJWT, "Challenge JWT payload is malformed (missing jti or difficulty).");
97
+ }
98
+ return {
99
+ challengeJWT: data.challengeJWT,
100
+ challenge: payload.jti,
101
+ difficulty: payload.difficulty,
102
+ };
103
+ }
104
+ async postSession(body) {
105
+ const res = await fetch(`${this.baseUrl}/api/v1/session`, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify(body),
109
+ });
110
+ return await this.parseJsonOrThrow(res, "session");
111
+ }
112
+ async parseJsonOrThrow(res, endpoint) {
113
+ const text = await res.text();
114
+ if (!res.ok) {
115
+ throw new AuthClientError(res.status, text, `auth-service ${endpoint} returned ${res.status}: ${text}`);
116
+ }
117
+ try {
118
+ return JSON.parse(text);
119
+ }
120
+ catch {
121
+ throw new AuthClientError(res.status, text, `auth-service ${endpoint} returned non-JSON body.`);
122
+ }
123
+ }
124
+ /**
125
+ * Brute-force a PoW nonce. Mirrors `generatePow` in
126
+ * services/auth-service/src/crypto.ts: scrypt(`${challenge}:${nonce}`, salt,
127
+ * 64) until `difficulty` leading bits of the digest are zero.
128
+ *
129
+ * Expected work at the server's POW_DIFFICULTY=6 is ~2^6 = 64 attempts; well
130
+ * within the challenge JWT's 3-minute TTL.
131
+ */
132
+ async solvePoW(challenge, difficulty) {
133
+ let nonce = 0n;
134
+ while (true) {
135
+ const digest = await scryptHash(`${challenge}:${nonce}`, this.scryptSaltHex);
136
+ if (hasLeadingZeroBits(digest, difficulty)) {
137
+ return { powHex: bytesToHex(digest), nonce };
138
+ }
139
+ nonce++;
140
+ }
141
+ }
142
+ }
143
+ function scryptHash(data, salt) {
144
+ const bytes = new TextEncoder().encode(data);
145
+ return new Promise((resolve, reject) => {
146
+ scrypt(bytes, salt, POW_HASH_BYTES, SCRYPT_PARAMS, (err, derived) => {
147
+ if (err)
148
+ return reject(err);
149
+ resolve(new Uint8Array(derived));
150
+ });
151
+ });
152
+ }
153
+ function hasLeadingZeroBits(hash, bits) {
154
+ if (bits > hash.length * 8)
155
+ return false;
156
+ const fullBytes = Math.floor(bits / 8);
157
+ const remainingBits = bits % 8;
158
+ for (let i = 0; i < fullBytes; i++) {
159
+ if (hash[i] !== 0)
160
+ return false;
161
+ }
162
+ if (remainingBits > 0) {
163
+ const mask = (0xff << (8 - remainingBits)) & 0xff;
164
+ if ((hash[fullBytes] & mask) !== 0)
165
+ return false;
166
+ }
167
+ return true;
168
+ }
169
+ function bytesToHex(bytes) {
170
+ let hex = "";
171
+ for (let i = 0; i < bytes.length; i++) {
172
+ hex += bytes[i].toString(16).padStart(2, "0");
173
+ }
174
+ return hex;
175
+ }
176
+ function decodeJwtPayload(jwt) {
177
+ const parts = jwt.split(".");
178
+ if (parts.length < 2) {
179
+ throw new Error("Malformed JWT: expected at least 2 dot-separated segments.");
180
+ }
181
+ const payloadB64Url = parts[1];
182
+ const padLen = (4 - (payloadB64Url.length % 4)) % 4;
183
+ const base64 = payloadB64Url
184
+ .replace(/-/g, "+")
185
+ .replace(/_/g, "/")
186
+ .padEnd(payloadB64Url.length + padLen, "=");
187
+ return JSON.parse(atob(base64));
188
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import "../_dnt.polyfills.js";
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/skill/cli.ts"],"names":[],"mappings":";AAEA,OAAO,sBAAsB,CAAC"}
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ // Atomic Mail AgentSkill — register | jmap_request | help
3
+ import "../_dnt.polyfills.js";
4
+ import process from "node:process";
5
+ import { parseArgs } from "node:util";
6
+ import { resolve } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { AgentSession, DEFAULT_JMAP_USING, DEFAULT_POW_SCRYPT_SALT_HEX, defaultFilesFromOutDir, getHelp, persistLoginWithApiKey, readCredentials, readOpsFile, runJmapRequest, } from "../lib/mod.js";
9
+ const USAGE = `Atomic Mail — AgentSkill
10
+
11
+ Usage:
12
+ atomicmail <command> [options]
13
+
14
+ Commands:
15
+ register PoW signup or login with API key (writes credentials)
16
+ jmap_request Send a JMAP batch (inline --ops or --ops-file preset)
17
+ help Full documentation [--topic TOPIC]
18
+
19
+ Examples:
20
+ atomicmail register --username alice
21
+ atomicmail register --api-key UUID
22
+ atomicmail jmap_request --credentials-dir ./.atomic-mail --ops-file fetch.json
23
+ atomicmail jmap_request --credentials-dir ./.atomic-mail --ops-file send.json --vars '{"TO":"a@b.com","SUBJECT":"Hi"}'
24
+ atomicmail help --topic presets
25
+
26
+ Run atomicmail <command> --help for command-specific flags.
27
+ `;
28
+ function exitUsage(code = 0) {
29
+ process.stdout.write(USAGE);
30
+ process.exit(code);
31
+ }
32
+ function fail(message, code = 1) {
33
+ process.stderr.write(`Error: ${message}\n`);
34
+ process.exit(code);
35
+ }
36
+ function resolveCredentialDir(dir) {
37
+ const raw = dir ?? process.env.ATOMIC_MAIL_CREDENTIALS_DIR ?? "~/.atomicmail";
38
+ if (raw === "~")
39
+ return homedir();
40
+ return resolve(raw.replace(/^~\//, `${homedir()}/`));
41
+ }
42
+ async function cmdRegister(argv) {
43
+ let parsed;
44
+ try {
45
+ parsed = parseArgs({
46
+ args: argv,
47
+ options: {
48
+ "auth-url": { type: "string" },
49
+ "api-url": { type: "string" },
50
+ "scrypt-salt": { type: "string" },
51
+ username: { type: "string" },
52
+ "api-key": { type: "string" },
53
+ "credentials-dir": { type: "string" },
54
+ quiet: { type: "boolean" },
55
+ help: { type: "boolean", short: "h" },
56
+ },
57
+ strict: true,
58
+ allowPositionals: false,
59
+ });
60
+ }
61
+ catch (err) {
62
+ fail(err.message, 2);
63
+ }
64
+ if (parsed.values.help) {
65
+ process.stdout.write(`Usage: atomicmail register [OPTIONS]
66
+
67
+ Register a new inbox (--username) or log in with an existing API key (--api-key).
68
+
69
+ Options:
70
+ --auth-url URL Auth-service base URL [env: ATOMIC_MAIL_AUTH_URL, default: https://auth.atomicmail.ai]
71
+ --api-url URL API / JMAP base URL [env: ATOMIC_MAIL_API_URL, default: https://api.atomicmail.ai]
72
+ --scrypt-salt SALT PoW salt override [env: ATOMIC_MAIL_SCRYPT_SALT]
73
+ --username NAME New account (mutually exclusive with --api-key)
74
+ --api-key KEY Existing API key (mutually exclusive with --username)
75
+ --credentials-dir DIR Credential directory (default: ~/.atomicmail)
76
+ --quiet Less stderr output
77
+ --help, -h This message
78
+ `);
79
+ process.exit(0);
80
+ }
81
+ const env = process.env;
82
+ const authUrl = parsed.values["auth-url"] ??
83
+ env.ATOMIC_MAIL_AUTH_URL ?? "https://auth.atomicmail.ai";
84
+ const apiUrl = parsed.values["api-url"] ??
85
+ env.ATOMIC_MAIL_API_URL ?? "https://api.atomicmail.ai";
86
+ const scryptSalt = parsed.values["scrypt-salt"] ??
87
+ env.ATOMIC_MAIL_SCRYPT_SALT ?? DEFAULT_POW_SCRYPT_SALT_HEX;
88
+ const dir = parsed.values["credentials-dir"];
89
+ const credentialDir = resolveCredentialDir(dir);
90
+ const username = parsed.values.username;
91
+ const apiKey = parsed.values["api-key"];
92
+ if (!!username === !!apiKey) {
93
+ fail("Provide exactly one of --username (new account) or --api-key (login).", 2);
94
+ }
95
+ const files = defaultFilesFromOutDir(credentialDir);
96
+ const log = (msg) => {
97
+ if (!parsed.values.quiet)
98
+ process.stderr.write(msg + "\n");
99
+ };
100
+ if (username) {
101
+ log(`Registering "${username}"...`);
102
+ const session = await AgentSession.create({
103
+ authUrl,
104
+ apiUrl,
105
+ scryptSalt,
106
+ credentialDir,
107
+ files,
108
+ });
109
+ const result = await session.register(username);
110
+ log(`Wrote credentials under ${credentialDir}`);
111
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
112
+ return;
113
+ }
114
+ log("Logging in with API key...");
115
+ const { inboxId } = await persistLoginWithApiKey({
116
+ authUrl,
117
+ apiUrl,
118
+ scryptSalt,
119
+ apiKey: apiKey,
120
+ files,
121
+ });
122
+ log(`Wrote ${files.credentialsFile}`);
123
+ process.stdout.write(JSON.stringify({ inboxId }, null, 2) + "\n");
124
+ }
125
+ async function cmdJmapRequest(argv) {
126
+ let parsed;
127
+ try {
128
+ parsed = parseArgs({
129
+ args: argv,
130
+ options: {
131
+ "credentials-dir": { type: "string" },
132
+ "credentials-file": { type: "string" },
133
+ "session-file": { type: "string" },
134
+ "capability-file": { type: "string" },
135
+ ops: { type: "string" },
136
+ "ops-file": { type: "string" },
137
+ using: { type: "string" },
138
+ "dry-run": { type: "boolean" },
139
+ vars: { type: "string" },
140
+ help: { type: "boolean", short: "h" },
141
+ },
142
+ strict: true,
143
+ allowPositionals: false,
144
+ });
145
+ }
146
+ catch (err) {
147
+ fail(err.message, 2);
148
+ }
149
+ if (parsed.values.help) {
150
+ process.stdout.write(`Usage: atomicmail jmap_request [OPTIONS]
151
+
152
+ Send a JMAP request using saved credentials.
153
+
154
+ Options:
155
+ --credentials-dir DIR Directory with credentials.json + JWTs (default: ~/.atomicmail)
156
+ --credentials-file PATH Override credentials.json path
157
+ --session-file PATH Override session.jwt path
158
+ --capability-file PATH Override capability.jwt path
159
+ --ops JSON Inline JMAP JSON (methodCalls or envelope)
160
+ --ops-file PATH Preset file ($VAR_NAME placeholders supported)
161
+ --vars JSON JSON object { VAR_NAME: string } for $VAR_NAME in ops / ops-file
162
+ --using LIST Comma-separated capability URNs (optional)
163
+ --dry-run Print resolved request only
164
+ --help, -h This message
165
+ `);
166
+ process.exit(0);
167
+ }
168
+ const dir = parsed.values["credentials-dir"];
169
+ const credentialDir = resolveCredentialDir(dir);
170
+ const defaults = defaultFilesFromOutDir(credentialDir);
171
+ const credentialsFile = parsed.values["credentials-file"] ??
172
+ defaults.credentialsFile;
173
+ const sessionFile = parsed.values["session-file"] ??
174
+ defaults.sessionFile;
175
+ const capabilityFile = parsed.values["capability-file"] ??
176
+ defaults.capabilityFile;
177
+ const ops = parsed.values.ops;
178
+ const opsFile = parsed.values["ops-file"];
179
+ if (ops && opsFile) {
180
+ fail("--ops and --ops-file are mutually exclusive.", 2);
181
+ }
182
+ if (!ops && !opsFile) {
183
+ fail("Provide --ops or --ops-file.", 2);
184
+ }
185
+ const usingFlag = parsed.values.using;
186
+ const defaultUsing = usingFlag
187
+ ? usingFlag.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
188
+ : [...DEFAULT_JMAP_USING];
189
+ let userVars;
190
+ const varsFlag = parsed.values.vars;
191
+ if (varsFlag !== undefined) {
192
+ let obj;
193
+ try {
194
+ obj = JSON.parse(varsFlag);
195
+ }
196
+ catch (err) {
197
+ fail(`--vars is not valid JSON: ${err.message}`, 2);
198
+ }
199
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
200
+ fail("--vars must be a JSON object of { VAR_NAME: string }.", 2);
201
+ }
202
+ for (const [k, v] of Object.entries(obj)) {
203
+ if (!/^[A-Z][A-Z0-9_]*$/.test(k)) {
204
+ fail(`--vars key '${k}' must match /^[A-Z][A-Z0-9_]*$/.`, 2);
205
+ }
206
+ if (typeof v !== "string") {
207
+ fail(`--vars value for '${k}' must be a string.`, 2);
208
+ }
209
+ }
210
+ userVars = obj;
211
+ }
212
+ const creds = await readCredentials(credentialsFile);
213
+ const files = {
214
+ credentialsFile,
215
+ sessionFile,
216
+ capabilityFile,
217
+ };
218
+ const session = await AgentSession.create({
219
+ authUrl: creds.authUrl,
220
+ apiUrl: creds.apiUrl,
221
+ scryptSalt: creds.scryptSalt,
222
+ apiKey: creds.apiKey,
223
+ inboxId: creds.inboxId,
224
+ credentialDir,
225
+ files,
226
+ });
227
+ let raw;
228
+ let sourceLabel;
229
+ if (opsFile) {
230
+ try {
231
+ raw = await readOpsFile(credentialDir, opsFile);
232
+ }
233
+ catch (err) {
234
+ fail(`Could not read --ops-file: ${err.message}`, 2);
235
+ }
236
+ sourceLabel = opsFile;
237
+ }
238
+ else {
239
+ raw = ops;
240
+ sourceLabel = "ops";
241
+ }
242
+ const { ok, status, bodyText } = await runJmapRequest({
243
+ session,
244
+ opsJson: raw,
245
+ defaultUsing,
246
+ sourceLabel,
247
+ dryRun: parsed.values["dry-run"] === true,
248
+ vars: userVars,
249
+ });
250
+ if (!ok) {
251
+ fail(`JMAP request failed (HTTP ${status}): ${bodyText}`, 1);
252
+ }
253
+ process.stdout.write(bodyText.endsWith("\n") ? bodyText : bodyText + "\n");
254
+ }
255
+ function cmdHelp(argv) {
256
+ let parsed;
257
+ try {
258
+ parsed = parseArgs({
259
+ args: argv,
260
+ options: {
261
+ topic: { type: "string" },
262
+ help: { type: "boolean", short: "h" },
263
+ },
264
+ strict: true,
265
+ allowPositionals: false,
266
+ });
267
+ }
268
+ catch (err) {
269
+ fail(err.message, 2);
270
+ }
271
+ if (parsed.values.help) {
272
+ process.stdout.write(`Usage: atomicmail help [--topic TOPIC]
273
+
274
+ Topics include: overview, installation, auth, jmap_cheatsheet, tools, presets, troubleshooting.
275
+ `);
276
+ process.exit(0);
277
+ }
278
+ const topic = parsed.values.topic;
279
+ process.stdout.write(getHelp(topic) + "\n");
280
+ }
281
+ async function main() {
282
+ const argv = process.argv.slice(2);
283
+ if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
284
+ exitUsage(0);
285
+ }
286
+ const cmd = argv[0];
287
+ const rest = argv.slice(1);
288
+ switch (cmd) {
289
+ case "register":
290
+ await cmdRegister(rest);
291
+ break;
292
+ case "jmap_request":
293
+ await cmdJmapRequest(rest);
294
+ break;
295
+ case "help":
296
+ cmdHelp(rest);
297
+ break;
298
+ default:
299
+ process.stderr.write(`Unknown command: ${cmd}\n\n`);
300
+ process.stdout.write(USAGE);
301
+ process.exit(2);
302
+ }
303
+ }
304
+ main().catch((err) => {
305
+ fail(err instanceof Error ? err.message : String(err));
306
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atomicmail/agent-skill",
3
- "version": "0.1.0",
4
- "description": "Atomic Mail agent skill PoW signup + JMAP request CLIs for AI agents.",
3
+ "version": "0.2.1",
4
+ "description": "Atomic Mail AgentSkillregister, jmap_request, and help CLI for AI agents.",
5
5
  "keywords": [
6
6
  "atomic-mail",
7
7
  "atomicmail",
@@ -16,16 +16,15 @@
16
16
  ],
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/atomic-mail/agentic-mail.git"
19
+ "url": "git+https://github.com/atomic-mail/agentic-clients.git"
20
20
  },
21
21
  "license": "MIT",
22
22
  "bugs": {
23
- "url": "https://github.com/atomic-mail/agentic-mail/issues"
23
+ "url": "https://github.com/atomic-mail/agentic-clients/issues"
24
24
  },
25
25
  "scripts": {},
26
26
  "bin": {
27
- "atomic-mail-signup": "./esm/skill/scripts/signup.js",
28
- "atomic-mail-jmap": "./esm/skill/scripts/jmap_request.js"
27
+ "atomicmail": "./esm/skill/cli.js"
29
28
  },
30
29
  "engines": {
31
30
  "node": ">=20"
@@ -0,0 +1,39 @@
1
+ {
2
+ "using": [
3
+ "urn:ietf:params:jmap:core",
4
+ "urn:ietf:params:jmap:mail"
5
+ ],
6
+ "methodCalls": [
7
+ [
8
+ "Email/query",
9
+ {
10
+ "accountId": "$ACCOUNT_ID",
11
+ "filter": { "inMailbox": "$INBOX" },
12
+ "sort": [{ "property": "receivedAt", "isAscending": false }],
13
+ "limit": "$COUNT"
14
+ },
15
+ "q0"
16
+ ],
17
+ [
18
+ "Email/get",
19
+ {
20
+ "accountId": "$ACCOUNT_ID",
21
+ "#ids": {
22
+ "resultOf": "q0",
23
+ "name": "Email/query",
24
+ "path": "/ids"
25
+ },
26
+ "properties": [
27
+ "id",
28
+ "threadId",
29
+ "receivedAt",
30
+ "from",
31
+ "to",
32
+ "subject",
33
+ "preview"
34
+ ]
35
+ },
36
+ "g0"
37
+ ]
38
+ ]
39
+ }