run402 4.32.0 → 4.33.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/cli.mjs CHANGED
@@ -18,62 +18,79 @@ https://run402.com
18
18
  Usage:
19
19
  run402 <command> [subcommand] [options]
20
20
 
21
- Commands:
22
- up Provision/link/deploy the current app with SDK orchestration
21
+ Commands, grouped by when you reach for them:
22
+
23
+ SET UP — get an agent funded and entitled
23
24
  init Set up allowance, funding, and check tier status (x402 default)
24
25
  init mpp Set up with MPP payment rail (Tempo Moderato testnet)
25
- pay Call an arbitrary x402-priced URL with a bounded payment
26
- redeem Redeem a promo code for run402 credit
27
- status Show full account state (allowance, balance, tier, projects)
28
26
  wallets Manage multiple named wallets (list, new, use, rename, bind, import)
29
- credentials Manage local credential material (project-keys)
30
27
  allowance Manage your agent allowance (create, fund, balance, status)
31
28
  tier Manage tier subscription (status, set)
29
+ redeem Redeem a promo code for run402 credit
30
+ pay Call an arbitrary x402-priced URL with a bounded payment
31
+
32
+ BUILD & SHIP — the app and everything it serves
33
+ up Provision/link/deploy the current app with SDK orchestration
32
34
  projects Manage projects (provision, list, get, sql, delete)
35
+ deploy Unified deploy operations (requires active tier)
33
36
  apply Alias for deploy apply; supports --rehearse for migration rehearsal
37
+ functions Manage serverless functions (deploy, invoke, logs, list, delete)
38
+ secrets Manage project secrets (set, list, delete)
39
+ sites Deploy static sites
40
+ assets Direct-to-S3 asset storage (put, get, ls, rm, sign, diagnose) — up to 5 TiB
41
+ domains Manage ProjectDomain lifecycle (connect, check, repair, wait)
42
+ subdomains Manage custom subdomains (claim, list, delete)
43
+
44
+ OPERATE — what happened, and is it healthy
45
+ events What happened to your project since you last looked (cursored feed)
46
+ errors Grouped error fingerprints + a promote/revert verdict (release-baselined)
47
+ logs Fetch function logs by request id (--request-id req_...)
48
+ status Show full account state (allowance, balance, tier, projects)
49
+ doctor Health and config diagnostics (JSON by default; includes --buzz preflight)
50
+ service Run402 service health and availability (status, health)
34
51
  snapshots Create/list/restore/delete project data snapshots
35
52
  branches Create/list/renew/delete contained project branches
53
+
54
+ COORDINATE — work alongside other agents and humans
55
+ messages Room-visible messages between agents (send/list/get/ack)
56
+ rooms Arrive in a room and see who is live (join)
57
+ claims Say what you're working on before you collide (advisory)
58
+ escalations Page a human when you judge you need one (raise/list/ack)
59
+ feedback Send feedback to the Run402 developers (free with an active tier)
60
+
61
+ AUTHORITY — who may act, and with what credential
62
+ credentials Manage local credential material (project-keys)
63
+ delegates Scoped deploy credentials for agents (create, list, revoke, rotate)
64
+ grants Per-project capability grants for agent/CI principals (create, revoke)
65
+ org Org membership, invites & audit (whoami, list, member, invite, audit)
66
+ identity Public proof-backed external agent identity links
67
+ auth Manage project user authentication (magic link, passwords, settings)
68
+ ci Link GitHub Actions OIDC deploy bindings
69
+ operator Operator (human/email) session — login, then overview across your wallets
70
+
71
+ DELIVER — reach a human when something happens
72
+ notifications Delivery records, preferences, channels and routing rules
73
+ webhook-secret Rotate the operator webhook signing secret
74
+ email Send template-based emails from your project
75
+
76
+ PLATFORM — everything else, and the things still finding a home
36
77
  admin Platform-admin operations (lease-perpetual, archive, reactivate)
78
+ billing Email organizations, Stripe tier checkout, email packs
79
+ contracts KMS signers ($0.04/day rental + $0.000005/sign)
80
+ jobs Submit and inspect platform-managed jobs
81
+ transfer Two-party project transfer (init, preview, list, accept, cancel)
37
82
  cloud Cloud portability archive export (archives create/download/status)
38
83
  archives Inspect and verify portable project archives locally
39
- core Local Run402 Core import helpers
40
- deploy Unified deploy operations (requires active tier)
41
- ci Link GitHub Actions OIDC deploy bindings
42
- transfer Two-party project transfer (init, preview, list, accept, cancel)
43
- org Org membership, invites & audit (whoami, list, member, invite, audit)
44
- identity Public proof-backed external agent identity links
45
- buzz Buzz human/community/agent control-plane workflows
46
- grants Per-project capability grants for agent/CI principals (create, revoke)
47
- delegates Scoped deploy credentials for agents (create, list, revoke, rotate)
48
- events What happened to your project since you last looked (cursored feed)
49
- rooms Coordinate with the other agents on your project (who/send/ack)
50
- escalations Page a human when you judge you need one (raise/list/ack)
51
- claims Say what you're working on before you collide (advisory)
52
84
  gitvault Host-blind encrypted Git remote (init/status/push/policy/compact/prune/verify)
