surface-cli 0.3.0 → 0.3.3

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 CHANGED
@@ -1,279 +1,300 @@
1
1
  # Surface CLI
2
2
 
3
- A lean, local-first mail CLI for multi-provider, multi-account email.
3
+ Outlook and Gmail from one local, JSON-first mail CLI.
4
4
 
5
- Surface normalizes Gmail and Outlook behind one contract, keeps local state in SQLite,
6
- stores auth/cache/downloads under `~/.surface-cli`, and prints machine-readable JSON to
7
- stdout for automation.
5
+ Surface solves the annoying mail automation case: you have a school or work
6
+ Microsoft 365 account, IMAP is disabled, Graph app permissions need tenant admin
7
+ approval, and normal mail CLIs stop there. Surface uses the Outlook web session
8
+ you can already access in Chrome, so there is no Microsoft app registration,
9
+ Graph permission grant, tenant consent, or IMAP toggle to ask IT for.
8
10
 
9
- ## Current V1 Shape
11
+ If you can sign in to Outlook on the web, Surface can give your agent a local
12
+ CLI for that mailbox.
10
13
 
11
- Top-level groups:
14
+ Surface is especially useful for coding agents and personal assistants because it
15
+ keeps mail work compact:
12
16
 
13
- - `surface account`
14
- - `surface auth`
15
- - `surface session`
16
- - `surface mail`
17
- - `surface attachment`
18
- - `surface cache`
17
+ - one CLI contract for Gmail and Outlook
18
+ - multi-account support with stable account names
19
+ - stable `thread_ref`, `message_ref`, `attachment_id`, and `session_id` values
20
+ - thread-first `search` and `fetch-unread` results
21
+ - optional automatic summaries for search/fetch triage, with cached summary reuse
22
+ so repeated checks do not keep bloating context
23
+ - local SQLite state, cached bodies, auth profiles, and downloads under
24
+ `~/.surface-cli`
25
+ - first-class headless/remote setup for Mac mini, VM, or server hosts
19
26
 
20
- Current command surface:
27
+ Surface is not an admin bypass. It uses the same mailbox access you already have.
28
+ If your organization blocks Outlook on the web or browser automation entirely,
29
+ Surface cannot override that policy.
21
30
 
