@openmarket/rooms-client 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,12 @@ The OM Rooms protocol client: wire types and frame schemas, the WebSocket client
6
6
 
7
7
  Everything in this package runs unmodified in a browser: no `node:` or `bun:` imports, no filesystem or OS access, static imports only. Environment reads go through `envString()` in `endpoints.ts`, which returns `undefined` where `process` does not exist. The repo's boundary gate enforces this on every CI run; if you need daemon-side facilities (SQLite, vault, HTTP server), they live in the `om` CLI, not here.
8
8
 
9
+ Provider adapters and agent runtime contracts are also CLI-owned. In particular,
10
+ LLM messages, streamed turn events, and usage accounting live under
11
+ `packages/cli/src/agent/`; this package must not import a provider SDK or expose
12
+ those contracts as public subpaths. Only browser-safe rooms protocol and view-model
13
+ types belong here, and the CLI may depend on them in that direction.
14
+
9
15
  ## Usage
10
16
 
11
17
  There is no root export; import the surface you need by subpath:
@@ -16,7 +22,7 @@ import { RoomClientMessageType, SUPPORTED_ROOM_PROTOCOL_VERSION } from "@openmar
16
22
  import type { RoomSpaceSummary } from "@openmarket/rooms-client/social-types";
17
23
  ```
18
24
 
19
- Subpaths: `client`, `ws-client`, `social-client`, `social-types`, `library-publisher`, `doc-reconcile`, `topic-uri`, `doc-uri`, `endpoints`, `jwt`, `names`, `suggestion-attribution`, `merge3`, `types`, `version`, `agent/{armed-mode, lane-draft, lane-prompts, library-context, library-model, room-context, sidebar-model, topic-inbox, topic-view-batcher}`, `agent/clients/common`, `shared/{rooms-protocol, mentions, emoji-data}`.
25
+ Subpaths: `client`, `ws-client`, `social-client`, `social-types`, `library-publisher`, `doc-reconcile`, `topic-uri`, `doc-uri`, `endpoints`, `jwt`, `names`, `suggestion-attribution`, `merge3`, `types`, `version`, `agent/{armed-mode, lane-draft, lane-prompts, library-context, library-model, room-context, sidebar-model, topic-inbox, topic-view-batcher}`, `shared/{rooms-protocol, mentions, emoji-data}`.
20
26
 
21
27
  Bun consumers resolve raw TypeScript via the `bun` export condition; Node and bundlers get compiled JS plus `.d.ts` from `dist/`.
22
28
 
@@ -181,10 +181,11 @@ export declare function canPostAutonomously(input: {
181
181
  * successful `post()`. Mutates `state`.
182
182
  */
183
183
  export declare function recordAutonomousPost(state: ArmedRoomState, now?: number): void;
184
- /** Record token spend from an armed turn against the session cost guard. Reads
185
- * `totalTokens` from a turn's `usage` (pi-ai shape) when present; a no-op when
186
- * the provider reported nothing (the post counter is the fallback guard). */
187
- export declare function recordArmedTurnUsage(state: ArmedRoomState, usage: Record<string, unknown> | undefined): void;
184
+ /** Record token spend from an armed turn against the session cost guard. The
185
+ * caller owns provider usage shapes; armed mode needs only the canonical
186
+ * total. A missing or invalid value is a no-op because the post counter is
187
+ * the fallback guard. */
188
+ export declare function recordArmedTurnUsage(state: ArmedRoomState, totalTokens: number | undefined): void;
188
189
  /** A human (non-agent) POST that is NOT our own resets the consecutive-chain
189
190
  * counter, freeing a chain-limit pause. Pure. */
190
191
  export declare function isHumanResetEntry(entry: RoomLedgerEntry, ownHandle: string, ownUserId: string): boolean;
@@ -299,25 +299,14 @@ export function recordAutonomousPost(state, now = Date.now()) {
299
299
  state.consecutiveAutoPosts += 1;
300
300
  state.postsThisSession += 1;
301
301
  }
302
- /** Record token spend from an armed turn against the session cost guard. Reads
303
- * `totalTokens` from a turn's `usage` (pi-ai shape) when present; a no-op when
304
- * the provider reported nothing (the post counter is the fallback guard). */
305
- export function recordArmedTurnUsage(state, usage) {
306
- const total = readTotalTokens(usage);
307
- if (total > 0)
308
- state.tokensThisSession += total;
309
- }
310
- function readTotalTokens(usage) {
311
- if (!usage)
312
- return 0;
313
- const direct = usage.totalTokens;
314
- if (typeof direct === "number" && Number.isFinite(direct) && direct > 0)
315
- return direct;
316
- // Fall back to summing input + output if a client reports those instead.
317
- const input = typeof usage.input === "number" ? usage.input : 0;
318
- const output = typeof usage.output === "number" ? usage.output : 0;
319
- const sum = input + output;
320
- return Number.isFinite(sum) && sum > 0 ? sum : 0;
302
+ /** Record token spend from an armed turn against the session cost guard. The
303
+ * caller owns provider usage shapes; armed mode needs only the canonical
304
+ * total. A missing or invalid value is a no-op because the post counter is
305
+ * the fallback guard. */
306
+ export function recordArmedTurnUsage(state, totalTokens) {
307
+ if (typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || totalTokens <= 0)
308
+ return;
309
+ state.tokensThisSession += totalTokens;
321
310
  }
322
311
  /** A human (non-agent) POST that is NOT our own resets the consecutive-chain
323
312
  * counter, freeing a chain-limit pause. Pure. */
@@ -0,0 +1,17 @@
1
+ import type { RoomRole } from "./rooms-protocol.js";
2
+ /** Channel-scope actions, byte-stable in store order. */
3
+ export declare const OM_ROOM_PERMISSION_ACTIONS: readonly ["post", "edit-own", "delete-any", "pin", "invite", "remove-member", "set-read-only", "manage-roles", "mute", "kick", "ban", "unban", "view-audit", "manage-room", "mass-mention"];
4
+ export type OmRoomPermissionAction = (typeof OM_ROOM_PERMISSION_ACTIONS)[number];
5
+ /** Space-scope actions, byte-stable in store order. */
6
+ export declare const OM_SPACE_PERMISSION_ACTIONS: readonly ["create-channel", "manage-server", "manage-server-roles", "create-invite", "manage-invites", "manage-members", "manage-nicknames", "change-nickname", "administrator"];
7
+ export type OmSpacePermissionAction = (typeof OM_SPACE_PERMISSION_ACTIONS)[number];
8
+ /** Any grantable action. The two vocabularies are disjoint; a role's
9
+ * grants[] may hold values from both. */
10
+ export type OmPermissionAction = OmRoomPermissionAction | OmSpacePermissionAction;
11
+ /** The space ladder (RoomSpaceMember.role). Channel tiers are RoomRole. */
12
+ export type OmSpaceLadderRole = "owner" | "admin" | "librarian" | "member";
13
+ /** Channel ladder base: what each channel tier natively holds. */
14
+ export declare const OM_ROOM_PERMISSION_MATRIX: Record<RoomRole, ReadonlySet<OmRoomPermissionAction>>;
15
+ /** Space ladder base: preserves the pre-grants behavior exactly (managers
16
+ * hold everything, everyone may change their own nickname). */
17
+ export declare const OM_SPACE_PERMISSION_MATRIX: Record<OmSpaceLadderRole, ReadonlySet<OmSpacePermissionAction>>;
@@ -0,0 +1,73 @@
1
+ // The permission vocabulary shared by the store, relay, and clients: the
2
+ // channel-scope actions (per-room moderation) and the space-scope actions
3
+ // (server management), plus the fixed ladder base matrices. Parity-pinned to
4
+ // the store's permission services; the store stays the authority (every verb
5
+ // re-checks server-side), this mirror only decides what clients offer.
6
+ // Effective permissions = ladder base UNION the grants of every held named
7
+ // role (one role's grants[] may mix both vocabularies). Pure and browser-safe.
8
+ /** Channel-scope actions, byte-stable in store order. */
9
+ export const OM_ROOM_PERMISSION_ACTIONS = [
10
+ "post",
11
+ "edit-own",
12
+ "delete-any",
13
+ "pin",
14
+ "invite",
15
+ "remove-member",
16
+ "set-read-only",
17
+ "manage-roles",
18
+ "mute",
19
+ "kick",
20
+ "ban",
21
+ "unban",
22
+ "view-audit",
23
+ "manage-room",
24
+ "mass-mention",
25
+ ];
26
+ /** Space-scope actions, byte-stable in store order. */
27
+ export const OM_SPACE_PERMISSION_ACTIONS = [
28
+ // create channels in this server
29
+ "create-channel",
30
+ // rename server, icon
31
+ "manage-server",
32
+ // create/edit/delete/assign named roles within reach
33
+ "manage-server-roles",
34
+ // mint a server invite
35
+ "create-invite",
36
+ // list/revoke invites, set/rotate/clear join code
37
+ "manage-invites",
38
+ // add and remove server members
39
+ "manage-members",
40
+ // change other members' nicknames
41
+ "manage-nicknames",
42
+ // change own nickname
43
+ "change-nickname",
44
+ // every action in both scopes, admin rank envelope; never the owner-only
45
+ // verbs (archive/purge space, ownership transfers)
46
+ "administrator",
47
+ ];
48
+ /** Channel ladder base: what each channel tier natively holds. */
49
+ export const OM_ROOM_PERMISSION_MATRIX = {
50
+ owner: new Set(OM_ROOM_PERMISSION_ACTIONS),
51
+ admin: new Set(OM_ROOM_PERMISSION_ACTIONS),
52
+ mod: new Set([
53
+ "post",
54
+ "edit-own",
55
+ "delete-any",
56
+ "pin",
57
+ "invite",
58
+ "remove-member",
59
+ "mute",
60
+ "kick",
61
+ "view-audit",
62
+ "mass-mention",
63
+ ]),
64
+ member: new Set(["post", "edit-own"]),
65
+ };
66
+ /** Space ladder base: preserves the pre-grants behavior exactly (managers
67
+ * hold everything, everyone may change their own nickname). */
68
+ export const OM_SPACE_PERMISSION_MATRIX = {
69
+ owner: new Set(OM_SPACE_PERMISSION_ACTIONS),
70
+ admin: new Set(OM_SPACE_PERMISSION_ACTIONS),
71
+ librarian: new Set(["change-nickname"]),
72
+ member: new Set(["change-nickname"]),
73
+ };
@@ -106,6 +106,7 @@ export declare const RoomServerMessageType: {
106
106
  readonly TOPIC_UPDATED: "TOPIC_UPDATED";
107
107
  readonly DOC_CHANGED: "DOC_CHANGED";
108
108
  readonly FRIEND_UPDATED: "FRIEND_UPDATED";
109
+ readonly SPACE_ROLES_UPDATED: "SPACE_ROLES_UPDATED";
109
110
  };
110
111
  export declare function addBounded<T>(set: Set<T>, value: T, max: number): void;
111
112
  export type RoomServerMessageType = (typeof RoomServerMessageType)[keyof typeof RoomServerMessageType];
@@ -175,6 +176,9 @@ export interface RoomMeta {
175
176
  viewerRole?: RoomRole | null | undefined;
176
177
  /** Head event sequence; diff against the read cursor for unread badges. */
177
178
  headSeq?: number | undefined;
179
+ /** Epoch milliseconds of the newest visible message or sealed message.
180
+ * Unlike room metadata timestamps, this changes only with chat content. */
181
+ lastMessageAt?: number | undefined;
178
182
  /** Topics dial for this channel. Absent reads as "allowed" since the
179
183
  * 2026-07-12 dial-default flip (post-flip relays always advertise the
180
184
  * stored value, "off" included); explicit "off" means topics are inert. */
@@ -842,6 +846,9 @@ export interface RoomSpace {
842
846
  /** Archived docs twin: keeps the Archive row reachable when only docs
843
847
  * are archived. */
844
848
  archivedDocCount?: number | undefined;
849
+ /** Space permission model revision; delegate-facing UI gates on >= 3.
850
+ * Absent on stores that predate space permissions. */
851
+ permissionsVersion?: number | undefined;
845
852
  }
846
853
  export interface RoomSpacesPayload {
847
854
  requestId?: string;
@@ -895,6 +902,12 @@ export interface SpaceUpdatedPayload {
895
902
  /** Rides WITH archived on hard deletion (see RoomUpdatedPayload). */
896
903
  deleted?: boolean | undefined;
897
904
  }
905
+ /** Space roles changed (role CRUD, member role assignment, @everyone
906
+ * grants): a poke, not a diff. Clients invalidate their space-roles cache
907
+ * and refetch lazily. */
908
+ export interface SpaceRolesUpdatedPayload {
909
+ spaceId: string;
910
+ }
898
911
  export interface RoomSpaceJoinedPayload {
899
912
  requestId?: string;
900
913
  space: RoomSpace;
@@ -1428,6 +1441,10 @@ export type RoomServerMessage = {
1428
1441
  type: typeof RoomServerMessageType.SPACE_UPDATED;
1429
1442
  payload: SpaceUpdatedPayload;
1430
1443
  timestamp?: number;
1444
+ } | {
1445
+ type: typeof RoomServerMessageType.SPACE_ROLES_UPDATED;
1446
+ payload: SpaceRolesUpdatedPayload;
1447
+ timestamp?: number;
1431
1448
  } | {
1432
1449
  type: typeof RoomServerMessageType.SPACE_INVITE;
1433
1450
  payload: SpaceInvitePayload;
@@ -105,6 +105,7 @@ export const RoomServerMessageType = {
105
105
  TOPIC_UPDATED: "TOPIC_UPDATED",
106
106
  DOC_CHANGED: "DOC_CHANGED",
107
107
  FRIEND_UPDATED: "FRIEND_UPDATED",
108
+ SPACE_ROLES_UPDATED: "SPACE_ROLES_UPDATED",
108
109
  };
109
110
  export function addBounded(set, value, max) {
110
111
  set.add(value);
@@ -1,4 +1,4 @@
1
- import type { RoomTopicAttention } from "./shared/rooms-protocol.js";
1
+ import type { RoomRole, RoomTopicAttention } from "./shared/rooms-protocol.js";
2
2
  import type { RoomBookmarkSaved, RoomBookmarksResult, RoomDmConversation, RoomDmHistory, RoomDmSendResult, RoomDmUnreadConversation, RoomDoc, RoomDocAttribution, RoomDocChanges, RoomDocConflictHead, RoomDocEditingLease, RoomDocProvenance, RoomDocRevision, RoomDocRunRevertResult, RoomDocWriteResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequests, RoomNotificationInboxResult, RoomPollVotersResult, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomRestEvent, RoomSocialConfig, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSuggestion, RoomSuggestionRequest, RoomUserBlock, RoomUserBlocksResult } from "./social-types.js";
3
3
  export type { RoomAttachment, RoomBookmark, RoomBookmarkSaved, RoomBookmarksResult, RoomDmConversation, RoomDmHistory, RoomDmMessage, RoomDmParticipant, RoomDmSendResult, RoomDmUnreadConversation, RoomDocAttribution, RoomDocEditingLease, RoomDocMergeRegion, RoomDocRevisionSource, RoomDocRunRevertResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequest, RoomFriendRequests, RoomNotificationInboxItem, RoomNotificationInboxResult, RoomNotificationKind, RoomNotificationMessageData, RoomNotifyQuietHours, RoomPollOptionVoters, RoomPollVotersResult, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomReplyInboxItem, RoomRestEvent, RoomSocialConfig, RoomSpaceNotificationOverride, RoomSpaceRestoreAction, RoomSpaceRestoreActionKind, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSpaceRestoreSummary, RoomSuggestion, RoomSuggestionIntent, RoomSuggestionRequest, RoomSuggestionSource, RoomsNotificationSettings, RoomUserBlock, RoomUserBlocksResult, } from "./social-types.js";
4
4
  /** Largest per-file size any tier accepts (plus tier). The server enforces
@@ -178,6 +178,9 @@ export interface RoomSpaceSummary {
178
178
  /** Archived docs in this server: the row must stay reachable when only
179
179
  * docs are archived, since they have no other retrieval surface. */
180
180
  archivedDocCount?: number | undefined;
181
+ /** Space permission model revision; delegate-facing UI gates on >= 3.
182
+ * Absent on stores that predate space permissions. */
183
+ permissionsVersion?: number | undefined;
181
184
  }
182
185
  export declare function listRoomSpacesViaChat(config: RoomSocialConfig): Promise<RoomSpaceSummary[]>;
183
186
  export interface RoomSpaceMember {
@@ -204,6 +207,13 @@ export interface RoomSpaceRole {
204
207
  color: string | null;
205
208
  hoist: boolean;
206
209
  position: number;
210
+ /** Permission actions this role grants, mixing the channel and space
211
+ * vocabularies (see shared/om-permissions). Absent on stores that
212
+ * predate roles-v2 grants. */
213
+ grants?: string[];
214
+ /** Discord's "Allow anyone to @mention this role". Absent on stores
215
+ * that predate it. */
216
+ mentionable?: boolean;
207
217
  }
208
218
  export declare function listSpaceRoles(config: RoomSocialConfig, spaceId: string): Promise<RoomSpaceRole[]>;
209
219
  export declare function createSpaceRole(config: RoomSocialConfig, spaceId: string, input: {
@@ -215,7 +225,12 @@ export declare function updateSpaceRole(config: RoomSocialConfig, spaceId: strin
215
225
  name?: string;
216
226
  color?: string | null;
217
227
  hoist?: boolean;
228
+ /** Discord's "Allow anyone to @mention this role". Only send it to
229
+ * stores that emit the field (strict schemas 400 on unknown keys). */
230
+ mentionable?: boolean;
218
231
  position?: number;
232
+ /** Full replace of the role's grants. */
233
+ grants?: string[];
219
234
  }): Promise<RoomSpaceRole>;
220
235
  export declare function deleteSpaceRole(config: RoomSocialConfig, spaceId: string, roleId: string): Promise<void>;
221
236
  /** A channel webhook: the Discord-compatible ingest capability. The
@@ -347,6 +362,35 @@ export declare function updateRoomChannel(config: RoomSocialConfig, room: string
347
362
  export declare function resolveRoomChannel(config: RoomSocialConfig, room: string): Promise<Record<string, unknown> & {
348
363
  topicsPolicy?: "off" | "allowed" | "required";
349
364
  }>;
365
+ /** Set or clear a space channel's role guest list. Null (or empty) clears:
366
+ * the channel opens back up to every space member. */
367
+ export declare function setRoomAllowedRoles(config: RoomSocialConfig, room: string, allowedRoleIds: string[] | null,
368
+ /** Per-member access rules (union with roles). ABSENT = leave the stored
369
+ * list untouched (and let a legacy conversion seed from current members);
370
+ * null or [] = clear; array = set. */
371
+ allowedUserIds?: string[] | null, reason?: string): Promise<Record<string, unknown>>;
372
+ export interface RoomAccessMeta {
373
+ /** Active role guest list; null = open to every space member. */
374
+ allowedRoleIds: string[] | null;
375
+ /** Per-member access rules (union with the roles); null = none. */
376
+ allowedUserIds: string[] | null;
377
+ /** Tiers allowed to post; the store default is all four. */
378
+ postingRoles: RoomRole[];
379
+ access: string | null;
380
+ spaceId: string | null;
381
+ kind: string | null;
382
+ /** The caller's resolved channel role (owner fallback = creator). */
383
+ viewerRole: RoomRole | null;
384
+ }
385
+ /** Authoritative access meta for the settings surface: the room's guest
386
+ * list, posting tiers and the caller's resolved role. Rides
387
+ * resolveRoomChannel (the relay's route ordering), which is also where the
388
+ * store places viewerRole (existence-hiding 404 preserved as a throw). */
389
+ export declare function resolveRoomAccess(config: RoomSocialConfig, room: string): Promise<RoomAccessMeta>;
390
+ /** Owner-only. The old owner becomes a channel admin; audited server-side. */
391
+ export declare function transferRoomOwnership(config: RoomSocialConfig, room: string, targetUserId: string, reason?: string): Promise<void>;
392
+ /** Lift a channel ban (no WS frame exists for unban; REST is the path). */
393
+ export declare function unbanRoomMember(config: RoomSocialConfig, room: string, targetUserId: string, reason?: string): Promise<void>;
350
394
  /** Archive a channel (owner/admin): drops from listings, refuses posts. */
351
395
  export declare function archiveRoomChannel(config: RoomSocialConfig, room: string): Promise<{
352
396
  roomId?: string;
@@ -358,6 +402,8 @@ export declare function updateRoomSpace(config: RoomSocialConfig, spaceId: strin
358
402
  access?: "open" | "invite";
359
403
  iconUrl?: string | null;
360
404
  }): Promise<RoomSpaceSummary>;
405
+ /** Owner-only. The old owner becomes a space admin; audited server-side. */
406
+ export declare function transferSpaceOwnership(config: RoomSocialConfig, spaceId: string, targetUserId: string, reason?: string): Promise<void>;
361
407
  /** Archive a server (owner only). */
362
408
  export declare function archiveRoomSpace(config: RoomSocialConfig, spaceId: string): Promise<{
363
409
  archived?: boolean;
@@ -552,6 +552,62 @@ export async function resolveRoomChannel(config, room) {
552
552
  const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/resolve`, { method: "GET" });