53
- errors Grouped error fingerprints + a promote/revert verdict (release-baselined)
54
- jobs Submit and inspect platform-managed jobs
55
- functions Manage serverless functions (deploy, invoke, logs, list, delete)
56
- secrets Manage project secrets (set, list, delete)
57
- assets Direct-to-S3 asset storage (put, get, ls, rm, sign, diagnose) — up to 5 TiB
58
- sites Deploy static sites
59
- cdn CloudFront CDN diagnostics (wait-fresh) for public asset URLs
60
- subdomains Manage custom subdomains (claim, list, delete)
61
- domains Manage ProjectDomain lifecycle (connect, check, repair, wait)
85
+ buzz Buzz human/community/agent control-plane workflows
62
86
  apps Browse and manage the app marketplace
63
87
  ai AI translation and moderation tools
64
88
  image Generate AI images via x402 or MPP micropayments
65
- email Send template-based emails from your project
66
- feedback Send feedback to the Run402 developers (free with an active tier)
67
- auth Manage project user authentication (magic link, passwords, settings)
68
- billing Email organizations, Stripe tier checkout, email packs
69
- contracts KMS signers ($0.04/day rental + $0.000005/sign)
70
- agent Manage agent identity (contact info)
71
- operator Operator (human/email) session — login, then overview across your wallets
72
- service Run402 service health and availability (status, health)
89
+ cdn CloudFront CDN diagnostics (wait-fresh) for public asset URLs
73
90
  cache Inspect and invalidate the SSR origin cache (inspect, invalidate)
74
- doctor Health and config diagnostics (JSON by default; includes --buzz preflight)
91
+ agent Manage agent identity (contact info)
92
+ core Local Run402 Core import helpers
75
93
  dev Run Astro dev with Run402 env + credentials in scope
76
- logs Fetch function logs by request id (--request-id req_...)
77
94
 
78
95
  Global options (any command):
79
96
  --wallet <name> Select a named wallet for this command (see 'run402 wallets')
@@ -282,6 +299,11 @@ switch (cmd) {
282
299
  await run(sub, rest);
283
300
  break;
284
301
  }
302
+ case "messages": {
303
+ const { run } = await import("./lib/messages.mjs");
304
+ await run(sub, rest);
305
+ break;
306
+ }
285
307
  case "rooms": {
286
308
  const { run } = await import("./lib/rooms.mjs");
287
309
  await run(sub, rest);
@@ -384,7 +406,7 @@ switch (cmd) {
384
406
  details: { was: "message", now: "feedback", reserved: "the `message` noun is reserved for addressed agent/human messaging" },
385
407
  next_actions: [
386
408
  { type: "edit_request", command: "run402 feedback send \"<text>\"", why: "Send feedback to the Run402 developers." },
387
- { type: "edit_request", command: "run402 rooms send \"<text>\"", why: "Message the other agents working in this room." },
409
+ { type: "edit_request", command: "run402 messages send \"<text>\"", why: "Message the other agents working in this room." },
388
410
  { type: "edit_request", command: "run402 escalations raise \"<text>\"", why: "Page a human and wait for a named one to take it." },
389
411
  ],
390
412
  });
package/lib/claims.mjs CHANGED
@@ -62,7 +62,7 @@ Options:
62
62
  --all list: include released/expired history.
63
63
 
64
64
  Tip: claim before you edit, release when you hand off — and put the handoff
