run402 4.17.9 → 4.18.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 +12 -0
- package/lib/buzz.mjs +3 -0
- package/lib/claims.mjs +165 -0
- package/lib/command-manifest.mjs +8 -0
- package/lib/rooms-context.mjs +132 -0
- package/lib/rooms.mjs +297 -0
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +9 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +9 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/rooms.d.ts +165 -0
- package/sdk/dist/namespaces/rooms.d.ts.map +1 -0
- package/sdk/dist/namespaces/rooms.js +399 -0
- package/sdk/dist/namespaces/rooms.js.map +1 -0
- package/sdk/dist/namespaces/rooms.types.d.ts +296 -0
- package/sdk/dist/namespaces/rooms.types.d.ts.map +1 -0
- package/sdk/dist/namespaces/rooms.types.js +27 -0
- package/sdk/dist/namespaces/rooms.types.js.map +1 -0
package/cli.mjs
CHANGED
|
@@ -45,6 +45,8 @@ Commands:
|
|
|
45
45
|
grants Per-project capability grants for agent/CI principals (create, revoke)
|
|
46
46
|
delegates Scoped deploy credentials for agents (create, list, revoke, rotate)
|
|
47
47
|
events What happened to your project since you last looked (cursored feed)
|
|
48
|
+
rooms Coordinate with the other agents on your project (who/send/ack)
|
|
49
|
+
claims Say what you're working on before you collide (advisory)
|
|
48
50
|
errors Grouped error fingerprints + a promote/revert verdict (release-baselined)
|
|
49
51
|
jobs Submit and inspect platform-managed jobs
|
|
50
52
|
functions Manage serverless functions (deploy, invoke, logs, list, delete)
|
|
@@ -273,6 +275,16 @@ switch (cmd) {
|
|
|
273
275
|
await run(sub, rest);
|
|
274
276
|
break;
|
|
275
277
|
}
|
|
278
|
+
case "rooms": {
|
|
279
|
+
const { run } = await import("./lib/rooms.mjs");
|
|
280
|
+
await run(sub, rest);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "claims": {
|
|
284
|
+
const { run } = await import("./lib/claims.mjs");
|
|
285
|
+
await run(sub, rest);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
276
288
|
case "errors": {
|
|
277
289
|
const { run } = await import("./lib/errors.mjs");
|
|
278
290
|
await run(sub, rest);
|
package/lib/buzz.mjs
CHANGED
|
@@ -22,6 +22,9 @@ The four independent states are:
|
|
|
22
22
|
Canonical workflows:
|
|
23
23
|
run402 buzz status
|
|
24
24
|
run402 buzz adopt offer --org <org_id> --identity-link <idlnk_id> [--deployment-context-file <json>]
|
|
25
|
+
--org the Run402 organization id as "run402 org whoami" returns it (a UUID)
|
|
26
|
+
--deployment-context-file JSON with exactly these five non-empty strings, and no others:
|
|
27
|
+
project_id, release_id, live_url, source_revision, verified_at
|
|
25
28
|
run402 buzz install --org <org_id> --community <buzz:community:host> --authority <hex-pubkey>
|
|
26
29
|
run402 buzz enroll --installation <buzzci_id> --identity-link <idlnk_id> --grants-file <json> --expires-at <ISO-8601>
|
|
27
30
|
|
package/lib/claims.mjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run402 claims` — advisory work claims for agent coordination.
|
|
3
|
+
*
|
|
4
|
+
* Gateway subsystem: add-agent-messaging (/orgs/v1/:org_id/rooms/:room_key/claims).
|
|
5
|
+
* A claim declares "I'm working on X until T" so concurrent agents discover
|
|
6
|
+
* collisions BEFORE they happen. Claims are ADVISORY: creating a conflicting
|
|
7
|
+
* claim succeeds and reports the complete conflict set — nothing, ever, is
|
|
8
|
+
* blocked by a claim (deploys included). JSON to stdout (pipe contract).
|
|
9
|
+
*/
|
|
10
|
+
import { getSdk } from "./sdk.mjs";
|
|
11
|
+
import { reportSdkError } from "./sdk-errors.mjs";
|
|
12
|
+
import {
|
|
13
|
+
normalizeArgv,
|
|
14
|
+
hasHelp,
|
|
15
|
+
assertKnownFlags,
|
|
16
|
+
assertAllowedValue,
|
|
17
|
+
parseIntegerFlag,
|
|
18
|
+
flagValue,
|
|
19
|
+
positionalArgs,
|
|
20
|
+
requirePositionalCount,
|
|
21
|
+
failUnknownSubcommand,
|
|
22
|
+
} from "./argparse.mjs";
|
|
23
|
+
import { resolveRoom, withPresenceRetry } from "./rooms-context.mjs";
|
|
24
|
+
|
|
25
|
+
export const MODES = ["exclusive", "shared"];
|
|
26
|
+
|
|
27
|
+
const ROOM_FLAGS = ["--project", "--org", "--room"];
|
|
28
|
+
|
|
29
|
+
const HELP = `run402 claims — say what you're working on before you collide
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
run402 claims create <resource> [--mode exclusive|shared] [--ttl <seconds>]
|
|
33
|
+
run402 claims list [--all]
|
|
34
|
+
run402 claims release <claim_id>
|
|
35
|
+
|
|
36
|
+
Resources (namespaced; conflicts never cross namespaces):
|
|
37
|
+
repo:<glob> Repo paths — repo:src/auth/** (glob overlap detection)
|
|
38
|
+
function:<name> A deployed function by name
|
|
39
|
+
table:<name> A database table
|
|
40
|
+
deploy The deploy itself (a soft mutex by convention)
|
|
41
|
+
<free-form> Anything else (exact-match overlap)
|
|
42
|
+
|
|
43
|
+
Advisory, always:
|
|
44
|
+
Creating a conflicting claim SUCCEEDS — the response carries the complete
|
|
45
|
+
conflicts[] (holder, resource, mode, expiry) and the deploy-path responses
|
|
46
|
+
surface other agents' claims automatically. A claim never blocks anything;
|
|
47
|
+
it makes collisions visible before they happen. Claims auto-expire (default
|
|
48
|
+
1h, max 24h) so a dead session cannot wedge the room; <=32 active per
|
|
49
|
+
presence.
|
|
50
|
+
|
|
51
|
+
Room addressing:
|
|
52
|
+
Default room of the active project with no flags; --project <id> for another
|
|
53
|
+
project; --org <org_id> --room <key> (or RUN402_ROOM=<org_id>/<key>) for a
|
|
54
|
+
named org room.
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--mode <m> exclusive (default) — one worker; shared — parallel-safe.
|
|
58
|
+
--ttl <seconds> Claim lifetime (default 3600, max 86400).
|
|
59
|
+
--note <text> Why you're claiming it (shown to everyone).
|
|
60
|
+
--all list: include released/expired history.
|
|
61
|
+
|
|
62
|
+
Tip: claim before you edit, release when you hand off — and put the handoff
|
|
63
|
+
in \`run402 rooms send\` so the room's timeline tells the story.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
run402 claims create repo:src/auth/** --note "migrating to passkeys"
|
|
67
|
+
run402 claims create deploy --ttl 900
|
|
68
|
+
run402 claims list
|
|
69
|
+
run402 claims release clm_1a
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
async function create(args) {
|
|
73
|
+
const a = normalizeArgv(args);
|
|
74
|
+
const valueFlags = [...ROOM_FLAGS, "--mode", "--ttl", "--note"];
|
|
75
|
+
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
76
|
+
const positionals = positionalArgs(a, valueFlags);
|
|
77
|
+
requirePositionalCount(positionals, valueFlags, {
|
|
78
|
+
min: 1, max: 1, command: "run402 claims create <resource>", missing: "<resource>",
|
|
79
|
+
});
|
|
80
|
+
const mode = flagValue(a, "--mode") ?? "exclusive";
|
|
81
|
+
assertAllowedValue(mode, MODES, "--mode");
|
|
82
|
+
const ttl = flagValue(a, "--ttl");
|
|
83
|
+
const room = await resolveRoom({
|
|
84
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const created = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
|
|
88
|
+
getSdk().rooms.createClaim(room.orgId, room.roomKey, {
|
|
89
|
+
resource: positionals[0],
|
|
90
|
+
mode,
|
|
91
|
+
ttlSeconds: ttl != null ? parseIntegerFlag("--ttl", ttl, { min: 1, max: 86400 }) : undefined,
|
|
92
|
+
note: flagValue(a, "--note") ?? undefined,
|
|
93
|
+
presenceId: presenceId ?? undefined,
|
|
94
|
+
}));
|
|
95
|
+
console.log(JSON.stringify(created, null, 2));
|
|
96
|
+
const conflicts = Array.isArray(created.conflicts) ? created.conflicts : [];
|
|
97
|
+
if (conflicts.length > 0) {
|
|
98
|
+
console.error(`Granted with ${conflicts.length} conflict(s) — advisory, nothing is blocked. Coordinate via run402 rooms send.`);
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
reportSdkError(err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function list(args) {
|
|
106
|
+
const a = normalizeArgv(args);
|
|
107
|
+
assertKnownFlags(a, [...ROOM_FLAGS, "--all", "--help", "-h"], ROOM_FLAGS);
|
|
108
|
+
requirePositionalCount(positionalArgs(a, ROOM_FLAGS), ROOM_FLAGS, {
|
|
109
|
+
min: 0, max: 0, command: "run402 claims list", missing: "",
|
|
110
|
+
});
|
|
111
|
+
const room = await resolveRoom({
|
|
112
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
const page = await getSdk().rooms.listClaims(room.orgId, room.roomKey, {
|
|
116
|
+
includeInactive: a.includes("--all"),
|
|
117
|
+
});
|
|
118
|
+
console.log(JSON.stringify(page, null, 2));
|
|
119
|
+
} catch (err) {
|
|
120
|
+
reportSdkError(err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function release(args) {
|
|
125
|
+
const a = normalizeArgv(args);
|
|
126
|
+
assertKnownFlags(a, [...ROOM_FLAGS, "--help", "-h"], ROOM_FLAGS);
|
|
127
|
+
const positionals = positionalArgs(a, ROOM_FLAGS);
|
|
128
|
+
requirePositionalCount(positionals, ROOM_FLAGS, {
|
|
129
|
+
min: 1, max: 1, command: "run402 claims release <claim_id>", missing: "<claim_id>",
|
|
130
|
+
});
|
|
131
|
+
const room = await resolveRoom({
|
|
132
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
console.log(JSON.stringify(await getSdk().rooms.releaseClaim(room.orgId, room.roomKey, positionals[0]), null, 2));
|
|
136
|
+
} catch (err) {
|
|
137
|
+
reportSdkError(err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function run(sub, args) {
|
|
142
|
+
const argv = Array.isArray(args) ? args : [];
|
|
143
|
+
if (!sub || hasHelp([sub, ...argv])) {
|
|
144
|
+
console.log(HELP);
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
switch (sub) {
|
|
148
|
+
case "create": {
|
|
149
|
+
await create(argv);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "list": {
|
|
153
|
+
await list(argv);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "release": {
|
|
157
|
+
await release(argv);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
failUnknownSubcommand("claims", sub, {
|
|
162
|
+
hint: "Run `run402 claims --help` for usage.",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
package/lib/command-manifest.mjs
CHANGED
|
@@ -200,6 +200,14 @@ export const COMMAND_MANIFEST = [
|
|
|
200
200
|
|
|
201
201
|
// ── events / errors (flat, merged runners) ───────────────────────────────
|
|
202
202
|
{ path: ["events"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
|
|
203
|
+
{ path: ["rooms", "who"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
|
|
204
|
+
{ path: ["rooms", "send"], positionals: [p("body")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["hello"], runStyle: "sub" },
|
|
205
|
+
{ path: ["rooms", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
|
|
206
|
+
{ path: ["rooms", "get"], positionals: [p("message_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["msg_1"], runStyle: "sub" },
|
|
207
|
+
{ path: ["rooms", "ack"], positionals: [p("message_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["msg_1"], runStyle: "sub" },
|
|
208
|
+
{ path: ["claims", "create"], positionals: [p("resource")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["deploy"], runStyle: "sub" },
|
|
209
|
+
{ path: ["claims", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
|
|
210
|
+
{ path: ["claims", "release"], positionals: [p("claim_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["clm_1"], runStyle: "sub" },
|
|
203
211
|
{ path: ["errors"], positionals: [p("fingerprint_id", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
|
|
204
212
|
|
|
205
213
|
// ── jobs ─────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared room resolution + per-checkout session state for the agent-messaging
|
|
3
|
+
* commands (`run402 messages`, `run402 claims`).
|
|
4
|
+
*
|
|
5
|
+
* Room addressing (precedence):
|
|
6
|
+
* --org <org_id> + --room <key> explicit (named org rooms)
|
|
7
|
+
* RUN402_ROOM=<org_id>/<key> env form of the same
|
|
8
|
+
* (default) the project's DEFAULT room — the room key
|
|
9
|
+
* IS the project id; org resolved via the
|
|
10
|
+
* project overview (`rooms.forProject`).
|
|
11
|
+
*
|
|
12
|
+
* Session state lives at ./.run402/messaging.json (per-checkout — the house
|
|
13
|
+
* discipline of one worktree per agent session makes per-checkout equal
|
|
14
|
+
* per-session). It caches, per room: the session's presence_id + name and the
|
|
15
|
+
* read cursor. Add `.run402/` to .gitignore. Two sessions sharing one checkout
|
|
16
|
+
* share a presence (coherent, just undifferentiated); RUN402_PRESENCE_ID
|
|
17
|
+
* overrides for harnesses that multiplex.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { fail } from "./sdk-errors.mjs";
|
|
22
|
+
import { resolveProjectId } from "./config.mjs";
|
|
23
|
+
import { getSdk } from "./sdk.mjs";
|
|
24
|
+
|
|
25
|
+
export const ROOM_ENV = "RUN402_ROOM";
|
|
26
|
+
export const PRESENCE_ENV = "RUN402_PRESENCE_ID";
|
|
27
|
+
|
|
28
|
+
const STATE_DIR = ".run402";
|
|
29
|
+
const STATE_FILE = "messaging.json";
|
|
30
|
+
|
|
31
|
+
/** Resolve the addressed room to { orgId, roomKey } (one SDK lookup at most). */
|
|
32
|
+
export async function resolveRoom({ org, room, project } = {}) {
|
|
33
|
+
if (room && !org) {
|
|
34
|
+
fail({
|
|
35
|
+
code: "BAD_USAGE",
|
|
36
|
+
message: "--room names an org room and needs --org <org_id> beside it.",
|
|
37
|
+
hint: "For the project's default room, omit both (or pass --project). For a named room: --org <org_id> --room <key>.",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (org && room) return { orgId: org, roomKey: room };
|
|
41
|
+
const envRoom = (process.env[ROOM_ENV] ?? "").trim();
|
|
42
|
+
if (envRoom) {
|
|
43
|
+
const slash = envRoom.indexOf("/");
|
|
44
|
+
if (slash <= 0 || slash === envRoom.length - 1) {
|
|
45
|
+
fail({
|
|
46
|
+
code: "BAD_USAGE",
|
|
47
|
+
message: `${ROOM_ENV} must be "<org_id>/<room_key>".`,
|
|
48
|
+
details: { value: envRoom },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return { orgId: envRoom.slice(0, slash), roomKey: envRoom.slice(slash + 1) };
|
|
52
|
+
}
|
|
53
|
+
const projectId = resolveProjectId(project);
|
|
54
|
+
const scoped = await getSdk().rooms.forProject(projectId);
|
|
55
|
+
return { orgId: scoped.orgId, roomKey: scoped.roomKey };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function statePath() {
|
|
59
|
+
return join(process.cwd(), STATE_DIR, STATE_FILE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loadState() {
|
|
63
|
+
try {
|
|
64
|
+
const raw = readFileSync(statePath(), "utf8");
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
return parsed && typeof parsed === "object" && parsed.rooms && typeof parsed.rooms === "object"
|
|
67
|
+
? parsed
|
|
68
|
+
: { rooms: {} };
|
|
69
|
+
} catch {
|
|
70
|
+
return { rooms: {} };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function saveState(state) {
|
|
75
|
+
try {
|
|
76
|
+
const dir = join(process.cwd(), STATE_DIR);
|
|
77
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
78
|
+
writeFileSync(statePath(), `${JSON.stringify(state, null, 2)}\n`);
|
|
79
|
+
} catch {
|
|
80
|
+
// Best-effort cache: an unwritable checkout degrades to stateless calls.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const roomStateKey = (orgId, roomKey) => `${orgId}/${roomKey}`;
|
|
85
|
+
|
|
86
|
+
export function getRoomState(orgId, roomKey) {
|
|
87
|
+
return loadState().rooms[roomStateKey(orgId, roomKey)] ?? {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function updateRoomState(orgId, roomKey, patch) {
|
|
91
|
+
const state = loadState();
|
|
92
|
+
const key = roomStateKey(orgId, roomKey);
|
|
93
|
+
state.rooms[key] = { ...(state.rooms[key] ?? {}), ...patch };
|
|
94
|
+
saveState(state);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The session's presence id for a room: env override, else the checkout cache. */
|
|
98
|
+
export function cachedPresenceId(orgId, roomKey) {
|
|
99
|
+
const env = (process.env[PRESENCE_ENV] ?? "").trim();
|
|
100
|
+
if (env) return env;
|
|
101
|
+
const cached = getRoomState(orgId, roomKey).presence_id;
|
|
102
|
+
return typeof cached === "string" && cached ? cached : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run a room call that carries the cached presence. On PRESENCE_EXPIRED (410 —
|
|
107
|
+
* this session's presence aged out), drop the cache and retry ONCE without a
|
|
108
|
+
* presence_id so the gateway's resolve-or-create issues a fresh one.
|
|
109
|
+
*/
|
|
110
|
+
export async function withPresenceRetry(orgId, roomKey, call) {
|
|
111
|
+
const presenceId = cachedPresenceId(orgId, roomKey);
|
|
112
|
+
try {
|
|
113
|
+
return await call(presenceId);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const code = err?.body?.code ?? err?.code;
|
|
116
|
+
if (code === "PRESENCE_EXPIRED" && presenceId) {
|
|
117
|
+
updateRoomState(orgId, roomKey, { presence_id: null, name: null });
|
|
118
|
+
return call(null);
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Cache the presence a response attributed this session to. */
|
|
125
|
+
export function rememberPresence(orgId, roomKey, presence) {
|
|
126
|
+
if (!presence || typeof presence !== "object") return;
|
|
127
|
+
const id = presence.presence_id;
|
|
128
|
+
const name = presence.name;
|
|
129
|
+
if (typeof id === "string" && id) {
|
|
130
|
+
updateRoomState(orgId, roomKey, { presence_id: id, ...(typeof name === "string" ? { name } : {}) });
|
|
131
|
+
}
|
|
132
|
+
}
|
package/lib/rooms.mjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run402 rooms` — agent-to-agent coordination rooms (agent messaging).
|
|
3
|
+
*
|
|
4
|
+
* 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).
|
|
9
|
+
*/
|
|
10
|
+
import { getSdk } from "./sdk.mjs";
|
|
11
|
+
import { fail, reportSdkError } from "./sdk-errors.mjs";
|
|
12
|
+
import {
|
|
13
|
+
normalizeArgv,
|
|
14
|
+
hasHelp,
|
|
15
|
+
assertKnownFlags,
|
|
16
|
+
assertAllowedValue,
|
|
17
|
+
parseIntegerFlag,
|
|
18
|
+
flagValue,
|
|
19
|
+
positionalArgs,
|
|
20
|
+
requirePositionalCount,
|
|
21
|
+
failUnknownSubcommand,
|
|
22
|
+
} from "./argparse.mjs";
|
|
23
|
+
import {
|
|
24
|
+
resolveRoom,
|
|
25
|
+
cachedPresenceId,
|
|
26
|
+
withPresenceRetry,
|
|
27
|
+
rememberPresence,
|
|
28
|
+
getRoomState,
|
|
29
|
+
updateRoomState,
|
|
30
|
+
} from "./rooms-context.mjs";
|
|
31
|
+
|
|
32
|
+
export const IMPORTANCE = ["normal", "high"];
|
|
33
|
+
|
|
34
|
+
const ROOM_FLAGS = ["--project", "--org", "--room"];
|
|
35
|
+
|
|
36
|
+
const HELP = `run402 rooms — coordinate with the other agents on your project
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
run402 rooms who [--name <name>] [--task <text>]
|
|
40
|
+
run402 rooms send <body> [--to <names>] [--ack] [--thread <id>]
|
|
41
|
+
run402 rooms list [--unread] [--cursor <mcr_...>] [--thread <id>]
|
|
42
|
+
run402 rooms get <message_id>
|
|
43
|
+
run402 rooms ack <message_id>
|
|
44
|
+
|
|
45
|
+
Room addressing (all subcommands):
|
|
46
|
+
(default) The active project's default room — the room key IS
|
|
47
|
+
the project id, so a checkout needs no flags.
|
|
48
|
+
--project <id> Another project's default room.
|
|
49
|
+
--org <org_id> --room <key>
|
|
50
|
+
A named org room (multi-repo products); also
|
|
51
|
+
RUN402_ROOM=<org_id>/<key>.
|
|
52
|
+
|
|
53
|
+
Subcommands:
|
|
54
|
+
who Who is live in the room (name, task, active claims). Registers your
|
|
55
|
+
session presence on first use — pass --name to choose your own name
|
|
56
|
+
(honored when free; suffixed Opus -> Opus-2 when taken, and the
|
|
57
|
+
output says so) and --task to say what you're working on.
|
|
58
|
+
send Send a room-visible message. --to routes attention (comma-separated
|
|
59
|
+
presence names) and --ack asks those recipients to acknowledge.
|
|
60
|
+
Messages are visible to the whole room; to/cc is not access control.
|
|
61
|
+
list Read messages, oldest-first from your stored cursor. --unread limits
|
|
62
|
+
to messages addressed to you that you haven't read; the cursor
|
|
63
|
+
auto-saves to ./.run402/messaging.json so the next list resumes.
|
|
64
|
+
get One message with its FULL body (lists carry snippets) + ack state.
|
|
65
|
+
ack Acknowledge a message addressed to you.
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--name <name> who/send: requested presence name (first use only).
|
|
69
|
+
--task <text> who/send: what this session is working on.
|
|
70
|
+
--to <a,b> send: presence names to address (comma-separated).
|
|
71
|
+
--cc <a,b> send: additional attention, no ack expectation.
|
|
72
|
+
--ack send: request acknowledgment from --to recipients.
|
|
73
|
+
--thread <id> send/list: conversation thread id (<=128 chars).
|
|
74
|
+
--importance <v> send: normal (default) | high.
|
|
75
|
+
--idempotency-key <k> send: safe-retry key — a replay returns the ORIGINAL
|
|
76
|
+
message with deduplicated: true, never a double-post.
|
|
77
|
+
--cursor <mcr_...> list: explicit resume point (overrides the cache).
|
|
78
|
+
--before <mcr_...> list: page OLDER history (newest-first display mode).
|
|
79
|
+
--limit <n> list: page size (default 50, max 200).
|
|
80
|
+
--unread list: only unread messages addressed to you.
|
|
81
|
+
--all who: include expired presences (history).
|
|
82
|
+
|
|
83
|
+
The cursor model:
|
|
84
|
+
- Every list response carries "cursor": the high-water mark. The CLI stores
|
|
85
|
+
it per room in ./.run402/messaging.json and resumes automatically.
|
|
86
|
+
- A stale cursor never errors: the response says reset: true and includes
|
|
87
|
+
earliest_cursor to restart from. Cursors are opaque — store, never parse.
|
|
88
|
+
- Reads hide the newest ~2s (the visibility watermark, same as the events
|
|
89
|
+
feed): a message you JUST sent appears on the next read, not instantly.
|
|
90
|
+
|
|
91
|
+
Presence and names:
|
|
92
|
+
- Your presence is this SESSION, not your model or wallet: two sessions of
|
|
93
|
+
the same agent are two presences. Names are unique per room forever.
|
|
94
|
+
- A presence expires after ~1h of silence; the CLI transparently re-registers
|
|
95
|
+
on the next call (your name will be new — introduce yourself).
|
|
96
|
+
|
|
97
|
+
Auth:
|
|
98
|
+
Org members (any role) reach all the org's rooms. A delegate
|
|
99
|
+
(RUN402_DELEGATE_TOKEN) reaches its own project's default room plus the
|
|
100
|
+
org's named rooms. Project service keys are read-only in their room.
|
|
101
|
+
|
|
102
|
+
Tip: start every session with \`run402 rooms who --name <yours> --task "<what you're doing>"\`
|
|
103
|
+
then \`run402 rooms list --unread\` — arrive, look, then work.
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
run402 rooms who --name Opus --task "migrating auth"
|
|
107
|
+
run402 rooms send "auth dir is mine until 14:30" --to BlueLake --ack
|
|
108
|
+
run402 rooms list --unread # catch up, cursor auto-saves
|
|
109
|
+
run402 rooms get msg_2f # full body
|
|
110
|
+
run402 rooms ack msg_2f
|
|
111
|
+
run402 rooms list --org 5f3a... --room run402-dev # named org room
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
async function ensurePresence(room, { name, task } = {}) {
|
|
115
|
+
const existing = cachedPresenceId(room.orgId, room.roomKey);
|
|
116
|
+
if (existing) return { presence_id: existing, registered: false };
|
|
117
|
+
const sdk = getSdk();
|
|
118
|
+
const registration = await sdk.rooms.registerPresence(room.orgId, room.roomKey, {
|
|
119
|
+
...(name ? { requestedName: name } : {}),
|
|
120
|
+
...(task ? { task } : {}),
|
|
121
|
+
});
|
|
122
|
+
rememberPresence(room.orgId, room.roomKey, registration);
|
|
123
|
+
return { ...registration, registered: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function who(args) {
|
|
127
|
+
const a = normalizeArgv(args);
|
|
128
|
+
const valueFlags = [...ROOM_FLAGS, "--name", "--task"];
|
|
129
|
+
assertKnownFlags(a, [...valueFlags, "--all", "--help", "-h"], valueFlags);
|
|
130
|
+
requirePositionalCount(positionalArgs(a, valueFlags), valueFlags, {
|
|
131
|
+
min: 0, max: 0, command: "run402 rooms who", missing: "",
|
|
132
|
+
});
|
|
133
|
+
const room = await resolveRoom({
|
|
134
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
const me = await ensurePresence(room, { name: flagValue(a, "--name"), task: flagValue(a, "--task") });
|
|
138
|
+
const page = await getSdk().rooms.listPresences(room.orgId, room.roomKey, {
|
|
139
|
+
includeExpired: a.includes("--all"),
|
|
140
|
+
});
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
org_id: room.orgId,
|
|
143
|
+
room_key: room.roomKey,
|
|
144
|
+
you: me,
|
|
145
|
+
...page,
|
|
146
|
+
}, null, 2));
|
|
147
|
+
if (me.registered && me.renamed) {
|
|
148
|
+
console.error(`You are ${me.name} — ${me.requested_name} was taken.`);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
reportSdkError(err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function splitNames(value) {
|
|
156
|
+
return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function send(args) {
|
|
160
|
+
const a = normalizeArgv(args);
|
|
161
|
+
const valueFlags = [...ROOM_FLAGS, "--to", "--cc", "--thread", "--importance", "--idempotency-key", "--name", "--task"];
|
|
162
|
+
assertKnownFlags(a, [...valueFlags, "--ack", "--help", "-h"], valueFlags);
|
|
163
|
+
const positionals = positionalArgs(a, valueFlags);
|
|
164
|
+
requirePositionalCount(positionals, valueFlags, {
|
|
165
|
+
min: 1, max: 1, command: 'run402 rooms send "<body>" [--to <names>]', missing: "<body>",
|
|
166
|
+
});
|
|
167
|
+
const importance = flagValue(a, "--importance");
|
|
168
|
+
if (importance != null) assertAllowedValue(importance, IMPORTANCE, "--importance");
|
|
169
|
+
const room = await resolveRoom({
|
|
170
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
const result = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
|
|
174
|
+
getSdk().rooms.sendMessage(room.orgId, room.roomKey, {
|
|
175
|
+
body: positionals[0],
|
|
176
|
+
to: splitNames(flagValue(a, "--to")),
|
|
177
|
+
cc: splitNames(flagValue(a, "--cc")),
|
|
178
|
+
threadId: flagValue(a, "--thread") ?? undefined,
|
|
179
|
+
importance: importance ?? undefined,
|
|
180
|
+
ackRequired: a.includes("--ack"),
|
|
181
|
+
idempotencyKey: flagValue(a, "--idempotency-key") ?? undefined,
|
|
182
|
+
presenceId: presenceId ?? undefined,
|
|
183
|
+
requestedName: flagValue(a, "--name") ?? undefined,
|
|
184
|
+
task: flagValue(a, "--task") ?? undefined,
|
|
185
|
+
}));
|
|
186
|
+
rememberPresence(room.orgId, room.roomKey, result.sender_presence);
|
|
187
|
+
console.log(JSON.stringify(result, null, 2));
|
|
188
|
+
} catch (err) {
|
|
189
|
+
reportSdkError(err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function list(args) {
|
|
194
|
+
const a = normalizeArgv(args);
|
|
195
|
+
const valueFlags = [...ROOM_FLAGS, "--cursor", "--before", "--thread", "--limit"];
|
|
196
|
+
assertKnownFlags(a, [...valueFlags, "--unread", "--help", "-h"], valueFlags);
|
|
197
|
+
requirePositionalCount(positionalArgs(a, valueFlags), valueFlags, {
|
|
198
|
+
min: 0, max: 0, command: "run402 rooms list", missing: "",
|
|
199
|
+
});
|
|
200
|
+
const limit = flagValue(a, "--limit");
|
|
201
|
+
const before = flagValue(a, "--before");
|
|
202
|
+
const unread = a.includes("--unread");
|
|
203
|
+
const room = await resolveRoom({
|
|
204
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
205
|
+
});
|
|
206
|
+
const stored = getRoomState(room.orgId, room.roomKey).cursor;
|
|
207
|
+
const cursor = flagValue(a, "--cursor") ?? (before ? undefined : (typeof stored === "string" ? stored : undefined));
|
|
208
|
+
try {
|
|
209
|
+
const page = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
|
|
210
|
+
getSdk().rooms.listMessages(room.orgId, room.roomKey, {
|
|
211
|
+
...(before ? { order: "desc", before } : cursor ? { cursor } : {}),
|
|
212
|
+
threadId: flagValue(a, "--thread") ?? undefined,
|
|
213
|
+
...(unread ? { addressedTo: "me", unread: true } : {}),
|
|
214
|
+
presenceId: presenceId ?? undefined,
|
|
215
|
+
limit: limit != null ? parseIntegerFlag("--limit", limit, { min: 1, max: 200 }) : undefined,
|
|
216
|
+
}));
|
|
217
|
+
// Ascending reads advance the stored cursor; display-mode (--before) never does.
|
|
218
|
+
if (!before && typeof page.cursor === "string") {
|
|
219
|
+
updateRoomState(room.orgId, room.roomKey, { cursor: page.cursor });
|
|
220
|
+
}
|
|
221
|
+
console.log(JSON.stringify(page, null, 2));
|
|
222
|
+
} catch (err) {
|
|
223
|
+
reportSdkError(err);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function get(args) {
|
|
228
|
+
const a = normalizeArgv(args);
|
|
229
|
+
assertKnownFlags(a, [...ROOM_FLAGS, "--help", "-h"], ROOM_FLAGS);
|
|
230
|
+
const positionals = positionalArgs(a, ROOM_FLAGS);
|
|
231
|
+
requirePositionalCount(positionals, ROOM_FLAGS, {
|
|
232
|
+
min: 1, max: 1, command: "run402 rooms get <message_id>", missing: "<message_id>",
|
|
233
|
+
});
|
|
234
|
+
const room = await resolveRoom({
|
|
235
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
236
|
+
});
|
|
237
|
+
try {
|
|
238
|
+
console.log(JSON.stringify(await getSdk().rooms.getMessage(room.orgId, room.roomKey, positionals[0]), null, 2));
|
|
239
|
+
} catch (err) {
|
|
240
|
+
reportSdkError(err);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function ack(args) {
|
|
245
|
+
const a = normalizeArgv(args);
|
|
246
|
+
assertKnownFlags(a, [...ROOM_FLAGS, "--help", "-h"], ROOM_FLAGS);
|
|
247
|
+
const positionals = positionalArgs(a, ROOM_FLAGS);
|
|
248
|
+
requirePositionalCount(positionals, ROOM_FLAGS, {
|
|
249
|
+
min: 1, max: 1, command: "run402 rooms ack <message_id>", missing: "<message_id>",
|
|
250
|
+
});
|
|
251
|
+
const room = await resolveRoom({
|
|
252
|
+
org: flagValue(a, "--org"), room: flagValue(a, "--room"), project: flagValue(a, "--project"),
|
|
253
|
+
});
|
|
254
|
+
try {
|
|
255
|
+
const result = await withPresenceRetry(room.orgId, room.roomKey, (presenceId) =>
|
|
256
|
+
getSdk().rooms.ackMessage(room.orgId, room.roomKey, positionals[0], {
|
|
257
|
+
presenceId: presenceId ?? undefined,
|
|
258
|
+
}));
|
|
259
|
+
console.log(JSON.stringify(result, null, 2));
|
|
260
|
+
} catch (err) {
|
|
261
|
+
reportSdkError(err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function run(sub, args) {
|
|
266
|
+
const argv = Array.isArray(args) ? args : [];
|
|
267
|
+
if (!sub || hasHelp([sub, ...argv])) {
|
|
268
|
+
console.log(HELP);
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
switch (sub) {
|
|
272
|
+
case "who": {
|
|
273
|
+
await who(argv);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "send": {
|
|
277
|
+
await send(argv);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "list": {
|
|
281
|
+
await list(argv);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "get": {
|
|
285
|
+
await get(argv);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case "ack": {
|
|
289
|
+
await ack(argv);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
failUnknownSubcommand("rooms", sub, {
|
|
294
|
+
hint: "Run `run402 rooms --help` for usage.",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
package/package.json
CHANGED