22
- ```bash
23
- surface account add work --provider gmail --email me@company.com
24
- surface account add school --provider outlook --email me@school.edu
25
- surface account identity set school --name "Your Name" --name-alias "FirstName"
26
- surface account identity show school
27
-
28
- surface auth login work
29
- surface auth status
30
- surface auth logout school
31
+ ## Fast Install
31
32
 
32
- surface session start --account school
33
- surface session list
34
- surface session stop sess_01...
33
+ Surface requires Node.js 20 or newer.
35
34
 
36
- surface mail fetch-unread --account work --limit 25
37
- surface mail fetch-unread --account school --session sess_01... --limit 25
38
- surface mail search --account work --text invoice --subject overdue --limit 10
39
- surface mail search --account school --session sess_01... --from billing@vendor.com --limit 10
40
- surface mail search --account work --from billing@vendor.com --mailbox inbox --label unread --limit 10
41
- surface mail thread get thr_01... --refresh --session sess_01...
42
- surface mail read msg_01... --refresh --session sess_01...
43
- surface mail send --account school --to me@example.com --subject "hello" --body "test"
44
- surface mail send --account school --to me@example.com --subject "hello" --body "test" --draft
45
- surface mail reply msg_01... --body "Thanks"
46
- surface mail reply-all msg_01... --body "Thanks all"
47
- surface mail forward msg_01... --to me@example.com --body "FYI"
48
- surface mail archive msg_01...
49
- surface mail rsvp msg_01... --response tentative
35
+ Install the CLI first:
50
36
 
51
- surface attachment list msg_01...
52
- surface attachment download msg_01... att_01...
53
-
54
- surface cache stats
55
- surface cache prune
56
- surface cache clear --account work
37
+ ```bash
38
+ npm install -g surface-cli
39
+ surface --help
57
40
  ```
58
41
 
59
- ## Core Decisions
42
+ Then install the skill for the agent you use.
60
43
 
61
- - `fetch-unread` is the public command name.
62
- - Threads are the top-level result unit.
63
- - Messages are elements within a thread.
64
- - `thread get` takes a stable `thread_ref`.
65
- - `read` takes a stable `message_ref`.
66
- - Attachment download is separate from `read`.
67
- - Machine-facing commands emit JSON on stdout.
68
- - SQLite is the local source of truth for refs and cache metadata.
69
- - Account-owner identity is stored in SQLite and used so summaries can interpret `needs_action`
70
- from the selected account owner's perspective.
44
+ ### OpenClaw
71
45
 
72
- See the source-of-truth docs for the exact contracts:
46
+ OpenClaw installs the hosted Surface skill from ClawHub:
73
47
 
74
- - `docs/cli-contract.md`
75
- - `docs/provider-contract.md`
76
- - `docs/cache-and-db.md`
77
- - `docs/config.md`
78
-
79
- ## Current Implementation Status
48
+ ```bash
49
+ openclaw skills install surface-cli
50
+ openclaw skills check
51
+ ```
80
52
 
81
- The repo now contains a working TypeScript scaffold under `src/`:
53
+ If `openclaw skills check` reports the `surface` binary as missing, run
54
+ `npm install -g surface-cli` on that same machine.
82
55
 
83
- - CLI entrypoint and command groups
84
- - config loading from `~/.surface-cli/config.toml`
85
- - SQLite-backed local account state
86
- - adapter registry for `gmail-api` and `outlook-web-playwright`
87
- - donor normalization utilities ported from the legacy Surface repo for Gmail and Outlook
88
- - Gmail OAuth login wired to Google desktop-app OAuth with stored refresh tokens under `~/.surface-cli/auth/<account_id>/gmail-token.json`
89
- - live Gmail `fetch-unread`, structured `search`, `thread get`, `read`, `attachment list`, `attachment download`, `send`, `reply`, `reply-all`, `forward`, `archive`, `mark-read`, `mark-unread`, `rsvp`, and `--draft` on send-like actions
90
- - Outlook Playwright auth lifecycle wired to persistent profiles under `~/.surface-cli/auth/<account_id>/profile`
91
- - live Outlook `fetch-unread`, structured `search`, `thread get`, `read`, `attachment list`, `attachment download`, `send`, `reply`, `reply-all`, `forward`, `archive`, `mark-read`, `mark-unread`, `rsvp`, and `--draft` on send-like actions
92
- - explicit warm session ids for Outlook read-path chaining (`session start/list/stop`, plus `--session` on `search`, `fetch-unread`, `thread get --refresh`, and `read`)
93
- - summary backends for `openrouter` and `openclaw`, with `openai/gpt-5.4-mini` as the default
94
- OpenRouter summarizer model when summarization is enabled
95
- - lean opt-in Outlook v1 and Gmail v1 live e2e coverage via `npm run e2e:outlook-v1` and `npm run e2e:gmail-v1`
56
+ ### Codex
96
57
 
97
- What is still intentionally incomplete:
58
+ Codex reads user skills from `~/.agents/skills`:
98
59
 
99
- - draft lifecycle commands
100
- - move / delete
101
- - broader automated coverage beyond the opt-in provider v1 e2e scripts and cache-prune policy
60
+ ```bash
61
+ mkdir -p ~/.agents/skills/surface-cli
62
+ curl -fsSL https://raw.githubusercontent.com/VishalJ99/surface-cli/main/skills/surface-cli/SKILL.md \
63
+ -o ~/.agents/skills/surface-cli/SKILL.md
64
+ ```
102
65
 
103
- ## Setup
66
+ Restart Codex if the skill does not appear immediately. Codex can invoke it
67
+ automatically from the description, or you can mention `$surface-cli`.
104
68
 
105
- Surface supports two setup modes:
69
+ ### Claude Code
106
70
 
107
- - standard single-machine setup
108
- Surface runs on the same machine where you can access the browser, localhost callback ports,
109
- and any required GUI prompts
110
- - headless remote setup
111
- Surface runs on a remote machine such as a Mac mini, while a second local machine helps with
112
- Gmail OAuth browser approval or Outlook browser-profile bootstrap
71
+ Claude Code reads personal skills from `~/.claude/skills`:
113
72
 
114
- The correct split is:
73
+ ```bash
74
+ mkdir -p ~/.claude/skills/surface-cli
75
+ curl -fsSL https://raw.githubusercontent.com/VishalJ99/surface-cli/main/skills/surface-cli/SKILL.md \
76
+ -o ~/.claude/skills/surface-cli/SKILL.md
77
+ ```
115
78
 
116
- - the machine that actually runs `surface` for day-to-day mail work is the canonical Surface host
117
- - that host owns:
118
- - `~/.surface-cli/state.db`
119
- - `~/.surface-cli/auth/`
120
- - `~/.surface-cli/cache/`
121
- - `~/.surface-cli/downloads/`
122
- - `~/.surface-cli/config.toml` is auto-created on first run and stores local policy only
123
- such as summarizer and write-safety settings
124
- - account registry and auth state do not live in `config.toml`
125
- - existing config files are not rewritten on upgrade; if an older config names
126
- `summarizer_model = "openai/gpt-4o-mini"`, change it to
127
- `summarizer_model = "openai/gpt-5.4-mini"` for the current recommended default
79
+ Claude Code exposes the skill as `/surface-cli` and may also load it
80
+ automatically when the task matches the skill description.
128
81
 
129
- ### Install Surface
82
+ ## Setup
130
83
 
131
- For development from a checkout:
84
+ Add the accounts you want Surface to manage:
132
85
 
133
86
  ```bash
