@mattstack/rt-client 0.6.1 → 0.6.2

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
@@ -74,6 +74,45 @@ doesn't error, it resolves empty. `parseIdentity` is strict — only strings
74
74
  `serializeIdentity` emitted parse — so validate payloads with it, and never
75
75
  hand-assemble or string-split a wire.
76
76
 
77
+ ## Chat
78
+
79
+ The `rt chat` surface the chat viewer (`~/Documents/GitHub/chat`) is built
80
+ on. Every call degrades to `{ ok: false, error }` instead of throwing, and a
81
+ daemon that is down and a daemon that refused look the same to the caller;
82
+ branch on `ok`, not on exceptions.
83
+
84
+ ```ts
85
+ import { chatRooms, chatMessages, chatPost, createRelay, daemonHealth } from '@mattstack/rt-client';
86
+
87
+ const rooms = await chatRooms({ handle: 'matt' }); // { ok, data: { rooms } }
88
+ const page = await chatMessages({ room: 'build', limit: 50 }); // newest page; `before: <id>` pages older
89
+ await chatPost({ handle: 'matt', room: 'build', body: 'on it' });
90
+
91
+ const health = await daemonHealth(); // { reachable, error? }, never throws
92
+ const stop = createRelay({ // one daemon subscription, republished
93
+ match: t => t.startsWith('chat/'), // `chat/<room>/msg`, `chat/wake/<handle>`
94
+ topic: 'chat',
95
+ publish: (topic, data) => server.publish(topic, data),
96
+ });
97
+ ```
98
+
99
+ | Function | Daemon verb |
100
+ | --- | --- |
101
+ | `chatSignIn` / `chatSignOut` / `chatAway` / `chatBack` / `chatPulse` | presence: the buddy-list row and its heartbeats |
102
+ | `chatBuddies` / `chatWho` / `chatRooms` | the roster, one room's members, a handle's rooms |
103
+ | `chatJoin` / `chatLeave` | membership (`wakeOn: mention \| all \| none`) |
104
+ | `chatPost` / `chatDm` / `chatRead` / `chatMessages` / `chatMark` | messages: post, DM, read-and-advance, page, advance the cursor |
105
+ | `chatArm` / `chatTouch` / `chatDisarm` / `chatUnreadWaking` | the tail's wake protocol; the CLI's `rt chat tail` uses these |
106
+ | `paneList` / `panePeek` / `paneSpawn` / `paneAccounts` / `paneDirectories` | herdr panes: list with presence joined, peek a screen, start claude in a tab, cswap accounts, directory suggestions |
107
+ | `chatInvite` | type `/chat:join <room>` into a pane; `accepted`, `queued` or `refused` |
108
+ | `createRelay` / `subscribe` | the event stream; `daemonHealth` the reachability probe |
109
+
110
+ Pass `{ sockPath }` as the trailing options to reach a non-default daemon
111
+ socket. The verbs, their payloads and the wake protocol are specified in
112
+ repo-tools `docs/superpowers/specs/2026-08-23-rt-chat-design.md` and
113
+ `2026-08-24-rt-chat-presence-design.md`; the agent-facing rules are
114
+ `skills/rt-chat/SKILL.md`.
115
+
77
116
  ## License
78
117
 