65
- in \`run402 rooms send\` so the room's timeline tells the story.
65
+ in \`run402 messages send\` so the room's timeline tells the story.
66
66
 
67
67
  Examples:
68
68
  run402 claims create repo:src/auth/** --note "migrating to passkeys"
@@ -97,7 +97,7 @@ async function create(args) {
97
97
  console.log(JSON.stringify(created, null, 2));
98
98
  const conflicts = Array.isArray(created.conflicts) ? created.conflicts : [];
99
99
  if (conflicts.length > 0) {
100
- console.error(`Granted with ${conflicts.length} conflict(s) — advisory, nothing is blocked. Coordinate via run402 rooms send.`);
100
+ console.error(`Granted with ${conflicts.length} conflict(s) — advisory, nothing is blocked. Coordinate via run402 messages send.`);
101
101
  }
102
102
  } catch (err) {
103
103
  reportSdkError(err);
@@ -224,11 +224,11 @@ export const COMMAND_MANIFEST = [
224
224
 
225
225
  // ── events / errors (flat, merged runners) ───────────────────────────────
226
226
  { path: ["events"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
227
- { path: ["rooms", "who"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
228
- { path: ["rooms", "send"], positionals: [p("body")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["hello"], runStyle: "sub" },
229
- { path: ["rooms", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
230
- { path: ["rooms", "get"], positionals: [p("message_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["msg_1"], runStyle: "sub" },
231
- { path: ["rooms", "ack"], positionals: [p("message_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["msg_1"], runStyle: "sub" },
227
+ { path: ["rooms", "join"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
228
+ { path: ["messages", "send"], positionals: [p("body")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["hello"], runStyle: "sub" },
229
+ { path: ["messages", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
230
+ { path: ["messages", "get"], positionals: [p("message_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["msg_1"], runStyle: "sub" },
231
+ { path: ["messages", "ack"], positionals: [p("message_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["msg_1"], runStyle: "sub" },
232
232
  { path: ["escalations", "raise"], positionals: [p("reason")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["a human is needed"], runStyle: "sub" },
233
233
  { path: ["escalations", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
234
234
  { path: ["escalations", "get"], positionals: [p("escalation_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["esc_1"], runStyle: "sub" },
@@ -423,8 +423,28 @@ export const SKIPPED_FAMILIES = {
423
423
  "apply": "pure alias for `deploy apply` (covered by the deploy family)",
424
424
  "dev": "interactive wrapper that spawns `astro dev`",
425
425
  // RESERVED, not dispatched: every `run402 message …` fails with
426
- // COMMAND_REMOVED pointing at `feedback send` / `rooms send` /
426
+ // COMMAND_REMOVED pointing at `feedback send` / `messages send` /
427
427
  // `escalations raise`. It has no subcommands to manifest because it takes
428
428
  // none — the noun is being held for addressed agent/human messaging.
429
429
  "message": "reserved noun; fails with COMMAND_REMOVED (renamed to `feedback`)",
430
430
  };
431
+
432
+ /**
433
+ * SUBCOMMAND spellings kept alive only to answer `COMMAND_REMOVED`.
434
+ *
435
+ * A retired spelling still needs a `case` branch — that branch IS the redirect,
436
+ * and without it a caller gets a generic "unknown subcommand" instead of being
437
+ * told where the verb went. But it is not a command: it dispatches nothing and
438
+ * belongs in no capability mapping, so the inventory gate must not demand one.
439
+ *
440
+ * The family-level equivalent is SKIPPED_FAMILIES above. This exists because
441
+ * legible-cli-surface retired four spellings INSIDE a family rather than a
442
+ * whole family, which the family list could not express.
443
+ */
444
+ export const RESERVED_SUBCOMMANDS = {
445
+ "rooms:who": "renamed to `rooms join` — an interrogative must not name a write",
446
+ "rooms:send": "moved to `messages send` — the verb acts on a message",
447
+ "rooms:list": "moved to `messages list` — the verb acts on a message",
448
+ "rooms:get": "moved to `messages get` — the verb acts on a message",
449
+ "rooms:ack": "moved to `messages ack` — the verb acts on a message",
450
+ };
@@ -0,0 +1,227 @@
1
+ /**
2
+ * `run402 messages` — the room-visible messages agents coordinate with.
3
+ *
4
+ * Split out of `run402 rooms` by legible-cli-surface: four of that family's
5
+ * five verbs acted on a MESSAGE, not on a room. The container was the noun and
6
+ * the thing you actually handle had none.
7
+ *
8
+ * Gateway subsystem: add-agent-messaging (/orgs/v1/:org_id/rooms/:room_key/*).
9
+ * Org-scoped; a project id names that project's DEFAULT room, so inside a
10
+ * checkout the room resolves from the active project with zero flags. JSON
11
+ * envelopes to stdout (pipe contract); flags map 1:1 to the HTTP surface.
12
+ * Session presence + read cursor cache: ./.run402/messaging.json (gitignore).
13
+ */
14
+ import { getSdk } from "./sdk.mjs";
15
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
16
+ import {
17
+ normalizeArgv,
18
+ hasHelp,
19
+ assertKnownFlags,
20
+ assertAllowedValue,
21
+ parseIntegerFlag,
22
+ flagValue,
23
+ positionalArgs,
24
+ requirePositionalCount,
25
+ failUnknownSubcommand,
26
+ } from "./argparse.mjs";
27
+ import {
28
+ resolveRoom,
29
+ cachedPresenceId,
30
+ withPresenceRetry,
31
+ registerFreshPresence,
32
+ rememberPresence,
33
+ getRoomState,
34
+ updateRoomState,
35
+ } from "./rooms-context.mjs";
36
+
37
+ export const IMPORTANCE = ["normal", "high"];
38
+
39
+ const ROOM_FLAGS = ["--project", "--org", "--room"];
40
+
41
+ const HELP = `run402 messages — room-visible messages between agents
42
+
43
+ Usage:
44
+ run402 messages send <body> [--to <names>] [--ack] [--thread <id>]
45
+ run402 messages list [--unread] [--cursor <mcr_...>] [--thread <id>]
46
+ run402 messages get <message_id>
47
+ run402 messages ack <message_id>
48
+
49
+ Addressing (all subcommands):
50
+ --project <id> That project's DEFAULT room (the room key IS the project id)
51
+ --org <id> --room <key> A named org room
52
+ (omit both) Resolved from RUN402_ROOM, a .run402.json binding, or the
53
+ wallet profile's selected org
54
+
55
+ Notes:
56
+ - to/cc route ATTENTION, not access: every agent in the room reads every
57
+ message. --ack asks the recipient to confirm they saw it.
58
+ - Messages are durable. An agent that is not running now reads them when it
59
+ next wakes.
60
+ - The room itself — arriving, leaving, seeing who is live — is \`run402 rooms\`.
61
+ `;
62
+
63
+ async function ensurePresence(room, { name, task } = {}) {
64
+ const existing = cachedPresenceId(room.orgId, room.roomKey);
65
+ if (existing) {
66
+ // Trust, but verify: a cached id whose presence aged out (or was swept)
67
+ // would otherwise ride along until the first send failed. Arrival is the
68
+ // right moment to notice — it costs one GET, once per session.
69
+ const live = await stillLive(room, existing);
70
+ if (live) return { ...live, presence_id: existing, registered: false };
71
+ }
72
+ const registration = await registerFreshPresence(room.orgId, room.roomKey, { name, task });
73
+ return { ...registration, registered: true };
74
+ }
75
+
76
+ /** The cached presence if it is still live in the room, else null. */
77
+ async function stillLive(room, presenceId) {
78
+ try {
79
+ const presence = await getSdk().rooms.getPresence(room.orgId, room.roomKey, presenceId);
80
+ const expiresAt = Date.parse(presence?.expires_at ?? "");
81
+ return Number.isFinite(expiresAt) && expiresAt > Date.now() ? presence : null;
82
+ } catch {
83
+ // Unknown/unreachable presence: fall through to registering a fresh one
84
+ // rather than failing arrival.
85
+ return null;
86
+ }
87
+ }
88
+
89
+ async function send(args) {
90
+ const a = normalizeArgv(args);
91
+ const valueFlags = [...ROOM_FLAGS, "--to", "--cc", "--thread", "--importance", "--idempotency-key", "--name", "--task"];
92
+ assertKnownFlags(a, [...valueFlags, "--ack", "--help", "-h"], valueFlags);
93
+ const positionals = positionalArgs(a, valueFlags);
94
+ requirePositionalCount(positionals, valueFlags, {
95
+ min: 1, max: 1, command: 'run402 messages send "<body>" [--to <names>]', missing: "<body>",
96
+ });
97
+ const importance = flagValue(a, "--importance");
98
+ if (importance != null) assertAllowedValue(importance, IMPORTANCE, "--importance");
99
+ const room = await resolveRoom({
100
+ org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
101
+ });
102
+ try {
103
+ const result = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
104
+ getSdk().rooms.sendMessage(room.orgId, room.roomKey, {
105
+ body: positionals[0],
106
+ to: splitNames(flagValue(a, "--to")),
107
+ cc: splitNames(flagValue(a, "--cc")),
108
+ threadId: flagValue(a, "--thread") ?? undefined,
109
+ importance: importance ?? undefined,
110
+ ackRequired: a.includes("--ack"),
111
+ idempotencyKey: flagValue(a, "--idempotency-key") ?? undefined,
112
+ presenceId: presenceId ?? undefined,
113
+ requestedName: flagValue(a, "--name") ?? undefined,
114
+ task: flagValue(a, "--task") ?? undefined,
115
+ }),
116
+ { name: flagValue(a, "--name"), task: flagValue(a, "--task") });
117
+ rememberPresence(room.orgId, room.roomKey, result.sender_presence, flagValue(a, "--name"));
118
+ console.log(JSON.stringify(result, null, 2));
119
+ } catch (err) {
120
+ reportSdkError(err);
121
+ }
122
+ }
123
+
124
+ async function list(args) {
125
+ const a = normalizeArgv(args);
126
+ const valueFlags = [...ROOM_FLAGS, "--cursor", "--before", "--thread", "--limit"];
127
+ assertKnownFlags(a, [...valueFlags, "--unread", "--help", "-h"], valueFlags);
128
+ requirePositionalCount(positionalArgs(a, valueFlags), valueFlags, {
129
+ min: 0, max: 0, command: "run402 rooms list", missing: "",
130
+ });
131
+ const limit = flagValue(a, "--limit");
132
+ const before = flagValue(a, "--before");
133
+ const unread = a.includes("--unread");
134
+ const room = await resolveRoom({
135
+ org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
136
+ });
137
+ const stored = getRoomState(room.orgId, room.roomKey).cursor;
138
+ const cursor = flagValue(a, "--cursor") ?? (before ? undefined : (typeof stored === "string" ? stored : undefined));
139
+ try {
140
+ const page = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
141
+ getSdk().rooms.listMessages(room.orgId, room.roomKey, {
142
+ ...(before ? { order: "desc", before } : cursor ? { cursor } : {}),
143
+ threadId: flagValue(a, "--thread") ?? undefined,
144
+ ...(unread ? { addressedTo: "me", unread: true } : {}),
145
+ presenceId: presenceId ?? undefined,
146
+ limit: limit != null ? parseIntegerFlag("--limit", limit, { min: 1, max: 200 }) : undefined,
147
+ }));
148
+ // NOTE: an unread/addressed_to=me read needs a resolvable "me"; the retry
149
+ // above registers a replacement when this session's presence has expired.
150
+ // Ascending reads advance the stored cursor; display-mode (--before) never does.
151
+ if (!before && typeof page.cursor === "string") {
152
+ updateRoomState(room.orgId, room.roomKey, { cursor: page.cursor });
153
+ }
154
+ console.log(JSON.stringify(page, null, 2));
155
+ } catch (err) {
156
+ reportSdkError(err);
157
+ }
158
+ }
159
+
160
+ async function get(args) {
161
+ const a = normalizeArgv(args);
162
+ assertKnownFlags(a, [...ROOM_FLAGS, "--help", "-h"], ROOM_FLAGS);
163
+ const positionals = positionalArgs(a, ROOM_FLAGS);
164
+ requirePositionalCount(positionals, ROOM_FLAGS, {
165
+ min: 1, max: 1, command: "run402 rooms get <message_id>", missing: "<message_id>",
166
+ });
167
+ const room = await resolveRoom({
168
+ org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
169
+ });
170
+ try {
171
+ console.log(JSON.stringify(await getSdk().rooms.getMessage(room.orgId, room.roomKey, positionals[0]), null, 2));
172
+ } catch (err) {
173
+ reportSdkError(err);
174
+ }
175
+ }
176
+
177
+ async function ack(args) {
178
+ const a = normalizeArgv(args);
179
+ assertKnownFlags(a, [...ROOM_FLAGS, "--help", "-h"], ROOM_FLAGS);
180
+ const positionals = positionalArgs(a, ROOM_FLAGS);
181
+ requirePositionalCount(positionals, ROOM_FLAGS, {
182
+ min: 1, max: 1, command: "run402 messages ack <message_id>", missing: "<message_id>",
183
+ });
184
+ const room = await resolveRoom({
185
+ org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
186
+ });
187
+ try {
188
+ const result = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
189
+ getSdk().rooms.ackMessage(room.orgId, room.roomKey, positionals[0], {
190
+ presenceId: presenceId ?? undefined,
191
+ }));
192
+ console.log(JSON.stringify(result, null, 2));
193
+ } catch (err) {
194
+ reportSdkError(err);
195
+ }
196
+ }
197
+
198
+
199
+ export async function run(sub, args) {
200
+ const argv = Array.isArray(args) ? args : [];
201
+ if (!sub || hasHelp([sub, ...argv])) {
202
+ console.log(HELP);
203
+ process.exit(0);
204
+ }
205
+ switch (sub) {
206
+ case "send": {
207
+ await send(argv);
208
+ break;
209
+ }
210
+ case "list": {
211
+ await list(argv);
212
+ break;
213
+ }
214
+ case "get": {
215
+ await get(argv);
216
+ break;
217
+ }
218
+ case "ack": {
219
+ await ack(argv);
220
+ break;
221
+ }
222
+ default:
223
+ failUnknownSubcommand("messages", sub, {
224
+ hint: "Run `run402 messages --help` for usage.",
225
+ });
226
+ }
227
+ }
@@ -97,7 +97,7 @@ export async function resolveRoom({ org, room, project } = {}) {
97
97
  hint: `Pass --org <org_id> --room <key>, set ${ROOM_ENV}="<org_id>/<room_key>", add a "room" key to .run402.json, or select a project with: run402 projects use <project_id>`,
98
98
  next_actions: [
99
99
  nextAction("edit_request", {
100
- command: "run402 rooms who --org <org_id> --room <key>",
100
+ command: "run402 rooms join --org <org_id> --room <key>",
101
101
  why: "Name the organization and room on this one call.",
102
102
  }),
103
103
  nextAction("edit_request", {
package/lib/rooms.mjs CHANGED
@@ -1,11 +1,16 @@
1
1
  /**
2
- * `run402 rooms` — agent-to-agent coordination rooms (agent messaging).
2
+ * `run402 rooms` — the coordination room itself: arriving, leaving, and who is
3
+ * in it.
4
+ *
5
+ * The MESSAGES exchanged in a room are `run402 messages` (legible-cli-surface).
6
+ * Four of this family's five verbs used to act on a message, which made the
7
+ * container the noun and left the thing you handle without one.
8
+ *
9
+ * `who` is now `join`: it REGISTERS a presence, and an interrogative must not
10
+ * name a write.
3
11
  *
4
12
  * Gateway subsystem: add-agent-messaging (/orgs/v1/:org_id/rooms/:room_key/*).
5
- * Org-scoped rooms; a project id names that project's DEFAULT room, so inside
6
- * a checkout the room resolves from the active project with zero flags. JSON
7
- * envelopes to stdout (pipe contract); flags map 1:1 to the HTTP surface.
8
- * Session presence + read cursor cache: ./.run402/messaging.json (gitignore).
13
+ * Session presence cache: ./.run402/messaging.json (gitignore).
9
14
  */
10
15
  import { getSdk } from "./sdk.mjs";
11
16
  import { fail, reportSdkError } from "./sdk-errors.mjs";
@@ -34,88 +39,24 @@ export const IMPORTANCE = ["normal", "high"];
34
39
 
35
40
  const ROOM_FLAGS = ["--project", "--org", "--room"];
36
41
 
37
- const HELP = `run402 rooms — coordinate with the other agents on your project
42
+ const HELP = `run402 rooms — arrive in a room, see who is live, leave when done
38
43
 
39
44
  Usage:
40
- run402 rooms who [--name <name>] [--task <text>]
41
- run402 rooms send <body> [--to <names>] [--ack] [--thread <id>]
42
- run402 rooms list [--unread] [--cursor <mcr_...>] [--thread <id>]
43
- run402 rooms get <message_id>
44
- run402 rooms ack <message_id>
45
-
46
- Room addressing (all subcommands):
47
- (default) The active project's default room — the room key IS
48
- the project id, so a checkout needs no flags.
49
- --project <id> Another project's default room.
50
- --org <org_id> --room <key>
51
- A named org room (multi-repo products); also
52
- RUN402_ROOM=<org_id>/<key>.
53
- --room <key> A named org room whose ORG comes from the current-org
54
- chain: --org / RUN402_ORG / the 'org' key of the nearest
55
- .run402.json / 'run402 org use'. A checkout that hosts
56
- nothing on run402 reaches a room this way.
57
- .run402.json {"org":"<org_id>","room":"<key>"} binds a checkout, so
58
- every agent in it lands in the same room with no flags.
59
-
60
- Subcommands:
61
- who Who is live in the room (name, task, active claims). Registers your
62
- session presence on first use — pass --name to choose your own name
63
- (honored when free; suffixed Opus -> Opus-2 when taken, and the
64
- output says so) and --task to say what you're working on.
65
- send Send a room-visible message. --to routes attention (comma-separated
66
- presence names) and --ack asks those recipients to acknowledge.
67
- Messages are visible to the whole room; to/cc is not access control.
68
- list Read messages, oldest-first from your stored cursor. --unread limits
69
- to messages addressed to you that you haven't read; the cursor
70
- auto-saves to ./.run402/messaging.json so the next list resumes.
71
- get One message with its FULL body (lists carry snippets) + ack state.
72
- ack Acknowledge a message addressed to you.
73
-
74
- Options:
75
- --name <name> who/send: requested presence name (first use only).
76
- --task <text> who/send: what this session is working on.
77
- --to <a,b> send: presence names to address (comma-separated).
78
- --cc <a,b> send: additional attention, no ack expectation.
79
- --ack send: request acknowledgment from --to recipients.
80
- --thread <id> send/list: conversation thread id (<=128 chars).
81
- --importance <v> send: normal (default) | high.
82
- --idempotency-key <k> send: safe-retry key — a replay returns the ORIGINAL
83
- message with deduplicated: true, never a double-post.
84
- --cursor <mcr_...> list: explicit resume point (overrides the cache).
85
- --before <mcr_...> list: page OLDER history (newest-first display mode).
86
- --limit <n> list: page size (default 50, max 200).
87
- --unread list: only unread messages addressed to you.
88
- --all who: include expired presences (history).
89
-
90
- The cursor model:
91
- - Every list response carries "cursor": the high-water mark. The CLI stores
92
- it per room in ./.run402/messaging.json and resumes automatically.
93
- - A stale cursor never errors: the response says reset: true and includes
94
- earliest_cursor to restart from. Cursors are opaque — store, never parse.
95
- - Reads hide the newest ~2s (the visibility watermark, same as the events
96
- feed): a message you JUST sent appears on the next read, not instantly.
97
-
98
- Presence and names:
99
- - Your presence is this SESSION, not your model or wallet: two sessions of
100
- the same agent are two presences. Names are unique per room forever.
101
- - A presence expires after ~1h of silence; the CLI transparently re-registers
102
- on the next call (your name will be new — introduce yourself).
103
-
104
- Auth:
105
- Org members (any role) reach all the org's rooms. A delegate
106
- (RUN402_DELEGATE_TOKEN) reaches its own project's default room plus the
107
- org's named rooms. Project service keys are read-only in their room.
108
-
109
- Tip: start every session with \`run402 rooms who --name <yours> --task "<what you're doing>"\`
110
- then \`run402 rooms list --unread\` — arrive, look, then work.
111
-
112
- Examples:
113
- run402 rooms who --name Opus --task "migrating auth"
114
- run402 rooms send "auth dir is mine until 14:30" --to BlueLake --ack
115
- run402 rooms list --unread # catch up, cursor auto-saves
116
- run402 rooms get msg_2f # full body
117
- run402 rooms ack msg_2f
118
- run402 rooms list --org 5f3a... --room run402-dev # named org room
45
+ run402 rooms join [--name <name>] [--task <text>]
46
+
47
+ Addressing:
48
+ --project <id> That project's DEFAULT room (the room key IS the project id)
49
+ --org <id> --room <key> A named org room
50
+ (omit both) Resolved from RUN402_ROOM, a .run402.json binding, or the
51
+ wallet profile's selected org
52
+
53
+ Notes:
54
+ - join registers this session's presence and returns who else is live, what
55
+ they are working on, and what they have claimed — the arrive-and-look call.
56
+ - Presence expires after ~1h of silence. Releasing it early (\`rooms leave\`),
57
+ enumerating reachable rooms, and inspecting one are not here yet — each
58
+ needs a gateway route that does not exist. Filed as follow-ups.
59
+ - The messages themselves are \`run402 messages\`.
119
60
  `;
120
61
 
121
62
  async function ensurePresence(room, { name, task } = {}) {
@@ -179,114 +120,6 @@ function splitNames(value) {
179
120
  return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : [];
180
121
  }
181
122
 
182
- async function send(args) {
183
- const a = normalizeArgv(args);
184
- const valueFlags = [...ROOM_FLAGS, "--to", "--cc", "--thread", "--importance", "--idempotency-key", "--name", "--task"];
185
- assertKnownFlags(a, [...valueFlags, "--ack", "--help", "-h"], valueFlags);
186
- const positionals = positionalArgs(a, valueFlags);
187
- requirePositionalCount(positionals, valueFlags, {
188
- min: 1, max: 1, command: 'run402 rooms send "<body>" [--to <names>]', missing: "<body>",
189
- });
190
- const importance = flagValue(a, "--importance");
191
- if (importance != null) assertAllowedValue(importance, IMPORTANCE, "--importance");
192
- const room = await resolveRoom({
193
- org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
194
- });
195
- try {
196
- const result = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
197
- getSdk().rooms.sendMessage(room.orgId, room.roomKey, {
198
- body: positionals[0],
199
- to: splitNames(flagValue(a, "--to")),
200
- cc: splitNames(flagValue(a, "--cc")),
201
- threadId: flagValue(a, "--thread") ?? undefined,
202
- importance: importance ?? undefined,
203
- ackRequired: a.includes("--ack"),
204
- idempotencyKey: flagValue(a, "--idempotency-key") ?? undefined,
205
- presenceId: presenceId ?? undefined,
206
- requestedName: flagValue(a, "--name") ?? undefined,
207
- task: flagValue(a, "--task") ?? undefined,
208
- }),
209
- { name: flagValue(a, "--name"), task: flagValue(a, "--task") });
210
- rememberPresence(room.orgId, room.roomKey, result.sender_presence, flagValue(a, "--name"));
211
- console.log(JSON.stringify(result, null, 2));
212
- } catch (err) {
213
- reportSdkError(err);
214
- }
215
- }
216
-
217
- async function list(args) {
218
- const a = normalizeArgv(args);
219
- const valueFlags = [...ROOM_FLAGS, "--cursor", "--before", "--thread", "--limit"];
220
- assertKnownFlags(a, [...valueFlags, "--unread", "--help", "-h"], valueFlags);
221
- requirePositionalCount(positionalArgs(a, valueFlags), valueFlags, {
222
- min: 0, max: 0, command: "run402 rooms list", missing: "",
223
- });
224
- const limit = flagValue(a, "--limit");
225
- const before = flagValue(a, "--before");
226
- const unread = a.includes("--unread");
227
- const room = await resolveRoom({
228
- org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
229
- });
230
- const stored = getRoomState(room.orgId, room.roomKey).cursor;
231
- const cursor = flagValue(a, "--cursor") ?? (before ? undefined : (typeof stored === "string" ? stored : undefined));
232
- try {
233
- const page = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
234
- getSdk().rooms.listMessages(room.orgId, room.roomKey, {
235
- ...(before ? { order: "desc", before } : cursor ? { cursor } : {}),
236
- threadId: flagValue(a, "--thread") ?? undefined,
237
- ...(unread ? { addressedTo: "me", unread: true } : {}),
238
- presenceId: presenceId ?? undefined,
239
- limit: limit != null ? parseIntegerFlag("--limit", limit, { min: 1, max: 200 }) : undefined,
240
- }));
241
- // NOTE: an unread/addressed_to=me read needs a resolvable "me"; the retry
242
- // above registers a replacement when this session's presence has expired.
243
- // Ascending reads advance the stored cursor; display-mode (--before) never does.
244
- if (!before && typeof page.cursor === "string") {
245
- updateRoomState(room.orgId, room.roomKey, { cursor: page.cursor });
246
- }
247
- console.log(JSON.stringify(page, null, 2));
248
- } catch (err) {
249
- reportSdkError(err);
250
- }
251
- }
252
-
253
- async function get(args) {
254
- const a = normalizeArgv(args);
255
- assertKnownFlags(a, [...ROOM_FLAGS, "--help", "-h"], ROOM_FLAGS);
256
- const positionals = positionalArgs(a, ROOM_FLAGS);
257
- requirePositionalCount(positionals, ROOM_FLAGS, {
258
- min: 1, max: 1, command: "run402 rooms get <message_id>", missing: "<message_id>",
259
- });
260
- const room = await resolveRoom({
261
- org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
262
- });
263
- try {
264
- console.log(JSON.stringify(await getSdk().rooms.getMessage(room.orgId, room.roomKey, positionals[0]), null, 2));
265
- } catch (err) {
266
- reportSdkError(err);
267
- }
268
- }
269
-
270
- async function ack(args) {
271
- const a = normalizeArgv(args);
272
- assertKnownFlags(a, [...ROOM_FLAGS, "--help", "-h"], ROOM_FLAGS);
273
- const positionals = positionalArgs(a, ROOM_FLAGS);
274
- requirePositionalCount(positionals, ROOM_FLAGS, {
275
- min: 1, max: 1, command: "run402 rooms ack <message_id>", missing: "<message_id>",
276
- });
277
- const room = await resolveRoom({
278
- org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
279
- });
280
- try {
281
- const result = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
282
- getSdk().rooms.ackMessage(room.orgId, room.roomKey, positionals[0], {
283
- presenceId: presenceId ?? undefined,
284
- }));
285
- console.log(JSON.stringify(result, null, 2));
286
- } catch (err) {
287
- reportSdkError(err);
288
- }
289
- }
290
123
 
291
124
  export async function run(sub, args) {
292
125
  const argv = Array.isArray(args) ? args : [];
@@ -295,26 +128,45 @@ export async function run(sub, args) {
295
128
  process.exit(0);
296
129
  }
297
130
  switch (sub) {
298
- case "who": {
131
+ case "join": {
299
132
  await who(argv);
300
133
  break;
301
134
  }
302
- case "send": {
303
- await send(argv);
304
- break;
305
- }
306
- case "list": {
307
- await list(argv);
308
- break;
309
- }
310
- case "get": {
311
- await get(argv);
135
+ // `list`, `get` and `leave` are NOT here. Each needs a gateway route that
136
+ // does not exist (there is no list-rooms, get-room, or release-presence
137
+ // endpoint), and this change states plainly that it adds none. They are
138
+ // filed as follow-ups rather than shipped as a CLI calling into nothing.
139
+ //
140
+ // `rooms list` and `rooms get` additionally carry design D3b: they are
141
+ // FREED by the move of the message verbs, and a freed spelling stays dead
142
+ // for one major before anything reuses it — a spelling that silently
143
+ // answers with different data is worse than one that fails.
144
+ // Retired here, and NOT aliased (design D3): each answers with its
145
+ // successor so one failed call teaches the new model, where an alias
146
+ // would teach the old one forever.
147
+ case "who":
148
+ fail({
149
+ code: "COMMAND_REMOVED",
150
+ message: "`run402 rooms who` was renamed to `run402 rooms join`.",
151
+ hint: "run402 rooms join --name <name> --task <text>",
152
+ details: { was: "rooms who", now: "rooms join", why: "an interrogative must not name a write — it registers a presence" },
153
+ });
312
154
  break;
313
- }
314
- case "ack": {
315
- await ack(argv);
155
+ case "list":
156
+ case "get":
157
+ case "send":
158
+ case "ack":
159
+ fail({
160
+ code: "COMMAND_REMOVED",
161
+ message: `\`run402 rooms ${sub}\` moved to \`run402 messages ${sub}\`.`,
162
+ hint: `run402 messages ${sub}`,
163
+ details: {
164
+ was: `rooms ${sub}`,
165
+ now: `messages ${sub}`,
166
+ why: "the verb acts on a message, not on the room that contains it",
167
+ },
168
+ });
316
169
  break;
317
- }
318
170
  default:
319
171
  failUnknownSubcommand("rooms", sub, {
320
172
  hint: "Run `run402 rooms --help` for usage.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.32.0",
3
+ "version": "4.33.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {