@mattstack/rt-client 0.10.1 → 0.11.1

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/dist/index.js CHANGED
@@ -14,7 +14,7 @@ async function rtCommand(cmd, payload, opts = {}) {
14
14
  const res = await fetch(`http://localhost/${cmd}`, {
15
15
  unix: sockPath,
16
16
  method: "POST",
17
- headers: { "Content-Type": "application/json" },
17
+ headers: { "Content-Type": "application/json", "X-RT-Client": `rt-client/${process.pid}` },
18
18
  body: JSON.stringify(payload),
19
19
  signal: AbortSignal.timeout(opts.timeoutMs ?? 15000)
20
20
  });
@@ -82,8 +82,19 @@ function chatPost(a, o = {}) {
82
82
  const payload = { room: a.room, handle: a.handle, body: a.body };
83
83
  if (a.mentions !== undefined)
84
84
  payload.mentions = a.mentions;
85
+ if (a.quiet)
86
+ payload.quiet = true;
85
87
  return rtCommand("chat:post", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
86
88
  }
89
+ function chatAck(a, o = {}) {
90
+ return rtCommand("chat:ack", { id: a.id, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
91
+ }
92
+ function chatClaim(a, o = {}) {
93
+ return rtCommand("chat:claim", { id: a.id, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
94
+ }
95
+ function chatRelease(a, o = {}) {
96
+ return rtCommand("chat:release", { id: a.id, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
97
+ }
87
98
  function chatRead(a, o = {}) {
88
99
  const payload = { handle: a.handle };
89
100
  if (a.room !== undefined)
@@ -259,6 +270,9 @@ var COMMAND_NAMES = [
259
270
  "runs:list",
260
271
  "runs:get",
261
272
  "runs:abandon",
273
+ "chat:ack",
274
+ "chat:claim",
275
+ "chat:release",
262
276
  "chat:join",
263
277
  "chat:leave",
264
278
  "chat:post",
@@ -286,7 +300,45 @@ var COMMAND_NAMES = [
286
300
  "pane:directories",
287
301
  "pane:spawn",
288
302
  "pane:send",
289
- "pane:focus"
303
+ "pane:focus",
304
+ "cache:read",
305
+ "branch:enrich",
306
+ "cache:refresh",
307
+ "daemon:log-level",
308
+ "ping",
309
+ "status",
310
+ "tray:status",
311
+ "tcc:check",
312
+ "repos",
313
+ "ports",
314
+ "notifications",
315
+ "discussions:refresh",
316
+ "discussions:resolve",
317
+ "discussions:reply",
318
+ "discussions:diffs",
319
+ "mr:action",
320
+ "mr:fetch-job-detail",
321
+ "mr:fetch-job-trace",
322
+ "endpoint:claim",
323
+ "endpoint:lookup",
324
+ "endpoint:release",
325
+ "endpoint:status",
326
+ "repos:locate",
327
+ "freshness:reconcile",
328
+ "hooks:repair",
329
+ "hooks:watch",
330
+ "sdm:catalog",
331
+ "sdm:snapshot",
332
+ "sdm:recents",
333
+ "sdm:reconnect",
334
+ "system-processes",
335
+ "worktree:provision",
336
+ "worktree:create",
337
+ "worktree:dispose",
338
+ "worktree:list",
339
+ "worktree:restore",
340
+ "worktree:freshen",
341
+ "worktree:adopt"
290
342
  ];
291
343
  // src/relay.ts
292
344
  var DEFAULT_WS_URL = "ws://127.0.0.1:9401/ws";
@@ -413,6 +465,56 @@ function repoNameForPath(repoPath, reposJsonPath) {
413
465
  return fromDb;
414
466
  return repoNameFromJson(repoPath, jsonPath);
415
467
  }
468
+ // src/smart-pane.ts
469
+ var DEFAULT_MIN_COLS = 50;
470
+ var DEFAULT_MIN_ROWS = 14;
471
+ function decidePlacement(rect, opts = {}) {
472
+ const minCols = opts.minCols ?? DEFAULT_MIN_COLS;
473
+ const minRows = opts.minRows ?? DEFAULT_MIN_ROWS;
474
+ if (!rect || rect.width <= 0 || rect.height <= 0)
475
+ return { kind: "split", direction: "right" };
476
+ const canRight = rect.width >= 2 * minCols;
477
+ const canDown = rect.height >= 2 * minRows;
478
+ if (!canRight && !canDown)
479
+ return { kind: "tab" };
480
+ if (canRight && canDown) {
481
+ return rect.width > 2 * rect.height ? { kind: "split", direction: "right" } : { kind: "split", direction: "down" };
482
+ }
483
+ return canRight ? { kind: "split", direction: "right" } : { kind: "split", direction: "down" };
484
+ }
485
+ async function anchorRect(herdr, anchorPaneId) {
486
+ const r = await herdr("pane.layout", { pane_id: anchorPaneId });
487
+ if (!r.ok)
488
+ return null;
489
+ const pane = (r.result?.layout?.panes ?? []).find((p) => p.pane_id === anchorPaneId);
490
+ return pane?.rect ? { width: pane.rect.width, height: pane.rect.height } : null;
491
+ }
492
+ async function openSmartPane(herdr, anchorPaneId, opts = {}) {
493
+ const focus = opts.focus ?? true;
494
+ const placement = decidePlacement(await anchorRect(herdr, anchorPaneId), opts);
495
+ let paneId;
496
+ if (placement.kind === "split") {
497
+ const s = await herdr("pane.split", { pane_id: anchorPaneId, direction: placement.direction, focus });
498
+ if (!s.ok)
499
+ throw new Error(`pane.split failed: ${s.message}`);
500
+ paneId = s.result?.pane?.pane_id;
501
+ if (!paneId)
502
+ throw new Error("pane.split returned no pane_id");
503
+ } else {
504
+ const workspaceId = anchorPaneId.split(":")[0];
505
+ const t = await herdr("tab.create", { workspace_id: workspaceId, focus });
506
+ if (!t.ok)
507
+ throw new Error(`tab.create failed: ${t.message}`);
508
+ paneId = t.result?.root_pane?.pane_id;
509
+ if (!paneId)
510
+ throw new Error("tab.create returned no pane_id");
511
+ }
512
+ if (opts.command) {
513
+ await herdr("pane.send_text", { pane_id: paneId, text: opts.command });
514
+ await herdr("pane.send_keys", { pane_id: paneId, keys: ["enter"] });
515
+ }
516
+ return { paneId, placement };
517
+ }
416
518
  // src/settings/resolve.ts
417
519
  import { homedir as homedir4 } from "os";
418
520
  import { join as join5 } from "path";
@@ -482,6 +584,15 @@ var REGISTRY = [
482
584
  migrated: true,
483
585
  description: "Per-repo worktree pool config (onDeck size, ready steps, name pool); root/branchFormat/ready computed-or-empty in the reader."
484
586
  },
587
+ {
588
+ key: "rt.worktreeReadyApproval",
589
+ type: "string",
590
+ scopes: ALL_SCOPES,
591
+ merge: "replace",
592
+ repoScoped: true,
593
+ migrated: true,
594
+ description: "Per-repo user approval of a team-authored `ready` shell ladder, as its content hash (RT-89). The reader trusts only user/machine scopes so a team store can never approve its own shell; a hash mismatch after a team edit re-holds the ladder until `rt worktree ready-approve` records the new one."
595
+ },
485
596
  {
486
597
  key: "rt.repoIdentityOverrides",
487
598
  type: "object",
@@ -627,6 +738,24 @@ var REGISTRY = [
627
738
  migrated: true,
628
739
  description: "Age floor in days for the log janitor pruning every surface's rotated log files under ~/.mattstack/rt/logs (default 14). A fresh key, not an ownership-latch port, so a default is fine here."
629
740
  },
741
+ {
742
+ key: "rt.logLevel",
743
+ type: "string",
744
+ scopes: ["machine", "user"],
745
+ default: "info",
746
+ merge: "replace",
747
+ migrated: true,
748
+ description: "Daemon log level (trace|debug|info|warn|error). RT_LOG_LEVEL env wins, then this setting, then info (lib/daemon-logger.ts resolveDaemonLogLevel). A fresh key, not an ownership-latch port, so a default is fine here."
749
+ },
750
+ {
751
+ key: "rt.apiPort",
752
+ type: "number",
753
+ scopes: ["machine", "user"],
754
+ default: 9401,
755
+ merge: "replace",
756
+ migrated: true,
757
+ description: "TCP port for the daemon's local HTTP/WS API. Escape hatch when 9401 is held: RT_API_PORT env wins, then this setting, then 9401 (lib/daemon-config.ts resolveApiPort(), read at bind time by lib/daemon/api-server.ts)."
758
+ },
630
759
  {
631
760
  key: "rt.hooks",
632
761
  type: "object",
@@ -636,6 +765,21 @@ var REGISTRY = [
636
765
  migrated: true,
637
766
  description: "Per-repo git hook enable/disable state ({enabled, hooks: {<hookName>: boolean}}); ownership-latch port of repos/<repo>/hooks.json, store wins per field once it owns the key — including per-hook-name entries inside the nested hooks map, each defaulting to enabled when absent. The installed git-hook shim still greps repos/<repo>/hooks.json with zero process spawns (a hook fires on every git operation); that file is now a DERIVED CACHE this key writes through, kept current by commands/hooks.ts's regenerateHooksCache at every write seam."
638
767
  },
768
+ {
769
+ key: "rt.daemonPath",
770
+ type: "string",
771
+ scopes: ["machine"],
772
+ merge: "replace",
773
+ description: "Absolute colon-separated PATH the daemon uses for every child it spawns, instead of probing your login shell. Set this when the daemon can't find node/git/bun/pnpm (e.g. a fish shell, a blocking .zshrc, or PATH exports that live only in .zshrc). Machine-scoped: it never travels to another machine."
774
+ },
775
+ {
776
+ key: "rt.trustedBrowserOrigins",
777
+ type: "array",
778
+ scopes: ["user", "machine"],
779
+ default: [],
780
+ merge: "replace",
781
+ description: "Browser Origins (scheme://host:port, exact string match) trusted to read the :9401 daemon API and subscribe to /ws without presenting the local api-token -- e.g. a locally-hosted console or chat-viewer dev server. Empty by default: every current mattstack consumer (the CLI, the Swift tray, rt-client from Bun/Node processes, the VS Code extension) is a non-browser client (sends no Origin header at all) and is unaffected either way."
782
+ },
639
783
  {
640
784
  key: "mattstack.integrations",
641
785
  type: "object",
@@ -1296,8 +1440,24 @@ function expandCtxFrom(opts) {
1296
1440
  teamsDir: teamsDir()
1297
1441
  };
1298
1442
  }
1443
+ var warnSink = null;
1444
+ var warnedOnce = new Set;
1445
+ function setSettingsWarnSink(sink) {
1446
+ warnSink = sink;
1447
+ warnedOnce.clear();
1448
+ }
1449
+ function emitSettingsWarning(msg) {
1450
+ if (warnSink) {
1451
+ if (warnedOnce.has(msg))
1452
+ return;
1453
+ warnedOnce.add(msg);
1454
+ warnSink(msg);
1455
+ return;
1456
+ }
1457
+ console.warn(msg);
1458
+ }
1299
1459
  function warnInvalid(key, entry) {
1300
- console.warn(`rt: ignoring "${key}" from the ${entry.scope} scope (${entry.file ?? "no file"}): ${entry.reason}`);
1460
+ emitSettingsWarning(`rt: ignoring "${key}" from the ${entry.scope} scope (${entry.file ?? "no file"}): ${entry.reason}`);
1301
1461
  }
1302
1462
  function getSetting(key, opts = {}) {
1303
1463
  const def = getDef(key);
@@ -1332,7 +1492,7 @@ function listSettings(opts = {}) {
1332
1492
  listed.value = expandVariables(resolution.value, ctx);
1333
1493
  } catch (err) {
1334
1494
  listed.expandError = err.message;
1335
- console.warn(`rt: showing "${def.key}" unexpanded — ${listed.expandError}`);
1495
+ emitSettingsWarning(`rt: showing "${def.key}" unexpanded — ${listed.expandError}`);
1336
1496
  }
1337
1497
  }
1338
1498
  out.push(listed);
@@ -1360,7 +1520,7 @@ function listUnregistered(stores, opts) {
1360
1520
  scan("machine", stores.machine.file, stores.machine.global);
1361
1521
  scan("machine.repo", stores.machine.file, repoSection(stores.machine));
1362
1522
  return [...found.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, hit]) => {
1363
- console.warn(`rt: unregistered setting "${key}" in ${hit.file} — ignoring it (this rt may be older than the store)`);
1523
+ emitSettingsWarning(`rt: unregistered setting "${key}" in ${hit.file} — ignoring it (this rt may be older than the store)`);
1364
1524
  return {
1365
1525
  key,
1366
1526
  value: hit.value,
@@ -1585,21 +1745,42 @@ async function runCapture(argv, opts = {}) {
1585
1745
  } catch {
1586
1746
  return { stdout: "", stderr: "", exitCode: -1 };
1587
1747
  }
1588
- const timer = setTimeout(() => {
1748
+ const timeoutMs = opts.timeoutMs ?? 1e4;
1749
+ let killTimer;
1750
+ const term = setTimeout(() => {
1589
1751
  try {
1590
- proc.kill();
1752
+ proc.kill("SIGTERM");
1591
1753
  } catch {}
1592
- }, opts.timeoutMs ?? 1e4);
1754
+ killTimer = setTimeout(() => {
1755
+ try {
1756
+ proc.kill("SIGKILL");
1757
+ } catch {}
1758
+ }, 2000);
1759
+ killTimer.unref?.();
1760
+ }, timeoutMs);
1761
+ const captured = (async () => {
1762
+ try {
1763
+ const stdoutPromise = new Response(proc.stdout).text();
1764
+ const stderrPromise = captureStderr ? new Response(proc.stderr).text() : Promise.resolve("");
1765
+ const [stdout, stderr, exitCode] = await Promise.all([
1766
+ stdoutPromise,
1767
+ stderrPromise,
1768
+ proc.exited
1769
+ ]);
1770
+ return { stdout, stderr, exitCode };
1771
+ } catch {
1772
+ return { stdout: "", stderr: "", exitCode: -1 };
1773
+ }
1774
+ })();
1775
+ let deadlineTimer;
1776
+ const deadline = new Promise((resolve) => {
1777
+ deadlineTimer = setTimeout(() => resolve({ stdout: "", stderr: "", exitCode: -1, timedOut: true }), timeoutMs);
1778
+ });
1593
1779
  try {
1594
- const stdoutPromise = new Response(proc.stdout).text();
1595
- const stderrPromise = captureStderr ? new Response(proc.stderr).text() : Promise.resolve("");
1596
- const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
1597
- const exitCode = await proc.exited;
1598
- return { stdout, stderr, exitCode };
1599
- } catch {
1600
- return { stdout: "", stderr: "", exitCode: -1 };
1780
+ return await Promise.race([captured, deadline]);
1601
1781
  } finally {
1602
- clearTimeout(timer);
1782
+ clearTimeout(term);
1783
+ clearTimeout(deadlineTimer);
1603
1784
  }
1604
1785
  }
1605
1786
 
@@ -1722,6 +1903,7 @@ export {
1722
1903
  validateValue,
1723
1904
  unsetSetting,
1724
1905
  subscribe,
1906
+ setSettingsWarnSink,
1725
1907
  setSetting,
1726
1908
  serializeIdentity,
1727
1909
  rtCommand,
@@ -1741,6 +1923,7 @@ export {
1741
1923
  paneFocus,
1742
1924
  paneDirectories,
1743
1925
  paneAccounts,
1926
+ openSmartPane,
1744
1927
  normalizeRemote,
1745
1928
  listTeams,
1746
1929
  listSettings,
@@ -1754,6 +1937,7 @@ export {
1754
1937
  expandVariables,
1755
1938
  eventsHead,
1756
1939
  deriveRepoIdentity,
1940
+ decidePlacement,
1757
1941
  daemonHealth,
1758
1942
  createRelay,
1759
1943
  clearIdentityMemo,
@@ -1761,6 +1945,7 @@ export {
1761
1945
  chatSignOut,
1762
1946
  chatSignIn,
1763
1947
  chatRooms,
1948
+ chatRelease,
1764
1949
  chatRead,
1765
1950
  chatPost,
1766
1951
  chatMessages,
@@ -1770,10 +1955,12 @@ export {
1770
1955
  chatInvite,
1771
1956
  chatDmOpen,
1772
1957
  chatDm,
1958
+ chatClaim,
1773
1959
  chatBuddies,
1774
1960
  chatBack,
1775
1961
  chatAway,
1776
1962
  chatArchive,
1963
+ chatAck,
1777
1964
  allDefs,
1778
1965
  agentStart,
1779
1966
  agentResume,
@@ -11,6 +11,8 @@ export interface RunResult {
11
11
  stdout: string;
12
12
  stderr: string;
13
13
  exitCode: number;
14
+ /** Set true only when the deadline fired before the child settled. */
15
+ timedOut?: boolean;
14
16
  }
15
17
  /**
16
18
  * Run argv and capture stdout. Never throws: spawn failures and timeouts
@@ -120,6 +120,11 @@ export interface ExpandCtx {
120
120
  * mutates its input.
121
121
  */
122
122
  export declare function expandVariables(value: unknown, ctx: ExpandCtx): unknown;
123
+ /** The daemon binds a deduped log.warn here so a hot-path getSetting on a
124
+ * disallowed-scope key warns once, not every tick. Default: console.warn
125
+ * (CLI/test behavior unchanged). null restores the default. */
126
+ export declare function setSettingsWarnSink(sink: ((msg: string) => void) | null): void;
127
+ export declare function emitSettingsWarning(msg: string): void;
123
128
  /**
124
129
  * Resolves one key across the whole ladder. Throws for an unregistered key —
125
130
  * an explicit get of something rt has never heard of is a caller bug, not a
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Layout-smart herdr pane placement. `decidePlacement` is pure (geometry ->
3
+ * where a new pane should go); `openSmartPane` reads an anchor pane's real
4
+ * rect, decides, and performs the herdr call through an injected caller so it
5
+ * works over any transport (socket API or a CLI adapter).
6
+ */
7
+ export type Placement = {
8
+ kind: "split";
9
+ direction: "right" | "down";
10
+ } | {
11
+ kind: "tab";
12
+ };
13
+ export interface PlacementOpts {
14
+ minCols?: number;
15
+ minRows?: number;
16
+ }
17
+ export interface HerdrCall {
18
+ (method: string, params: Record<string, unknown>): Promise<{
19
+ ok: true;
20
+ result: any;
21
+ } | {
22
+ ok: false;
23
+ code: string;
24
+ message: string;
25
+ }>;
26
+ }
27
+ /**
28
+ * Where to put a new pane relative to an anchor whose rect (in cells) is given.
29
+ * Cells are ~2:1 (height:width in px), so a pane is visually wider than tall
30
+ * when width > 2*height; split the longer visual axis so both halves stay
31
+ * usable. A null/degenerate rect falls back to a right split (old behavior).
32
+ */
33
+ export declare function decidePlacement(rect: {
34
+ width: number;
35
+ height: number;
36
+ } | null, opts?: PlacementOpts): Placement;
37
+ /**
38
+ * Open a herdr pane placed intelligently next to `anchorPaneId` (split right or
39
+ * down, or a new tab when crowded). Optionally run `command` in it. Returns the
40
+ * new pane id and the placement chosen. Throws on a herdr failure.
41
+ */
42
+ export declare function openSmartPane(herdr: HerdrCall, anchorPaneId: string, opts?: {
43
+ command?: string;
44
+ focus?: boolean;
45
+ } & PlacementOpts): Promise<{
46
+ paneId: string;
47
+ placement: Placement;
48
+ }>;
@@ -2,6 +2,13 @@ export interface RtResponse<T = unknown> {
2
2
  ok: boolean;
3
3
  data?: T;
4
4
  error?: string;
5
+ /** Structured form of `error` on a handler throw (R035): `code` defaults
6
+ * to "handler-threw" when the thrown error carries none. Additive; older
7
+ * daemons and the reject path never set this. */
8
+ failure?: {
9
+ code: string;
10
+ message: string;
11
+ };
5
12
  }
6
13
  export interface RtClientOptions {
7
14
  sockPath?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -51,5 +51,8 @@
51
51
  },
52
52
  "publishConfig": {
53
53
  "access": "public"
54
+ },
55
+ "devDependencies": {
56
+ "typescript": "^5.9.2"
54
57
  }
55
58
  }
package/src/client.ts CHANGED
@@ -18,6 +18,7 @@ import type {
18
18
  WakeMode,
19
19
  ChatMember,
20
20
  ChatMessage,
21
+ ChatClaimOutcome,
21
22
  RoomSummary,
22
23
  BuddyStatus,
23
24
  PresenceRow,
@@ -166,12 +167,32 @@ export function chatLeave(
166
167
  }
167
168
 
168
169
  export function chatPost(
169
- a: { room: string; handle: string; body: string; mentions?: string[] },
170
+ a: { room: string; handle: string; body: string; mentions?: string[]; quiet?: boolean },
170
171
  o: RtClientOptions = {},
171
- ): Promise<RtResponse<{ id: number; recipients: string[] }>> {
172
+ ): Promise<RtResponse<{ id: number; recipients: string[]; others: number }>> {
172
173
  const payload: Record<string, unknown> = { room: a.room, handle: a.handle, body: a.body };
173
174
  if (a.mentions !== undefined) payload.mentions = a.mentions;
174
- return rtCommand<{ id: number; recipients: string[] }>("chat:post", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
175
+ if (a.quiet) payload.quiet = true;
176
+ return rtCommand<{ id: number; recipients: string[]; others: number }>("chat:post", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
177
+ }
178
+
179
+ export function chatAck(
180
+ a: { id: number; handle: string },
181
+ o: RtClientOptions = {},
182
+ ): Promise<RtResponse<{ author: string; room: string; already: boolean }>> {
183
+ return rtCommand<{ author: string; room: string; already: boolean }>(
184
+ "chat:ack",
185
+ { id: a.id, handle: a.handle },
186
+ { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 },
187
+ );
188
+ }
189
+
190
+ export function chatClaim(a: { id: number; handle: string }, o: RtClientOptions = {}): Promise<RtResponse<ChatClaimOutcome>> {
191
+ return rtCommand<ChatClaimOutcome>("chat:claim", { id: a.id, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
192
+ }
193
+
194
+ export function chatRelease(a: { id: number; handle: string }, o: RtClientOptions = {}): Promise<RtResponse<{ holder: string }>> {
195
+ return rtCommand<{ holder: string }>("chat:release", { id: a.id, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
175
196
  }
176
197
 
177
198
  export function chatRead(