79
118
  MIT
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RtResponse, RtClientOptions } from "./transport.ts";
2
- import type { DemandDecl, ProjectMRsData, DiscussionsData, MrByBranchData, BranchEnrichment, ForgeSlug, ForgeTokenData, RunSummary, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow } from "./commands.ts";
2
+ import type { DemandDecl, ProjectMRsData, DiscussionsData, MrByBranchData, BranchEnrichment, ForgeSlug, ForgeTokenData, RunSummary, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow, AgentRecord, Commands } from "./commands.ts";
3
3
  /**
4
4
  * One repo's project open-MR store. A cold repo forces a full paginated sync
5
5
  * on the daemon side when maxAgeMs demands it, which can run tens of seconds
@@ -102,6 +102,7 @@ export declare function chatArm(a: {
102
102
  }, o?: RtClientOptions): Promise<RtResponse<Record<string, never>>>;
103
103
  export declare function chatTouch(a: {
104
104
  handle: string;
105
+ room?: string;
105
106
  sessionId?: string;
106
107
  }, o?: RtClientOptions): Promise<RtResponse<Record<string, never>>>;
107
108
  export declare function chatDisarm(a: {
@@ -173,3 +174,20 @@ export declare function chatDm(a: {
173
174
  export declare function eventsHead(o?: RtClientOptions): Promise<RtResponse<{
174
175
  cursor: number;
175
176
  }>>;
177
+ export declare function agentStart(a: Commands["agent:start"]["payload"], o?: RtClientOptions): Promise<RtResponse<AgentRecord>>;
178
+ export declare function agentResume(a: Commands["agent:resume"]["payload"], o?: RtClientOptions): Promise<RtResponse<AgentRecord>>;
179
+ export declare function agentGet(a: {
180
+ id: string;
181
+ }, o?: RtClientOptions): Promise<RtResponse<AgentRecord>>;
182
+ export declare function agentList(a: {
183
+ repo?: string;
184
+ }, o?: RtClientOptions): Promise<RtResponse<{
185
+ agents: AgentRecord[];
186
+ }>>;
187
+ export declare function paneList(o?: RtClientOptions): Promise<RtResponse<Commands["pane:list"]["data"]>>;
188
+ export declare function panePeek(a: Commands["pane:peek"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:peek"]["data"]>>;
189
+ /** The spawn waits on claude starting, so its budget is minutes, not seconds. */
190
+ export declare function paneSpawn(a: Commands["pane:spawn"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:spawn"]["data"]>>;
191
+ export declare function paneAccounts(o?: RtClientOptions): Promise<RtResponse<Commands["pane:accounts"]["data"]>>;
192
+ export declare function paneDirectories(a: Commands["pane:directories"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:directories"]["data"]>>;
193
+ export declare function chatInvite(a: Commands["chat:invite"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["chat:invite"]["data"]>>;
@@ -151,6 +151,39 @@ export interface PresenceRow {
151
151
  armedAt?: number;
152
152
  signedOutAt?: number;
153
153
  }
154
+ export type AgentStatus = "idle" | "working" | "blocked" | "done" | "unknown";
155
+ /** Duplicated shape on purpose (see EventsBusEvent above): mirrors the daemon's pane row, which rt-client cannot import. */
156
+ export interface ChatPane {
157
+ paneId: string;
158
+ workspace: string;
159
+ title?: string;
160
+ cwd?: string;
161
+ repo?: string;
162
+ branch?: string;
163
+ agentStatus: AgentStatus;
164
+ sessionId?: string;
165
+ presence?: {
166
+ handle: string;
167
+ status: BuddyStatus;
168
+ rooms: string[];
169
+ };
170
+ }
171
+ export interface PaneAccount {
172
+ slot: number;
173
+ email: string;
174
+ alias?: string;
175
+ headroom?: string;
176
+ }
177
+ export interface PaneDirectory {
178
+ path: string;
179
+ repo: string;
180
+ branch?: string;
181
+ }
182
+ export interface InviteResult {
183
+ paneId: string;
184
+ delivered: "accepted" | "queued" | "refused";
185
+ reason?: string;
186
+ }
154
187
  export type Attention = {
155
188
  needs: boolean;
156
189
  reason: "failed" | "stale" | "stranded" | "blocked" | null;
@@ -223,6 +256,29 @@ export interface RunDetail {
223
256
  decisions: RunDecisionRow[];
224
257
  schemaAhead: boolean;
225
258
  }
259
+ export type AgentSurface = "herdr" | "headless";
260
+ export interface AgentRecord {
261
+ id: string;
262
+ repo: string;
263
+ cwd: string;
264
+ provider: string;
265
+ surface: AgentSurface;
266
+ sessionId: string;
267
+ model?: string;
268
+ effort?: string;
269
+ account?: string;
270
+ label?: string;
271
+ caller?: string;
272
+ paneId?: string;
273
+ tabId?: string;
274
+ workspaceId?: string;
275
+ extraArgs?: string;
276
+ exitCode?: number;
277
+ resultPath?: string;
278
+ createdAt: number;
279
+ lastResumedAt?: number;
280
+ finishedAt?: number;
281
+ }
226
282
  export interface Commands {
227
283
  "project-mrs:read": {
228
284
  payload: {
@@ -456,6 +512,7 @@ export interface Commands {
456
512
  "chat:touch": {
457
513
  payload: {
458
514
  handle: string;
515
+ room?: string;
459
516
  sessionId?: string;
460
517
  };
461
518
  data: Record<string, never>;
@@ -554,6 +611,99 @@ export interface Commands {
554
611
  recipients: string[];
555
612
  };
556
613
  };
614
+ "agent:start": {
615
+ payload: {
616
+ repo: string;
617
+ cwd: string;
618
+ prompt?: string;
619
+ surface?: AgentSurface;
620
+ model?: string;
621
+ effort?: string;
622
+ account?: string;
623
+ label?: string;
624
+ caller?: string;
625
+ workspace?: string;
626
+ tab?: string;
627
+ extraArgs?: string;
628
+ };
629
+ data: AgentRecord;
630
+ };
631
+ "agent:resume": {
632
+ payload: {
633
+ id: string;
634
+ prompt?: string;
635
+ surface?: AgentSurface;
636
+ };
637
+ data: AgentRecord;
638
+ };
639
+ "agent:get": {
640
+ payload: {
641
+ id: string;
642
+ };
643
+ data: AgentRecord;
644
+ };
645
+ "agent:list": {
646
+ payload: {
647
+ repo?: string;
648
+ };
649
+ data: {
650
+ agents: AgentRecord[];
651
+ };
652
+ };
653
+ "chat:invite": {
654
+ payload: {
655
+ paneId: string;
656
+ room: string;
657
+ note?: string;
658
+ from: string;
659
+ callerPane?: string;
660
+ };
661
+ data: InviteResult;
662
+ };
663
+ "pane:list": {
664
+ payload: Record<string, never>;
665
+ data: {
666
+ panes: ChatPane[];
667
+ };
668
+ };
669
+ "pane:peek": {
670
+ payload: {
671
+ paneId: string;
672
+ lines?: number;
673
+ };
674
+ data: {
675
+ paneId: string;
676
+ lines: string[];
677
+ };
678
+ };
679
+ "pane:accounts": {
680
+ payload: Record<string, never>;
681
+ data: {
682
+ accounts: PaneAccount[];
683
+ };
684
+ };
685
+ "pane:directories": {
686
+ payload: {
687
+ q?: string;
688
+ };
689
+ data: {
690
+ directories: PaneDirectory[];
691
+ };
692
+ };
693
+ "pane:spawn": {
694
+ payload: {
695
+ cwd: string;
696
+ account?: string;
697
+ model?: string;
698
+ effort?: string;
699
+ prompt?: string;
700
+ workspace?: string;
701
+ };
702
+ data: {
703
+ pane: ChatPane;
704
+ ready: boolean;
705
+ };
706
+ };
557
707
  }
558
708
  export type CommandName = keyof Commands;
559
709
  export declare const COMMAND_NAMES: readonly CommandName[];
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { rtCommand, DEFAULT_SOCK } from "./transport.ts";
2
2
  export type { RtResponse, RtClientOptions } from "./transport.ts";
3
- export { readProjectMRs, readDiscussions, readMrsByBranch, readBranchCache, resolveForgeToken, listRuns, getRun, abandonRun, chatJoin, chatLeave, chatPost, chatRead, chatRooms, chatWho, chatMark, chatMessages, chatArm, chatTouch, chatDisarm, chatUnreadWaking, chatSignIn, chatSignOut, chatAway, chatBack, chatBuddies, chatPulse, chatDm, eventsHead, } from "./client.ts";
3
+ export { readProjectMRs, readDiscussions, readMrsByBranch, readBranchCache, resolveForgeToken, listRuns, getRun, abandonRun, chatJoin, chatLeave, chatPost, chatRead, chatRooms, chatWho, chatMark, chatMessages, chatArm, chatTouch, chatDisarm, chatUnreadWaking, chatSignIn, chatSignOut, chatAway, chatBack, chatBuddies, chatPulse, chatDm, eventsHead, agentStart, agentResume, agentGet, agentList, paneList, panePeek, paneSpawn, paneAccounts, paneDirectories, chatInvite, } from "./client.ts";
4
4
  export { COMMAND_NAMES } from "./commands.ts";
5
- export type { Discussion, DemandDecl, ProjectMRsScope, ProjectMRsData, DiscussionsData, MrByBranchEntry, MrByBranchData, BranchEnrichment, Commands, CommandName, ForgeSlug, ForgeTokenData, Attention, RunSummary, RunStageRow, RunFieldRow, RunDecisionRow, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow, } from "./commands.ts";
5
+ export type { Discussion, DemandDecl, ProjectMRsScope, ProjectMRsData, DiscussionsData, MrByBranchEntry, MrByBranchData, BranchEnrichment, Commands, CommandName, ForgeSlug, ForgeTokenData, Attention, RunSummary, RunStageRow, RunFieldRow, RunDecisionRow, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow, AgentRecord, AgentSurface, AgentStatus, ChatPane, PaneAccount, PaneDirectory, InviteResult, } from "./commands.ts";
6
6
  export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
7
7
  export type { RelayEventType } from "./relay.ts";
8
8
  export { daemonHealth } from "./health.ts";
package/dist/index.js CHANGED
@@ -124,6 +124,8 @@ function chatArm(a, o = {}) {
124
124
  }
125
125
  function chatTouch(a, o = {}) {
126
126
  const payload = { handle: a.handle };
127
+ if (a.room !== undefined)
128
+ payload.room = a.room;
127
129
  if (a.sessionId !== undefined)
128
130
  payload.sessionId = a.sessionId;
129
131
  return rtCommand("chat:touch", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
@@ -187,6 +189,64 @@ function chatDm(a, o = {}) {
187
189
  function eventsHead(o = {}) {
188
190
  return rtCommand("events:head", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
189
191
  }
192
+ function agentStart(a, o = {}) {
193
+ const payload = { repo: a.repo, cwd: a.cwd };
194
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs"]) {
195
+ if (a[k] !== undefined)
196
+ payload[k] = a[k];
197
+ }
198
+ return rtCommand("agent:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
199
+ }
200
+ function agentResume(a, o = {}) {
201
+ const payload = { id: a.id };
202
+ if (a.prompt !== undefined)
203
+ payload.prompt = a.prompt;
204
+ if (a.surface !== undefined)
205
+ payload.surface = a.surface;
206
+ return rtCommand("agent:resume", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
207
+ }
208
+ function agentGet(a, o = {}) {
209
+ return rtCommand("agent:get", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
210
+ }
211
+ function agentList(a, o = {}) {
212
+ const payload = {};
213
+ if (a.repo !== undefined)
214
+ payload.repo = a.repo;
215
+ return rtCommand("agent:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
216
+ }
217
+ function paneList(o = {}) {
218
+ return rtCommand("pane:list", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
219
+ }
220
+ function panePeek(a, o = {}) {
221
+ const payload = { paneId: a.paneId };
222
+ if (a.lines !== undefined)
223
+ payload.lines = a.lines;
224
+ return rtCommand("pane:peek", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
225
+ }
226
+ function paneSpawn(a, o = {}) {
227
+ const payload = { cwd: a.cwd };
228
+ for (const k of ["account", "model", "effort", "prompt", "workspace"])
229
+ if (a[k] !== undefined)
230
+ payload[k] = a[k];
231
+ return rtCommand("pane:spawn", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 90000 });
232
+ }
233
+ function paneAccounts(o = {}) {
234
+ return rtCommand("pane:accounts", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
235
+ }
236
+ function paneDirectories(a, o = {}) {
237
+ const payload = {};
238
+ if (a.q !== undefined)
239
+ payload.q = a.q;
240
+ return rtCommand("pane:directories", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
241
+ }
242
+ function chatInvite(a, o = {}) {
243
+ const payload = { paneId: a.paneId, room: a.room, from: a.from };
244
+ if (a.note !== undefined)
245
+ payload.note = a.note;
246
+ if (a.callerPane !== undefined)
247
+ payload.callerPane = a.callerPane;
248
+ return rtCommand("chat:invite", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
249
+ }
190
250
  // src/commands.ts
191
251
  var COMMAND_NAMES = [
192
252
  "project-mrs:read",
@@ -219,7 +279,17 @@ var COMMAND_NAMES = [
219
279
  "chat:back",
220
280
  "chat:buddies",
221
281
  "chat:pulse",
222
- "chat:dm"
282
+ "chat:dm",
283
+ "agent:start",
284
+ "agent:resume",
285
+ "agent:get",
286
+ "agent:list",
287
+ "chat:invite",
288
+ "pane:list",
289
+ "pane:peek",
290
+ "pane:accounts",
291
+ "pane:directories",
292
+ "pane:spawn"
223
293
  ];
224
294
  // src/relay.ts
225
295
  var DEFAULT_WS_URL = "ws://127.0.0.1:9401/ws";
@@ -659,7 +729,7 @@ var REGISTRY = [
659
729
  type: "array",
660
730
  scopes: ["team"],
661
731
  merge: "replace",
662
- description: "The board's full member roster, including hidden-by-default entries."
732
+ description: "The authors tab's roster: whose MRs the classic board lists, including hidden-by-default entries. Codeowners tabs list MRs from anyone."
663
733
  },
664
734
  {
665
735
  key: "board.title",
@@ -680,14 +750,14 @@ var REGISTRY = [
680
750
  type: "array",
681
751
  scopes: ["team"],
682
752
  merge: "replace",
683
- description: "Ticket key prefixes (e.g. RT, MAT) the board links out to Linear from an MR title."
753
+ description: "Ticket key prefixes (e.g. RT, MAT) that keep the authors tab to the team's own tickets and link out to Linear from an MR title; rows on a codeowners tab pass regardless of prefix."
684
754
  },
685
755
  {
686
756
  key: "board.slack",
687
757
  type: "object",
688
758
  scopes: ["team"],
689
759
  merge: "deep",
690
- description: "The board's Slack posting config (app id, client id, channel, callback port); client secrets stay out of this store."
760
+ description: "The board's Slack posting config (app id, client id, channel, callback port); channel is the default that a tab's slackChannel overrides per MR. Client secrets stay out of this store."
691
761
  },
692
762
  {
693
763
  key: "board.doctorSkill",
@@ -708,7 +778,7 @@ var REGISTRY = [
708
778
  type: "array",
709
779
  scopes: ["team"],
710
780
  merge: "replace",
711
- description: "Board tab definitions ({id, label, source, slackChannel?, reviewSkill?}); source.kind 'authors' is the classic roster board, 'codeowners' lists MRs blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here)."
781
+ description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists MRs from any author blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here)."
712
782
  },
713
783
  {
714
784
  key: "board.staleAfterDays",
@@ -736,7 +806,7 @@ var REGISTRY = [
736
806
  type: "array",
737
807
  scopes: ["user"],
738
808
  merge: "replace",
739
- description: "Usernames this developer hides from the team roster's board.members list; overlays the team truth without editing it."
809
+ description: "Usernames this developer hides from the authors tab's board.members roster; overlays the team truth without editing it."
740
810
  },
741
811
  {
742
812
  key: "board.triage",
@@ -816,6 +886,21 @@ var REGISTRY = [
816
886
  merge: "replace",
817
887
  description: "The human's own chat handle, so agents can @-mention them by name."
818
888
  },
889
+ {
890
+ key: "chat.viewerUrl",
891
+ type: "string",
892
+ scopes: ["user"],
893
+ merge: "replace",
894
+ description: "Base URL of the chat viewer, no trailing slash. When set, rt chat post and the tail's wake lines end with a link to the room or message."
895
+ },
896
+ {
897
+ key: "chat.herdrWorkspace",
898
+ type: "string",
899
+ scopes: ["user"],
900
+ default: "chat",
901
+ merge: "replace",
902
+ description: "herdr workspace label that rt pane spawn opens new agent tabs in; created when missing."
903
+ },
819
904
  {
820
905
  key: "chat.push.provider",
821
906
  type: "string",
@@ -829,6 +914,34 @@ var REGISTRY = [
829
914
  scopes: ["user"],
830
915
  merge: "replace",
831
916
  description: "Destination (topic/URL/token) the configured chat.push.provider sends to."
917
+ },
918
+ {
919
+ key: "agent.model",
920
+ type: "string",
921
+ scopes: ["user", "machine"],
922
+ merge: "replace",
923
+ description: "Default --model for rt agent launches; unset omits the flag."
924
+ },
925
+ {
926
+ key: "agent.effort",
927
+ type: "string",
928
+ scopes: ["user", "machine"],
929
+ merge: "replace",
930
+ description: "Default --effort for rt agent launches; unset omits the flag."
931
+ },
932
+ {
933
+ key: "agent.account",
934
+ type: "string",
935
+ scopes: ["user", "machine"],
936
+ merge: "replace",
937
+ description: "cswap account email rt agent launches under; unset uses the default claude profile."
938
+ },
939
+ {
940
+ key: "agent.extraArgs",
941
+ type: "string",
942
+ scopes: ["user", "machine"],
943
+ merge: "replace",
944
+ description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch)."
832
945
  }
833
946
  ];
834
947
 
@@ -1624,6 +1737,11 @@ export {
1624
1737
  readDiscussions,
1625
1738
  readBranchCache,
1626
1739
  parseIdentity,
1740
+ paneSpawn,
1741
+ panePeek,
1742
+ paneList,
1743
+ paneDirectories,
1744
+ paneAccounts,
1627
1745
  normalizeRemote,
1628
1746
  listTeams,
1629
1747
  listSettings,
@@ -1653,6 +1771,7 @@ export {
1653
1771
  chatMark,
1654
1772
  chatLeave,
1655
1773
  chatJoin,
1774
+ chatInvite,
1656
1775
  chatDm,
1657
1776
  chatDisarm,
1658
1777
  chatBuddies,
@@ -1660,6 +1779,10 @@ export {
1660
1779
  chatAway,
1661
1780
  chatArm,
1662
1781
  allDefs,
1782
+ agentStart,
1783
+ agentResume,
1784
+ agentList,
1785
+ agentGet,
1663
1786
  abandonRun,
1664
1787
  SCOPE_ORDER,
1665
1788
  REGISTRY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/client.ts CHANGED
@@ -21,6 +21,8 @@ import type {
21
21
  RoomSummary,
22
22
  BuddyStatus,
23
23
  PresenceRow,
24
+ AgentRecord,
25
+ Commands,
24
26
  } from "./commands.ts";
25
27
 
26
28
  /**
@@ -227,10 +229,11 @@ export function chatArm(
227
229
  }
228
230
 
229
231
  export function chatTouch(
230
- a: { handle: string; sessionId?: string },
232
+ a: { handle: string; room?: string; sessionId?: string },
231
233
  o: RtClientOptions = {},
232
234
  ): Promise<RtResponse<Record<string, never>>> {
233
235
  const payload: Record<string, unknown> = { handle: a.handle };
236
+ if (a.room !== undefined) payload.room = a.room;
234
237
  if (a.sessionId !== undefined) payload.sessionId = a.sessionId;
235
238
  return rtCommand<Record<string, never>>("chat:touch", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
236
239
  }
@@ -323,3 +326,77 @@ export function chatDm(
323
326
  export function eventsHead(o: RtClientOptions = {}): Promise<RtResponse<{ cursor: number }>> {
324
327
  return rtCommand<{ cursor: number }>("events:head", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
325
328
  }
329
+
330
+ // ─── Agent handoff (rt agent) ─────────────────────────────────────────────
331
+
332
+ export function agentStart(
333
+ a: Commands["agent:start"]["payload"], o: RtClientOptions = {},
334
+ ): Promise<RtResponse<AgentRecord>> {
335
+ const payload: Record<string, unknown> = { repo: a.repo, cwd: a.cwd };
336
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs"] as const) {
337
+ if (a[k] !== undefined) payload[k] = a[k];
338
+ }
339
+ return rtCommand<AgentRecord>("agent:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
340
+ }
341
+
342
+ export function agentResume(
343
+ a: Commands["agent:resume"]["payload"], o: RtClientOptions = {},
344
+ ): Promise<RtResponse<AgentRecord>> {
345
+ const payload: Record<string, unknown> = { id: a.id };
346
+ if (a.prompt !== undefined) payload.prompt = a.prompt;
347
+ if (a.surface !== undefined) payload.surface = a.surface;
348
+ return rtCommand<AgentRecord>("agent:resume", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
349
+ }
350
+
351
+ export function agentGet(a: { id: string }, o: RtClientOptions = {}): Promise<RtResponse<AgentRecord>> {
352
+ return rtCommand<AgentRecord>("agent:get", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
353
+ }
354
+
355
+ export function agentList(a: { repo?: string }, o: RtClientOptions = {}): Promise<RtResponse<{ agents: AgentRecord[] }>> {
356
+ const payload: Record<string, unknown> = {};
357
+ if (a.repo !== undefined) payload.repo = a.repo;
358
+ return rtCommand<{ agents: AgentRecord[] }>("agent:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
359
+ }
360
+
361
+ // ─── Panes (rt chat invite) ────────────────────────────────────────────────
362
+ // herdr-facing verbs; the daemon answers `herdr unavailable` without herdr.
363
+
364
+ export function paneList(o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:list"]["data"]>> {
365
+ return rtCommand<Commands["pane:list"]["data"]>("pane:list", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
366
+ }
367
+
368
+ export function panePeek(a: Commands["pane:peek"]["payload"], o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:peek"]["data"]>> {
369
+ const payload: Record<string, unknown> = { paneId: a.paneId };
370
+ if (a.lines !== undefined) payload.lines = a.lines;
371
+ return rtCommand<Commands["pane:peek"]["data"]>("pane:peek", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
372
+ }
373
+
374
+ /** The spawn waits on claude starting, so its budget is minutes, not seconds. */
375
+ export function paneSpawn(
376
+ a: Commands["pane:spawn"]["payload"],
377
+ o: RtClientOptions = {},
378
+ ): Promise<RtResponse<Commands["pane:spawn"]["data"]>> {
379
+ const payload: Record<string, unknown> = { cwd: a.cwd };
380
+ for (const k of ["account", "model", "effort", "prompt", "workspace"] as const) if (a[k] !== undefined) payload[k] = a[k];
381
+ return rtCommand<Commands["pane:spawn"]["data"]>("pane:spawn", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 90_000 });
382
+ }
383
+
384
+ export function paneAccounts(o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:accounts"]["data"]>> {
385
+ return rtCommand<Commands["pane:accounts"]["data"]>("pane:accounts", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
386
+ }
387
+
388
+ export function paneDirectories(a: Commands["pane:directories"]["payload"], o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:directories"]["data"]>> {
389
+ const payload: Record<string, unknown> = {};
390
+ if (a.q !== undefined) payload.q = a.q;
391
+ return rtCommand<Commands["pane:directories"]["data"]>("pane:directories", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
392
+ }
393
+
394
+ export function chatInvite(
395
+ a: Commands["chat:invite"]["payload"],
396
+ o: RtClientOptions = {},
397
+ ): Promise<RtResponse<Commands["chat:invite"]["data"]>> {
398
+ const payload: Record<string, unknown> = { paneId: a.paneId, room: a.room, from: a.from };
399
+ if (a.note !== undefined) payload.note = a.note;
400
+ if (a.callerPane !== undefined) payload.callerPane = a.callerPane;
401
+ return rtCommand<Commands["chat:invite"]["data"]>("chat:invite", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
402
+ }
package/src/commands.ts CHANGED
@@ -146,6 +146,25 @@ export interface PresenceRow {
146
146
  signedOutAt?: number;
147
147
  }
148
148
 
149
+ export type AgentStatus = "idle" | "working" | "blocked" | "done" | "unknown";
150
+
151
+ /** Duplicated shape on purpose (see EventsBusEvent above): mirrors the daemon's pane row, which rt-client cannot import. */
152
+ export interface ChatPane {
153
+ paneId: string;
154
+ workspace: string;
155
+ title?: string;
156
+ cwd?: string;
157
+ repo?: string;
158
+ branch?: string;
159
+ agentStatus: AgentStatus;
160
+ sessionId?: string;
161
+ presence?: { handle: string; status: BuddyStatus; rooms: string[] };
162
+ }
163
+
164
+ export interface PaneAccount { slot: number; email: string; alias?: string; headroom?: string }
165
+ export interface PaneDirectory { path: string; repo: string; branch?: string }
166
+ export interface InviteResult { paneId: string; delivered: "accepted" | "queued" | "refused"; reason?: string }
167
+
149
168
  // SKILLS-53: one judgment, computed once in rt, so the console and the tray
150
169
  // never derive two verdicts that can disagree.
151
170
  export type Attention = {
@@ -192,6 +211,18 @@ export interface RunFieldRow { key: string; value: string; produced_by: string;
192
211
  export interface RunDecisionRow { contract: string; scope: string; selection: string; decided_by: string; decided_at: number; }
193
212
  export interface RunDetail { run: RunSummary; stages: RunStageRow[]; fields: RunFieldRow[]; decisions: RunDecisionRow[]; schemaAhead: boolean; }
194
213
 
214
+ export type AgentSurface = "herdr" | "headless";
215
+
216
+ export interface AgentRecord {
217
+ id: string; repo: string; cwd: string; provider: string;
218
+ surface: AgentSurface; sessionId: string;
219
+ model?: string; effort?: string; account?: string;
220
+ label?: string; caller?: string;
221
+ paneId?: string; tabId?: string; workspaceId?: string;
222
+ extraArgs?: string; exitCode?: number; resultPath?: string;
223
+ createdAt: number; lastResumedAt?: number; finishedAt?: number;
224
+ }
225
+
195
226
  export interface Commands {
196
227
  "project-mrs:read": { payload: { repoName: string; maxAgeMs?: number; demand?: DemandDecl }; data: ProjectMRsData };
197
228
  "discussions:read": { payload: { repoName: string; iid: number }; data: DiscussionsData };
@@ -257,7 +288,7 @@ export interface Commands {
257
288
  "chat:mark": { payload: { handle: string; room?: string }; data: Record<string, never> };
258
289
  "chat:messages": { payload: { room: string; before?: number; limit?: number }; data: { messages: ChatMessage[] } };
259
290
  "chat:arm": { payload: { handle: string; room?: string; sessionId?: string }; data: Record<string, never> };
260
- "chat:touch": { payload: { handle: string; sessionId?: string }; data: Record<string, never> };
291
+ "chat:touch": { payload: { handle: string; room?: string; sessionId?: string }; data: Record<string, never> };
261
292
  "chat:disarm": { payload: { handle: string; sessionId?: string }; data: Record<string, never> };
262
293
  "chat:unread-waking": { payload: { handle: string; room?: string }; data: { rooms: { room: string; count: number; mentions: number; maxId: number }[] } };
263
294
 
@@ -277,6 +308,21 @@ export interface Commands {
277
308
  data: { unread: { dms: number; mentions: number; rooms: number }; status: BuddyStatus };
278
309
  };
279
310
  "chat:dm": { payload: { from: string; to: string; body: string; sessionId?: string }; data: { room: string; id: number; recipients: string[] } };
311
+
312
+ // ─── Agent handoff (rt agent) ────────────────────────────────────────────
313
+ "agent:start": { payload: { repo: string; cwd: string; prompt?: string; surface?: AgentSurface; model?: string; effort?: string; account?: string; label?: string; caller?: string; workspace?: string; tab?: string; extraArgs?: string }; data: AgentRecord };
314
+ "agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface }; data: AgentRecord };
315
+ "agent:get": { payload: { id: string }; data: AgentRecord };
316
+ "agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
317
+ "chat:invite": { payload: { paneId: string; room: string; note?: string; from: string; callerPane?: string }; data: InviteResult };
318
+ "pane:list": { payload: Record<string, never>; data: { panes: ChatPane[] } };
319
+ "pane:peek": { payload: { paneId: string; lines?: number }; data: { paneId: string; lines: string[] } };
320
+ "pane:accounts": { payload: Record<string, never>; data: { accounts: PaneAccount[] } };
321
+ "pane:directories": { payload: { q?: string }; data: { directories: PaneDirectory[] } };
322
+ "pane:spawn": {
323
+ payload: { cwd: string; account?: string; model?: string; effort?: string; prompt?: string; workspace?: string };
324
+ data: { pane: ChatPane; ready: boolean };
325
+ };
280
326
  }
281
327
 
282
328
  export type CommandName = keyof Commands;
@@ -313,4 +359,14 @@ export const COMMAND_NAMES: readonly CommandName[] = [
313
359
  "chat:buddies",
314
360
  "chat:pulse",
315
361
  "chat:dm",
362
+ "agent:start",
363
+ "agent:resume",
364
+ "agent:get",
365
+ "agent:list",
366
+ "chat:invite",
367
+ "pane:list",
368
+ "pane:peek",
369
+ "pane:accounts",
370
+ "pane:directories",
371
+ "pane:spawn",
316
372
  ];
package/src/index.ts CHANGED
@@ -30,6 +30,16 @@ export {
30
30
  chatPulse,
31
31
  chatDm,
32
32
  eventsHead,
33
+ agentStart,
34
+ agentResume,
35
+ agentGet,
36
+ agentList,
37
+ paneList,
38
+ panePeek,
39
+ paneSpawn,
40
+ paneAccounts,
41
+ paneDirectories,
42
+ chatInvite,
33
43
  } from "./client.ts";
34
44
 
35
45
  export { COMMAND_NAMES } from "./commands.ts";
@@ -58,6 +68,13 @@ export type {
58
68
  RoomSummary,
59
69
  BuddyStatus,
60
70
  PresenceRow,
71
+ AgentRecord,
72
+ AgentSurface,
73
+ AgentStatus,
74
+ ChatPane,
75
+ PaneAccount,
76
+ PaneDirectory,
77
+ InviteResult,
61
78
  } from "./commands.ts";
62
79
 
63
80
  export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
@@ -308,7 +308,7 @@ export const REGISTRY: readonly SettingDef[] = [
308
308
  type: "array",
309
309
  scopes: ["team"],
310
310
  merge: "replace",
311
- description: "The board's full member roster, including hidden-by-default entries.",
311
+ description: "The authors tab's roster: whose MRs the classic board lists, including hidden-by-default entries. Codeowners tabs list MRs from anyone.",
312
312
  },
313
313
  {
314
314
  key: "board.title",
@@ -329,14 +329,14 @@ export const REGISTRY: readonly SettingDef[] = [
329
329
  type: "array",
330
330
  scopes: ["team"],
331
331
  merge: "replace",
332
- description: "Ticket key prefixes (e.g. RT, MAT) the board links out to Linear from an MR title.",
332
+ description: "Ticket key prefixes (e.g. RT, MAT) that keep the authors tab to the team's own tickets and link out to Linear from an MR title; rows on a codeowners tab pass regardless of prefix.",
333
333
  },
334
334
  {
335
335
  key: "board.slack",
336
336
  type: "object",
337
337
  scopes: ["team"],
338
338
  merge: "deep",
339
- description: "The board's Slack posting config (app id, client id, channel, callback port); client secrets stay out of this store.",
339
+ description: "The board's Slack posting config (app id, client id, channel, callback port); channel is the default that a tab's slackChannel overrides per MR. Client secrets stay out of this store.",
340
340
  },
341
341
  {
342
342
  key: "board.doctorSkill",
@@ -357,7 +357,7 @@ export const REGISTRY: readonly SettingDef[] = [
357
357
  type: "array",
358
358
  scopes: ["team"],
359
359
  merge: "replace",
360
- description: "Board tab definitions ({id, label, source, slackChannel?, reviewSkill?}); source.kind 'authors' is the classic roster board, 'codeowners' lists MRs blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here).",
360
+ description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists MRs from any author blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here).",
361
361
  },
362
362
 
363
363
  // --- board (user) ----------------------------------------------------------
@@ -387,7 +387,7 @@ export const REGISTRY: readonly SettingDef[] = [
387
387
  type: "array",
388
388
  scopes: ["user"],
389
389
  merge: "replace",
390
- description: "Usernames this developer hides from the team roster's board.members list; overlays the team truth without editing it.",
390
+ description: "Usernames this developer hides from the authors tab's board.members roster; overlays the team truth without editing it.",
391
391
  },
392
392
  {
393
393
  key: "board.triage",
@@ -473,6 +473,21 @@ export const REGISTRY: readonly SettingDef[] = [
473
473
  merge: "replace",
474
474
  description: "The human's own chat handle, so agents can @-mention them by name.",
475
475
  },
476
+ {
477
+ key: "chat.viewerUrl",
478
+ type: "string",
479
+ scopes: ["user"],
480
+ merge: "replace",
481
+ description: "Base URL of the chat viewer, no trailing slash. When set, rt chat post and the tail's wake lines end with a link to the room or message.",
482
+ },
483
+ {
484
+ key: "chat.herdrWorkspace",
485
+ type: "string",
486
+ scopes: ["user"],
487
+ default: "chat",
488
+ merge: "replace",
489
+ description: "herdr workspace label that rt pane spawn opens new agent tabs in; created when missing.",
490
+ },
476
491
  {
477
492
  key: "chat.push.provider",
478
493
  type: "string",
@@ -487,4 +502,36 @@ export const REGISTRY: readonly SettingDef[] = [
487
502
  merge: "replace",
488
503
  description: "Destination (topic/URL/token) the configured chat.push.provider sends to.",
489
504
  },
505
+
506
+ // --- agent (rt agent handoff) --------------------------------------------
507
+ // No defaults by design: an unset key means the flag is omitted from the
508
+ // claude invocation entirely (spec "Settings").
509
+ {
510
+ key: "agent.model",
511
+ type: "string",
512
+ scopes: ["user", "machine"],
513
+ merge: "replace",
514
+ description: "Default --model for rt agent launches; unset omits the flag.",
515
+ },
516
+ {
517
+ key: "agent.effort",
518
+ type: "string",
519
+ scopes: ["user", "machine"],
520
+ merge: "replace",
521
+ description: "Default --effort for rt agent launches; unset omits the flag.",
522
+ },
523
+ {
524
+ key: "agent.account",
525
+ type: "string",
526
+ scopes: ["user", "machine"],
527
+ merge: "replace",
528
+ description: "cswap account email rt agent launches under; unset uses the default claude profile.",
529
+ },
530
+ {
531
+ key: "agent.extraArgs",
532
+ type: "string",
533
+ scopes: ["user", "machine"],
534
+ merge: "replace",
535
+ description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch).",
536
+ },
490
537
  ];