@plotio/cli 0.3.0 → 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.
Files changed (3) hide show
  1. package/README.md +24 -17
  2. package/dist/plot.js +291 -68
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @plotio/cli
2
2
 
3
- `plot`: read Plot pages, follow a meeting while it happens, and post back, from
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
@@ -33,6 +33,10 @@ a hash. Each is kept once redeemed, so later shells need nothing.
33
33
  browser, against the authorization server the API itself names — so `--api`
34
34
  at a local or preview deployment signs in on that deployment's own web app,
35
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.
36
40
 
37
41
  `plot login` ends three ways, and what it can see decides which:
38
42
 
@@ -48,21 +52,23 @@ a hash. Each is kept once redeemed, so later shells need nothing.
48
52
 
49
53
  ## Commands
50
54
 
51
- | Command | Does |
52
- | ----------------------------------------------------------- | ----------------------------------------------------------------------------- |
53
- | `plot open <temp_code_…> [--agent <name>]` | Redeem a handoff code and store the page token |
54
- | `plot login [--agent] [--no-browser]` | Connect a workspace: sign in and approve in the browser |
55
- | `plot login --code <code>` | Finish a login started without a terminal, with the code the browser showed |
56
- | `plot logout [<page>] [--all]` | Revoke a credential on the server, then forget it |
57
- | `plot whoami [<page>] [--json]` | What the credential acts for, its scope and expiry |
58
- | `plot pages [--limit <n>] [--next <c>] [--json]` | Pages the workspace grant can read, newest first |
59
- | `plot read <page> [--json]` | The page as Markdown (topics included), or as JSON; stores the updates cursor |
60
- | `plot watch <page> [--level …] [options]` | Wait for the next update, print it, store the cursor |
61
- | `plot topics <page>` | Topics as JSON, with the quotes behind each point |
62
- | `plot search <query> [--limit] [--next] [--json]` | Search every page the workspace grant can read |
63
- | `plot post <page> [--branch <id>] [--body <t>]` | Post a message (body from `--body`, else stdin), idempotent per run |
64
- | `plot mcp-config [--host <h>] [--token] [--write] [<page>]` | The MCP server block for a host: the `<api>/mcp` URL, signed in with OAuth |
65
- | `plot help agents` | The guide for an AI agent |
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 |
66
72
 
67
73
  `<page>` is a page id or a page URL (`https://www.plot.io/p/<id>`).
68
74
 
@@ -119,7 +125,8 @@ calls expired or revoked is removed and the next one tried.
119
125
  `~/.config/plot/credentials.json`, mode `0600` in a `0700` directory. It is
120
126
  keyed by API base, so production's tokens and a preview's never collide — and
121
127
  so changing `--api` or `PLOT_API_URL` changes which credential is in play. It
122
- also holds each page's last `watch` cursor, which is how a new shell resumes.
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.
123
130
  `plot logout --all` revokes and removes everything for the current API base.
124
131
 
125
132
  A `plot login` with no TTY leaves a `pending-login.json` beside it, same mode,
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(message, exitCode = 1) {
10
- super(message);
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(message) {
17
- super(`${message}
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.3.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, message) {
33
- super(message);
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. `id` makes the post idempotent, which is
256
- * what lets a transport failure (no answer, or a gateway 5xx) be retried
257
- * with the same id: the server posts it once however many times it
258
- * arrives. A refusal with a reason is never retried.
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 post(token, pageId, message, retry = {}) {
261
- const body = {
262
- body: message.body,
263
- id: message.id
264
- };
265
- if (message.branchId) body.branchId = message.branchId;
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
- return await json(
270
- await request("POST", `/v1/pages/${enc(pageId)}/messages`, {
271
- token,
272
- body
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 message = typeof body.message === "string" && body.message ? body.message : `Request failed with HTTP ${response.status} (${code}).`;
300
- return new ApiError(response.status, code, message);
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
- post <page> [--branch <id>] [--body <text> | stdin]
410
- Post a message onto the page
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,7 +492,9 @@ 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 post <page> --body "\u2026" Appears as the person, marked "via <your name>"
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
 
@@ -531,13 +576,37 @@ source quotes and message ids.`,
531
576
 
532
577
  Searches every page the workspace grant can read: messages, topics and page
533
578
  fields, each with pageId, pageTitle, url, snippet and at. Needs "plot login".`,
534
- post: `plot post <page> [--branch <id>] [--body <text>]
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>]
584
+
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.
535
607
 
536
- Posts a text message onto the page, from --body or else from stdin. It
537
- appears as the person who granted access, marked as posted via your agent.
538
- Needs a member who can post; a viewer's grant is refused (read_only). Each
539
- run sends one idempotency id, so a retry after a dropped connection posts
540
- once.`,
608
+ Needs a member who can post: a viewer's grant is refused (read_only) for a
609
+ draft as for a send.`,
541
610
  "mcp-config": `plot mcp-config [--host <host>] [--write] [--token]
542
611
  [--inline-token] [<page>]
543
612
 
@@ -807,6 +876,27 @@ function renderUpdate(update) {
807
876
  // src/store.ts
808
877
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
809
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
+ }
810
900
  function credentialsPath(env, homeDir) {
811
901
  const configHome = env.XDG_CONFIG_HOME?.trim() || join(homeDir, ".config");
812
902
  return join(configHome, "plot", "credentials.json");
@@ -935,7 +1025,7 @@ async function withToken(rt, pageId, fn) {
935
1025
  });
936
1026
  if (candidates.length === 0) {
937
1027
  throw new CliError(
938
- 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).`
939
1029
  );
940
1030
  }
941
1031
  for (let i = 0; i < candidates.length; i++) {
@@ -960,10 +1050,8 @@ async function withToken(rt, pageId, fn) {
960
1050
  async function forget(rt, candidate, pageId) {
961
1051
  await updateOrigin(rt, (entry) => {
962
1052
  if (candidate.source === "workspace") delete entry.workspace;
963
- if (candidate.source === "page" && pageId && entry.pages?.[pageId]) {
964
- const { cursor, title } = entry.pages[pageId];
965
- if (cursor) entry.pages[pageId] = { cursor, title };
966
- else delete entry.pages[pageId];
1053
+ if (candidate.source === "page" && pageId) {
1054
+ dropPageCredential(entry, pageId);
967
1055
  }
968
1056
  });
969
1057
  }
@@ -1027,7 +1115,7 @@ async function open(rt) {
1027
1115
  rt.ctx.out(
1028
1116
  [
1029
1117
  `Opened "${title}"${by}. ${canPost ? "Read and post" : "Read-only"} access until ${session.expiresAt}; each read extends it${session.absoluteExpiresAt ? `, up to ${session.absoluteExpiresAt}` : ""}.`,
1030
- `Next: plot read ${pageId}${canPost ? `, plot post ${pageId} --body "\u2026"` : ""} (plot help agents for more)`,
1118
+ `Next: plot read ${pageId}${canPost ? `, plot message ${pageId} --body "\u2026"` : ""} (plot help agents for more)`,
1031
1119
  DATA_NOT_INSTRUCTIONS
1032
1120
  ].join("\n")
1033
1121
  );
@@ -1210,11 +1298,7 @@ async function logout(rt) {
1210
1298
  for (const [id, page2] of Object.entries(e.pages ?? {})) {
1211
1299
  if (page2.token && revoked.has(page2.token)) {
1212
1300
  if (rt.values.all) delete e.pages[id];
1213
- else {
1214
- delete page2.token;
1215
- delete page2.expiresAt;
1216
- if (!page2.cursor) delete e.pages[id];
1217
- }
1301
+ else dropPageCredential(e, id);
1218
1302
  }
1219
1303
  }
1220
1304
  });
@@ -1503,35 +1587,166 @@ async function search(rt) {
1503
1587
  if (rt.values.json) printJson(rt, result);
1504
1588
  else rt.ctx.out(renderSearch(query, result.results ?? [], result.next));
1505
1589
  }
1506
- async function post(rt) {
1507
- const pageId = parsePageRef(rt.positionals[0]);
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) {
1508
1600
  let body = typeof rt.values.body === "string" ? rt.values.body : void 0;
1509
- if (body === void 0) {
1510
- if (rt.ctx.stdinIsTTY) {
1511
- throw new UsageError("Give the message with --body, or pipe it on stdin.");
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;
1512
1690
  }
1513
- body = await rt.ctx.readStdin();
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
+ );
1514
1723
  }
1515
- body = body.trim();
1516
- if (!body) throw new UsageError("The message is empty.");
1517
- const id = newClientId();
1518
1724
  const created = await withToken(
1519
1725
  rt,
1520
1726
  pageId,
1521
- (token) => rt.api.post(
1727
+ (token) => rt.api.createMessage(
1522
1728
  token,
1523
1729
  pageId,
1524
- { body, branchId: stringFlag(rt, "branch"), id },
1730
+ {
1731
+ body,
1732
+ id,
1733
+ branchId,
1734
+ send,
1735
+ note
1736
+ },
1525
1737
  { sleep: rt.ctx.sleep }
1526
1738
  )
1527
- );
1528
- if (rt.values.json) printJson(rt, created);
1529
- else {
1530
- const via = created.postedByAgent ? ` via ${created.postedByAgent}` : "";
1531
- rt.ctx.out(
1532
- `Posted${via}: ${created.message?.url ?? created.message?.id ?? ""}`.trim()
1533
- );
1534
- }
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));
1535
1750
  }
1536
1751
 
1537
1752
  // src/commands/watch.ts
@@ -1642,9 +1857,17 @@ var COMMANDS = {
1642
1857
  run: search,
1643
1858
  options: { next: { type: "string" }, limit: { type: "string" } }
1644
1859
  },
1645
- post: {
1646
- run: post,
1647
- options: { branch: { type: "string" }, body: { type: "string" } }
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
+ }
1648
1871
  },
1649
1872
  "mcp-config": {
1650
1873
  run: mcpConfig,
@@ -1796,8 +2019,8 @@ function oauthFailure(error, description) {
1796
2019
  }
1797
2020
  return new CliError(`Login failed: the browser returned "${error}".`);
1798
2021
  }
1799
- function page(res, status, title, message) {
1800
- 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(message)}</p></body>`;
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>`;
1801
2024
  res.writeHead(status, {
1802
2025
  "Content-Type": "text/html; charset=utf-8",
1803
2026
  "Cache-Control": "no-store",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plotio/cli",
3
- "version": "0.3.0",
4
- "description": "Read Plot pages, follow a meeting as it happens, and post back, from any agent with a shell.",
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": {