gws-axi 0.8.4 → 0.9.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.
Files changed (36) hide show
  1. package/README.md +39 -3
  2. package/dist/src/auth/scopes.d.ts +1 -0
  3. package/dist/src/auth/scopes.js +9 -1
  4. package/dist/src/auth/scopes.js.map +1 -1
  5. package/dist/src/commands/gmail/batch-modify.d.ts +2 -0
  6. package/dist/src/commands/gmail/batch-modify.js +158 -0
  7. package/dist/src/commands/gmail/batch-modify.js.map +1 -0
  8. package/dist/src/commands/gmail/compose.d.ts +24 -0
  9. package/dist/src/commands/gmail/compose.js +47 -0
  10. package/dist/src/commands/gmail/compose.js.map +1 -0
  11. package/dist/src/commands/gmail/compose.test.d.ts +1 -0
  12. package/dist/src/commands/gmail/compose.test.js +75 -0
  13. package/dist/src/commands/gmail/compose.test.js.map +1 -0
  14. package/dist/src/commands/gmail/draft.d.ts +3 -0
  15. package/dist/src/commands/gmail/draft.js +148 -0
  16. package/dist/src/commands/gmail/draft.js.map +1 -0
  17. package/dist/src/commands/gmail/filters.d.ts +6 -0
  18. package/dist/src/commands/gmail/filters.js +305 -0
  19. package/dist/src/commands/gmail/filters.js.map +1 -0
  20. package/dist/src/commands/gmail/label-ops.d.ts +6 -0
  21. package/dist/src/commands/gmail/label-ops.js +208 -0
  22. package/dist/src/commands/gmail/label-ops.js.map +1 -0
  23. package/dist/src/commands/gmail/labels-shared.d.ts +23 -0
  24. package/dist/src/commands/gmail/labels-shared.js +61 -0
  25. package/dist/src/commands/gmail/labels-shared.js.map +1 -0
  26. package/dist/src/commands/gmail/labels-shared.test.d.ts +1 -0
  27. package/dist/src/commands/gmail/labels-shared.test.js +59 -0
  28. package/dist/src/commands/gmail/labels-shared.test.js.map +1 -0
  29. package/dist/src/commands/gmail/modify.d.ts +2 -0
  30. package/dist/src/commands/gmail/modify.js +130 -0
  31. package/dist/src/commands/gmail/modify.js.map +1 -0
  32. package/dist/src/commands/gmail/search.js +2 -30
  33. package/dist/src/commands/gmail/search.js.map +1 -1
  34. package/dist/src/commands/gmail.js +39 -53
  35. package/dist/src/commands/gmail.js.map +1 -1
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ Agent-ergonomic CLI for Google Workspace — Gmail, Calendar, Docs, Drive, and S
4
4
 
5
5
  Designed for use by AI agents. Every response is structured, every error names a specific fix, and write operations lock to the explicit account when multiple are authenticated — so two agents in parallel sessions can't silently touch the wrong mailbox.
6
6
 
7
- > **Status:** Read coverage is in across all five services — Calendar, Gmail, Docs, Drive, and Slides — and Calendar has full write support (`create`, `update`, `delete`, `respond`). Gmail writes (`send`, `draft`, `modify`) and Docs writes (`append`, `insert-text`, `delete-range`) are the next frontier. Auth (including a single-developer `auth publish` walkthrough that retires the 7-day Testing-token expiry), doctor with live per-account API probes, and multi-account write-protection are stable.
7
+ > **Status:** Read coverage is in across all five services — Calendar, Gmail, Docs, Drive, and Slides. **Calendar** and **Gmail** both have full write support: Calendar does `create` / `update` / `delete` / `respond`, and Gmail does triage (`modify`, `batch-modify`), drafting (`draft`), label CRUD (`label-create` / `label-update` / `label-delete`), and server-side filters (`filter-create` / `filter-delete` / `filter-list`). Gmail **`send` is intentionally out of scope** — gws-axi drafts mail for human review but never sends it. Docs writes (`append`, `insert-text`, `delete-range`) are the next frontier. Auth (including a single-developer `auth publish` walkthrough that retires the 7-day Testing-token expiry), doctor with live per-account API probes, and multi-account write-protection are stable.
8
8
 
9
9
  ## Requirements
10
10
 
@@ -123,6 +123,8 @@ Writes default `--send-updates` to `none` so agent-created events don't spam att
123
123
 
124
124
  ### Gmail
125
125
 
126
+ **Reads:**
127
+
126
128
  ```bash
127
129
  gws-axi gmail search # in:inbox by default
128
130
  gws-axi gmail search --query "from:alice has:attachment"
@@ -134,8 +136,41 @@ gws-axi gmail labels # list labels with IDs
134
136
  gws-axi gmail download <message-id> <attachment-id> # fetch attachment bytes
135
137
  ```
136
138
 
