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 +271 -202
- package/dist/cli.js +45 -1
- package/dist/cli.js.map +1 -1
- package/dist/lib/public-mail.js +7 -0
- package/dist/lib/public-mail.js.map +1 -1
- package/dist/lib/unread-state.js +68 -0
- package/dist/lib/unread-state.js.map +1 -0
- package/dist/lib/unread-state.test.js +191 -0
- package/dist/lib/unread-state.test.js.map +1 -0
- package/dist/providers/gmail/adapter.js +59 -1
- package/dist/providers/gmail/adapter.js.map +1 -1
- package/dist/providers/gmail/api.js +10 -0
- package/dist/providers/gmail/api.js.map +1 -1
- package/dist/providers/outlook/adapter.js +62 -1
- package/dist/providers/outlook/adapter.js.map +1 -1
- package/dist/providers/outlook/extract.js +8 -0
- package/dist/providers/outlook/extract.js.map +1 -1
- package/dist/providers/shared/html.js +54 -2
- package/dist/providers/shared/html.js.map +1 -1
- package/dist/providers/shared/html.test.js +16 -0
- package/dist/providers/shared/html.test.js.map +1 -0
- package/dist/session-daemon.js +36 -8
- package/dist/session-daemon.js.map +1 -1
- package/dist/session.js +6 -0
- package/dist/session.js.map +1 -1
- package/dist/state/database.js +61 -5
- package/dist/state/database.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,279 +1,300 @@
|
|
|
1
1
|
# Surface CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Outlook and Gmail from one local, JSON-first mail CLI.
|
|
4
4
|
|
|
5
|
-
Surface
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
Surface is especially useful for coding agents and personal assistants because it
|
|
15
|
+
keeps mail work compact:
|
|
12
16
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
17
|
-
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
surface session list
|
|
34
|
-
surface session stop sess_01...
|
|
33
|
+
Surface requires Node.js 20 or newer.
|
|
35
34
|
|
|
36
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
42
|
+
Then install the skill for the agent you use.
|
|
60
43
|
|
|
61
|
-
|
|
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
|
-
|
|
46
|
+
OpenClaw installs the hosted Surface skill from ClawHub:
|
|
73
47
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
## Current Implementation Status
|
|
48
|
+
```bash
|
|
49
|
+
openclaw skills install surface-cli
|
|
50
|
+
openclaw skills check
|
|
51
|
+
```
|
|
80
52
|
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
Codex reads user skills from `~/.agents/skills`:
|
|
98
59
|
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
### Claude Code
|
|
106
70
|
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
82
|
+
## Setup
|
|
130
83
|
|
|
131
|
-
|
|
84
|
+
Add the accounts you want Surface to manage:
|
|
132
85
|
|
|
133
86
|
```bash
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
96
|
+
surface account identity set uni \
|
|
97
|
+
--email you@school.edu \
|
|
98
|
+
--name "Your Name" \
|
|
99
|
+
--name-alias "FirstName"
|
|
143
100
|
```
|
|
144
101
|
|
|
145
|
-
|
|
102
|
+
Log in:
|
|
146
103
|
|
|
147
|
-
|
|
104
|
+
```bash
|
|
105
|
+
surface auth login uni
|
|
106
|
+
surface auth status uni
|
|
107
|
+
```
|
|
148
108
|
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
+
```bash
|
|
138
|
+
ssh macmini 'npm install -g surface-cli && openclaw skills install surface-cli'
|
|
139
|
+
```
|
|
180
140
|
|
|
181
|
-
|
|
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
|
-
|
|
144
|
+
Outlook remote setup:
|
|
184
145
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
150
|
+
surface auth login uni --remote-host macmini
|
|
151
|
+
ssh macmini 'surface auth status uni'
|
|
152
|
+
```
|
|
197
153
|
|
|
198
|
-
The remote
|
|
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
|
-
|
|
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
|
|
161
|
+
surface auth login personal --remote-host macmini
|
|
206
162
|
```
|
|
207
163
|
|
|
208
|
-
|
|
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
|
-
|
|
167
|
+
## Token-Efficient Mail Triage
|
|
220
168
|
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
176
|
+
surface mail fetch-unread --account uni --limit 10
|
|
177
|
+
surface mail sync-unread-state --account uni --limit 50
|
|
230
178
|
```
|
|
231
179
|
|
|
232
|
-
|
|
180
|
+
List recent sent messages:
|
|
233
181
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
190
|
+
Search with structured filters:
|
|
245
191
|
|
|
246
192
|
```bash
|
|
247
|
-
|
|
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
|
-
|
|
197
|
+
Read only the thread or message you need:
|
|
251
198
|
|
|
252
199
|
```bash
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
378
|
+
Release order:
|
|
312
379
|
|
|
313
|
-
1. publish the CLI package to npm so
|
|
314
|
-
2.
|
|
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.")
|