@plotio/cli 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -156
- package/dist/plot.js +347 -92
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# @plotio/cli
|
|
2
2
|
|
|
3
|
-
`plot`: read Plot pages, follow a meeting while it happens, and
|
|
3
|
+
`plot`: read Plot pages, follow a meeting while it happens, and write back, from
|
|
4
4
|
any agent with a shell (Claude Code, Codex CLI, Amp, OpenCode) or a script.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Plot records conversations — meetings, calls, voice notes — as shared pages: a
|
|
7
|
+
transcript of who said what, plus topics Plot's own model keeps up to date as
|
|
8
|
+
people talk.
|
|
7
9
|
|
|
8
10
|
## Install
|
|
9
11
|
|
|
@@ -12,183 +14,121 @@ npx @plotio/cli help agents # no install
|
|
|
12
14
|
npm install -g @plotio/cli # or install the `plot` binary
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
Node 22 or later
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
the command
|
|
17
|
+
Node 22 or later.
|
|
18
|
+
|
|
19
|
+
**An agent should run `plot help agents` first.** It is the whole guide, and it
|
|
20
|
+
is the one that matches the version installed. `plot help <command>` lists a
|
|
21
|
+
command's options; everything below is the map, not a second copy of it.
|
|
19
22
|
|
|
20
23
|
## Getting access
|
|
21
24
|
|
|
22
|
-
Two kinds of credential, both `plot_agent_sk_…` tokens that Plot stores only
|
|
23
|
-
|
|
25
|
+
Two kinds of credential, both `plot_agent_sk_…` tokens that Plot stores only as
|
|
26
|
+
a hash. Each is kept once redeemed, so later shells need nothing.
|
|
24
27
|
|
|
25
|
-
- **A page.** Someone picks "Copy agent instructions" on a page, which gives
|
|
26
|
-
|
|
28
|
+
- **A page.** Someone picks "Copy agent instructions" on a page, which gives a
|
|
29
|
+
one-time `temp_code_…` (ten minutes, single use).
|
|
27
30
|
`plot open temp_code_… --agent "Claude Code"` redeems it for a token that
|
|
28
31
|
reads that one page (24 hours from the last read, a week at most).
|
|
29
|
-
- **A workspace.** `plot login --agent "Claude Code"` signs in
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
code the page shows at `Paste code here if prompted:`. Whichever arrives
|
|
50
|
-
first wins, within ten minutes. `--no-browser` prints only that URL and
|
|
51
|
-
does not listen at all.
|
|
52
|
-
- **An agent or a sandbox (no TTY).** Nothing listens and nothing waits.
|
|
53
|
-
`plot login` prints the out-of-band URL and
|
|
54
|
-
`After approving, run: plot login --code <code>`, saves a pending login
|
|
55
|
-
(API base, verifier, state, agent name, time) in
|
|
56
|
-
`$XDG_CONFIG_HOME/plot/pending-login.json` at `0600`, and exits 0. The agent
|
|
57
|
-
hands the URL to its person, who approves and sends the code back;
|
|
58
|
-
`plot login --code <code>` exchanges it, stores the grant, deletes the
|
|
59
|
-
pending file and prints `Connected to <workspace>`. A pending login lasts
|
|
60
|
-
ten minutes, and `--code` works at a terminal too.
|
|
32
|
+
- **A workspace.** `plot login --agent "Claude Code"` signs in with OAuth in a
|
|
33
|
+
browser, against the authorization server the API itself names — so `--api`
|
|
34
|
+
at a local or preview deployment signs in on that deployment's own web app,
|
|
35
|
+
never production's.
|
|
36
|
+
- **A workspace, with no browser anywhere.** Someone signed in to Plot creates
|
|
37
|
+
a token in Settings > Agents and hands it over. Set `PLOT_TOKEN` (and
|
|
38
|
+
`PLOT_API_URL` unless it is production) and every command works with no login
|
|
39
|
+
and nothing stored, which is what a sandbox or a CI job wants.
|
|
40
|
+
|
|
41
|
+
`plot login` ends three ways, and what it can see decides which:
|
|
42
|
+
|
|
43
|
+
- **This machine's browser.** It opens, you approve, and it hands the terminal
|
|
44
|
+
the credential itself. Nothing to paste.
|
|
45
|
+
- **A browser somewhere else** (SSH, another computer). Approve at the URL the
|
|
46
|
+
terminal prints, then paste the code that page shows. Ten minutes.
|
|
47
|
+
`--no-browser` prints that URL and listens for nothing.
|
|
48
|
+
- **An agent or a sandbox** (no TTY). Nothing waits: `plot login` prints the URL
|
|
49
|
+
and exits 0. Hand the URL to your person; they approve and send back the code
|
|
50
|
+
the page showed. `plot login --code <code>` finishes the login, within ten
|
|
51
|
+
minutes, and prints the workspace.
|
|
61
52
|
|
|
62
53
|
## Commands
|
|
63
54
|
|
|
64
|
-
| Command
|
|
65
|
-
|
|
|
66
|
-
| `plot open <temp_code_…> [--agent <name>]`
|
|
67
|
-
| `plot login [--agent] [--no-browser]`
|
|
68
|
-
| `plot login --code <code>`
|
|
69
|
-
| `plot logout [<page>] [--all]`
|
|
70
|
-
| `plot whoami [<page>] [--json]`
|
|
71
|
-
| `plot pages [--limit <n>] [--next <c>] [--json]`
|
|
72
|
-
| `plot read <page> [--json]`
|
|
73
|
-
| `plot watch <page> [--level …] [options]`
|
|
74
|
-
| `plot topics <page>`
|
|
75
|
-
| `plot search <query> [--limit] [--next] [--json]`
|
|
76
|
-
| `plot
|
|
77
|
-
| `plot
|
|
78
|
-
| `plot
|
|
55
|
+
| Command | Does |
|
|
56
|
+
| ------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
57
|
+
| `plot open <temp_code_…> [--agent <name>]` | Redeem a handoff code and store the page token |
|
|
58
|
+
| `plot login [--agent] [--no-browser]` | Connect a workspace: sign in and approve in the browser |
|
|
59
|
+
| `plot login --code <code>` | Finish a login started without a terminal, with the code the browser showed |
|
|
60
|
+
| `plot logout [<page>] [--all]` | Revoke a credential on the server, then forget it |
|
|
61
|
+
| `plot whoami [<page>] [--json]` | What the credential acts for, its scope and expiry |
|
|
62
|
+
| `plot pages [--limit <n>] [--next <c>] [--json]` | Pages the workspace grant can read, newest first |
|
|
63
|
+
| `plot read <page> [--json]` | The page as Markdown (topics included), or as JSON; stores the updates cursor |
|
|
64
|
+
| `plot watch <page> [--level …] [options]` | Wait for the next update, print it, store the cursor |
|
|
65
|
+
| `plot topics <page>` | Topics as JSON, with the quotes behind each point |
|
|
66
|
+
| `plot search <query> [--limit] [--next] [--json]` | Search every page the workspace grant can read |
|
|
67
|
+
| `plot message <page> [--body <t>] [--branch] [--note] [--id]` | Draft a message into the person's composer (body from `--body`, else stdin) |
|
|
68
|
+
| `plot message <page> --send …` | Post it as the person instead, marked "via" your agent |
|
|
69
|
+
| `plot message <page> --show\|--discard [<id>]` | Read back or withdraw a draft (the last one made here by default) |
|
|
70
|
+
| `plot mcp-config [--host <h>] [--token] [--write] [<page>]` | The MCP server block for a host: the `<api>/mcp` URL, signed in with OAuth |
|
|
71
|
+
| `plot help agents` | The guide for an AI agent |
|
|
79
72
|
|
|
80
73
|
`<page>` is a page id or a page URL (`https://www.plot.io/p/<id>`).
|
|
81
74
|
|
|
82
|
-
Every command takes `--api <url>`, `--token <token
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
### `watch`
|
|
86
|
-
|
|
87
|
-
One command is one turn: it blocks until something qualifies, prints only
|
|
88
|
-
what is new since the last `watch` of that page, stores the new cursor and
|
|
89
|
-
exits. Levels: `substantial` (default, when Plot's topic model completed a
|
|
90
|
-
run), `every` (any new words), `topics` (a topic changed). Each request asks
|
|
91
|
-
the server to hold it for up to 240 seconds (`wait=240`, kept under the 300
|
|
92
|
-
seconds Node's fetch waits for response headers); where the server
|
|
93
|
-
answers at once instead (Convex reached directly, in local dev and previews),
|
|
94
|
-
`watch` pauses `--interval` seconds (default 3) and asks again.
|
|
95
|
-
`--timeout <seconds>` gives up, cursor unchanged; keep it under your shell
|
|
96
|
-
tool's own timeout. `--from-start` and `--cursor` override the stored cursor,
|
|
97
|
-
and `--partial` includes words still being spoken (marked `interim`).
|
|
98
|
-
|
|
99
|
-
An update is `{notice, page, level, lines, topics, room, lastRunAt, cursor}`:
|
|
100
|
-
`lines` carry `authorName`, `text` and the flags `continues`, `revised`,
|
|
101
|
-
`removed`, `interim`; `topics` carry their `points` with source quotes;
|
|
102
|
-
`room` is who is present. `plot read` also stores the cursor (from its
|
|
103
|
-
`X-Plot-Updates-Cursor` header), so a `watch` after a `read` brings only what
|
|
104
|
-
was said after it. A stored cursor the server refuses (`bad_cursor`,
|
|
105
|
-
`wrong_cursor_page`) is dropped and the watch starts over once.
|
|
106
|
-
|
|
107
|
-
### `pages` and `search`
|
|
108
|
-
|
|
109
|
-
Both page with an opaque `next`; keep passing `--next` until the output has
|
|
110
|
-
no "More:" line, since a short batch does not mean the end. A page token on
|
|
111
|
-
either is refused (`wrong_scope`): they need `plot login`.
|
|
112
|
-
|
|
113
|
-
### `post`
|
|
114
|
-
|
|
115
|
-
Each run generates one idempotency `id` and reuses it if the connection drops
|
|
116
|
-
or a gateway answers 502 to 504, so the message lands once. A refusal is never
|
|
117
|
-
retried: `read_only` means the person who granted access cannot post there.
|
|
118
|
-
|
|
119
|
-
### `mcp-config`
|
|
120
|
-
|
|
121
|
-
Hosts: `claude-code` (default, `.mcp.json`), `cursor` (`~/.cursor/mcp.json`),
|
|
122
|
-
`vscode` (`.vscode/mcp.json`), `zed` (its `settings.json`) and
|
|
123
|
-
`claude-desktop` (a connector added in its settings, OAuth only). The
|
|
124
|
-
`Authorization` header names the `PLOT_TOKEN` environment variable in the
|
|
125
|
-
host's own syntax (`${PLOT_TOKEN}` for Claude Code, `${env:PLOT_TOKEN}` for
|
|
126
|
-
Cursor and VS Code) rather than holding the token, because `.mcp.json` and
|
|
127
|
-
`.vscode/mcp.json` are usually committed. The token itself (the stored
|
|
128
|
-
workspace one, or `<page>`'s) is printed to stderr as an `export` line to run
|
|
129
|
-
where the host starts. `--inline-token` puts the token in the header instead
|
|
130
|
-
(and makes a file it writes `0600`); Zed cannot read a variable there, so its
|
|
131
|
-
block always holds the token. `--no-token` leaves the header out so the host
|
|
132
|
-
signs in with OAuth. `--write` merges the block into the host's file, keeping
|
|
133
|
-
every other server, and refuses a file whose top level is not a JSON object.
|
|
134
|
-
|
|
135
|
-
## Environment
|
|
136
|
-
|
|
137
|
-
| Variable | Meaning |
|
|
138
|
-
| ----------------- | ------------------------------------------------------------------------- |
|
|
139
|
-
| `PLOT_API_URL` | API base. Default `https://agents.plot.io`; a deployment's `.convex.site` |
|
|
140
|
-
| | host for local dev and previews. `--api` wins over it. |
|
|
141
|
-
| `PLOT_TOKEN` | Use this token instead of the store (a sandbox or CI job). |
|
|
142
|
-
| `PLOT_AGENT_NAME` | Default `--agent`. Inside Claude Code the default is "Claude Code". |
|
|
143
|
-
| `XDG_CONFIG_HOME` | Where the credential store lives (below). |
|
|
75
|
+
Every command takes `--api <url>`, `--token <token>`, `--json` and `-h`/`--help`
|
|
76
|
+
(except `mcp-config`, where `--token` is a switch taking no value).
|
|
144
77
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
78
|
+
`watch` is one update per run: it blocks until something qualifies at its
|
|
79
|
+
`--level`, prints only what is new since the last `watch` or `read` of that
|
|
80
|
+
page, stores the cursor and exits — so the next run continues where it left
|
|
81
|
+
off. Keep `--timeout` under your shell tool's own. See `plot help watch`.
|
|
148
82
|
|
|
149
|
-
##
|
|
83
|
+
## `mcp-config`
|
|
150
84
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
85
|
+
By default the block is the URL and nothing else, which is the standard way to
|
|
86
|
+
reach a remote MCP server: the host gets a 401 pointing at Plot's OAuth
|
|
87
|
+
metadata, registers itself, and has you sign in the first time it connects.
|
|
154
88
|
|
|
155
89
|
```json
|
|
156
90
|
{
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"token": "plot_agent_sk_…",
|
|
160
|
-
"workspace": "Acme",
|
|
161
|
-
"expiresAt": "…"
|
|
162
|
-
},
|
|
163
|
-
"pages": {
|
|
164
|
-
"<pageId>": {
|
|
165
|
-
"token": "plot_agent_sk_…",
|
|
166
|
-
"expiresAt": "…",
|
|
167
|
-
"title": "…",
|
|
168
|
-
"cursor": "…"
|
|
169
|
-
}
|
|
170
|
-
}
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"plot": { "type": "http", "url": "https://agents.plot.io/mcp" }
|
|
171
93
|
}
|
|
172
94
|
}
|
|
173
95
|
```
|
|
174
96
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
97
|
+
Hosts: `claude-code` (default, `.mcp.json`), `cursor`, `vscode`, `zed` and
|
|
98
|
+
`claude-desktop`. `--write` merges the block into that host's file, keeping
|
|
99
|
+
every other server. For Claude Code,
|
|
100
|
+
`claude mcp add --transport http --scope project plot <api>/mcp` is equivalent.
|
|
178
101
|
|
|
179
|
-
|
|
102
|
+
`--token` adds a bearer header instead, for where OAuth cannot be used — a page
|
|
103
|
+
grant, which has no OAuth flow, or a sandbox or CI job with no browser. The
|
|
104
|
+
header names `PLOT_TOKEN` rather than holding the token, since these files are
|
|
105
|
+
usually committed. See `plot help mcp-config`.
|
|
180
106
|
|
|
181
|
-
|
|
182
|
-
pnpm --filter @plotio/cli test # node --test, no network
|
|
183
|
-
pnpm --filter @plotio/cli type-check
|
|
184
|
-
pnpm --filter @plotio/cli lint
|
|
185
|
-
pnpm --filter @plotio/cli build # dist/plot.js, one bundled file
|
|
186
|
-
```
|
|
107
|
+
## Environment
|
|
187
108
|
|
|
188
|
-
|
|
189
|
-
|
|
109
|
+
| Variable | Meaning |
|
|
110
|
+
| ----------------- | ---------------------------------------------------------------------------- |
|
|
111
|
+
| `PLOT_API_URL` | API base. Default `https://agents.plot.io`; `https://agents-staging.plot.io` |
|
|
112
|
+
| | for staging; a deployment's `.convex.site` host for local dev and previews. |
|
|
113
|
+
| | `--api` wins over it. |
|
|
114
|
+
| `PLOT_TOKEN` | Use this token instead of the store (a sandbox or CI job). |
|
|
115
|
+
| `PLOT_AGENT_NAME` | Default `--agent`. Inside Claude Code the default is "Claude Code". |
|
|
116
|
+
| `XDG_CONFIG_HOME` | Where the credential store lives (below). |
|
|
117
|
+
|
|
118
|
+
Token resolution for a page command: `--token`, then `PLOT_TOKEN`, then the
|
|
119
|
+
page's stored grant, then the stored workspace grant. A stored token the server
|
|
120
|
+
calls expired or revoked is removed and the next one tried.
|
|
190
121
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
122
|
+
## Where credentials live
|
|
123
|
+
|
|
124
|
+
`$XDG_CONFIG_HOME/plot/credentials.json`, by default
|
|
125
|
+
`~/.config/plot/credentials.json`, mode `0600` in a `0700` directory. It is
|
|
126
|
+
keyed by API base, so production's tokens and a preview's never collide — and
|
|
127
|
+
so changing `--api` or `PLOT_API_URL` changes which credential is in play. It
|
|
128
|
+
also holds each page's last `watch` cursor, which is how a new shell resumes,
|
|
129
|
+
and the id of the last message `plot message` made there.
|
|
130
|
+
`plot logout --all` revokes and removes everything for the current API base.
|
|
131
|
+
|
|
132
|
+
A `plot login` with no TTY leaves a `pending-login.json` beside it, same mode,
|
|
133
|
+
holding what `--code` needs to finish the login. It is deleted on success and
|
|
134
|
+
is useless after ten minutes.
|
package/dist/plot.js
CHANGED
|
@@ -6,22 +6,22 @@ import { parseArgs } from "node:util";
|
|
|
6
6
|
// src/errors.ts
|
|
7
7
|
var CliError = class extends Error {
|
|
8
8
|
exitCode;
|
|
9
|
-
constructor(
|
|
10
|
-
super(
|
|
9
|
+
constructor(message2, exitCode = 1) {
|
|
10
|
+
super(message2);
|
|
11
11
|
this.name = "CliError";
|
|
12
12
|
this.exitCode = exitCode;
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
var UsageError = class extends CliError {
|
|
16
|
-
constructor(
|
|
17
|
-
super(`${
|
|
16
|
+
constructor(message2) {
|
|
17
|
+
super(`${message2}
|
|
18
18
|
Run "plot --help" for usage.`, 2);
|
|
19
19
|
this.name = "UsageError";
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
// src/version.ts
|
|
24
|
-
var VERSION = "0.
|
|
24
|
+
var VERSION = "0.4.0";
|
|
25
25
|
|
|
26
26
|
// src/api.ts
|
|
27
27
|
var DEFAULT_API_URL = "https://agents.plot.io";
|
|
@@ -29,8 +29,8 @@ var ApiError = class extends Error {
|
|
|
29
29
|
status;
|
|
30
30
|
/** The server's machine reason (`expired`, `no_access`, …), or `network`/`http`. */
|
|
31
31
|
code;
|
|
32
|
-
constructor(status, code,
|
|
33
|
-
super(
|
|
32
|
+
constructor(status, code, message2) {
|
|
33
|
+
super(message2);
|
|
34
34
|
this.name = "ApiError";
|
|
35
35
|
this.status = status;
|
|
36
36
|
this.code = code;
|
|
@@ -252,32 +252,65 @@ function createApi(opts) {
|
|
|
252
252
|
);
|
|
253
253
|
},
|
|
254
254
|
/**
|
|
255
|
-
* POST /v1/pages/:id/messages
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
255
|
+
* POST /v1/pages/:id/messages: a draft in the person's composer, or with
|
|
256
|
+
* `send` a message on the page. `id` makes it idempotent, which is what
|
|
257
|
+
* lets a transport failure (no answer, or a gateway 5xx) be retried with
|
|
258
|
+
* the same id: the server acts once however many times it arrives. A
|
|
259
|
+
* refusal with a reason is never retried. Returns the HTTP status too,
|
|
260
|
+
* since 201 (new) and 200 (an existing draft updated) differ only there.
|
|
259
261
|
*/
|
|
260
|
-
async
|
|
261
|
-
const body = {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
262
|
+
async createMessage(token, pageId, message2, retry = {}) {
|
|
263
|
+
const body = {};
|
|
264
|
+
if (message2.body !== void 0) body.body = message2.body;
|
|
265
|
+
body.id = message2.id;
|
|
266
|
+
if (message2.branchId) body.branchId = message2.branchId;
|
|
267
|
+
if (message2.send) body.send = true;
|
|
268
|
+
if (message2.note) body.note = message2.note;
|
|
266
269
|
const attempts = retry.attempts ?? 3;
|
|
267
270
|
for (let attempt = 1; ; attempt++) {
|
|
268
271
|
try {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
})
|
|
272
|
+
const response = await request(
|
|
273
|
+
"POST",
|
|
274
|
+
`/v1/pages/${enc(pageId)}/messages`,
|
|
275
|
+
{ token, body }
|
|
274
276
|
);
|
|
277
|
+
return {
|
|
278
|
+
status: response.status,
|
|
279
|
+
body: await json(response)
|
|
280
|
+
};
|
|
275
281
|
} catch (err) {
|
|
276
282
|
const transport = err instanceof ApiError && (err.code === "network" || [502, 503, 504].includes(err.status));
|
|
277
283
|
if (!transport || attempt >= attempts) throw err;
|
|
278
284
|
await retry.sleep?.(1e3 * attempt);
|
|
279
285
|
}
|
|
280
286
|
}
|
|
287
|
+
},
|
|
288
|
+
/**
|
|
289
|
+
* GET /v1/pages/:id/messages/:messageId: a draft's words and note, or
|
|
290
|
+
* just `status: 'sent'` once it is on the page. 404 `no_draft` when the
|
|
291
|
+
* id names nothing of the person's.
|
|
292
|
+
*/
|
|
293
|
+
async getMessage(token, pageId, messageId) {
|
|
294
|
+
const body = await json(
|
|
295
|
+
await request(
|
|
296
|
+
"GET",
|
|
297
|
+
`/v1/pages/${enc(pageId)}/messages/${enc(messageId)}`,
|
|
298
|
+
{ token }
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
return "message" in body && body.message && typeof body.message === "object" ? body.message : body;
|
|
302
|
+
},
|
|
303
|
+
/**
|
|
304
|
+
* DELETE /v1/pages/:id/messages/:messageId: withdraw a draft. 204; 404
|
|
305
|
+
* `no_draft`; 403 `not_a_draft` for a message already sent, which the
|
|
306
|
+
* agent surface never deletes.
|
|
307
|
+
*/
|
|
308
|
+
async discardMessage(token, pageId, messageId) {
|
|
309
|
+
await request(
|
|
310
|
+
"DELETE",
|
|
311
|
+
`/v1/pages/${enc(pageId)}/messages/${enc(messageId)}`,
|
|
312
|
+
{ token }
|
|
313
|
+
);
|
|
281
314
|
}
|
|
282
315
|
};
|
|
283
316
|
}
|
|
@@ -296,8 +329,8 @@ async function errorFrom(response) {
|
|
|
296
329
|
const body = JSON.parse(text);
|
|
297
330
|
if (body && typeof body === "object") {
|
|
298
331
|
const code = typeof body.error === "string" ? body.error : "http";
|
|
299
|
-
const
|
|
300
|
-
return new ApiError(response.status, code,
|
|
332
|
+
const message2 = typeof body.message === "string" && body.message ? body.message : `Request failed with HTTP ${response.status} (${code}).`;
|
|
333
|
+
return new ApiError(response.status, code, message2);
|
|
301
334
|
}
|
|
302
335
|
} catch {
|
|
303
336
|
}
|
|
@@ -349,6 +382,7 @@ var KNOWN_NODES = /* @__PURE__ */ new Set([
|
|
|
349
382
|
...INLINE_NODES
|
|
350
383
|
]);
|
|
351
384
|
var KNOWN_MARKS = new Set(MARKS);
|
|
385
|
+
var MAX_DOC_DEPTH = 20;
|
|
352
386
|
var MAX_DOC_BYTES = 256 * 1024;
|
|
353
387
|
var MARKDOWN_SYNTAX = new RegExp(
|
|
354
388
|
[
|
|
@@ -385,6 +419,9 @@ var MARKDOWN_SYNTAX = new RegExp(
|
|
|
385
419
|
].join("|"),
|
|
386
420
|
"m"
|
|
387
421
|
);
|
|
422
|
+
var MAX_PARAGRAPH_DEPTH = MAX_DOC_DEPTH - 1;
|
|
423
|
+
var MAX_QUOTE_DEPTH = MAX_DOC_DEPTH - 2;
|
|
424
|
+
var MAX_LIST_DEPTH = MAX_DOC_DEPTH - 3;
|
|
388
425
|
|
|
389
426
|
// ../shared/src/transcriptDelta.ts
|
|
390
427
|
var INT64_MAX = BigInt("9223372036854775807");
|
|
@@ -406,8 +443,10 @@ Usage: plot <command> [options]
|
|
|
406
443
|
watch <page> [--level <level>] Wait for what is new since the last watch
|
|
407
444
|
topics <page> The page's topics as JSON
|
|
408
445
|
search <query> [--limit] [--next] Search your pages (workspace login)
|
|
409
|
-
|
|
410
|
-
|
|
446
|
+
message <page> [--body <text> | stdin] Draft a message into the person's composer
|
|
447
|
+
message <page> --send \u2026 Or post it as them, only when they asked
|
|
448
|
+
message <page> --show|--discard [<id>]
|
|
449
|
+
Read back or withdraw a draft
|
|
411
450
|
mcp-config [--host <host>] [--write] Config block for an MCP host
|
|
412
451
|
help agents How an AI agent should use this
|
|
413
452
|
|
|
@@ -430,6 +469,10 @@ Get access (once; the token is stored, so later shells need nothing):
|
|
|
430
469
|
the code the page shows. Then:
|
|
431
470
|
plot login --code <code> Finishes it (within ten minutes)
|
|
432
471
|
|
|
472
|
+
If nobody can open a browser for you at all, ask your person for a token from
|
|
473
|
+
Plot's Settings > Agents, and export PLOT_TOKEN=plot_agent_sk_\u2026 in your shell
|
|
474
|
+
(plus PLOT_API_URL if they say so). Then no login is needed.
|
|
475
|
+
|
|
433
476
|
Read:
|
|
434
477
|
plot read <page> The page as Markdown: transcript, then Topics.
|
|
435
478
|
A later watch starts from the end of this read.
|
|
@@ -449,11 +492,20 @@ Follow a meeting that is still happening:
|
|
|
449
492
|
tool's own timeout, e.g. --timeout 100)
|
|
450
493
|
|
|
451
494
|
Write back (only if the person who gave you access can post):
|
|
452
|
-
plot
|
|
495
|
+
plot message <page> --body "\u2026" Goes into their composer as a draft for them to send
|
|
496
|
+
plot message <page> --send --body "\u2026" Appears as the person at once, marked "via <your name>":
|
|
497
|
+
only when they asked you to post
|
|
453
498
|
|
|
454
499
|
<page> is a page id or a link like https://www.plot.io/p/<id>.
|
|
455
500
|
|
|
456
501
|
${DATA_NOT_INSTRUCTIONS}
|
|
502
|
+
Connect an MCP host (Claude Code, Cursor, VS Code, Zed) instead:
|
|
503
|
+
plot mcp-config --host <host> The host's config block: the URL only. The
|
|
504
|
+
host signs in to Plot itself (OAuth).
|
|
505
|
+
plot mcp-config --token A bearer header instead, for a host with no
|
|
506
|
+
browser to sign in with (a sandbox, CI).
|
|
507
|
+
plot mcp-config <page> A page grant, which always needs the header.
|
|
508
|
+
|
|
457
509
|
Keep tokens out of files you commit. "plot logout" gives a credential up.`;
|
|
458
510
|
var COMMAND_HELP = {
|
|
459
511
|
open: `plot open <temp_code_\u2026> [--agent <name>]
|
|
@@ -524,27 +576,56 @@ source quotes and message ids.`,
|
|
|
524
576
|
|
|
525
577
|
Searches every page the workspace grant can read: messages, topics and page
|
|
526
578
|
fields, each with pageId, pageTitle, url, snippet and at. Needs "plot login".`,
|
|
527
|
-
|
|
579
|
+
message: `plot message <page> [--branch <id>] [--note <line>] [--id <id>]
|
|
580
|
+
[--body <text> | stdin]
|
|
581
|
+
plot message <page> --send \u2026
|
|
582
|
+
plot message <page> --show [<id>]
|
|
583
|
+
plot message <page> --discard [<id>]
|
|
528
584
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
585
|
+
Puts Markdown words (from --body, else stdin) into the composer of the
|
|
586
|
+
person who granted access, on that page, as a draft: nobody else sees it,
|
|
587
|
+
and it reaches the page only when they send it (or they discard it). Prints
|
|
588
|
+
the message id and the page URL. One draft per lane: a second draft on the
|
|
589
|
+
same lane replaces the first, and says so.
|
|
590
|
+
--branch <id> Draft a reply on that branch instead of the main thread
|
|
591
|
+
--note <line> One line to the person, shown with the draft (200 chars)
|
|
592
|
+
--id <id> Use this message id (8 to 64 of A-Z a-z 0-9 _ -). The same
|
|
593
|
+
id again updates that draft in place. Each run otherwise
|
|
594
|
+
makes one id, so a retry after a dropped connection lands
|
|
595
|
+
once
|
|
596
|
+
|
|
597
|
+
--send posts it now instead, as the person, marked "via <your agent>". Use it
|
|
598
|
+
only when they asked you to post. With no words given it sends your earlier
|
|
599
|
+
draft as it stands, formatting intact: the one --id names, else the last one
|
|
600
|
+
made here. A grant that cannot post is refused (read_only), never quietly
|
|
601
|
+
turned into a draft.
|
|
602
|
+
|
|
603
|
+
--show reads a draft back (its words and note), or says it was sent.
|
|
604
|
+
--discard withdraws a draft; a sent message cannot be withdrawn here
|
|
605
|
+
(not_a_draft). Both act on <id>, else --id, else the last message this
|
|
606
|
+
machine made on the page.
|
|
607
|
+
|
|
608
|
+
Needs a member who can post: a viewer's grant is refused (read_only) for a
|
|
609
|
+
draft as for a send.`,
|
|
610
|
+
"mcp-config": `plot mcp-config [--host <host>] [--write] [--token]
|
|
535
611
|
[--inline-token] [<page>]
|
|
536
612
|
|
|
537
|
-
Prints the MCP server config for a host
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
613
|
+
Prints the MCP server config for a host: just the URL, <api>/mcp. The host
|
|
614
|
+
signs in to Plot with OAuth the first time it connects (it opens the
|
|
615
|
+
browser and keeps the token itself), so no token is read or printed. Hosts:
|
|
616
|
+
claude-code (default), cursor, vscode, zed, claude-desktop (a connector in
|
|
617
|
+
its own settings). --write merges it into the host's config file instead,
|
|
618
|
+
keeping every other server.
|
|
619
|
+
|
|
620
|
+
--token is for when OAuth cannot be used: a page grant, which has no OAuth
|
|
621
|
+
flow (a <page> argument implies --token), or a sandbox or CI job with no
|
|
622
|
+
browser to sign in with. It adds a bearer header that names the PLOT_TOKEN
|
|
623
|
+
environment variable rather than holding the token, because .mcp.json and
|
|
624
|
+
.vscode/mcp.json are usually committed; the token (the stored workspace one,
|
|
625
|
+
or <page>'s) is printed separately for you to export where the host runs.
|
|
626
|
+
Zed cannot read an environment variable there, so its block holds the token.
|
|
627
|
+
--token Add a bearer header instead of leaving sign-in to OAuth
|
|
628
|
+
--inline-token Put the token itself in the header (implies --token)`
|
|
548
629
|
};
|
|
549
630
|
|
|
550
631
|
// src/login.ts
|
|
@@ -795,6 +876,27 @@ function renderUpdate(update) {
|
|
|
795
876
|
// src/store.ts
|
|
796
877
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
797
878
|
import { basename, dirname, join } from "node:path";
|
|
879
|
+
function pageMemory(page2) {
|
|
880
|
+
const kept = {};
|
|
881
|
+
if (page2?.title) kept.title = page2.title;
|
|
882
|
+
if (page2?.cursor) kept.cursor = page2.cursor;
|
|
883
|
+
if (page2?.lastMessageId) kept.lastMessageId = page2.lastMessageId;
|
|
884
|
+
return kept;
|
|
885
|
+
}
|
|
886
|
+
function dropPageCredential(entry, pageId) {
|
|
887
|
+
const page2 = entry.pages?.[pageId];
|
|
888
|
+
if (!page2) return;
|
|
889
|
+
const kept = pageMemory(page2);
|
|
890
|
+
if (kept.cursor || kept.lastMessageId) entry.pages[pageId] = kept;
|
|
891
|
+
else delete entry.pages[pageId];
|
|
892
|
+
}
|
|
893
|
+
function setLastMessageId(entry, pageId, messageId) {
|
|
894
|
+
const page2 = { ...entry.pages?.[pageId], lastMessageId: messageId };
|
|
895
|
+
if (messageId === void 0) delete page2.lastMessageId;
|
|
896
|
+
entry.pages ??= {};
|
|
897
|
+
if (Object.keys(page2).length === 0) delete entry.pages[pageId];
|
|
898
|
+
else entry.pages[pageId] = page2;
|
|
899
|
+
}
|
|
798
900
|
function credentialsPath(env, homeDir) {
|
|
799
901
|
const configHome = env.XDG_CONFIG_HOME?.trim() || join(homeDir, ".config");
|
|
800
902
|
return join(configHome, "plot", "credentials.json");
|
|
@@ -923,7 +1025,7 @@ async function withToken(rt, pageId, fn) {
|
|
|
923
1025
|
});
|
|
924
1026
|
if (candidates.length === 0) {
|
|
925
1027
|
throw new CliError(
|
|
926
|
-
pageId ? `No credential for page ${pageId} on ${rt.origin}. Redeem the code you were given with "plot open temp_code_\u2026", or connect a workspace with "plot login".` : `No workspace credential for ${rt.origin}. Run "plot login" (a code from "plot open" reaches only its one page).`
|
|
1028
|
+
pageId ? `No credential for page ${pageId} on ${rt.origin}. Redeem the code you were given with "plot open temp_code_\u2026", or connect a workspace with "plot login".` : `No workspace credential for ${rt.origin}. Run "plot login", or set PLOT_TOKEN to a token from Plot's Settings > Agents (a code from "plot open" reaches only its one page).`
|
|
927
1029
|
);
|
|
928
1030
|
}
|
|
929
1031
|
for (let i = 0; i < candidates.length; i++) {
|
|
@@ -948,10 +1050,8 @@ async function withToken(rt, pageId, fn) {
|
|
|
948
1050
|
async function forget(rt, candidate, pageId) {
|
|
949
1051
|
await updateOrigin(rt, (entry) => {
|
|
950
1052
|
if (candidate.source === "workspace") delete entry.workspace;
|
|
951
|
-
if (candidate.source === "page" && pageId
|
|
952
|
-
|
|
953
|
-
if (cursor) entry.pages[pageId] = { cursor, title };
|
|
954
|
-
else delete entry.pages[pageId];
|
|
1053
|
+
if (candidate.source === "page" && pageId) {
|
|
1054
|
+
dropPageCredential(entry, pageId);
|
|
955
1055
|
}
|
|
956
1056
|
});
|
|
957
1057
|
}
|
|
@@ -1015,7 +1115,7 @@ async function open(rt) {
|
|
|
1015
1115
|
rt.ctx.out(
|
|
1016
1116
|
[
|
|
1017
1117
|
`Opened "${title}"${by}. ${canPost ? "Read and post" : "Read-only"} access until ${session.expiresAt}; each read extends it${session.absoluteExpiresAt ? `, up to ${session.absoluteExpiresAt}` : ""}.`,
|
|
1018
|
-
`Next: plot read ${pageId}${canPost ? `, plot
|
|
1118
|
+
`Next: plot read ${pageId}${canPost ? `, plot message ${pageId} --body "\u2026"` : ""} (plot help agents for more)`,
|
|
1019
1119
|
DATA_NOT_INSTRUCTIONS
|
|
1020
1120
|
].join("\n")
|
|
1021
1121
|
);
|
|
@@ -1198,11 +1298,7 @@ async function logout(rt) {
|
|
|
1198
1298
|
for (const [id, page2] of Object.entries(e.pages ?? {})) {
|
|
1199
1299
|
if (page2.token && revoked.has(page2.token)) {
|
|
1200
1300
|
if (rt.values.all) delete e.pages[id];
|
|
1201
|
-
else
|
|
1202
|
-
delete page2.token;
|
|
1203
|
-
delete page2.expiresAt;
|
|
1204
|
-
if (!page2.cursor) delete e.pages[id];
|
|
1205
|
-
}
|
|
1301
|
+
else dropPageCredential(e, id);
|
|
1206
1302
|
}
|
|
1207
1303
|
}
|
|
1208
1304
|
});
|
|
@@ -1334,20 +1430,26 @@ async function mcpConfig(rt) {
|
|
|
1334
1430
|
);
|
|
1335
1431
|
return;
|
|
1336
1432
|
}
|
|
1433
|
+
const pageRef = rt.positionals[0];
|
|
1434
|
+
if (rt.positionals.some((p) => p.startsWith("plot_agent_sk_"))) {
|
|
1435
|
+
throw new UsageError(
|
|
1436
|
+
`In mcp-config, --token takes no value: it uses ${TOKEN_ENV} or the stored login. Set ${TOKEN_ENV} instead of passing the token.`
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
const pageId = pageRef ? parsePageRef(pageRef) : void 0;
|
|
1440
|
+
const wantToken = rt.values.token === true || rt.values["inline-token"] === true || !!pageId;
|
|
1441
|
+
if (pageId && rt.values.token !== true && rt.values["inline-token"] !== true) {
|
|
1442
|
+
rt.ctx.err(
|
|
1443
|
+
"A page grant has no OAuth flow for a host to run, so this block carries a bearer token (as with --token)."
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1337
1446
|
let token;
|
|
1338
|
-
if (
|
|
1339
|
-
const pageId = rt.positionals[0] ? parsePageRef(rt.positionals[0]) : void 0;
|
|
1447
|
+
if (wantToken) {
|
|
1340
1448
|
const entry = await originEntry(rt);
|
|
1341
|
-
|
|
1342
|
-
flag: stringFlag(rt, "token"),
|
|
1343
|
-
env: rt.ctx.env,
|
|
1344
|
-
entry,
|
|
1345
|
-
pageId
|
|
1346
|
-
});
|
|
1347
|
-
token = candidates[0]?.token;
|
|
1449
|
+
token = tokenCandidates({ env: rt.ctx.env, entry, pageId })[0]?.token;
|
|
1348
1450
|
if (!token) {
|
|
1349
|
-
|
|
1350
|
-
|
|
1451
|
+
throw new CliError(
|
|
1452
|
+
pageId ? `No stored credential for that page. Run "plot open <temp_code_\u2026>" (or "plot login") first, or set ${TOKEN_ENV}.` : `No stored credential for --token. Run "plot login" first, or set ${TOKEN_ENV}. Without --token the host signs in with OAuth itself.`
|
|
1351
1453
|
);
|
|
1352
1454
|
}
|
|
1353
1455
|
}
|
|
@@ -1371,9 +1473,21 @@ async function mcpConfig(rt) {
|
|
|
1371
1473
|
].join("\n")
|
|
1372
1474
|
);
|
|
1373
1475
|
};
|
|
1476
|
+
const oauthHint = () => {
|
|
1477
|
+
if (token) return;
|
|
1478
|
+
if (host === "claude-code" && !rt.values.write) {
|
|
1479
|
+
rt.ctx.err(
|
|
1480
|
+
`Or: claude mcp add --transport http --scope project plot ${url}`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
rt.ctx.err(
|
|
1484
|
+
"The host will ask you to sign in to Plot the first time it connects."
|
|
1485
|
+
);
|
|
1486
|
+
};
|
|
1374
1487
|
if (!rt.values.write) {
|
|
1375
1488
|
rt.ctx.out(JSON.stringify(block, null, 2));
|
|
1376
1489
|
rt.ctx.err(`Add this to ${spec.where}.${credentialNote}`);
|
|
1490
|
+
oauthHint();
|
|
1377
1491
|
exportHint();
|
|
1378
1492
|
return;
|
|
1379
1493
|
}
|
|
@@ -1418,6 +1532,7 @@ async function mcpConfig(rt) {
|
|
|
1418
1532
|
});
|
|
1419
1533
|
if (inline && !created) await chmod2(file, 384);
|
|
1420
1534
|
rt.ctx.out(`Wrote the "plot" server to ${file}.${credentialNote}`);
|
|
1535
|
+
oauthHint();
|
|
1421
1536
|
exportHint();
|
|
1422
1537
|
}
|
|
1423
1538
|
|
|
@@ -1472,35 +1587,166 @@ async function search(rt) {
|
|
|
1472
1587
|
if (rt.values.json) printJson(rt, result);
|
|
1473
1588
|
else rt.ctx.out(renderSearch(query, result.results ?? [], result.next));
|
|
1474
1589
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1590
|
+
var MESSAGE_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
1591
|
+
function checkedId(raw, what) {
|
|
1592
|
+
if (!MESSAGE_ID.test(raw)) {
|
|
1593
|
+
throw new UsageError(
|
|
1594
|
+
`${what} "${raw}" is not a message id (8 to 64 of A-Z a-z 0-9 _ -).`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
return raw;
|
|
1598
|
+
}
|
|
1599
|
+
async function messageBody(rt) {
|
|
1477
1600
|
let body = typeof rt.values.body === "string" ? rt.values.body : void 0;
|
|
1478
|
-
if (body === void 0)
|
|
1479
|
-
|
|
1480
|
-
|
|
1601
|
+
if (body === void 0 && !rt.ctx.stdinIsTTY) body = await rt.ctx.readStdin();
|
|
1602
|
+
body = body?.trim();
|
|
1603
|
+
return body || void 0;
|
|
1604
|
+
}
|
|
1605
|
+
async function existingId(rt, pageId) {
|
|
1606
|
+
const named = rt.positionals[1] ?? stringFlag(rt, "id");
|
|
1607
|
+
if (named) return checkedId(named, "Message id");
|
|
1608
|
+
const remembered = (await originEntry(rt)).pages?.[pageId]?.lastMessageId;
|
|
1609
|
+
if (remembered) return remembered;
|
|
1610
|
+
throw new UsageError(
|
|
1611
|
+
`No message id given, and none remembered for page ${pageId}: name one with --id.`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
async function rememberMessage(rt, pageId, messageId) {
|
|
1615
|
+
await updateOrigin(rt, (entry) => setLastMessageId(entry, pageId, messageId));
|
|
1616
|
+
}
|
|
1617
|
+
function where(title, url) {
|
|
1618
|
+
const named = title ? `"${title}"` : "";
|
|
1619
|
+
if (named && url) return `${named} (${url})`;
|
|
1620
|
+
return named || url || "the page";
|
|
1621
|
+
}
|
|
1622
|
+
function renderCreated(status, created, title) {
|
|
1623
|
+
const m = created.message;
|
|
1624
|
+
const agent = created.agent || "your agent";
|
|
1625
|
+
const place = where(title, m?.url);
|
|
1626
|
+
const lines = [];
|
|
1627
|
+
if (created.replaced) {
|
|
1628
|
+
lines.push("Replaced the earlier draft on this lane; its id is gone.");
|
|
1629
|
+
}
|
|
1630
|
+
if (m?.status === "sent" && status === 200) {
|
|
1631
|
+
lines.push(
|
|
1632
|
+
created.agent ? `Already sent as the person who granted access, marked "via ${created.agent}", on ${place} (id ${m.id}). Nothing new was posted.` : `Already sent by the person who granted access, from their composer, on ${place} (id ${m.id}). Nothing new was posted.`
|
|
1633
|
+
);
|
|
1634
|
+
} else if (m?.status === "sent") {
|
|
1635
|
+
lines.push(
|
|
1636
|
+
`Sent as the person who granted access, marked "via ${agent}", on ${place} (id ${m.id}).`
|
|
1637
|
+
);
|
|
1638
|
+
} else {
|
|
1639
|
+
const verb = status === 201 || created.replaced ? "Drafted" : "Updated the draft";
|
|
1640
|
+
lines.push(
|
|
1641
|
+
`${verb} into the composer of the person who granted access, on ${place} (id ${m?.id}).`,
|
|
1642
|
+
"Nobody else sees it: they edit it and send it, or discard it. Send it yourself only if they ask:",
|
|
1643
|
+
` plot message ${m?.pageId ?? "<page>"} --send --id ${m?.id}`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
return lines.join("\n");
|
|
1647
|
+
}
|
|
1648
|
+
function renderMessage(m, title) {
|
|
1649
|
+
const place = where(title, m.url);
|
|
1650
|
+
if (m.status === "sent") {
|
|
1651
|
+
return `Message ${m.id} is sent: it is on ${place}${m.messageId ? ` as ${m.messageId}` : ""}.`;
|
|
1652
|
+
}
|
|
1653
|
+
const lines = [
|
|
1654
|
+
`Draft ${m.id} is waiting in the person's composer on ${place}${m.branchId ? `, in reply ${m.branchId}` : ""}.`
|
|
1655
|
+
];
|
|
1656
|
+
if (m.note) lines.push(`Note: ${m.note}`);
|
|
1657
|
+
if (m.body !== void 0) lines.push("", m.body);
|
|
1658
|
+
return lines.join("\n");
|
|
1659
|
+
}
|
|
1660
|
+
async function message(rt) {
|
|
1661
|
+
const pageId = parsePageRef(rt.positionals[0]);
|
|
1662
|
+
const show = rt.values.show === true;
|
|
1663
|
+
const discard = rt.values.discard === true;
|
|
1664
|
+
const send = rt.values.send === true;
|
|
1665
|
+
if ([show, discard, send].filter(Boolean).length > 1) {
|
|
1666
|
+
throw new UsageError("Choose one of --send, --show and --discard.");
|
|
1667
|
+
}
|
|
1668
|
+
const title = (await originEntry(rt)).pages?.[pageId]?.title;
|
|
1669
|
+
if (show || discard) {
|
|
1670
|
+
for (const flag of ["body", "branch", "note"]) {
|
|
1671
|
+
if (rt.values[flag] !== void 0) {
|
|
1672
|
+
throw new UsageError(
|
|
1673
|
+
`--${flag} does not go with --${show ? "show" : "discard"}.`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (rt.positionals.length > 2) {
|
|
1678
|
+
throw new UsageError("Too many arguments: <page> and at most one id.");
|
|
1679
|
+
}
|
|
1680
|
+
const id2 = await existingId(rt, pageId);
|
|
1681
|
+
if (show) {
|
|
1682
|
+
const found = await withToken(
|
|
1683
|
+
rt,
|
|
1684
|
+
pageId,
|
|
1685
|
+
(token) => rt.api.getMessage(token, pageId, id2)
|
|
1686
|
+
);
|
|
1687
|
+
if (rt.values.json) printJson(rt, found);
|
|
1688
|
+
else rt.ctx.out(renderMessage(found, title));
|
|
1689
|
+
return;
|
|
1481
1690
|
}
|
|
1482
|
-
|
|
1691
|
+
await withToken(
|
|
1692
|
+
rt,
|
|
1693
|
+
pageId,
|
|
1694
|
+
(token) => rt.api.discardMessage(token, pageId, id2)
|
|
1695
|
+
);
|
|
1696
|
+
const remembered = (await originEntry(rt)).pages?.[pageId]?.lastMessageId;
|
|
1697
|
+
if (remembered === id2) await rememberMessage(rt, pageId, void 0);
|
|
1698
|
+
if (rt.values.json) printJson(rt, { id: id2, discarded: true });
|
|
1699
|
+
else
|
|
1700
|
+
rt.ctx.out(
|
|
1701
|
+
`Discarded draft ${id2}; it is gone from the person's composer.`
|
|
1702
|
+
);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (rt.positionals.length > 1) {
|
|
1706
|
+
throw new UsageError(
|
|
1707
|
+
`Unexpected argument "${rt.positionals[1]}". Give the words with --body, or pipe them on stdin.`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
const branchId = stringFlag(rt, "branch");
|
|
1711
|
+
const note = stringFlag(rt, "note");
|
|
1712
|
+
const body = await messageBody(rt);
|
|
1713
|
+
let id;
|
|
1714
|
+
if (body !== void 0) {
|
|
1715
|
+
const named = stringFlag(rt, "id");
|
|
1716
|
+
id = named ? checkedId(named, "--id") : newClientId();
|
|
1717
|
+
} else if (send) {
|
|
1718
|
+
id = await existingId(rt, pageId);
|
|
1719
|
+
} else {
|
|
1720
|
+
throw new UsageError(
|
|
1721
|
+
rt.ctx.stdinIsTTY ? "Give the message with --body, or pipe it on stdin." : "The message is empty."
|
|
1722
|
+
);
|
|
1483
1723
|
}
|
|
1484
|
-
body = body.trim();
|
|
1485
|
-
if (!body) throw new UsageError("The message is empty.");
|
|
1486
|
-
const id = newClientId();
|
|
1487
1724
|
const created = await withToken(
|
|
1488
1725
|
rt,
|
|
1489
1726
|
pageId,
|
|
1490
|
-
(token) => rt.api.
|
|
1727
|
+
(token) => rt.api.createMessage(
|
|
1491
1728
|
token,
|
|
1492
1729
|
pageId,
|
|
1493
|
-
{
|
|
1730
|
+
{
|
|
1731
|
+
body,
|
|
1732
|
+
id,
|
|
1733
|
+
branchId,
|
|
1734
|
+
send,
|
|
1735
|
+
note
|
|
1736
|
+
},
|
|
1494
1737
|
{ sleep: rt.ctx.sleep }
|
|
1495
1738
|
)
|
|
1496
|
-
)
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1739
|
+
).catch((err) => {
|
|
1740
|
+
if (body === void 0 && err instanceof ApiError && err.code === "no_draft") {
|
|
1741
|
+
throw new CliError(
|
|
1742
|
+
`No draft to send here: ${id} is not a draft of yours on page ${pageId}. (${err.message})`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
throw err;
|
|
1746
|
+
});
|
|
1747
|
+
await rememberMessage(rt, pageId, created.body.message?.id ?? id);
|
|
1748
|
+
if (rt.values.json) printJson(rt, created.body);
|
|
1749
|
+
else rt.ctx.out(renderCreated(created.status, created.body, title));
|
|
1504
1750
|
}
|
|
1505
1751
|
|
|
1506
1752
|
// src/commands/watch.ts
|
|
@@ -1611,16 +1857,25 @@ var COMMANDS = {
|
|
|
1611
1857
|
run: search,
|
|
1612
1858
|
options: { next: { type: "string" }, limit: { type: "string" } }
|
|
1613
1859
|
},
|
|
1614
|
-
|
|
1615
|
-
run:
|
|
1616
|
-
options: {
|
|
1860
|
+
message: {
|
|
1861
|
+
run: message,
|
|
1862
|
+
options: {
|
|
1863
|
+
branch: { type: "string" },
|
|
1864
|
+
body: { type: "string" },
|
|
1865
|
+
note: { type: "string" },
|
|
1866
|
+
id: { type: "string" },
|
|
1867
|
+
send: { type: "boolean" },
|
|
1868
|
+
show: { type: "boolean" },
|
|
1869
|
+
discard: { type: "boolean" }
|
|
1870
|
+
}
|
|
1617
1871
|
},
|
|
1618
1872
|
"mcp-config": {
|
|
1619
1873
|
run: mcpConfig,
|
|
1620
1874
|
options: {
|
|
1621
1875
|
host: { type: "string" },
|
|
1622
1876
|
write: { type: "boolean" },
|
|
1623
|
-
|
|
1877
|
+
// A switch here, not the global --token <token>: see mcpConfig.ts.
|
|
1878
|
+
token: { type: "boolean" },
|
|
1624
1879
|
"inline-token": { type: "boolean" }
|
|
1625
1880
|
}
|
|
1626
1881
|
}
|
|
@@ -1764,8 +2019,8 @@ function oauthFailure(error, description) {
|
|
|
1764
2019
|
}
|
|
1765
2020
|
return new CliError(`Login failed: the browser returned "${error}".`);
|
|
1766
2021
|
}
|
|
1767
|
-
function page(res, status, title,
|
|
1768
|
-
const body = `<!doctype html><meta charset="utf-8"><title>${escape(title)}</title><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1 style="font-size:1.25rem">${escape(title)}</h1><p>${escape(
|
|
2022
|
+
function page(res, status, title, message2) {
|
|
2023
|
+
const body = `<!doctype html><meta charset="utf-8"><title>${escape(title)}</title><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1 style="font-size:1.25rem">${escape(title)}</h1><p>${escape(message2)}</p></body>`;
|
|
1769
2024
|
res.writeHead(status, {
|
|
1770
2025
|
"Content-Type": "text/html; charset=utf-8",
|
|
1771
2026
|
"Cache-Control": "no-store",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plotio/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Read Plot pages, follow a meeting as it happens, and
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Read Plot pages, follow a meeting as it happens, and draft or send messages, from any agent with a shell.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|