139
+ **Writes** (triage, drafting, labels, filters — but never *send*):
140
+
141
+ ```bash
142
+ # Triage — Gmail state IS labels, so archive/read/star are label edits:
143
+ gws-axi gmail modify <message-id> --remove-label INBOX # archive
144
+ gws-axi gmail modify <message-id> --remove-label UNREAD # mark read
145
+ gws-axi gmail modify <message-id> --add-label STARRED # star
146
+ gws-axi gmail modify <message-id> --add-label "Work/Clients" --remove-label INBOX
147
+ gws-axi gmail modify <thread-id> --thread --remove-label UNREAD # whole thread
148
+
149
+ # Bulk triage by query:
150
+ gws-axi gmail batch-modify --query "from:news@x.com" --remove-label INBOX
151
+ gws-axi gmail batch-modify --query "is:unread older_than:30d" --remove-label UNREAD
152
+
153
+ # Drafts (composed for you; you send from Gmail):
154
+ gws-axi gmail draft --to alice@x.com --subject "Re: budget" --body "Approved."
155
+ gws-axi gmail draft --to a@x.com,b@x.com --subject Hi --body-file ./note.txt --thread <id>
156
+
157
+ # Label management:
158
+ gws-axi gmail label-create --name "Work/Clients"
159
+ gws-axi gmail label-update <label-id|name> --name "Archive"
160
+ gws-axi gmail label-delete <label-id|name>
161
+
162
+ # Server-side auto-sort filters:
163
+ gws-axi gmail filter-list
164
+ gws-axi gmail filter-create --from news@x.com --remove-label INBOX --add-label News
165
+ gws-axi gmail filter-delete <filter-id>
166
+ ```
167
+
137
168
  Bodies are decoded per declared `Content-Type` charset (UTF-8, latin-1, ascii, utf-16). HTML-only messages convert to markdown via turndown. Address headers parse with `addressparser` so display names with commas don't get split.
138
169
 
170
+ **On `send`:** gws-axi has no `send` command by design — an agent can triage, label, and draft replies, but a human reviews and sends from Gmail. This is a guardrail against *accidental* sends, not a hard security boundary (the OAuth token is send-capable; there is no Gmail scope that grants drafting + label edits while withholding send). `gws-axi gmail send` returns a `NOT_SUPPORTED` error pointing at `draft`.
171
+
172
+ **Scope note:** the filter commands need the `gmail.settings.basic` scope, which `gmail.modify` does not include. Accounts authenticated before this release will get a `SCOPE_MISSING` (403) on filter commands until you re-run `gws-axi auth login --account <email>` once to re-consent.
173
+
139
174
  ### Docs
140
175
 
141
176
  ```bash
@@ -198,8 +233,9 @@ help[2]:
198
233
 
199
234
  ## Known issues & roadmap
200
235
 
201
- - **Gmail writes** (`send`, `draft`, `modify`, label add/remove): scaffolded as `NOT_IMPLEMENTED`. Coming next.
202
- - **Docs writes** (`append`, `insert-text`, `delete-range`, etc.): scaffolded as `NOT_IMPLEMENTED`. Coming alongside Gmail writes.
236
+ - **Gmail `send`**: intentionally unsupported — gws-axi drafts mail but leaves sending to a human in the Gmail UI. See the Gmail section above.
237
+ - **Docs writes** (`append`, `insert-text`, `delete-range`, etc.): scaffolded as `NOT_IMPLEMENTED`. The next frontier.
238
+ - **Drive/Slides writes**: still scaffolded as `NOT_IMPLEMENTED` (reads are complete).
203
239
  - **Testing-mode tokens** still expire every 7 days *if* you haven't published your OAuth app yet. Run `gws-axi auth publish` for the walkthrough — it covers the single-developer Production flow and removes the expiry once you've re-auth'd each account.
204
240
 
205
241
  ## Contributing
@@ -6,6 +6,7 @@ export declare const SERVICE_SCOPES: {
6
6
  readonly drive: "https://www.googleapis.com/auth/drive";
7
7
  readonly slides: "https://www.googleapis.com/auth/presentations";
8
8
  };
9
+ export declare const ADDITIONAL_SCOPES: readonly ["https://www.googleapis.com/auth/gmail.settings.basic"];
9
10
  export type ServiceName = keyof typeof SERVICE_SCOPES;
10
11
  export declare const SERVICES: ServiceName[];
11
12
  export declare const REQUIRED_APIS: Record<ServiceName, string>;
@@ -6,6 +6,14 @@ export const SERVICE_SCOPES = {
6
6
  drive: "https://www.googleapis.com/auth/drive",
7
7
  slides: "https://www.googleapis.com/auth/presentations",
8
8
  };
9
+ // Scopes layered on top of the representative per-service scope above.
10
+ // gmail.settings.basic is required for Gmail filter management
11
+ // (users.settings.filters.*); it is NOT covered by gmail.modify. Kept
12
+ // separate so the per-service probe/health checks keep keying off the
13
+ // single representative scope in SERVICE_SCOPES.
14
+ export const ADDITIONAL_SCOPES = [
15
+ "https://www.googleapis.com/auth/gmail.settings.basic",
16
+ ];
9
17
  export const SERVICES = [
10
18
  "gmail",
11
19
  "calendar",
@@ -21,7 +29,7 @@ export const REQUIRED_APIS = {
21
29
  slides: "slides.googleapis.com",
22
30
  };
23
31
  export function allScopes() {
24
- return [...BASE_SCOPES, ...Object.values(SERVICE_SCOPES)];
32
+ return [...BASE_SCOPES, ...Object.values(SERVICE_SCOPES), ...ADDITIONAL_SCOPES];
25
33
  }
26
34
  export function allApis() {
27
35
  return Object.values(REQUIRED_APIS);
@@ -1 +1 @@
1
- {"version":3,"file":"scopes.js","sourceRoot":"","sources":["../../../src/auth/scopes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAU,CAAC;AAEnE,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,8CAA8C;IACrD,QAAQ,EAAE,0CAA0C;IACpD,IAAI,EAAE,2CAA2C;IACjD,KAAK,EAAE,uCAAuC;IAC9C,MAAM,EAAE,+CAA+C;CAC/C,CAAC;AAIX,MAAM,CAAC,MAAM,QAAQ,GAAkB;IACrC,OAAO;IACP,UAAU;IACV,MAAM;IACN,OAAO;IACP,QAAQ;CACT,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAgC;IACxD,KAAK,EAAE,sBAAsB;IAC7B,QAAQ,EAAE,8BAA8B;IACxC,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,sBAAsB;IAC7B,MAAM,EAAE,uBAAuB;CAChC,CAAC;AAEF,MAAM,UAAU,SAAS;IACvB,OAAO,CAAC,GAAG,WAAW,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;AACtC,CAAC"}
1
+ {"version":3,"file":"scopes.js","sourceRoot":"","sources":["../../../src/auth/scopes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAU,CAAC;AAEnE,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,8CAA8C;IACrD,QAAQ,EAAE,0CAA0C;IACpD,IAAI,EAAE,2CAA2C;IACjD,KAAK,EAAE,uCAAuC;IAC9C,MAAM,EAAE,+CAA+C;CAC/C,CAAC;AAEX,uEAAuE;AACvE,+DAA+D;AAC/D,sEAAsE;AACtE,sEAAsE;AACtE,iDAAiD;AACjD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,sDAAsD;CAC9C,CAAC;AAIX,MAAM,CAAC,MAAM,QAAQ,GAAkB;IACrC,OAAO;IACP,UAAU;IACV,MAAM;IACN,OAAO;IACP,QAAQ;CACT,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAgC;IACxD,KAAK,EAAE,sBAAsB;IAC7B,QAAQ,EAAE,8BAA8B;IACxC,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,sBAAsB;IAC7B,MAAM,EAAE,uBAAuB;CAChC,CAAC;AAEF,MAAM,UAAU,SAAS;IACvB,OAAO,CAAC,GAAG,WAAW,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,GAAG,iBAAiB,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const BATCH_MODIFY_HELP = "usage: gws-axi gmail batch-modify --query <text> [--add-label <name>...] [--remove-label <name>...] [flags]\nflags[6]:\n --query <text> REQUIRED \u2014 Gmail search query selecting the messages\n to modify (full Gmail syntax: from:, is:unread, etc.)\n --add-label <name> Label to apply to every match. Repeatable / comma list.\n Accepts user + system labels (INBOX, UNREAD, STARRED\u2026)\n --remove-label <name> Label to remove from every match. Same name rules\n --limit <n> Max messages to affect (default: 100, max: 1000)\n --include-spam-trash Include spam/trash in the selection\n --account <email> REQUIRED when 2+ accounts are authenticated\nexamples:\n gws-axi gmail batch-modify --query \"from:newsletters@x.com\" --remove-label INBOX\n gws-axi gmail batch-modify --query \"is:unread older_than:30d\" --remove-label UNREAD\n gws-axi gmail batch-modify --query \"from:boss@x.com\" --add-label \"Important/Boss\"\nnotes:\n Operates on MESSAGES matching --query (not threads). At least one of\n --add-label / --remove-label is required. If more messages match than\n --limit, only the first --limit are modified and the response says so \u2014\n re-run to continue. Idempotent per message (see `gmail modify`).\n";
2
+ export declare function gmailBatchModifyCommand(account: string, args: string[]): Promise<string>;
@@ -0,0 +1,158 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { gmailClient, translateGoogleError } from "../../google/client.js";
3
+ import { joinBlocks, renderHelp, renderObject } from "../../output/index.js";
4
+ import { fetchLabels, labelNamesFor, resolveLabelIds, } from "./labels-shared.js";
5
+ const DEFAULT_LIMIT = 100;
6
+ const MAX_LIMIT = 1000; // messages.batchModify accepts up to 1000 ids per call.
7
+ export const BATCH_MODIFY_HELP = `usage: gws-axi gmail batch-modify --query <text> [--add-label <name>...] [--remove-label <name>...] [flags]
8
+ flags[6]:
9
+ --query <text> REQUIRED — Gmail search query selecting the messages
10
+ to modify (full Gmail syntax: from:, is:unread, etc.)
11
+ --add-label <name> Label to apply to every match. Repeatable / comma list.
12
+ Accepts user + system labels (INBOX, UNREAD, STARRED…)
13
+ --remove-label <name> Label to remove from every match. Same name rules
14
+ --limit <n> Max messages to affect (default: 100, max: 1000)
15
+ --include-spam-trash Include spam/trash in the selection
16
+ --account <email> REQUIRED when 2+ accounts are authenticated
17
+ examples:
18
+ gws-axi gmail batch-modify --query "from:newsletters@x.com" --remove-label INBOX
19
+ gws-axi gmail batch-modify --query "is:unread older_than:30d" --remove-label UNREAD
20
+ gws-axi gmail batch-modify --query "from:boss@x.com" --add-label "Important/Boss"
21
+ notes:
22
+ Operates on MESSAGES matching --query (not threads). At least one of
23
+ --add-label / --remove-label is required. If more messages match than
24
+ --limit, only the first --limit are modified and the response says so —
25
+ re-run to continue. Idempotent per message (see \`gmail modify\`).
26
+ `;
27
+ function collect(target, value) {
28
+ if (!value)
29
+ return;
30
+ for (const part of value.split(",")) {
31
+ const t = part.trim();
32
+ if (t)
33
+ target.push(t);
34
+ }
35
+ }
36
+ function parseFlags(args) {
37
+ const flags = {
38
+ query: undefined,
39
+ add: [],
40
+ remove: [],
41
+ limit: DEFAULT_LIMIT,
42
+ includeSpamTrash: false,
43
+ };
44
+ for (let i = 0; i < args.length; i++) {
45
+ const arg = args[i];
46
+ const next = args[i + 1];
47
+ switch (arg) {
48
+ case "--query":
49
+ flags.query = next;
50
+ i++;
51
+ break;
52
+ case "--add-label":
53
+ collect(flags.add, next);
54
+ i++;
55
+ break;
56
+ case "--remove-label":
57
+ collect(flags.remove, next);
58
+ i++;
59
+ break;
60
+ case "--limit":
61
+ flags.limit = Math.max(1, Math.min(MAX_LIMIT, parseInt(next, 10) || DEFAULT_LIMIT));
62
+ i++;
63
+ break;
64
+ case "--include-spam-trash":
65
+ flags.includeSpamTrash = true;
66
+ break;
67
+ }
68
+ }
69
+ return flags;
70
+ }
71
+ /** Page through messages.list until we hit `limit` ids or run out. */
72
+ async function collectMessageIds(api, account, query, limit, includeSpamTrash) {
73
+ const ids = [];
74
+ let pageToken;
75
+ try {
76
+ while (ids.length < limit) {
77
+ const res = await api.users.messages.list({
78
+ userId: "me",
79
+ q: query,
80
+ includeSpamTrash,
81
+ maxResults: Math.min(500, limit - ids.length),
82
+ pageToken,
83
+ });
84
+ for (const m of res.data.messages ?? []) {
85
+ if (m.id)
86
+ ids.push(m.id);
87
+ }
88
+ pageToken = res.data.nextPageToken ?? undefined;
89
+ if (!pageToken)
90
+ return { ids, more: false };
91
+ }
92
+ }
93
+ catch (err) {
94
+ throw translateGoogleError(err, {
95
+ account,
96
+ operation: "gmail.messages.list",
97
+ });
98
+ }
99
+ // Reached the limit; `more` is true iff another page exists.
100
+ return { ids: ids.slice(0, limit), more: Boolean(pageToken) };
101
+ }
102
+ export async function gmailBatchModifyCommand(account, args) {
103
+ const flags = parseFlags(args);
104
+ if (!flags.query) {
105
+ throw new AxiError("--query is required", "VALIDATION_ERROR", [
106
+ "Usage: gws-axi gmail batch-modify --query <text> [--add-label <name>] [--remove-label <name>]",
107
+ ]);
108
+ }
109
+ if (flags.add.length === 0 && flags.remove.length === 0) {
110
+ throw new AxiError("Nothing to do — pass --add-label and/or --remove-label", "VALIDATION_ERROR", ["Archive matches: --remove-label INBOX · Mark read: --remove-label UNREAD"]);
111
+ }
112
+ const api = await gmailClient(account);
113
+ const labels = await fetchLabels(api, account);
114
+ const addLabelIds = resolveLabelIds(flags.add, labels);
115
+ const removeLabelIds = resolveLabelIds(flags.remove, labels);
116
+ const { ids, more } = await collectMessageIds(api, account, flags.query, flags.limit, flags.includeSpamTrash);
117
+ if (ids.length === 0) {
118
+ return joinBlocks(renderObject({
119
+ action: "noop",
120
+ account,
121
+ query: flags.query,
122
+ matched: 0,
123
+ reason: "no messages matched the query",
124
+ }), renderHelp([
125
+ `Preview matches first with \`gws-axi gmail search --query ${JSON.stringify(flags.query)}\``,
126
+ ]));
127
+ }
128
+ try {
129
+ await api.users.messages.batchModify({
130
+ userId: "me",
131
+ requestBody: { ids, addLabelIds, removeLabelIds },
132
+ });
133
+ }
134
+ catch (err) {
135
+ throw translateGoogleError(err, {
136
+ account,
137
+ operation: "gmail.messages.batchModify",
138
+ });
139
+ }
140
+ const result = {
141
+ action: "batch_modified",
142
+ account,
143
+ query: flags.query,
144
+ matched: ids.length,
145
+ };
146
+ if (flags.add.length)
147
+ result.added = labelNamesFor(addLabelIds, labels).join(", ");
148
+ if (flags.remove.length) {
149
+ result.removed = labelNamesFor(removeLabelIds, labels).join(", ");
150
+ }
151
+ const suggestions = [];
152
+ if (more) {
153
+ suggestions.push(`More messages match than --limit ${flags.limit} — re-run the same command to modify the next batch`);
154
+ }
155
+ suggestions.push(`Verify with \`gws-axi gmail search --query ${JSON.stringify(flags.query)}\``);
156
+ return joinBlocks(renderObject(result), renderHelp(suggestions));
157
+ }
158
+ //# sourceMappingURL=batch-modify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch-modify.js","sourceRoot":"","sources":["../../../../src/commands/gmail/batch-modify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Minimal RFC 5322 message builder for draft creation. We only need the
3
+ * plain-text single-part case: agents draft text bodies for human review,
4
+ * and Gmail's web composer handles any rich formatting on send. The raw
5
+ * message is base64url-encoded as the Gmail API's `raw` field expects.
6
+ */
7
+ export interface ComposeFields {
8
+ /** Sender — the authenticated account's address. */
9
+ from: string;
10
+ to: string[];
11
+ cc?: string[];
12
+ bcc?: string[];
13
+ subject: string;
14
+ body: string;
15
+ }
16
+ /** Split a comma-separated recipient flag into trimmed, non-empty addresses. */
17
+ export declare function parseRecipients(value: string): string[];
18
+ /**
19
+ * Build a base64url-encoded RFC 5322 message suitable for
20
+ * `users.drafts.create` / `users.messages.send` `raw` fields. Body is sent
21
+ * as a single text/plain part with base64 transfer-encoding so any UTF-8
22
+ * content survives intact.
23
+ */
24
+ export declare function buildRawMessage(fields: ComposeFields): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Minimal RFC 5322 message builder for draft creation. We only need the
3
+ * plain-text single-part case: agents draft text bodies for human review,
4
+ * and Gmail's web composer handles any rich formatting on send. The raw
5
+ * message is base64url-encoded as the Gmail API's `raw` field expects.
6
+ */
7
+ /** Split a comma-separated recipient flag into trimmed, non-empty addresses. */
8
+ export function parseRecipients(value) {
9
+ return value
10
+ .split(",")
11
+ .map((s) => s.trim())
12
+ .filter(Boolean);
13
+ }
14
+ /**
15
+ * RFC 2047 "encoded-word" wrap a header value when it carries non-ASCII.
16
+ * Pure-ASCII values pass through untouched so common subjects stay readable
17
+ * on the wire.
18
+ */
19
+ function encodeHeaderValue(value) {
20
+ if (/^[\x00-\x7F]*$/.test(value))
21
+ return value;
22
+ return `=?UTF-8?B?${Buffer.from(value, "utf8").toString("base64")}?=`;
23
+ }
24
+ /**
25
+ * Build a base64url-encoded RFC 5322 message suitable for
26
+ * `users.drafts.create` / `users.messages.send` `raw` fields. Body is sent
27
+ * as a single text/plain part with base64 transfer-encoding so any UTF-8
28
+ * content survives intact.
29
+ */
30
+ export function buildRawMessage(fields) {
31
+ const headers = [`From: ${fields.from}`, `To: ${fields.to.join(", ")}`];
32
+ if (fields.cc?.length)
33
+ headers.push(`Cc: ${fields.cc.join(", ")}`);
34
+ if (fields.bcc?.length)
35
+ headers.push(`Bcc: ${fields.bcc.join(", ")}`);
36
+ headers.push(`Subject: ${encodeHeaderValue(fields.subject)}`);
37
+ headers.push("MIME-Version: 1.0");
38
+ headers.push('Content-Type: text/plain; charset="UTF-8"');
39
+ headers.push("Content-Transfer-Encoding: base64");
40
+ // Wrap the base64 body at 76 columns (RFC 2045) with CRLF line endings.
41
+ const encodedBody = Buffer.from(fields.body, "utf8")
42
+ .toString("base64")
43
+ .replace(/(.{76})/g, "$1\r\n");
44
+ const raw = `${headers.join("\r\n")}\r\n\r\n${encodedBody}`;
45
+ return Buffer.from(raw, "utf8").toString("base64url");
46
+ }
47
+ //# sourceMappingURL=compose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.js","sourceRoot":"","sources":["../../../../src/commands/gmail/compose.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,KAAa;IACtC,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,aAAa,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;AACxE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqB;IACnD,MAAM,OAAO,GAAa,CAAC,SAAS,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClF,IAAI,MAAM,CAAC,EAAE,EAAE,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnE,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,YAAY,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9D,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAClC,OAAO,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IAElD,wEAAwE;IACxE,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC;SACjD,QAAQ,CAAC,QAAQ,CAAC;SAClB,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAEjC,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,WAAW,EAAE,CAAC;IAC5D,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildRawMessage, parseRecipients } from "./compose.js";
3
+ /** Decode a base64url raw message back into its header block and body. */
4
+ function decodeRaw(raw) {
5
+ const text = Buffer.from(raw, "base64url").toString("utf8");
6
+ const idx = text.indexOf("\r\n\r\n");
7
+ const header = text.slice(0, idx);
8
+ const bodyB64 = text.slice(idx + 4).replace(/\r\n/g, "");
9
+ return { header, body: Buffer.from(bodyB64, "base64").toString("utf8") };
10
+ }
11
+ describe("parseRecipients", () => {
12
+ it("splits, trims, and drops empties", () => {
13
+ expect(parseRecipients("a@x.com, b@x.com ,, c@x.com")).toEqual([
14
+ "a@x.com",
15
+ "b@x.com",
16
+ "c@x.com",
17
+ ]);
18
+ });
19
+ it("handles a single address", () => {
20
+ expect(parseRecipients("solo@x.com")).toEqual(["solo@x.com"]);
21
+ });
22
+ });
23
+ describe("buildRawMessage", () => {
24
+ it("emits the core headers and round-trips the body", () => {
25
+ const raw = buildRawMessage({
26
+ from: "me@x.com",
27
+ to: ["alice@x.com", "bob@x.com"],
28
+ subject: "Hello",
29
+ body: "Line one\nLine two",
30
+ });
31
+ const { header, body } = decodeRaw(raw);
32
+ expect(header).toContain("From: me@x.com");
33
+ expect(header).toContain("To: alice@x.com, bob@x.com");
34
+ expect(header).toContain("Subject: Hello");
35
+ expect(header).toContain('Content-Type: text/plain; charset="UTF-8"');
36
+ expect(body).toBe("Line one\nLine two");
37
+ });
38
+ it("includes Cc/Bcc only when provided", () => {
39
+ const withCc = decodeRaw(buildRawMessage({
40
+ from: "me@x.com",
41
+ to: ["a@x.com"],
42
+ cc: ["c@x.com"],
43
+ bcc: ["d@x.com"],
44
+ subject: "s",
45
+ body: "b",
46
+ })).header;
47
+ expect(withCc).toContain("Cc: c@x.com");
48
+ expect(withCc).toContain("Bcc: d@x.com");
49
+ const without = decodeRaw(buildRawMessage({ from: "me@x.com", to: ["a@x.com"], subject: "s", body: "b" })).header;
50
+ expect(without).not.toContain("Cc:");
51
+ expect(without).not.toContain("Bcc:");
52
+ });
53
+ it("RFC 2047 encodes non-ASCII subjects but leaves the body decodable", () => {
54
+ const { header, body } = decodeRaw(buildRawMessage({
55
+ from: "me@x.com",
56
+ to: ["a@x.com"],
57
+ subject: "café ☕",
58
+ body: "naïve résumé — ☕",
59
+ }));
60
+ expect(header).toContain("Subject: =?UTF-8?B?");
61
+ expect(header).not.toContain("café");
62
+ expect(body).toBe("naïve résumé — ☕");
63
+ });
64
+ it("produces valid base64url (no +,/,= padding chars)", () => {
65
+ const raw = buildRawMessage({
66
+ from: "me@x.com",
67
+ to: ["a@x.com"],
68
+ subject: "s",
69
+ body: "x".repeat(200), // force base64 line wrapping
70
+ });
71
+ expect(raw).not.toMatch(/[+/=]/);
72
+ expect(decodeRaw(raw).body).toBe("x".repeat(200));
73
+ });
74
+ });
75
+ //# sourceMappingURL=compose.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.test.js","sourceRoot":"","sources":["../../../../src/commands/gmail/compose.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEhE,0EAA0E;AAC1E,SAAS,SAAS,CAAC,GAAW;IAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;AAC3E,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,eAAe,CAAC,6BAA6B,CAAC,CAAC,CAAC,OAAO,CAAC;YAC7D,SAAS;YACT,SAAS;YACT,SAAS;SACV,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,GAAG,GAAG,eAAe,CAAC;YAC1B,IAAI,EAAE,UAAU;YAChB,EAAE,EAAE,CAAC,aAAa,EAAE,WAAW,CAAC;YAChC,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,oBAAoB;SAC3B,CAAC,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACvD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,2CAA2C,CAAC,CAAC;QACtE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,MAAM,GAAG,SAAS,CACtB,eAAe,CAAC;YACd,IAAI,EAAE,UAAU;YAChB,EAAE,EAAE,CAAC,SAAS,CAAC;YACf,EAAE,EAAE,CAAC,SAAS,CAAC;YACf,GAAG,EAAE,CAAC,SAAS,CAAC;YAChB,OAAO,EAAE,GAAG;YACZ,IAAI,EAAE,GAAG;SACV,CAAC,CACH,CAAC,MAAM,CAAC;QACT,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAEzC,MAAM,OAAO,GAAG,SAAS,CACvB,eAAe,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAChF,CAAC,MAAM,CAAC;QACT,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAChC,eAAe,CAAC;YACd,IAAI,EAAE,UAAU;YAChB,EAAE,EAAE,CAAC,SAAS,CAAC;YACf,OAAO,EAAE,QAAQ;YACjB,IAAI,EAAE,kBAAkB;SACzB,CAAC,CACH,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,GAAG,GAAG,eAAe,CAAC;YAC1B,IAAI,EAAE,UAAU;YAChB,EAAE,EAAE,CAAC,SAAS,CAAC;YACf,OAAO,EAAE,GAAG;YACZ,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,6BAA6B;SACrD,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const DRAFT_HELP = "usage: gws-axi gmail draft --to <emails> --subject <text> --body <markdown> [flags]\nflags[8]:\n --to <emails> REQUIRED \u2014 comma-separated recipient addresses\n --subject <text> Subject line (default: empty)\n --body <text> Body text. Pass via quoted string or shell heredoc\n --body-file <path> Read the body from a file instead of --body (mutually\n exclusive with --body; use for long/multi-line bodies)\n --cc <emails> Comma-separated Cc addresses\n --bcc <emails> Comma-separated Bcc addresses\n --thread <thread-id> Attach the draft to an existing thread (reply draft)\n --account <email> REQUIRED when 2+ accounts are authenticated\nexamples:\n gws-axi gmail draft --to alice@x.com --subject \"Re: budget\" --body \"Looks good \u2014 approving.\"\n gws-axi gmail draft --to a@x.com,b@x.com --subject Hi --body-file ./note.txt\n gws-axi gmail draft --to alice@x.com --subject \"Re: thread\" --body \"...\" --thread 1899abcd\nnotes:\n Creates a DRAFT only \u2014 gws-axi never sends mail. Review and send from the\n Gmail UI. Body is sent as plain text (text/plain; UTF-8); any markdown is\n preserved verbatim, not rendered to HTML.\noutput:\n Returns `action: drafted` plus the new draft_id, message_id, recipients,\n and subject. A help line links to where to review/send it.\n";
2
+ export declare const SEND_HELP = "usage: gws-axi gmail send \u2014 INTENTIONALLY OUT OF SCOPE\nstatus: not supported by design\nnotes:\n gws-axi deliberately does not send mail. It can draft messages for you, but\n sending is left to a human in the Gmail UI so an automated agent can't email\n people on your behalf. Use `gws-axi gmail draft` to compose, then review and\n send the draft yourself in Gmail.\n";
3
+ export declare function gmailDraftCommand(account: string, args: string[]): Promise<string>;
@@ -0,0 +1,148 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { AxiError } from "axi-sdk-js";
3
+ import { gmailClient, translateGoogleError } from "../../google/client.js";
4
+ import { joinBlocks, renderHelp, renderObject } from "../../output/index.js";
5
+ import { buildRawMessage, parseRecipients } from "./compose.js";
6
+ export const DRAFT_HELP = `usage: gws-axi gmail draft --to <emails> --subject <text> --body <markdown> [flags]
7
+ flags[8]:
8
+ --to <emails> REQUIRED — comma-separated recipient addresses
9
+ --subject <text> Subject line (default: empty)
10
+ --body <text> Body text. Pass via quoted string or shell heredoc
11
+ --body-file <path> Read the body from a file instead of --body (mutually
12
+ exclusive with --body; use for long/multi-line bodies)
13
+ --cc <emails> Comma-separated Cc addresses
14
+ --bcc <emails> Comma-separated Bcc addresses
15
+ --thread <thread-id> Attach the draft to an existing thread (reply draft)
16
+ --account <email> REQUIRED when 2+ accounts are authenticated
17
+ examples:
18
+ gws-axi gmail draft --to alice@x.com --subject "Re: budget" --body "Looks good — approving."
19
+ gws-axi gmail draft --to a@x.com,b@x.com --subject Hi --body-file ./note.txt
20
+ gws-axi gmail draft --to alice@x.com --subject "Re: thread" --body "..." --thread 1899abcd
21
+ notes:
22
+ Creates a DRAFT only — gws-axi never sends mail. Review and send from the
23
+ Gmail UI. Body is sent as plain text (text/plain; UTF-8); any markdown is
24
+ preserved verbatim, not rendered to HTML.
25
+ output:
26
+ Returns \`action: drafted\` plus the new draft_id, message_id, recipients,
27
+ and subject. A help line links to where to review/send it.
28
+ `;
29
+ export const SEND_HELP = `usage: gws-axi gmail send — INTENTIONALLY OUT OF SCOPE
30
+ status: not supported by design
31
+ notes:
32
+ gws-axi deliberately does not send mail. It can draft messages for you, but
33
+ sending is left to a human in the Gmail UI so an automated agent can't email
34
+ people on your behalf. Use \`gws-axi gmail draft\` to compose, then review and
35
+ send the draft yourself in Gmail.
36
+ `;
37
+ function parseFlags(args) {
38
+ const flags = {
39
+ to: [],
40
+ cc: [],
41
+ bcc: [],
42
+ subject: "",
43
+ body: undefined,
44
+ bodyFile: undefined,
45
+ thread: undefined,
46
+ };
47
+ for (let i = 0; i < args.length; i++) {
48
+ const arg = args[i];
49
+ const next = args[i + 1];
50
+ switch (arg) {
51
+ case "--to":
52
+ flags.to = parseRecipients(next ?? "");
53
+ i++;
54
+ break;
55
+ case "--cc":
56
+ flags.cc = parseRecipients(next ?? "");
57
+ i++;
58
+ break;
59
+ case "--bcc":
60
+ flags.bcc = parseRecipients(next ?? "");
61
+ i++;
62
+ break;
63
+ case "--subject":
64
+ flags.subject = next ?? "";
65
+ i++;
66
+ break;
67
+ case "--body":
68
+ flags.body = next;
69
+ i++;
70
+ break;
71
+ case "--body-file":
72
+ flags.bodyFile = next;
73
+ i++;
74
+ break;
75
+ case "--thread":
76
+ flags.thread = next;
77
+ i++;
78
+ break;
79
+ }
80
+ }
81
+ return flags;
82
+ }
83
+ function resolveBody(flags) {
84
+ if (flags.body !== undefined && flags.bodyFile !== undefined) {
85
+ throw new AxiError("--body and --body-file are mutually exclusive", "VALIDATION_ERROR", ["Pass the body inline with --body OR from a file with --body-file, not both"]);
86
+ }
87
+ if (flags.bodyFile !== undefined) {
88
+ try {
89
+ return readFileSync(flags.bodyFile, "utf8");
90
+ }
91
+ catch {
92
+ throw new AxiError(`Cannot read --body-file: ${flags.bodyFile}`, "VALIDATION_ERROR", ["Check the path exists and is readable"]);
93
+ }
94
+ }
95
+ return flags.body ?? "";
96
+ }
97
+ export async function gmailDraftCommand(account, args) {
98
+ const flags = parseFlags(args);
99
+ if (flags.to.length === 0) {
100
+ throw new AxiError("--to is required", "VALIDATION_ERROR", [
101
+ `Usage: gws-axi gmail draft --to <emails> --subject <text> --body <text>`,
102
+ ]);
103
+ }
104
+ const body = resolveBody(flags);
105
+ const raw = buildRawMessage({
106
+ from: account,
107
+ to: flags.to,
108
+ cc: flags.cc.length ? flags.cc : undefined,
109
+ bcc: flags.bcc.length ? flags.bcc : undefined,
110
+ subject: flags.subject,
111
+ body,
112
+ });
113
+ const message = { raw };
114
+ if (flags.thread)
115
+ message.threadId = flags.thread;
116
+ const api = await gmailClient(account);
117
+ let draft;
118
+ try {
119
+ const res = await api.users.drafts.create({
120
+ userId: "me",
121
+ requestBody: { message },
122
+ });
123
+ draft = res.data;
124
+ }
125
+ catch (err) {
126
+ throw translateGoogleError(err, {
127
+ account,
128
+ operation: "gmail.drafts.create",
129
+ });
130
+ }
131
+ const result = {
132
+ action: "drafted",
133
+ account,
134
+ draft_id: draft.id ?? "",
135
+ message_id: draft.message?.id ?? "",
136
+ to: flags.to.join(", "),
137
+ subject: flags.subject || "(no subject)",
138
+ };
139
+ if (flags.cc.length)
140
+ result.cc = flags.cc.join(", ");
141
+ if (flags.thread)
142
+ result.thread_id = flags.thread;
143
+ return joinBlocks(renderObject(result), renderHelp([
144
+ "Draft saved — NOT sent. Review and send it from the Gmail UI (Drafts folder)",
145
+ `Edit or delete it later via the draft_id (${draft.id ?? ""})`,
146
+ ]));
147
+ }
148
+ //# sourceMappingURL=draft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../../../../src/commands/gmail/draft.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export declare const FILTER_LIST_HELP = "usage: gws-axi gmail filter-list [flags]\nflags[1]:\n --account <email> Account override when 2+ are configured\noutput:\n A `filters[N]{id,criteria,action}` table. Each row summarizes one\n server-side rule. Pass an `id` to `gws-axi gmail filter-delete <id>`.\nnotes:\n Filters are server-side auto-sort rules that run continuously on Google's\n side (separate from labels). Filters require the gmail.settings.basic scope. If this 403s, run `gws-axi auth login --account <email>` to re-consent.\n";
2
+ export declare const FILTER_CREATE_HELP = "usage: gws-axi gmail filter-create <criteria> <action> [flags]\ncriteria (>=1 required):\n --from <text> Match sender\n --to <text> Match recipient\n --subject <text> Match subject\n --query <text> Match an arbitrary Gmail search query\n --has-attachment Match only messages with attachments\naction (>=1 required):\n --add-label <name> Apply a label. Repeatable / comma list. Accepts user\n + system labels (e.g. STARRED, IMPORTANT)\n --remove-label <name> Remove a label. Use INBOX to skip-inbox (archive),\n UNREAD to mark read\n --forward <email> Forward matching mail to this address (must be a\n verified forwarding address in Gmail settings)\nflags:\n --account <email> REQUIRED when 2+ accounts are authenticated\nexamples:\n gws-axi gmail filter-create --from newsletters@x.com --remove-label INBOX --add-label News\n gws-axi gmail filter-create --subject \"[ALERT]\" --add-label STARRED --add-label IMPORTANT\nnotes:\n Idempotent: an existing filter with identical criteria + action returns\n `action: exists`. Filters require the gmail.settings.basic scope. If this 403s, run `gws-axi auth login --account <email>` to re-consent.\n";
3
+ export declare const FILTER_DELETE_HELP = "usage: gws-axi gmail filter-delete <filter-id> [flags]\nargs[1]:\n <filter-id> Filter ID (from `gws-axi gmail filter-list`)\nflags[1]:\n --account <email> REQUIRED when 2+ accounts are authenticated\nnotes:\n Idempotent: deleting an unknown filter id returns `action: noop`.\n Filters require the gmail.settings.basic scope. If this 403s, run `gws-axi auth login --account <email>` to re-consent.\n";
4
+ export declare function gmailFilterListCommand(account: string, _args: string[]): Promise<string>;
5
+ export declare function gmailFilterCreateCommand(account: string, args: string[]): Promise<string>;
6
+ export declare function gmailFilterDeleteCommand(account: string, args: string[]): Promise<string>;