553
553
  return result.room;
554
554
  }
555
+ /** Set or clear a space channel's role guest list. Null (or empty) clears:
556
+ * the channel opens back up to every space member. */
557
+ export async function setRoomAllowedRoles(config, room, allowedRoleIds,
558
+ /** Per-member access rules (union with roles). ABSENT = leave the stored
559
+ * list untouched (and let a legacy conversion seed from current members);
560
+ * null or [] = clear; array = set. */
561
+ allowedUserIds, reason) {
562
+ const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/allowed-roles`, {
563
+ method: "PATCH",
564
+ body: {
565
+ allowedRoleIds,
566
+ ...(allowedUserIds !== undefined ? { allowedUserIds } : {}),
567
+ ...(reason ? { reason } : {}),
568
+ },
569
+ });
570
+ return result.room ?? {};
571
+ }
572
+ /** Authoritative access meta for the settings surface: the room's guest
573
+ * list, posting tiers and the caller's resolved role. Rides
574
+ * resolveRoomChannel (the relay's route ordering), which is also where the
575
+ * store places viewerRole (existence-hiding 404 preserved as a throw). */
576
+ export async function resolveRoomAccess(config, room) {
577
+ const meta = (await resolveRoomChannel(config, room)) ?? {};
578
+ const allowed = Array.isArray(meta.allowedRoleIds)
579
+ ? meta.allowedRoleIds.filter((value) => typeof value === "string")
580
+ : null;
581
+ const allowedUsers = Array.isArray(meta.allowedUserIds)
582
+ ? meta.allowedUserIds.filter((value) => typeof value === "string")
583
+ : null;
584
+ const posting = Array.isArray(meta.postingRoles)
585
+ ? meta.postingRoles.filter((value) => value === "owner" || value === "admin" || value === "mod" || value === "member")
586
+ : [];
587
+ const viewer = typeof meta.viewerRole === "string" ? meta.viewerRole : null;
588
+ return {
589
+ allowedRoleIds: allowed && allowed.length > 0 ? allowed : null,
590
+ allowedUserIds: allowedUsers && allowedUsers.length > 0 ? allowedUsers : null,
591
+ postingRoles: posting.length > 0 ? posting : ["owner", "admin", "mod", "member"],
592
+ access: typeof meta.access === "string" ? meta.access : null,
593
+ spaceId: typeof meta.spaceId === "string" ? meta.spaceId : null,
594
+ kind: typeof meta.kind === "string" ? meta.kind : null,
595
+ viewerRole: viewer === "owner" || viewer === "admin" || viewer === "mod" || viewer === "member"
596
+ ? viewer
597
+ : null,
598
+ };
599
+ }
600
+ /** Owner-only. The old owner becomes a channel admin; audited server-side. */
601
+ export async function transferRoomOwnership(config, room, targetUserId, reason) {
602
+ await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/ownership/transfer`, { method: "POST", body: { targetUserId, ...(reason ? { reason } : {}) } });
603
+ }
604
+ /** Lift a channel ban (no WS frame exists for unban; REST is the path). */
605
+ export async function unbanRoomMember(config, room, targetUserId, reason) {
606
+ await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/mod/unban`, {
607
+ method: "POST",
608
+ body: { targetUserId, ...(reason ? { reason } : {}) },
609
+ });
610
+ }
555
611
  /** Archive a channel (owner/admin): drops from listings, refuses posts. */
556
612
  export async function archiveRoomChannel(config, room) {
557
613
  return socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}`, {
@@ -563,6 +619,13 @@ export async function updateRoomSpace(config, spaceId, input) {
563
619
  const result = await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}`, { method: "PATCH", body: input });
564
620
  return result.space;
565
621
  }
622
+ /** Owner-only. The old owner becomes a space admin; audited server-side. */
623
+ export async function transferSpaceOwnership(config, spaceId, targetUserId, reason) {
624
+ await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/ownership/transfer`, {
625
+ method: "POST",
626
+ body: { targetUserId, ...(reason ? { reason } : {}) },
627
+ });
628
+ }
566
629
  /** Archive a server (owner only). */