134
- npm install
135
- npm run build
136
- npm link
87
+ surface account add uni --provider outlook --email you@school.edu
88
+ surface account add personal --provider gmail --email you@gmail.com
89
+ surface account list
137
90
  ```
138
91
 
139
- For a published install, use npm:
92
+ For Outlook school/work accounts, set the mailbox owner's identity explicitly.
93
+ This helps summaries decide whether a thread needs action from you:
140
94
 
141
95
  ```bash
142
- npm install -g surface-cli
96
+ surface account identity set uni \
97
+ --email you@school.edu \
98
+ --name "Your Name" \
99
+ --name-alias "FirstName"
143
100
  ```
144
101
 
145
- ### Standard Single-Machine Setup
102
+ Log in:
146
103
 
147
- Use this when the same machine can:
104
+ ```bash
105
+ surface auth login uni
106
+ surface auth status uni
107
+ ```
148
108
 
149
- - open Chrome locally
150
- - receive loopback OAuth callbacks on `localhost`
151
- - show any required Microsoft or Google auth UI
109
+ Outlook auth opens Chrome and stores a dedicated browser profile under
110
+ `~/.surface-cli/auth/<account_id>/profile`. You do not need Azure, Graph, IMAP,
111
+ Exchange app passwords, or admin approval. You do need Chrome and the ability to
112
+ complete your normal Microsoft sign-in flow.
152
113
 
153
- Typical flow:
114
+ For Gmail, `surface auth login <account>` uses a Google desktop OAuth client and
115
+ stores the refresh token under `~/.surface-cli/auth/<account_id>/`. Place the
116
+ client secret at `./client_secret.json` or set `SURFACE_GMAIL_CLIENT_SECRET_FILE`.
154
117
 
155
- 1. install Surface on that machine
156
- 2. add accounts there
157
- 3. optionally set account-owner name/aliases with `surface account identity set <account> ...`
158
- 4. run `surface auth login <account>` there
159
- 5. use that same machine for normal `surface mail ...` commands
118
+ ## Headless Remote Setup
160
119
 
161
- Gmail:
120
+ Surface is designed for remote hosts, including a Mac mini or other headless box
121
+ running OpenClaw, Codex, or Claude Code.
162
122
 
163
- - place a Google desktop OAuth client secret at `./client_secret.json` or set
164
- `SURFACE_GMAIL_CLIENT_SECRET_FILE`
165
- - add the account first:
166
- - `surface account add personal --provider gmail --email you@example.com`
167
- - run:
168
- - `surface auth login personal`
169
- - Gmail auth verifies the mailbox email and updates account-owner identity automatically.
123
+ The rule is simple: install Surface and the agent skill on the machine where the
124
+ agent will run day to day. That machine is the canonical Surface host and owns:
170
125
 
171
- For Outlook auth:
126
+ ```text
127
+ ~/.surface-cli/state.db
128
+ ~/.surface-cli/auth/
129
+ ~/.surface-cli/cache/
130
+ ~/.surface-cli/downloads/
131
+ ```
132
+
133
+ Use your laptop only as an auth helper when the host cannot show a browser.
172
134
 
173
- - `surface auth login <account>` opens Chrome against the account profile directory
174
- - `surface auth status [account]` probes Outlook headlessly and reports whether the profile lands in the mailbox or a sign-in flow
175
- - `surface auth logout <account>` clears the stored Outlook profile for that account
176
- - if the Outlook account email is opaque or placeholder-like, set identity explicitly:
177
- - `surface account identity set uni --email you@school.edu --name "Your Name" --name-alias "FirstName"`
135
+ For example, if OpenClaw runs on a Mac mini, install both pieces there:
178
136
 
179
- ### Headless Remote Setup
137
+ ```bash
138
+ ssh macmini 'npm install -g surface-cli && openclaw skills install surface-cli'
139
+ ```
180
140
 
181
- Use this when your real Surface host is remote, for example a headless Mac mini, VM, or server.
141
+ If Codex or Claude Code runs on the remote host instead, run the matching skill
142
+ install command from the Fast Install section inside that remote shell.
182
143
 
183
- In this mode:
144
+ Outlook remote setup:
184
145
 
185
- - install Surface on the remote machine first
186
- - add accounts on the remote machine first
187
- - set account-owner identity on the remote machine when the mailbox address is opaque or
188
- placeholder-like
189
- - the remote machine is the source of truth for all Surface state
190
- - install Surface locally too if you want to use `--remote-host` auth helpers
191
- - `--remote-host` assumes the named account already exists on the remote machine
192
- - remote auth only warns before replacement when the remote account already reports `authenticated`
193
- - if the remote auth-state probe times out or fails, Surface proceeds without an overwrite warning
194
- instead of blocking the remote auth flow
146
+ ```bash
147
+ ssh macmini 'surface account add uni --provider outlook --email you@school.edu'
148
+ ssh macmini 'surface account identity set uni --email you@school.edu --name "Your Name"'
195
149
 
196
- #### Gmail On A Headless Remote Host
150
+ surface auth login uni --remote-host macmini
151
+ ssh macmini 'surface auth status uni'
152
+ ```
197
153
 
198
- The remote host is the real Surface runtime. Your local machine is only a browser helper.
154
+ The remote Outlook auth flow opens Chrome locally, lets you complete Microsoft
155
+ sign-in on your laptop, syncs the dedicated Surface browser profile to the remote
156
+ host, then validates it there.
199
157
 
200
- 1. on the remote host, install Surface, add the Gmail account, and optionally set identity aliases
201
- 2. on the local machine, ensure `surface` is installed too
202
- 3. on the local machine, run:
158
+ Gmail uses the same public remote command:
203
159
 
204
160
  ```bash
205
- surface auth login <gmail-account> --remote-host <ssh-host>
161
+ surface auth login personal --remote-host macmini
206
162
  ```
207
163
 
208
- What happens:
209
-
210
- - Surface starts SSH port forwarding first
211
- - Surface reuses the remote account's stored `client_secret.json` when present
212
- - Surface runs the Gmail OAuth listener on the remote host
213
- - if the remote host does not already have Gmail OAuth client credentials stored for that account,
214
- Surface falls back to a local `client_secret.json` or `SURFACE_GMAIL_CLIENT_SECRET_FILE`
215
- - you open the Google auth URL locally
216
- - the OAuth callback is forwarded back to the remote host
217
- - the refresh token is stored on the remote host under `~/.surface-cli/auth/<account_id>/`
164
+ For Gmail, Surface starts SSH port forwarding so the OAuth callback lands on the
165
+ remote Surface process and the refresh token is stored on the remote host.
218
166
 
219
- #### Outlook On A Headless Remote Host
167
+ ## Token-Efficient Mail Triage
220
168
 
221
- The remote host is again the real Surface runtime. Your local machine is only an auth/bootstrap
222
- helper.
169
+ Surface commands print JSON on stdout. Agents should parse the JSON and act on
170
+ stable refs rather than scraping terminal text or copying whole mail bodies into
171
+ chat.
223
172
 
224
- 1. on the remote host, install Surface, add the Outlook account, and set identity aliases if needed
225
- 2. on the local machine, ensure `surface` is installed too
226
- 3. on the local machine, run:
173
+ Fetch unread threads:
227
174
 
228
175
  ```bash
229
- surface auth login <outlook-account> --remote-host <ssh-host>
176
+ surface mail fetch-unread --account uni --limit 10
177
+ surface mail sync-unread-state --account uni --limit 50
230
178
  ```
231
179
 
232
- What happens:
180
+ List recent sent messages:
233
181
 
234
- - Surface opens local Chrome in a dedicated Surface profile
235
- - you complete the Microsoft sign-in locally
236
- - Surface syncs that profile to the remote host
237
- - Surface validates the copied profile on the remote host with `surface auth status <account>`
238
-
239
- This is why headless remote auth currently requires `surface` to exist on both machines:
182
+ ```bash
183
+ surface mail sent --account uni
184
+ surface mail sent --account uni --recipient person@example.com --limit 10
185
+ ```
240
186
 
241
- - local machine: helper for browser/UI work
242
- - remote machine: canonical Surface runtime and state owner
187
+ `sent` is message-first: the default limit is the last 10 sent messages, and each result includes a
188
+ stable `thread_ref` so agents can open the full conversation when needed.
243
189
 
244
- If Chrome is installed in a non-default location, set:
190
+ Search with structured filters:
245
191
 
246
192
  ```bash
247
- export SURFACE_CHROME_PATH="/absolute/path/to/Google Chrome"
193
+ surface mail search --account uni --from registrar@school.edu --subject "deadline" --limit 10
194
+ surface mail search --account personal --mailbox inbox --label unread --text "invoice" --limit 10
248
195
  ```
249
196
 
250
- For the live Outlook v1 e2e script:
197
+ Read only the thread or message you need:
251
198
 
252
199
  ```bash
253
- export SURFACE_E2E_ENABLE=1
254
- export SURFACE_TEST_RECIPIENTS='sender@example.com,recipient@example.com,observer@example.com'
255
- export SURFACE_TEST_ACCOUNT_ALLOWLIST='uni'
256
- npm run e2e:outlook-v1
200
+ surface mail thread get thr_01...
201
+ surface mail thread get thr_01... --refresh
202
+ surface mail read msg_01...
203
+ surface mail read msg_01... --refresh
257
204
  ```
258
205
 
259
- For the live Gmail v1 e2e script:
206
+ For Outlook-heavy sessions, start a warm browser session and pass its `session_id`
207
+ to follow-up read commands:
260
208
 
261
209
  ```bash
262
- export SURFACE_E2E_ENABLE=1
263
- export SURFACE_E2E_ACCOUNT='personal_2'
264
- npm run e2e:gmail-v1
210
+ surface session start --account uni
211
+ surface mail fetch-unread --account uni --session sess_01... --limit 10
212
+ surface mail sync-unread-state --account uni --session sess_01... --limit 50
213
+ surface mail sent --account uni --session sess_01... --limit 10
214
+ surface mail search --account uni --session sess_01... --text "exam board" --limit 10
215
+ surface session stop sess_01...
265
216
  ```
266
217
 
267
- For live write-path testing, also set:
218
+ Optional summaries are controlled locally. New configs default to
219
+ `summarizer_backend = "none"` so mail reads never require paid or external model
220
+ calls unless you opt in. To enable summaries, set `summarizer_backend` in
221
+ `~/.surface-cli/config.toml` or `SURFACE_SUMMARIZER_BACKEND` in the environment.
222
+
223
+ Supported summary backends:
224
+
225
+ - `openrouter`, using `OPENROUTER_API_KEY`
226
+ - `openclaw`, using the local `openclaw` CLI
227
+
228
+ When summaries are enabled, Surface summarizes a capped canonical per-thread
229
+ payload, stores summary fingerprints in SQLite, and reuses matching summaries on
230
+ later checks. This keeps recurring inbox watches and searches from repeatedly
231
+ feeding unchanged threads back into your agent context.
232
+
233
+ Email content may be sent to the configured summarizer provider when summaries
234
+ are enabled. Keep `summarizer_backend = "none"` if all mail content must remain
235
+ local to the provider and Surface cache.
236
+
237
+ ## Common Commands
268
238
 
269
239
  ```bash
270
- export SURFACE_WRITES_ENABLED=1
271
- export SURFACE_SEND_MODE=allow_send
272
- export SURFACE_TEST_RECIPIENTS='sender@example.com,recipient@example.com,observer@example.com'
273
- export SURFACE_TEST_ACCOUNT_ALLOWLIST='uni'
240
+ surface account list
241
+ surface account identity show uni
242
+
243
+ surface auth status
244
+ surface auth logout uni
245
+
246
+ surface mail fetch-unread --account uni --limit 25
247
+ surface mail sync-unread-state --account uni --limit 50
248
+ surface mail search --account uni --text "project update" --limit 10
249
+ surface mail thread get thr_01... --refresh
250
+ surface mail read msg_01... --refresh
251
+
252
+ surface attachment list msg_01...
253
+ surface attachment download msg_01... att_01...
254
+
255
+ surface mail send --account uni --to you@example.com --subject "hello" --body "test" --draft
256
+ surface mail reply msg_01... --body "Thanks" --draft
257
+ surface mail archive msg_01...
258
+ surface mail mark-read msg_01...
259
+ surface mail mark-unread msg_01...
260
+ surface mail rsvp msg_01... --response tentative
261
+
262
+ surface cache stats
263
+ surface cache prune
274
264
  ```
275
265
 
276
- ## Local State
266
+ Write actions are guarded by local policy in `~/.surface-cli/config.toml` and
267
+ `SURFACE_*` environment variables. Use `--draft` for safe compose flows unless
268
+ you have explicitly enabled live sends.
269
+
270
+ ## What Works Today
271
+
272
+ Surface v1 supports:
273
+
274
+ - Gmail via Google APIs
275
+ - Outlook via Outlook Web and Playwright
276
+ - account add/list/remove
277
+ - auth login/status/logout
278
+ - account-owner identity for summaries
279
+ - `search`, `fetch-unread`, and message-first `sent`
280
+ - bounded unread-state refresh with `sync-unread-state`
281
+ - thread refresh and message read
282
+ - attachment list/download
283
+ - send/reply/reply-all/forward with `--draft`
284
+ - archive, mark-read, mark-unread, RSVP
285
+ - Outlook warm sessions for repeated read-path commands
286
+ - optional summaries through OpenRouter or OpenClaw
287
+
288
+ Intentionally incomplete:
289
+
290
+ - draft lifecycle commands
291
+ - move/delete
292
+ - broad automated live coverage beyond the opt-in Gmail and Outlook v1 e2e
293
+ scripts
294
+
295
+ ## State And Config
296
+
297
+ Surface stores local state under `~/.surface-cli`:
277
298
 
278
299
  ```text
