@atomicmail/agent-skill 0.1.0 → 0.2.0
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 +56 -0
- package/SKILL.md +67 -187
- package/esm/{skill/scripts/lib/auth.d.ts → lib/src/agent-auth-http.d.ts} +1 -17
- package/esm/lib/src/agent-auth-http.d.ts.map +1 -0
- package/esm/lib/src/agent-auth-http.js +76 -0
- package/esm/{skill/scripts/lib/credentials.d.ts → lib/src/agent-credentials-store.d.ts} +4 -1
- package/esm/lib/src/agent-credentials-store.d.ts.map +1 -0
- package/esm/{skill/scripts/lib/credentials.js → lib/src/agent-credentials-store.js} +28 -8
- package/esm/lib/src/agent-help-content.d.ts +4 -0
- package/esm/lib/src/agent-help-content.d.ts.map +1 -0
- package/esm/lib/src/agent-help-content.js +236 -0
- package/esm/lib/src/agent-jmap.d.ts +49 -0
- package/esm/lib/src/agent-jmap.d.ts.map +1 -0
- package/esm/lib/src/agent-jmap.js +130 -0
- package/esm/lib/src/agent-jwt.d.ts +14 -0
- package/esm/lib/src/agent-jwt.d.ts.map +1 -0
- package/esm/lib/src/agent-jwt.js +29 -0
- package/esm/lib/src/agent-pow.d.ts +5 -0
- package/esm/lib/src/agent-pow.d.ts.map +1 -0
- package/esm/lib/src/agent-pow.js +49 -0
- package/esm/lib/src/agent-session.d.ts +62 -0
- package/esm/lib/src/agent-session.d.ts.map +1 -0
- package/esm/lib/src/agent-session.js +206 -0
- package/esm/lib/src/agent-vars.d.ts +23 -0
- package/esm/lib/src/agent-vars.d.ts.map +1 -0
- package/esm/lib/src/agent-vars.js +65 -0
- package/esm/skill/scripts/cli.d.ts +3 -0
- package/esm/skill/scripts/cli.d.ts.map +1 -0
- package/esm/skill/scripts/cli.js +309 -0
- package/package.json +3 -4
- package/esm/skill/scripts/jmap_request.d.ts +0 -3
- package/esm/skill/scripts/jmap_request.d.ts.map +0 -1
- package/esm/skill/scripts/jmap_request.js +0 -265
- package/esm/skill/scripts/lib/auth.d.ts.map +0 -1
- package/esm/skill/scripts/lib/auth.js +0 -163
- package/esm/skill/scripts/lib/credentials.d.ts.map +0 -1
- package/esm/skill/scripts/signup.d.ts +0 -3
- package/esm/skill/scripts/signup.d.ts.map +0 -1
- package/esm/skill/scripts/signup.js +0 -170
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Stateful PoW + capability JWT + optional cached JMAP session (accountId).
|
|
2
|
+
import { tryReadCredentials, tryReadJwtFile, unlinkCredentialArtifacts, writeCredentials, writeJwtFile, } from "./agent-credentials-store.js";
|
|
3
|
+
import { CAPABILITY_SAFETY_MARGIN_MS, decodeJwtPayload, isJwtExpired, SESSION_SAFETY_MARGIN_MS, } from "./agent-jwt.js";
|
|
4
|
+
import { extractPrimaryMailAccountId, fetchJmapWellKnown, } from "./agent-jmap.js";
|
|
5
|
+
import { fetchCapability, performPoWAndSession } from "./agent-auth-http.js";
|
|
6
|
+
function normalizeUsername(u) {
|
|
7
|
+
return u.trim().toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
/** Local-part of an inbox email, or the whole string if no @. */
|
|
10
|
+
export function inboxLocalPart(inboxId) {
|
|
11
|
+
const i = inboxId.indexOf("@");
|
|
12
|
+
return i === -1
|
|
13
|
+
? normalizeUsername(inboxId)
|
|
14
|
+
: normalizeUsername(inboxId.slice(0, i));
|
|
15
|
+
}
|
|
16
|
+
export class AgentSession {
|
|
17
|
+
authUrl;
|
|
18
|
+
apiUrl;
|
|
19
|
+
scryptSalt;
|
|
20
|
+
apiKey;
|
|
21
|
+
inboxId;
|
|
22
|
+
credentialDir;
|
|
23
|
+
files;
|
|
24
|
+
sessionJWT;
|
|
25
|
+
capabilityJWT;
|
|
26
|
+
cachedMailAccountId;
|
|
27
|
+
constructor(cfg) {
|
|
28
|
+
this.authUrl = cfg.authUrl.replace(/\/+$/, "");
|
|
29
|
+
this.apiUrl = cfg.apiUrl.replace(/\/+$/, "");
|
|
30
|
+
this.scryptSalt = cfg.scryptSalt;
|
|
31
|
+
this.apiKey = cfg.apiKey;
|
|
32
|
+
this.inboxId = cfg.inboxId;
|
|
33
|
+
this.credentialDir = cfg.credentialDir;
|
|
34
|
+
this.files = cfg.files;
|
|
35
|
+
}
|
|
36
|
+
static async create(cfg) {
|
|
37
|
+
const session = new AgentSession(cfg);
|
|
38
|
+
await session.loadFromDisk();
|
|
39
|
+
return session;
|
|
40
|
+
}
|
|
41
|
+
get hasApiKey() {
|
|
42
|
+
return this.apiKey !== undefined && this.apiKey.length > 0;
|
|
43
|
+
}
|
|
44
|
+
get currentInboxId() {
|
|
45
|
+
return this.inboxId;
|
|
46
|
+
}
|
|
47
|
+
async loadFromDisk() {
|
|
48
|
+
this.sessionJWT = await tryReadJwtFile(this.files.sessionFile);
|
|
49
|
+
this.capabilityJWT = await tryReadJwtFile(this.files.capabilityFile);
|
|
50
|
+
const disk = await tryReadCredentials(this.files.credentialsFile);
|
|
51
|
+
if (disk) {
|
|
52
|
+
this.apiKey = this.apiKey ?? disk.apiKey;
|
|
53
|
+
this.inboxId = this.inboxId ?? disk.inboxId;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Primary JMAP mail accountId from GET /.well-known/jmap (cached).
|
|
58
|
+
*/
|
|
59
|
+
async getPrimaryMailAccountId() {
|
|
60
|
+
if (this.cachedMailAccountId)
|
|
61
|
+
return this.cachedMailAccountId;
|
|
62
|
+
const cap = await this.getCapabilityToken();
|
|
63
|
+
const session = await fetchJmapWellKnown(this.apiUrl, cap);
|
|
64
|
+
const id = extractPrimaryMailAccountId(session);
|
|
65
|
+
this.cachedMailAccountId = id;
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
invalidateJmapSessionCache() {
|
|
69
|
+
this.cachedMailAccountId = undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Register or return existing inbox when username matches (idempotent).
|
|
73
|
+
* Different username replaces on-disk credentials and creates a new inbox.
|
|
74
|
+
*/
|
|
75
|
+
async register(username) {
|
|
76
|
+
const want = normalizeUsername(username);
|
|
77
|
+
if (this.hasApiKey && !this.inboxId) {
|
|
78
|
+
throw new Error("Cannot register: an API key is configured but inboxId is unknown. " +
|
|
79
|
+
"Fix credentials.json or unset ATOMIC_MAIL_API_KEY before registering.");
|
|
80
|
+
}
|
|
81
|
+
if (this.hasApiKey && this.inboxId) {
|
|
82
|
+
const have = inboxLocalPart(this.inboxId);
|
|
83
|
+
if (have === want) {
|
|
84
|
+
const accountId = await this.getPrimaryMailAccountId();
|
|
85
|
+
return {
|
|
86
|
+
inbox: this.inboxId,
|
|
87
|
+
accountId,
|
|
88
|
+
idempotent: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
await unlinkCredentialArtifacts(this.files);
|
|
92
|
+
this.apiKey = undefined;
|
|
93
|
+
this.inboxId = undefined;
|
|
94
|
+
this.sessionJWT = undefined;
|
|
95
|
+
this.capabilityJWT = undefined;
|
|
96
|
+
this.cachedMailAccountId = undefined;
|
|
97
|
+
}
|
|
98
|
+
const result = await performPoWAndSession({
|
|
99
|
+
authUrl: this.authUrl,
|
|
100
|
+
scryptSalt: this.scryptSalt,
|
|
101
|
+
username,
|
|
102
|
+
});
|
|
103
|
+
if (!result.apiKey) {
|
|
104
|
+
throw new Error("Signup did not return an apiKey — this indicates a server bug.");
|
|
105
|
+
}
|
|
106
|
+
this.apiKey = result.apiKey;
|
|
107
|
+
this.sessionJWT = result.sessionJWT;
|
|
108
|
+
await writeJwtFile(this.files.sessionFile, this.sessionJWT);
|
|
109
|
+
const capability = await fetchCapability(this.authUrl, this.sessionJWT);
|
|
110
|
+
this.capabilityJWT = capability;
|
|
111
|
+
await writeJwtFile(this.files.capabilityFile, capability);
|
|
112
|
+
const claims = decodeJwtPayload(capability);
|
|
113
|
+
if (typeof claims.inboxId !== "string" || claims.inboxId.length === 0) {
|
|
114
|
+
throw new Error("Capability JWT missing inboxId claim after signup.");
|
|
115
|
+
}
|
|
116
|
+
this.inboxId = claims.inboxId;
|
|
117
|
+
this.cachedMailAccountId = undefined;
|
|
118
|
+
const creds = {
|
|
119
|
+
apiKey: this.apiKey,
|
|
120
|
+
inboxId: this.inboxId,
|
|
121
|
+
authUrl: this.authUrl,
|
|
122
|
+
apiUrl: this.apiUrl,
|
|
123
|
+
scryptSalt: this.scryptSalt,
|
|
124
|
+
};
|
|
125
|
+
await writeCredentials(this.files.credentialsFile, creds);
|
|
126
|
+
const accountId = await this.getPrimaryMailAccountId();
|
|
127
|
+
return {
|
|
128
|
+
inbox: this.inboxId,
|
|
129
|
+
accountId,
|
|
130
|
+
apiKey: this.apiKey,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async getCapabilityToken() {
|
|
134
|
+
if (this.capabilityJWT &&
|
|
135
|
+
!isJwtExpired(this.capabilityJWT, CAPABILITY_SAFETY_MARGIN_MS)) {
|
|
136
|
+
return this.capabilityJWT;
|
|
137
|
+
}
|
|
138
|
+
await this.ensureSession();
|
|
139
|
+
if (!this.sessionJWT) {
|
|
140
|
+
throw new Error("Internal: ensureSession() left sessionJWT unset.");
|
|
141
|
+
}
|
|
142
|
+
const cap = await fetchCapability(this.authUrl, this.sessionJWT);
|
|
143
|
+
this.capabilityJWT = cap;
|
|
144
|
+
await writeJwtFile(this.files.capabilityFile, cap);
|
|
145
|
+
try {
|
|
146
|
+
const claims = decodeJwtPayload(cap);
|
|
147
|
+
if (typeof claims.inboxId === "string" && claims.inboxId.length > 0) {
|
|
148
|
+
this.inboxId = claims.inboxId;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// non-fatal
|
|
153
|
+
}
|
|
154
|
+
return cap;
|
|
155
|
+
}
|
|
156
|
+
async ensureSession() {
|
|
157
|
+
if (this.sessionJWT &&
|
|
158
|
+
!isJwtExpired(this.sessionJWT, SESSION_SAFETY_MARGIN_MS)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!this.apiKey) {
|
|
162
|
+
throw new Error("No API key configured and no valid session on disk. Run register " +
|
|
163
|
+
"first, set ATOMIC_MAIL_API_KEY, or place credentials.json in the " +
|
|
164
|
+
"credential directory.");
|
|
165
|
+
}
|
|
166
|
+
const result = await performPoWAndSession({
|
|
167
|
+
authUrl: this.authUrl,
|
|
168
|
+
scryptSalt: this.scryptSalt,
|
|
169
|
+
apiKey: this.apiKey,
|
|
170
|
+
});
|
|
171
|
+
this.sessionJWT = result.sessionJWT;
|
|
172
|
+
this.capabilityJWT = undefined;
|
|
173
|
+
this.cachedMailAccountId = undefined;
|
|
174
|
+
await writeJwtFile(this.files.sessionFile, this.sessionJWT);
|
|
175
|
+
}
|
|
176
|
+
destroy() {
|
|
177
|
+
// reserved
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** PoW login with an existing API key; writes credentials + JWT files. */
|
|
181
|
+
export async function persistLoginWithApiKey(input) {
|
|
182
|
+
const authUrl = input.authUrl.replace(/\/+$/, "");
|
|
183
|
+
const apiUrl = input.apiUrl.replace(/\/+$/, "");
|
|
184
|
+
const session = await performPoWAndSession({
|
|
185
|
+
authUrl,
|
|
186
|
+
scryptSalt: input.scryptSalt,
|
|
187
|
+
apiKey: input.apiKey,
|
|
188
|
+
onPowProgress: input.onPowProgress,
|
|
189
|
+
});
|
|
190
|
+
const capabilityJWT = await fetchCapability(authUrl, session.sessionJWT);
|
|
191
|
+
const claims = decodeJwtPayload(capabilityJWT);
|
|
192
|
+
const inboxId = claims.inboxId;
|
|
193
|
+
if (typeof inboxId !== "string" || inboxId.length === 0) {
|
|
194
|
+
throw new Error("Capability JWT did not contain an inboxId claim.");
|
|
195
|
+
}
|
|
196
|
+
await writeCredentials(input.files.credentialsFile, {
|
|
197
|
+
apiKey: input.apiKey,
|
|
198
|
+
inboxId,
|
|
199
|
+
authUrl,
|
|
200
|
+
apiUrl,
|
|
201
|
+
scryptSalt: input.scryptSalt,
|
|
202
|
+
});
|
|
203
|
+
await writeJwtFile(input.files.sessionFile, session.sessionJWT);
|
|
204
|
+
await writeJwtFile(input.files.capabilityFile, capabilityJWT);
|
|
205
|
+
return { inboxId };
|
|
206
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Matches `$FOO_BAR`; excludes JMAP keywords like `$draft` (lowercase). */
|
|
2
|
+
export declare const VAR_PATTERN: RegExp;
|
|
3
|
+
/** Names substituted from JMAP session / credentials when not overridden in `vars`. */
|
|
4
|
+
export declare const SESSION_VAR_NAMES: Set<string>;
|
|
5
|
+
export interface SubstituteVarsInput {
|
|
6
|
+
raw: string;
|
|
7
|
+
/** Caller-supplied values; keys are names without `$` (e.g. `TO`, `SUBJECT`). */
|
|
8
|
+
vars?: Record<string, string>;
|
|
9
|
+
/** Invoked only when the name appears in `raw`, is absent from `vars`, and a resolver exists. */
|
|
10
|
+
autoResolvers?: Record<string, () => Promise<string> | string>;
|
|
11
|
+
}
|
|
12
|
+
export interface SubstituteVarsResult {
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
/** Unique variable names in order of first occurrence (without leading `$`). */
|
|
16
|
+
export declare function findVarReferences(raw: string): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Replaces every `$VAR_NAME` in `raw` with the corresponding string.
|
|
19
|
+
* Single pass — values are not scanned for further `$` tokens.
|
|
20
|
+
* Throws if any referenced variable has no value (after vars + autoResolvers).
|
|
21
|
+
*/
|
|
22
|
+
export declare function substituteVars(input: SubstituteVarsInput): Promise<SubstituteVarsResult>;
|
|
23
|
+
//# sourceMappingURL=agent-vars.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-vars.d.ts","sourceRoot":"","sources":["../../../src/lib/src/agent-vars.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,eAAO,MAAM,WAAW,QAAyB,CAAC;AAMlD,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,aAA2C,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,iGAAiG;IACjG,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAWvD;AAeD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CA+B/B"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Variable substitution for JMAP presets / inline ops ($VAR_NAME tokens).
|
|
2
|
+
/** Matches `$FOO_BAR`; excludes JMAP keywords like `$draft` (lowercase). */
|
|
3
|
+
export const VAR_PATTERN = /\$([A-Z][A-Z0-9_]*)/g;
|
|
4
|
+
function varPattern() {
|
|
5
|
+
return new RegExp(VAR_PATTERN.source, VAR_PATTERN.flags);
|
|
6
|
+
}
|
|
7
|
+
/** Names substituted from JMAP session / credentials when not overridden in `vars`. */
|
|
8
|
+
export const SESSION_VAR_NAMES = new Set(["ACCOUNT_ID", "INBOX"]);
|
|
9
|
+
/** Unique variable names in order of first occurrence (without leading `$`). */
|
|
10
|
+
export function findVarReferences(raw) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const order = [];
|
|
13
|
+
for (const m of raw.matchAll(varPattern())) {
|
|
14
|
+
const name = m[1];
|
|
15
|
+
if (!seen.has(name)) {
|
|
16
|
+
seen.add(name);
|
|
17
|
+
order.push(name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return order;
|
|
21
|
+
}
|
|
22
|
+
function formatMissingError(missing) {
|
|
23
|
+
const tokens = missing.map((n) => `$${n}`);
|
|
24
|
+
const hasSession = missing.some((n) => SESSION_VAR_NAMES.has(n));
|
|
25
|
+
let msg = `Missing values for variables: ${tokens.join(", ")}. ` +
|
|
26
|
+
"Pass custom placeholders in vars (MCP) or --vars (skill).";
|
|
27
|
+
if (hasSession) {
|
|
28
|
+
msg +=
|
|
29
|
+
" For $ACCOUNT_ID and $INBOX, ensure register completed and credentials are valid, " +
|
|
30
|
+
"or pass overrides in vars.";
|
|
31
|
+
}
|
|
32
|
+
return new Error(msg);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Replaces every `$VAR_NAME` in `raw` with the corresponding string.
|
|
36
|
+
* Single pass — values are not scanned for further `$` tokens.
|
|
37
|
+
* Throws if any referenced variable has no value (after vars + autoResolvers).
|
|
38
|
+
*/
|
|
39
|
+
export async function substituteVars(input) {
|
|
40
|
+
const names = findVarReferences(input.raw);
|
|
41
|
+
if (names.length === 0) {
|
|
42
|
+
return { text: input.raw };
|
|
43
|
+
}
|
|
44
|
+
const userVars = input.vars ?? {};
|
|
45
|
+
const resolved = new Map();
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
if (Object.prototype.hasOwnProperty.call(userVars, name)) {
|
|
48
|
+
resolved.set(name, userVars[name]);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const resolver = input.autoResolvers?.[name];
|
|
52
|
+
if (resolver) {
|
|
53
|
+
resolved.set(name, await resolver());
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const missing = names.filter((n) => !resolved.has(n));
|
|
58
|
+
if (missing.length > 0) {
|
|
59
|
+
throw formatMissingError(missing);
|
|
60
|
+
}
|
|
61
|
+
const text = input.raw.replace(varPattern(), (_full, name) => {
|
|
62
|
+
return resolved.get(name);
|
|
63
|
+
});
|
|
64
|
+
return { text };
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../../src/skill/scripts/cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Atomic Mail AgentSkill — register | jmap_request | help
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { DEFAULT_POW_SCRYPT_SALT_HEX } from "../../lib/src/consts.js";
|
|
8
|
+
import { getHelp } from "../../lib/src/agent-help-content.js";
|
|
9
|
+
import { DEFAULT_JMAP_USING, readOpsFile, runJmapRequest, } from "../../lib/src/agent-jmap.js";
|
|
10
|
+
import { AgentSession, persistLoginWithApiKey, } from "../../lib/src/agent-session.js";
|
|
11
|
+
import { defaultFilesFromOutDir, readCredentials, } from "../../lib/src/agent-credentials-store.js";
|
|
12
|
+
const USAGE = `Atomic Mail — AgentSkill
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
atomicmail <command> [options]
|
|
16
|
+
|
|
17
|
+
Commands:
|
|
18
|
+
register PoW signup or login with API key (writes credentials)
|
|
19
|
+
jmap_request Send a JMAP batch (inline --ops or --ops-file preset)
|
|
20
|
+
help Full documentation [--topic TOPIC]
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
atomicmail register --username alice
|
|
24
|
+
atomicmail register --api-key UUID
|
|
25
|
+
atomicmail jmap_request --credentials-dir ./.atomic-mail --ops-file fetch.json
|
|
26
|
+
atomicmail jmap_request --credentials-dir ./.atomic-mail --ops-file send.json --vars '{"TO":"a@b.com","SUBJECT":"Hi"}'
|
|
27
|
+
atomicmail help --topic presets
|
|
28
|
+
|
|
29
|
+
Run atomicmail <command> --help for command-specific flags.
|
|
30
|
+
`;
|
|
31
|
+
function exitUsage(code = 0) {
|
|
32
|
+
process.stdout.write(USAGE);
|
|
33
|
+
process.exit(code);
|
|
34
|
+
}
|
|
35
|
+
function fail(message, code = 1) {
|
|
36
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
37
|
+
process.exit(code);
|
|
38
|
+
}
|
|
39
|
+
function resolveCredentialDir(dir) {
|
|
40
|
+
const raw = dir ?? process.env.ATOMIC_MAIL_CREDENTIALS_DIR ?? "~/.atomicmail";
|
|
41
|
+
if (raw === "~")
|
|
42
|
+
return homedir();
|
|
43
|
+
return resolve(raw.replace(/^~\//, `${homedir()}/`));
|
|
44
|
+
}
|
|
45
|
+
async function cmdRegister(argv) {
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = parseArgs({
|
|
49
|
+
args: argv,
|
|
50
|
+
options: {
|
|
51
|
+
"auth-url": { type: "string" },
|
|
52
|
+
"api-url": { type: "string" },
|
|
53
|
+
"scrypt-salt": { type: "string" },
|
|
54
|
+
username: { type: "string" },
|
|
55
|
+
"api-key": { type: "string" },
|
|
56
|
+
"credentials-dir": { type: "string" },
|
|
57
|
+
quiet: { type: "boolean" },
|
|
58
|
+
help: { type: "boolean", short: "h" },
|
|
59
|
+
},
|
|
60
|
+
strict: true,
|
|
61
|
+
allowPositionals: false,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
fail(err.message, 2);
|
|
66
|
+
}
|
|
67
|
+
if (parsed.values.help) {
|
|
68
|
+
process.stdout.write(`Usage: atomicmail register [OPTIONS]
|
|
69
|
+
|
|
70
|
+
Register a new inbox (--username) or log in with an existing API key (--api-key).
|
|
71
|
+
|
|
72
|
+
Options:
|
|
73
|
+
--auth-url URL Auth-service base URL [env: ATOMIC_MAIL_AUTH_URL, default: https://auth.atomicmail.ai]
|
|
74
|
+
--api-url URL API / JMAP base URL [env: ATOMIC_MAIL_API_URL, default: https://api.atomicmail.ai]
|
|
75
|
+
--scrypt-salt SALT PoW salt override [env: ATOMIC_MAIL_SCRYPT_SALT]
|
|
76
|
+
--username NAME New account (mutually exclusive with --api-key)
|
|
77
|
+
--api-key KEY Existing API key (mutually exclusive with --username)
|
|
78
|
+
--credentials-dir DIR Credential directory (default: ~/.atomicmail)
|
|
79
|
+
--quiet Less stderr output
|
|
80
|
+
--help, -h This message
|
|
81
|
+
`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
const env = process.env;
|
|
85
|
+
const authUrl = parsed.values["auth-url"] ??
|
|
86
|
+
env.ATOMIC_MAIL_AUTH_URL ?? "https://auth.atomicmail.ai";
|
|
87
|
+
const apiUrl = parsed.values["api-url"] ??
|
|
88
|
+
env.ATOMIC_MAIL_API_URL ?? "https://api.atomicmail.ai";
|
|
89
|
+
const scryptSalt = parsed.values["scrypt-salt"] ??
|
|
90
|
+
env.ATOMIC_MAIL_SCRYPT_SALT ?? DEFAULT_POW_SCRYPT_SALT_HEX;
|
|
91
|
+
const dir = parsed.values["credentials-dir"];
|
|
92
|
+
const credentialDir = resolveCredentialDir(dir);
|
|
93
|
+
const username = parsed.values.username;
|
|
94
|
+
const apiKey = parsed.values["api-key"];
|
|
95
|
+
if (!!username === !!apiKey) {
|
|
96
|
+
fail("Provide exactly one of --username (new account) or --api-key (login).", 2);
|
|
97
|
+
}
|
|
98
|
+
const files = defaultFilesFromOutDir(credentialDir);
|
|
99
|
+
const log = (msg) => {
|
|
100
|
+
if (!parsed.values.quiet)
|
|
101
|
+
process.stderr.write(msg + "\n");
|
|
102
|
+
};
|
|
103
|
+
if (username) {
|
|
104
|
+
log(`Registering "${username}"...`);
|
|
105
|
+
const session = await AgentSession.create({
|
|
106
|
+
authUrl,
|
|
107
|
+
apiUrl,
|
|
108
|
+
scryptSalt,
|
|
109
|
+
credentialDir,
|
|
110
|
+
files,
|
|
111
|
+
});
|
|
112
|
+
const result = await session.register(username);
|
|
113
|
+
log(`Wrote credentials under ${credentialDir}`);
|
|
114
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
log("Logging in with API key...");
|
|
118
|
+
const { inboxId } = await persistLoginWithApiKey({
|
|
119
|
+
authUrl,
|
|
120
|
+
apiUrl,
|
|
121
|
+
scryptSalt,
|
|
122
|
+
apiKey: apiKey,
|
|
123
|
+
files,
|
|
124
|
+
});
|
|
125
|
+
log(`Wrote ${files.credentialsFile}`);
|
|
126
|
+
process.stdout.write(JSON.stringify({ inboxId }, null, 2) + "\n");
|
|
127
|
+
}
|
|
128
|
+
async function cmdJmapRequest(argv) {
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = parseArgs({
|
|
132
|
+
args: argv,
|
|
133
|
+
options: {
|
|
134
|
+
"credentials-dir": { type: "string" },
|
|
135
|
+
"credentials-file": { type: "string" },
|
|
136
|
+
"session-file": { type: "string" },
|
|
137
|
+
"capability-file": { type: "string" },
|
|
138
|
+
ops: { type: "string" },
|
|
139
|
+
"ops-file": { type: "string" },
|
|
140
|
+
using: { type: "string" },
|
|
141
|
+
"dry-run": { type: "boolean" },
|
|
142
|
+
vars: { type: "string" },
|
|
143
|
+
help: { type: "boolean", short: "h" },
|
|
144
|
+
},
|
|
145
|
+
strict: true,
|
|
146
|
+
allowPositionals: false,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
fail(err.message, 2);
|
|
151
|
+
}
|
|
152
|
+
if (parsed.values.help) {
|
|
153
|
+
process.stdout.write(`Usage: atomicmail jmap_request [OPTIONS]
|
|
154
|
+
|
|
155
|
+
Send a JMAP request using saved credentials.
|
|
156
|
+
|
|
157
|
+
Options:
|
|
158
|
+
--credentials-dir DIR Directory with credentials.json + JWTs (default: ~/.atomicmail)
|
|
159
|
+
--credentials-file PATH Override credentials.json path
|
|
160
|
+
--session-file PATH Override session.jwt path
|
|
161
|
+
--capability-file PATH Override capability.jwt path
|
|
162
|
+
--ops JSON Inline JMAP JSON (methodCalls or envelope)
|
|
163
|
+
--ops-file PATH Preset file ($VAR_NAME placeholders supported)
|
|
164
|
+
--vars JSON JSON object { VAR_NAME: string } for $VAR_NAME in ops / ops-file
|
|
165
|
+
--using LIST Comma-separated capability URNs (optional)
|
|
166
|
+
--dry-run Print resolved request only
|
|
167
|
+
--help, -h This message
|
|
168
|
+
`);
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
const dir = parsed.values["credentials-dir"];
|
|
172
|
+
const credentialDir = resolveCredentialDir(dir);
|
|
173
|
+
const defaults = defaultFilesFromOutDir(credentialDir);
|
|
174
|
+
const credentialsFile = parsed.values["credentials-file"] ??
|
|
175
|
+
defaults.credentialsFile;
|
|
176
|
+
const sessionFile = parsed.values["session-file"] ??
|
|
177
|
+
defaults.sessionFile;
|
|
178
|
+
const capabilityFile = parsed.values["capability-file"] ??
|
|
179
|
+
defaults.capabilityFile;
|
|
180
|
+
const ops = parsed.values.ops;
|
|
181
|
+
const opsFile = parsed.values["ops-file"];
|
|
182
|
+
if (ops && opsFile) {
|
|
183
|
+
fail("--ops and --ops-file are mutually exclusive.", 2);
|
|
184
|
+
}
|
|
185
|
+
if (!ops && !opsFile) {
|
|
186
|
+
fail("Provide --ops or --ops-file.", 2);
|
|
187
|
+
}
|
|
188
|
+
const usingFlag = parsed.values.using;
|
|
189
|
+
const defaultUsing = usingFlag
|
|
190
|
+
? usingFlag.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
|
|
191
|
+
: [...DEFAULT_JMAP_USING];
|
|
192
|
+
let userVars;
|
|
193
|
+
const varsFlag = parsed.values.vars;
|
|
194
|
+
if (varsFlag !== undefined) {
|
|
195
|
+
let obj;
|
|
196
|
+
try {
|
|
197
|
+
obj = JSON.parse(varsFlag);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
fail(`--vars is not valid JSON: ${err.message}`, 2);
|
|
201
|
+
}
|
|
202
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
203
|
+
fail("--vars must be a JSON object of { VAR_NAME: string }.", 2);
|
|
204
|
+
}
|
|
205
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
206
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(k)) {
|
|
207
|
+
fail(`--vars key '${k}' must match /^[A-Z][A-Z0-9_]*$/.`, 2);
|
|
208
|
+
}
|
|
209
|
+
if (typeof v !== "string") {
|
|
210
|
+
fail(`--vars value for '${k}' must be a string.`, 2);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
userVars = obj;
|
|
214
|
+
}
|
|
215
|
+
const creds = await readCredentials(credentialsFile);
|
|
216
|
+
const files = {
|
|
217
|
+
credentialsFile,
|
|
218
|
+
sessionFile,
|
|
219
|
+
capabilityFile,
|
|
220
|
+
};
|
|
221
|
+
const session = await AgentSession.create({
|
|
222
|
+
authUrl: creds.authUrl,
|
|
223
|
+
apiUrl: creds.apiUrl,
|
|
224
|
+
scryptSalt: creds.scryptSalt,
|
|
225
|
+
apiKey: creds.apiKey,
|
|
226
|
+
inboxId: creds.inboxId,
|
|
227
|
+
credentialDir,
|
|
228
|
+
files,
|
|
229
|
+
});
|
|
230
|
+
let raw;
|
|
231
|
+
let sourceLabel;
|
|
232
|
+
if (opsFile) {
|
|
233
|
+
try {
|
|
234
|
+
raw = await readOpsFile(credentialDir, opsFile);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
fail(`Could not read --ops-file: ${err.message}`, 2);
|
|
238
|
+
}
|
|
239
|
+
sourceLabel = opsFile;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
raw = ops;
|
|
243
|
+
sourceLabel = "ops";
|
|
244
|
+
}
|
|
245
|
+
const { ok, status, bodyText } = await runJmapRequest({
|
|
246
|
+
session,
|
|
247
|
+
opsJson: raw,
|
|
248
|
+
defaultUsing,
|
|
249
|
+
sourceLabel,
|
|
250
|
+
dryRun: parsed.values["dry-run"] === true,
|
|
251
|
+
vars: userVars,
|
|
252
|
+
});
|
|
253
|
+
if (!ok) {
|
|
254
|
+
fail(`JMAP request failed (HTTP ${status}): ${bodyText}`, 1);
|
|
255
|
+
}
|
|
256
|
+
process.stdout.write(bodyText.endsWith("\n") ? bodyText : bodyText + "\n");
|
|
257
|
+
}
|
|
258
|
+
function cmdHelp(argv) {
|
|
259
|
+
let parsed;
|
|
260
|
+
try {
|
|
261
|
+
parsed = parseArgs({
|
|
262
|
+
args: argv,
|
|
263
|
+
options: {
|
|
264
|
+
topic: { type: "string" },
|
|
265
|
+
help: { type: "boolean", short: "h" },
|
|
266
|
+
},
|
|
267
|
+
strict: true,
|
|
268
|
+
allowPositionals: false,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
fail(err.message, 2);
|
|
273
|
+
}
|
|
274
|
+
if (parsed.values.help) {
|
|
275
|
+
process.stdout.write(`Usage: atomicmail help [--topic TOPIC]
|
|
276
|
+
|
|
277
|
+
Topics include: overview, installation, auth, jmap_cheatsheet, tools, presets, troubleshooting.
|
|
278
|
+
`);
|
|
279
|
+
process.exit(0);
|
|
280
|
+
}
|
|
281
|
+
const topic = parsed.values.topic;
|
|
282
|
+
process.stdout.write(getHelp(topic) + "\n");
|
|
283
|
+
}
|
|
284
|
+
async function main() {
|
|
285
|
+
const argv = process.argv.slice(2);
|
|
286
|
+
if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
|
|
287
|
+
exitUsage(0);
|
|
288
|
+
}
|
|
289
|
+
const cmd = argv[0];
|
|
290
|
+
const rest = argv.slice(1);
|
|
291
|
+
switch (cmd) {
|
|
292
|
+
case "register":
|
|
293
|
+
await cmdRegister(rest);
|
|
294
|
+
break;
|
|
295
|
+
case "jmap_request":
|
|
296
|
+
await cmdJmapRequest(rest);
|
|
297
|
+
break;
|
|
298
|
+
case "help":
|
|
299
|
+
cmdHelp(rest);
|
|
300
|
+
break;
|
|
301
|
+
default:
|
|
302
|
+
process.stderr.write(`Unknown command: ${cmd}\n\n`);
|
|
303
|
+
process.stdout.write(USAGE);
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
main().catch((err) => {
|
|
308
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
309
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomicmail/agent-skill",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Atomic Mail
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Atomic Mail AgentSkill — register, jmap_request, and help CLI for AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atomic-mail",
|
|
7
7
|
"atomicmail",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {},
|
|
26
26
|
"bin": {
|
|
27
|
-
"
|
|
28
|
-
"atomic-mail-jmap": "./esm/skill/scripts/jmap_request.js"
|
|
27
|
+
"atomicmail": "./esm/skill/scripts/cli.js"
|
|
29
28
|
},
|
|
30
29
|
"engines": {
|
|
31
30
|
"node": ">=20"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"jmap_request.d.ts","sourceRoot":"","sources":["../../../src/skill/scripts/jmap_request.ts"],"names":[],"mappings":""}
|