567
630
  export async function archiveRoomSpace(config, spaceId) {
568
631
  return socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}`, { method: "DELETE" });
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.9.1";
2
- export declare const RUNNER_VERSION = "0.9.1";
1
+ export declare const VERSION = "0.11.0";
2
+ export declare const RUNNER_VERSION = "0.11.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.9.1";
2
- export const RUNNER_VERSION = "0.9.1";
1
+ export const VERSION = "0.11.0";
2
+ export const RUNNER_VERSION = "0.11.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmarket/rooms-client",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "OM Rooms protocol client: wire types, WebSocket + REST clients, and chat view-models. Browser-safe (no node:/bun: imports).",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -137,11 +137,6 @@
137
137
  "types": "./dist/agent/topic-view-batcher.d.ts",
138
138
  "default": "./dist/agent/topic-view-batcher.js"
139
139
  },
140
- "./agent/clients/common": {
141
- "bun": "./src/agent/clients/common.ts",
142
- "types": "./dist/agent/clients/common.d.ts",
143
- "default": "./dist/agent/clients/common.js"
144
- },
145
140
  "./shared/rooms-protocol": {
146
141
  "bun": "./src/shared/rooms-protocol.ts",
147
142
  "types": "./dist/shared/rooms-protocol.d.ts",
@@ -157,6 +152,11 @@
157
152
  "types": "./dist/shared/emoji-data.d.ts",
158
153
  "default": "./dist/shared/emoji-data.js"
159
154
  },
155
+ "./shared/om-permissions": {
156
+ "bun": "./src/shared/om-permissions.ts",
157
+ "types": "./dist/shared/om-permissions.d.ts",
158
+ "default": "./dist/shared/om-permissions.js"
159
+ },
160
160
  "./version": {
161
161
  "bun": "./src/version.ts",
162
162
  "types": "./dist/version.d.ts",
@@ -171,7 +171,6 @@
171
171
  "typecheck": "tsc --noEmit -p tsconfig.json"
172
172
  },
173
173
  "dependencies": {
174
- "@earendil-works/pi-ai": "0.78.1",
175
174
  "picocolors": "1.1.1"
176
175
  },
177
176
  "devDependencies": {
@@ -362,26 +362,13 @@ export function recordAutonomousPost(state: ArmedRoomState, now = Date.now()): v
362
362
  state.postsThisSession += 1;
363
363
  }
364
364
 
365
- /** Record token spend from an armed turn against the session cost guard. Reads
366
- * `totalTokens` from a turn's `usage` (pi-ai shape) when present; a no-op when
367
- * the provider reported nothing (the post counter is the fallback guard). */
368
- export function recordArmedTurnUsage(
369
- state: ArmedRoomState,
370
- usage: Record<string, unknown> | undefined,
371
- ): void {
372
- const total = readTotalTokens(usage);
373
- if (total > 0) state.tokensThisSession += total;
374
- }
375
-
376
- function readTotalTokens(usage: Record<string, unknown> | undefined): number {
377
- if (!usage) return 0;
378
- const direct = usage.totalTokens;
379
- if (typeof direct === "number" && Number.isFinite(direct) && direct > 0) return direct;
380
- // Fall back to summing input + output if a client reports those instead.
381
- const input = typeof usage.input === "number" ? usage.input : 0;
382
- const output = typeof usage.output === "number" ? usage.output : 0;
383
- const sum = input + output;
384
- return Number.isFinite(sum) && sum > 0 ? sum : 0;
365
+ /** Record token spend from an armed turn against the session cost guard. The
366
+ * caller owns provider usage shapes; armed mode needs only the canonical
367
+ * total. A missing or invalid value is a no-op because the post counter is
368
+ * the fallback guard. */
369
+ export function recordArmedTurnUsage(state: ArmedRoomState, totalTokens: number | undefined): void {
370
+ if (typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || totalTokens <= 0) return;
371
+ state.tokensThisSession += totalTokens;
385
372
  }
386
373
 
387
374
  /** A human (non-agent) POST that is NOT our own resets the consecutive-chain
@@ -0,0 +1,93 @@
1
+ // The permission vocabulary shared by the store, relay, and clients: the
2
+ // channel-scope actions (per-room moderation) and the space-scope actions
3
+ // (server management), plus the fixed ladder base matrices. Parity-pinned to
4
+ // the store's permission services; the store stays the authority (every verb
5
+ // re-checks server-side), this mirror only decides what clients offer.
6
+ // Effective permissions = ladder base UNION the grants of every held named
7
+ // role (one role's grants[] may mix both vocabularies). Pure and browser-safe.
8
+
9
+ import type { RoomRole } from "./rooms-protocol.js";
10
+
11
+ /** Channel-scope actions, byte-stable in store order. */
12
+ export const OM_ROOM_PERMISSION_ACTIONS = [
13
+ "post",
14
+ "edit-own",
15
+ "delete-any",
16
+ "pin",
17
+ "invite",
18
+ "remove-member",
19
+ "set-read-only",
20
+ "manage-roles",
21
+ "mute",
22
+ "kick",
23
+ "ban",
24
+ "unban",
25
+ "view-audit",
26
+ "manage-room",
27
+ "mass-mention",
28
+ ] as const;
29
+
30
+ export type OmRoomPermissionAction = (typeof OM_ROOM_PERMISSION_ACTIONS)[number];
31
+
32
+ /** Space-scope actions, byte-stable in store order. */
33
+ export const OM_SPACE_PERMISSION_ACTIONS = [
34
+ // create channels in this server
35
+ "create-channel",
36
+ // rename server, icon
37
+ "manage-server",
38
+ // create/edit/delete/assign named roles within reach
39
+ "manage-server-roles",
40
+ // mint a server invite
41
+ "create-invite",
42
+ // list/revoke invites, set/rotate/clear join code
43
+ "manage-invites",
44
+ // add and remove server members
45
+ "manage-members",
46
+ // change other members' nicknames
47
+ "manage-nicknames",
48
+ // change own nickname
49
+ "change-nickname",
50
+ // every action in both scopes, admin rank envelope; never the owner-only
51
+ // verbs (archive/purge space, ownership transfers)
52
+ "administrator",
53
+ ] as const;
54
+
55
+ export type OmSpacePermissionAction = (typeof OM_SPACE_PERMISSION_ACTIONS)[number];
56
+
57
+ /** Any grantable action. The two vocabularies are disjoint; a role's
58
+ * grants[] may hold values from both. */
59
+ export type OmPermissionAction = OmRoomPermissionAction | OmSpacePermissionAction;
60
+
61
+ /** The space ladder (RoomSpaceMember.role). Channel tiers are RoomRole. */
62
+ export type OmSpaceLadderRole = "owner" | "admin" | "librarian" | "member";
63
+
64
+ /** Channel ladder base: what each channel tier natively holds. */
65
+ export const OM_ROOM_PERMISSION_MATRIX: Record<RoomRole, ReadonlySet<OmRoomPermissionAction>> = {
66
+ owner: new Set<OmRoomPermissionAction>(OM_ROOM_PERMISSION_ACTIONS),
67
+ admin: new Set<OmRoomPermissionAction>(OM_ROOM_PERMISSION_ACTIONS),
68
+ mod: new Set<OmRoomPermissionAction>([
69
+ "post",
70
+ "edit-own",
71
+ "delete-any",
72
+ "pin",
73
+ "invite",
74
+ "remove-member",
75
+ "mute",
76
+ "kick",
77
+ "view-audit",
78
+ "mass-mention",
79
+ ]),
80
+ member: new Set<OmRoomPermissionAction>(["post", "edit-own"]),
81
+ };
82
+
83
+ /** Space ladder base: preserves the pre-grants behavior exactly (managers
84
+ * hold everything, everyone may change their own nickname). */
85
+ export const OM_SPACE_PERMISSION_MATRIX: Record<
86
+ OmSpaceLadderRole,
87
+ ReadonlySet<OmSpacePermissionAction>
88
+ > = {
89
+ owner: new Set<OmSpacePermissionAction>(OM_SPACE_PERMISSION_ACTIONS),
90
+ admin: new Set<OmSpacePermissionAction>(OM_SPACE_PERMISSION_ACTIONS),
91
+ librarian: new Set<OmSpacePermissionAction>(["change-nickname"]),
92
+ member: new Set<OmSpacePermissionAction>(["change-nickname"]),
93
+ };
@@ -109,6 +109,7 @@ export const RoomServerMessageType = {
109
109
  TOPIC_UPDATED: "TOPIC_UPDATED",
110
110
  DOC_CHANGED: "DOC_CHANGED",
111
111
  FRIEND_UPDATED: "FRIEND_UPDATED",
112
+ SPACE_ROLES_UPDATED: "SPACE_ROLES_UPDATED",
112
113
  } as const;
113
114
 
114
115
  export function addBounded<T>(set: Set<T>, value: T, max: number): void {
@@ -202,6 +203,9 @@ export interface RoomMeta {
202
203
  viewerRole?: RoomRole | null | undefined;
203
204
  /** Head event sequence; diff against the read cursor for unread badges. */
204
205
  headSeq?: number | undefined;
206
+ /** Epoch milliseconds of the newest visible message or sealed message.
207
+ * Unlike room metadata timestamps, this changes only with chat content. */
208
+ lastMessageAt?: number | undefined;
205
209
  /** Topics dial for this channel. Absent reads as "allowed" since the
206
210
  * 2026-07-12 dial-default flip (post-flip relays always advertise the
207
211
  * stored value, "off" included); explicit "off" means topics are inert. */
@@ -967,6 +971,9 @@ export interface RoomSpace {
967
971
  /** Archived docs twin: keeps the Archive row reachable when only docs
968
972
  * are archived. */
969
973
  archivedDocCount?: number | undefined;
974
+ /** Space permission model revision; delegate-facing UI gates on >= 3.
975
+ * Absent on stores that predate space permissions. */
976
+ permissionsVersion?: number | undefined;
970
977
  }
971
978
 
972
979
  export interface RoomSpacesPayload {
@@ -1025,6 +1032,13 @@ export interface SpaceUpdatedPayload {
1025
1032
  deleted?: boolean | undefined;
1026
1033
  }
1027
1034
 
1035
+ /** Space roles changed (role CRUD, member role assignment, @everyone
1036
+ * grants): a poke, not a diff. Clients invalidate their space-roles cache
1037
+ * and refetch lazily. */
1038
+ export interface SpaceRolesUpdatedPayload {
1039
+ spaceId: string;
1040
+ }
1041
+
1028
1042
  export interface RoomSpaceJoinedPayload {
1029
1043
  requestId?: string;
1030
1044
  space: RoomSpace;
@@ -1600,6 +1614,11 @@ export type RoomServerMessage =
1600
1614
  payload: SpaceUpdatedPayload;
1601
1615
  timestamp?: number;
1602
1616
  }
1617
+ | {
1618
+ type: typeof RoomServerMessageType.SPACE_ROLES_UPDATED;
1619
+ payload: SpaceRolesUpdatedPayload;
1620
+ timestamp?: number;
1621
+ }
1603
1622
  | {
1604
1623
  type: typeof RoomServerMessageType.SPACE_INVITE;
1605
1624
  payload: SpaceInvitePayload;
@@ -1,6 +1,6 @@
1
1
  import { resolveRoomsWsUrl } from "./client.js";
2
2
  import { envString, ROOM_CHAT_API_URL } from "./endpoints.js";
3
- import type { RoomTopicAttention } from "./shared/rooms-protocol.js";
3
+ import type { RoomRole, RoomTopicAttention } from "./shared/rooms-protocol.js";
4
4
  import type {
5
5
  RoomBookmarkSaved,
6
6
  RoomBookmarksResult,
@@ -678,6 +678,9 @@ export interface RoomSpaceSummary {
678
678
  /** Archived docs in this server: the row must stay reachable when only
679
679
  * docs are archived, since they have no other retrieval surface. */
680
680
  archivedDocCount?: number | undefined;
681
+ /** Space permission model revision; delegate-facing UI gates on >= 3.
682
+ * Absent on stores that predate space permissions. */
683
+ permissionsVersion?: number | undefined;
681
684
  }
682
685
 
683
686
  export async function listRoomSpacesViaChat(config: RoomSocialConfig): Promise<RoomSpaceSummary[]> {
@@ -720,6 +723,13 @@ export interface RoomSpaceRole {
720
723
  color: string | null;
721
724
  hoist: boolean;
722
725
  position: number;
726
+ /** Permission actions this role grants, mixing the channel and space
727
+ * vocabularies (see shared/om-permissions). Absent on stores that
728
+ * predate roles-v2 grants. */
729
+ grants?: string[];
730
+ /** Discord's "Allow anyone to @mention this role". Absent on stores
731
+ * that predate it. */
732
+ mentionable?: boolean;
723
733
  }
724
734
 
725
735
  export async function listSpaceRoles(
@@ -751,7 +761,17 @@ export async function updateSpaceRole(
751
761
  config: RoomSocialConfig,
752
762
  spaceId: string,
753
763
  roleId: string,
754
- patch: { name?: string; color?: string | null; hoist?: boolean; position?: number },
764
+ patch: {
765
+ name?: string;
766
+ color?: string | null;
767
+ hoist?: boolean;
768
+ /** Discord's "Allow anyone to @mention this role". Only send it to
769
+ * stores that emit the field (strict schemas 400 on unknown keys). */
770
+ mentionable?: boolean;
771
+ position?: number;
772
+ /** Full replace of the role's grants. */
773
+ grants?: string[];
774
+ },
755
775
  ): Promise<RoomSpaceRole> {
756
776
  const result = await socialRequest<{ role?: RoomSpaceRole }>(
757
777
  config,
@@ -1154,6 +1174,114 @@ export async function resolveRoomChannel(
1154
1174
  };
1155
1175
  }
1156
1176
 
1177
+ /** Set or clear a space channel's role guest list. Null (or empty) clears:
1178
+ * the channel opens back up to every space member. */
1179
+ export async function setRoomAllowedRoles(
1180
+ config: RoomSocialConfig,
1181
+ room: string,
1182
+ allowedRoleIds: string[] | null,
1183
+ /** Per-member access rules (union with roles). ABSENT = leave the stored
1184
+ * list untouched (and let a legacy conversion seed from current members);
1185
+ * null or [] = clear; array = set. */
1186
+ allowedUserIds?: string[] | null,
1187
+ reason?: string,
1188
+ ): Promise<Record<string, unknown>> {
1189
+ const result = await socialRequest<{ room?: Record<string, unknown> }>(
1190
+ config,
1191
+ `/rooms/${encodeURIComponent(cleanRoomId(room))}/allowed-roles`,
1192
+ {
1193
+ method: "PATCH",
1194
+ body: {
1195
+ allowedRoleIds,
1196
+ ...(allowedUserIds !== undefined ? { allowedUserIds } : {}),
1197
+ ...(reason ? { reason } : {}),
1198
+ },
1199
+ },
1200
+ );
1201
+ return result.room ?? {};
1202
+ }
1203
+
1204
+ export interface RoomAccessMeta {
1205
+ /** Active role guest list; null = open to every space member. */
1206
+ allowedRoleIds: string[] | null;
1207
+ /** Per-member access rules (union with the roles); null = none. */
1208
+ allowedUserIds: string[] | null;
1209
+ /** Tiers allowed to post; the store default is all four. */
1210
+ postingRoles: RoomRole[];
1211
+ access: string | null;
1212
+ spaceId: string | null;
1213
+ kind: string | null;
1214
+ /** The caller's resolved channel role (owner fallback = creator). */
1215
+ viewerRole: RoomRole | null;
1216
+ }
1217
+
1218
+ /** Authoritative access meta for the settings surface: the room's guest
1219
+ * list, posting tiers and the caller's resolved role. Rides
1220
+ * resolveRoomChannel (the relay's route ordering), which is also where the
1221
+ * store places viewerRole (existence-hiding 404 preserved as a throw). */
1222
+ export async function resolveRoomAccess(
1223
+ config: RoomSocialConfig,
1224
+ room: string,
1225
+ ): Promise<RoomAccessMeta> {
1226
+ const meta: Record<string, unknown> = (await resolveRoomChannel(config, room)) ?? {};
1227
+ const allowed = Array.isArray(meta.allowedRoleIds)
1228
+ ? (meta.allowedRoleIds as unknown[]).filter(
1229
+ (value): value is string => typeof value === "string",
1230
+ )
1231
+ : null;
1232
+ const allowedUsers = Array.isArray(meta.allowedUserIds)
1233
+ ? (meta.allowedUserIds as unknown[]).filter(
1234
+ (value): value is string => typeof value === "string",
1235
+ )
1236
+ : null;
1237
+ const posting = Array.isArray(meta.postingRoles)
1238
+ ? (meta.postingRoles as unknown[]).filter(
1239
+ (value): value is RoomRole =>
1240
+ value === "owner" || value === "admin" || value === "mod" || value === "member",
1241
+ )
1242
+ : [];
1243
+ const viewer = typeof meta.viewerRole === "string" ? meta.viewerRole : null;
1244
+ return {
1245
+ allowedRoleIds: allowed && allowed.length > 0 ? allowed : null,
1246
+ allowedUserIds: allowedUsers && allowedUsers.length > 0 ? allowedUsers : null,
1247
+ postingRoles: posting.length > 0 ? posting : ["owner", "admin", "mod", "member"],
1248
+ access: typeof meta.access === "string" ? meta.access : null,
1249
+ spaceId: typeof meta.spaceId === "string" ? meta.spaceId : null,
1250
+ kind: typeof meta.kind === "string" ? meta.kind : null,
1251
+ viewerRole:
1252
+ viewer === "owner" || viewer === "admin" || viewer === "mod" || viewer === "member"
1253
+ ? viewer
1254
+ : null,
1255
+ };
1256
+ }
1257
+
1258
+ /** Owner-only. The old owner becomes a channel admin; audited server-side. */
1259
+ export async function transferRoomOwnership(
1260
+ config: RoomSocialConfig,
1261
+ room: string,
1262
+ targetUserId: string,
1263
+ reason?: string,
1264
+ ): Promise<void> {
1265
+ await socialRequest(
1266
+ config,
1267
+ `/rooms/${encodeURIComponent(cleanRoomId(room))}/ownership/transfer`,
1268
+ { method: "POST", body: { targetUserId, ...(reason ? { reason } : {}) } },
1269
+ );
1270
+ }
1271
+
1272
+ /** Lift a channel ban (no WS frame exists for unban; REST is the path). */
1273
+ export async function unbanRoomMember(
1274
+ config: RoomSocialConfig,
1275
+ room: string,
1276
+ targetUserId: string,
1277
+ reason?: string,
1278
+ ): Promise<void> {
1279
+ await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/mod/unban`, {
1280
+ method: "POST",
1281
+ body: { targetUserId, ...(reason ? { reason } : {}) },
1282
+ });
1283
+ }
1284
+
1157
1285
  /** Archive a channel (owner/admin): drops from listings, refuses posts. */
1158
1286
  export async function archiveRoomChannel(
1159
1287
  config: RoomSocialConfig,
@@ -1178,6 +1306,19 @@ export async function updateRoomSpace(
1178
1306
  return result.space;
1179
1307
  }
1180
1308
 
1309
+ /** Owner-only. The old owner becomes a space admin; audited server-side. */
1310
+ export async function transferSpaceOwnership(
1311
+ config: RoomSocialConfig,
1312
+ spaceId: string,
1313
+ targetUserId: string,
1314
+ reason?: string,
1315
+ ): Promise<void> {
1316
+ await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/ownership/transfer`, {
1317
+ method: "POST",
1318
+ body: { targetUserId, ...(reason ? { reason } : {}) },
1319
+ });
1320
+ }
1321
+
1181
1322
  /** Archive a server (owner only). */
1182
1323
  export async function archiveRoomSpace(
1183
1324
  config: RoomSocialConfig,
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.9.1";
2
- export const RUNNER_VERSION = "0.9.1";
1
+ export const VERSION = "0.11.0";
2
+ export const RUNNER_VERSION = "0.11.0";
@@ -1,82 +0,0 @@
1
- import { type KnownProvider } from "@earendil-works/pi-ai";
2
- /** Every chat surface, as a runtime list. The `SurfaceId` union derives from
3
- * this literal so per-surface policy checklists (packages/cli/test/
4
- * harness-budgets.test.ts) can iterate the surfaces at runtime while a new
5
- * entry still breaks type-exhaustive tables at compile time. Adding a
6
- * surface here requires classifying it in EVERY policy set (see AGENTS.md,
7
- * "Harness invariants"). */
8
- export declare const SURFACE_IDS: readonly ["cli", "telegram", "discord", "slack", "webui"];
9
- export type SurfaceId = (typeof SURFACE_IDS)[number];
10
- export interface AgentToolCall {
11
- id: string;
12
- name: string;
13
- args: unknown;
14
- }
15
- export type AgentMessage = {
16
- role: "user";
17
- content: string;
18
- } | {
19
- role: "assistant";
20
- content: string;
21
- toolCalls?: AgentToolCall[];
22
- } | {
23
- role: "tool";
24
- content: string;
25
- toolCallId: string;
26
- name: string;
27
- };
28
- export interface ToolDispatchResult {
29
- isError?: true;
30
- content: string;
31
- /** Full-fidelity render payload for in-process SURFACES (e.g. the TUI's
32
- * backtest card). Present only on the streamed copy channels consume —
33
- * the agent runtime strips it before the result reaches the model's
34
- * messages or the conversation store, so it never costs context. */
35
- artifact?: unknown;
36
- }
37
- export interface LlmTool {
38
- name: string;
39
- description: string;
40
- inputSchema: Record<string, unknown>;
41
- handler: (args: unknown) => Promise<ToolDispatchResult>;
42
- }
43
- /** OM-level provider for any OpenAI-compatible /v1/chat/completions endpoint
44
- * (Ollama, vLLM, LM Studio, llama.cpp, custom proxies). Not a pi-ai
45
- * KnownProvider: the model object is constructed at turn time from the
46
- * credential's stored base URL instead of pi-ai's catalog. */
47
- export declare const OPENAI_COMPATIBLE_PROVIDER: "openai-compatible";
48
- export type LlmProvider = KnownProvider | typeof OPENAI_COMPATIBLE_PROVIDER;
49
- export declare const PRIMARY_LLM_PROVIDERS: readonly LlmProvider[];
50
- export declare function isLlmProvider(value: string): value is LlmProvider;
51
- export type TurnEvent = {
52
- kind: "assistant.text";
53
- delta: string;
54
- } | {
55
- kind: "tool.call";
56
- id: string;
57
- name: string;
58
- args: unknown;
59
- } | {
60
- kind: "tool.result";
61
- id: string;
62
- name: string;
63
- result: ToolDispatchResult;
64
- } | {
65
- kind: "turn.done";
66
- usage?: Record<string, unknown>;
67
- } | {
68
- kind: "turn.error";
69
- code?: string;
70
- message: string;
71
- };
72
- export interface RunLlmTurnInput {
73
- apiKey: string;
74
- system: string;
75
- messages: AgentMessage[];
76
- tools: LlmTool[];
77
- model: string;
78
- signal?: AbortSignal | undefined;
79
- }
80
- export type RunLlmTurn = (input: RunLlmTurnInput) => AsyncIterable<TurnEvent>;
81
- export declare function stringifyToolContent(value: unknown): string;
82
- export declare function parseToolArguments(raw: unknown): unknown;
@@ -1,56 +0,0 @@
1
- import { getProviders } from "@earendil-works/pi-ai";
2
- /** Every chat surface, as a runtime list. The `SurfaceId` union derives from
3
- * this literal so per-surface policy checklists (packages/cli/test/
4
- * harness-budgets.test.ts) can iterate the surfaces at runtime while a new
5
- * entry still breaks type-exhaustive tables at compile time. Adding a
6
- * surface here requires classifying it in EVERY policy set (see AGENTS.md,
7
- * "Harness invariants"). */
8
- export const SURFACE_IDS = ["cli", "telegram", "discord", "slack", "webui"];
9
- /** OM-level provider for any OpenAI-compatible /v1/chat/completions endpoint
10
- * (Ollama, vLLM, LM Studio, llama.cpp, custom proxies). Not a pi-ai
11
- * KnownProvider: the model object is constructed at turn time from the
12
- * credential's stored base URL instead of pi-ai's catalog. */
13
- export const OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
14
- // Provider-resolution precedence (authProviderCandidates): these are tried
15
- // before any store.order / generic provider. Invariant: an active profile for
16
- // an earlier provider here shadows a later-configured non-primary one (groq,
17
- // openai-compatible, …). The headless/env path short-circuits this, but an
18
- // interactive switch via `om init` to a non-primary provider only takes effect
19
- // once the shadowing primary profile is removed. Changing that is a product
20
- // decision (explicit active-provider selection), not a per-provider concern.
21
- export const PRIMARY_LLM_PROVIDERS = [
22
- "anthropic",
23
- "openai-codex",
24
- "openai",
25
- "google",
26
- "openrouter",
27
- ];
28
- let knownProviderSet = null;
29
- export function isLlmProvider(value) {
30
- if (value === OPENAI_COMPATIBLE_PROVIDER)
31
- return true;
32
- if (knownProviderSet === null) {
33
- knownProviderSet = new Set(getProviders());
34
- }
35
- return knownProviderSet.has(value);
36
- }
37
- export function stringifyToolContent(value) {
38
- if (value === undefined)
39
- return "";
40
- if (typeof value === "string")
41
- return value;
42
- // Compact on purpose: this string is LLM input, and 2-space pretty-printing
43
- // inflated every structured tool result by roughly a fifth in tokens for
44
- // zero model benefit.
45
- return JSON.stringify(value);
46
- }
47
- export function parseToolArguments(raw) {
48
- if (raw === undefined || raw === null)
49
- return {};
50
- if (typeof raw !== "string")
51
- return raw;
52
- const trimmed = raw.trim();
53
- if (!trimmed)
54
- return {};
55
- return JSON.parse(trimmed);
56
- }
@@ -1,107 +0,0 @@
1
- import { getProviders, type KnownProvider } from "@earendil-works/pi-ai";
2
-
3
- /** Every chat surface, as a runtime list. The `SurfaceId` union derives from
4
- * this literal so per-surface policy checklists (packages/cli/test/
5
- * harness-budgets.test.ts) can iterate the surfaces at runtime while a new
6
- * entry still breaks type-exhaustive tables at compile time. Adding a
7
- * surface here requires classifying it in EVERY policy set (see AGENTS.md,
8
- * "Harness invariants"). */
9
- export const SURFACE_IDS = ["cli", "telegram", "discord", "slack", "webui"] as const;
10
-
11
- export type SurfaceId = (typeof SURFACE_IDS)[number];
12
-
13
- export interface AgentToolCall {
14
- id: string;
15
- name: string;
16
- args: unknown;
17
- }
18
-
19
- export type AgentMessage =
20
- | { role: "user"; content: string }
21
- | { role: "assistant"; content: string; toolCalls?: AgentToolCall[] }
22
- | { role: "tool"; content: string; toolCallId: string; name: string };
23
-
24
- export interface ToolDispatchResult {
25
- isError?: true;
26
- content: string;
27
- /** Full-fidelity render payload for in-process SURFACES (e.g. the TUI's
28
- * backtest card). Present only on the streamed copy channels consume —
29
- * the agent runtime strips it before the result reaches the model's
30
- * messages or the conversation store, so it never costs context. */
31
- artifact?: unknown;
32
- }
33
-
34
- export interface LlmTool {
35
- name: string;
36
- description: string;
37
- inputSchema: Record<string, unknown>;
38
- handler: (args: unknown) => Promise<ToolDispatchResult>;
39
- }
40
-
41
- /** OM-level provider for any OpenAI-compatible /v1/chat/completions endpoint
42
- * (Ollama, vLLM, LM Studio, llama.cpp, custom proxies). Not a pi-ai
43
- * KnownProvider: the model object is constructed at turn time from the
44
- * credential's stored base URL instead of pi-ai's catalog. */
45
- export const OPENAI_COMPATIBLE_PROVIDER = "openai-compatible" as const;
46
-
47
- export type LlmProvider = KnownProvider | typeof OPENAI_COMPATIBLE_PROVIDER;
48
-
49
- // Provider-resolution precedence (authProviderCandidates): these are tried
50
- // before any store.order / generic provider. Invariant: an active profile for
51
- // an earlier provider here shadows a later-configured non-primary one (groq,
52
- // openai-compatible, …). The headless/env path short-circuits this, but an
53
- // interactive switch via `om init` to a non-primary provider only takes effect
54
- // once the shadowing primary profile is removed. Changing that is a product
55
- // decision (explicit active-provider selection), not a per-provider concern.
56
- export const PRIMARY_LLM_PROVIDERS: readonly LlmProvider[] = [
57
- "anthropic",
58
- "openai-codex",
59
- "openai",
60
- "google",
61
- "openrouter",
62
- ];
63
-
64
- let knownProviderSet: Set<string> | null = null;
65
-
66
- export function isLlmProvider(value: string): value is LlmProvider {
67
- if (value === OPENAI_COMPATIBLE_PROVIDER) return true;
68
- if (knownProviderSet === null) {
69
- knownProviderSet = new Set(getProviders());
70
- }
71
- return knownProviderSet.has(value);
72
- }
73
-
74
- export type TurnEvent =
75
- | { kind: "assistant.text"; delta: string }
76
- | { kind: "tool.call"; id: string; name: string; args: unknown }
77
- | { kind: "tool.result"; id: string; name: string; result: ToolDispatchResult }
78
- | { kind: "turn.done"; usage?: Record<string, unknown> }
79
- | { kind: "turn.error"; code?: string; message: string };
80
-
81
- export interface RunLlmTurnInput {
82
- apiKey: string;
83
- system: string;
84
- messages: AgentMessage[];
85
- tools: LlmTool[];
86
- model: string;
87
- signal?: AbortSignal | undefined;
88
- }
89
-
90
- export type RunLlmTurn = (input: RunLlmTurnInput) => AsyncIterable<TurnEvent>;
91
-
92
- export function stringifyToolContent(value: unknown): string {
93
- if (value === undefined) return "";
94
- if (typeof value === "string") return value;
95
- // Compact on purpose: this string is LLM input, and 2-space pretty-printing
96
- // inflated every structured tool result by roughly a fifth in tokens for
97
- // zero model benefit.
98
- return JSON.stringify(value);
99
- }
100
-
101
- export function parseToolArguments(raw: unknown): unknown {
102
- if (raw === undefined || raw === null) return {};
103
- if (typeof raw !== "string") return raw;
104
- const trimmed = raw.trim();
105
- if (!trimmed) return {};
106
- return JSON.parse(trimmed) as unknown;
107
- }