279
300
  ~/.surface-cli/
@@ -290,6 +311,26 @@ export SURFACE_TEST_ACCOUNT_ALLOWLIST='uni'
290
311
  <message_ref>/
291
312
  ```
292
313
 
314
+ `config.toml` stores local policy and preferences only. Account registry, auth
315
+ material, cache metadata, and account-owner identity live in SQLite and auth
316
+ storage, not in `config.toml`.
317
+
318
+ ## Contract Docs
319
+
320
+ These are the source-of-truth docs for behavior changes:
321
+
322
+ - `docs/cli-contract.md`
323
+ - `docs/provider-contract.md`
324
+ - `docs/cache-and-db.md`
325
+ - `docs/config.md`
326
+ - `docs/decisions/`
327
+
328
+ External skill docs:
329
+
330
+ - OpenClaw skills: https://docs.openclaw.ai/cli/skills
331
+ - Codex skills: https://developers.openai.com/codex/skills
332
+ - Claude Code skills: https://code.claude.com/docs/en/skills
333
+
293
334
  ## Development
294
335
 
295
336
  ```bash
@@ -299,22 +340,47 @@ npm run build
299
340
  npm run surface -- --help
300
341
  ```
301
342
 
343
+ For live Outlook v1 e2e:
344
+
345
+ ```bash
346
+ export SURFACE_E2E_ENABLE=1
347
+ export SURFACE_TEST_RECIPIENTS='sender@example.com,recipient@example.com,observer@example.com'
348
+ export SURFACE_TEST_ACCOUNT_ALLOWLIST='uni'
349
+ npm run e2e:outlook-v1
350
+ ```
351
+
352
+ For live Gmail v1 e2e:
353
+
354
+ ```bash
355
+ export SURFACE_E2E_ENABLE=1
356
+ export SURFACE_E2E_ACCOUNT='personal'
357
+ npm run e2e:gmail-v1
358
+ ```
359
+
360
+ For live write-path testing:
361
+
362
+ ```bash
363
+ export SURFACE_WRITES_ENABLED=1
364
+ export SURFACE_SEND_MODE=allow_send
365
+ export SURFACE_TEST_RECIPIENTS='sender@example.com,recipient@example.com,observer@example.com'
366
+ export SURFACE_TEST_ACCOUNT_ALLOWLIST='uni'
367
+ ```
368
+
302
369
  ## Publish To ClawHub
303
370
 
304
- ClawHub publishes the skill folder, not the whole repo. The publish unit is:
371
+ ClawHub publishes the skill folder, not the whole repo:
305
372
 
306
373
  ```text
307
374
  skills/surface-cli/
308
375
  SKILL.md
309
376
  ```
310
377
 
311
- Recommended release order:
378
+ Release order:
312
379
 
313
- 1. publish the CLI package to npm so ClawHub can install `surface`
314
- 2. log into ClawHub
380
+ 1. publish the CLI package to npm so the skill can install `surface`
381
+ 2. sync the intended `SKILL.md` into the active OpenClaw workspace if needed
315
382
  3. publish the skill folder
316
-
317
- Example:
383
+ 4. inspect the hosted skill
318
384
 
319
385
  ```bash
320
386
  npm publish
@@ -324,6 +390,9 @@ clawhub publish ./skills/surface-cli \
324
390
  --name "Surface CLI" \
325
391
  --version <version> \
326
392
  --changelog "<release notes>"
393
+
394
+ clawhub inspect surface-cli --json
395
+ clawhub inspect surface-cli --file SKILL.md
327
396
  ```
328
397
 
329
398
  Before publishing, verify the package payload:
package/dist/cli.js CHANGED
@@ -9,9 +9,11 @@ import { toPublicThread } from "./lib/public-mail.js";
9
9
  import { runRemoteAuthLogin } from "./lib/remote-auth.js";
10
10
  import { loadStoredThread, threadHasReadableCache } from "./lib/stored-mail.js";
11
11
  import { nowIsoUtc } from "./lib/time.js";
12
+ import { syncUnreadState } from "./lib/unread-state.js";
12
13
  import { resolveProviderAdapter } from "./providers/index.js";
13
14
  import { createAccountRuntimeContext, createRuntimeContext } from "./runtime.js";
14
- import { DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS, DEFAULT_SESSION_MAX_AGE_SECONDS, listWarmSessions, sessionFetchUnread, sessionReadMessage, sessionRefreshThread, sessionSearch, sessionStartEnvelope, startWarmSession, stopWarmSession, } from "./session.js";
15
+ import { DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS, DEFAULT_SESSION_MAX_AGE_SECONDS, listWarmSessions, sessionFetchUnread, sessionReadMessage, sessionRefreshThread, sessionSearch, sessionSent, sessionStartEnvelope, startWarmSession, stopWarmSession, } from "./session.js";
16
+ const DEFAULT_SENT_LIMIT = 10;
15
17
  function positiveInt(value) {
16
18
  const parsed = Number.parseInt(value, 10);
17
19
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -454,6 +456,48 @@ mailCommand
454
456
  });
455
457
  });
456
458
  });
459
+ mailCommand
460
+ .command("sent")
461
+ .description("List recent sent messages.")
462
+ .requiredOption("--account <account>", "Logical account name")
463
+ .option("--session <session_id>", "Use a warm session id")
464
+ .option("--recipient <email>", "Recipient filter for sent messages")
465
+ .addOption(new Option("--limit <limit>", "Max sent messages to return").argParser(positiveInt))
466
+ .action(async (options, command) => {
467
+ await runAccountAction(command.optsWithGlobals(), options.account, async (context) => {
468
+ const recipient = normalizeOptionalString(options.recipient);
469
+ const query = {
470
+ ...(recipient ? { recipient } : {}),
471
+ limit: options.limit ?? DEFAULT_SENT_LIMIT,
472
+ };
473
+ const messages = options.session
474
+ ? await sessionSent(context, options.session, query)
475
+ : await resolveProviderAdapter(context.account).fetchSent(context.account, query, context);
476
+ writeJson({
477
+ schema_version: "1",
478
+ command: "sent",
479
+ generated_at: nowIsoUtc(),
480
+ account: context.account.name,
481
+ query,
482
+ messages,
483
+ });
484
+ });
485
+ });
486
+ mailCommand
487
+ .command("sync-unread-state")
488
+ .requiredOption("--account <account>", "Logical account name")
489
+ .option("--session <session_id>", "Use a warm session id")
490
+ .addOption(new Option("--limit <limit>", "Max unread threads to fetch and local candidates to compare").argParser(positiveInt))
491
+ .option("--rebaseline", "Clear local unread for the account before repopulating fetched unread", false)
492
+ .action(async (options, command) => {
493
+ await runAccountAction(command.optsWithGlobals(), options.account, async (context) => {
494
+ writeJson(await syncUnreadState(context, {
495
+ limit: options.limit ?? context.config.defaultResultLimit,
496
+ rebaseline: Boolean(options.rebaseline),
497
+ ...(options.session ? { session: options.session } : {}),
498
+ }));
499
+ });
500
+ });
457
501
  mailCommand
458
502
  .command("thread")
459
503
  .description("Inspect thread state.")