@lesscap/baton-cli 0.3.1 → 0.3.3

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.
Files changed (2) hide show
  1. package/dist/baton.mjs +317 -108
  2. package/package.json +1 -1
package/dist/baton.mjs CHANGED
@@ -949,28 +949,33 @@ function getBaseURL() {
949
949
  // src/client/channel.ts
950
950
  var channelClient = (baseUrl) => {
951
951
  const u = (p) => `${baseUrl}${p}`;
952
- const authed = (token) => ({ authorization: `Bearer ${token}` });
953
- const sendJson = (token) => ({ "content-type": "application/json", ...authed(token) });
952
+ const JSON_CT = { "content-type": "application/json" };
954
953
  const member = (channelId, name) => u(`/channels/${channelId}/members/${encodeURIComponent(name)}`);
955
954
  return {
956
- create: async (title, description) => {
957
- const res = await fetch(u("/channels"), {
955
+ create: async (workspaceId, title, description, token) => {
956
+ const res = await fetch(u(`/workspaces/${workspaceId}/channels`), {
958
957
  method: "POST",
959
- headers: { "content-type": "application/json" },
960
- body: JSON.stringify({ ...title ? { title } : {}, ...description ? { description } : {} })
958
+ headers: { ...JSON_CT, ...token ? { authorization: `Bearer ${token}` } : {} },
959
+ body: JSON.stringify({
960
+ ...title ? { title } : {},
961
+ ...description ? { description } : {}
962
+ })
961
963
  });
962
- if (!res.ok) throw new Error(`POST /channels \u2192 ${res.status}: ${await res.text()}`);
964
+ if (!res.ok)
965
+ throw new Error(
966
+ `POST /workspaces/${workspaceId}/channels \u2192 ${res.status}: ${await res.text()}`
967
+ );
963
968
  return await res.json();
964
969
  },
965
- manifest: async (channelId, token) => {
966
- const res = await fetch(u(`/channels/${channelId}`), { headers: authed(token) });
970
+ manifest: async (channelId) => {
971
+ const res = await fetch(u(`/channels/${channelId}`));
967
972
  if (!res.ok) throw new Error(`GET channel \u2192 ${res.status}: ${await res.text()}`);
968
973
  return await res.json();
969
974
  },
970
- update: async (channelId, token, patch2) => {
975
+ update: async (channelId, patch2) => {
971
976
  const res = await fetch(u(`/channels/${channelId}`), {
972
977
  method: "PATCH",
973
- headers: sendJson(token),
978
+ headers: JSON_CT,
974
979
  body: JSON.stringify(patch2)
975
980
  });
976
981
  if (!res.ok) throw new Error(`PATCH channel \u2192 ${res.status}: ${await res.text()}`);
@@ -981,79 +986,63 @@ var channelClient = (baseUrl) => {
981
986
  if (!res.ok) throw new Error(`GET /channels/help \u2192 ${res.status}: ${await res.text()}`);
982
987
  return await res.text();
983
988
  },
984
- destroy: async (channelId, token) => {
985
- const res = await fetch(u(`/channels/${channelId}`), {
986
- method: "DELETE",
987
- headers: authed(token)
988
- });
989
+ destroy: async (channelId) => {
990
+ const res = await fetch(u(`/channels/${channelId}`), { method: "DELETE" });
989
991
  if (!res.ok) throw new Error(`DELETE channel \u2192 ${res.status}: ${await res.text()}`);
990
992
  },
991
- join: async (channelId, token, name, kind) => {
993
+ join: async (channelId, name, kind) => {
992
994
  const res = await fetch(member(channelId, name), {
993
995
  method: "PUT",
994
- headers: sendJson(token),
996
+ headers: JSON_CT,
995
997
  body: JSON.stringify({ kind: kind ?? "agent" })
996
998
  });
997
999
  if (!res.ok) throw new Error(`join channel \u2192 ${res.status}: ${await res.text()}`);
998
1000
  return (await res.json()).members;
999
1001
  },
1000
- leave: async (channelId, token, name) => {
1001
- const res = await fetch(member(channelId, name), { method: "DELETE", headers: authed(token) });
1002
+ leave: async (channelId, name) => {
1003
+ const res = await fetch(member(channelId, name), { method: "DELETE" });
1002
1004
  if (!res.ok) throw new Error(`leave channel \u2192 ${res.status}: ${await res.text()}`);
1003
1005
  },
1004
- members: async (channelId, token) => {
1005
- const res = await fetch(u(`/channels/${channelId}/members`), { headers: authed(token) });
1006
+ members: async (channelId) => {
1007
+ const res = await fetch(u(`/channels/${channelId}/members`));
1006
1008
  if (!res.ok) throw new Error(`GET members \u2192 ${res.status}: ${await res.text()}`);
1007
1009
  return (await res.json()).members;
1008
1010
  },
1009
- send: async (channelId, token, msg) => {
1011
+ send: async (channelId, msg) => {
1010
1012
  const res = await fetch(u(`/channels/${channelId}/messages`), {
1011
1013
  method: "POST",
1012
- headers: sendJson(token),
1014
+ headers: JSON_CT,
1013
1015
  body: JSON.stringify(msg)
1014
1016
  });
1015
1017
  if (!res.ok) throw new Error(`POST channel message \u2192 ${res.status}: ${await res.text()}`);
1016
1018
  return await res.json();
1017
1019
  },
1018
- uploadAttachment: async (channelId, token, input) => {
1020
+ uploadAttachment: async (channelId, input) => {
1019
1021
  const res = await fetch(
1020
1022
  u(`/channels/${channelId}/attachments?filename=${encodeURIComponent(input.filename)}`),
1021
- {
1022
- method: "POST",
1023
- headers: { "content-type": input.contentType, ...authed(token) },
1024
- body: input.body
1025
- }
1023
+ { method: "POST", headers: { "content-type": input.contentType }, body: input.body }
1026
1024
  );
1027
1025
  if (!res.ok) throw new Error(`upload attachment \u2192 ${res.status}: ${await res.text()}`);
1028
1026
  return await res.json();
1029
1027
  },
1030
- downloadAttachment: async (channelId, token, attId) => {
1031
- const res = await fetch(u(`/channels/${channelId}/attachments/${attId}`), {
1032
- headers: authed(token)
1033
- });
1028
+ downloadAttachment: async (channelId, attId) => {
1029
+ const res = await fetch(u(`/channels/${channelId}/attachments/${attId}`));
1034
1030
  if (!res.ok) throw new Error(`download attachment \u2192 ${res.status}: ${await res.text()}`);
1035
1031
  return res;
1036
1032
  },
1037
- read: async (channelId, token, opts) => {
1033
+ read: async (channelId, opts) => {
1038
1034
  const params = new URLSearchParams({ since: String(opts?.since ?? 0) });
1039
1035
  if (opts?.for) params.set("for", opts.for);
1040
- const res = await fetch(u(`/channels/${channelId}/messages?${params}`), {
1041
- headers: authed(token)
1042
- });
1036
+ const res = await fetch(u(`/channels/${channelId}/messages?${params}`));
1043
1037
  if (!res.ok) throw new Error(`GET messages \u2192 ${res.status}: ${await res.text()}`);
1044
1038
  return (await res.json()).messages;
1045
1039
  },
1046
- listen: (channelId, token, opts) => {
1040
+ listen: (channelId, opts) => {
1047
1041
  let cursor = opts.since ?? 0;
1048
1042
  const params = new URLSearchParams({ since: String(cursor) });
1049
1043
  if (opts.for) params.set("for", opts.for);
1050
1044
  if (opts.as) params.set("as", opts.as);
1051
- const es = new EventSource(u(`/channels/${channelId}/stream?${params}`), {
1052
- fetch: (url, init2) => fetch(url, {
1053
- ...init2,
1054
- headers: { ...init2?.headers, ...authed(token) }
1055
- })
1056
- });
1045
+ const es = new EventSource(u(`/channels/${channelId}/stream?${params}`));
1057
1046
  es.onmessage = (e) => {
1058
1047
  try {
1059
1048
  const m = JSON.parse(e.data);
@@ -1145,9 +1134,20 @@ var fmtTask = (t) => `${t.code} [${t.status}] ${t.title}`;
1145
1134
  var fmtComment = (c) => `${c.workerId !== void 0 ? `worker#${c.workerId}` : "you"} ${c.body}`;
1146
1135
  var fmtSession = (s) => `${s.id} ${s.name} ${s.attached ? "[active]" : "[inactive]"} ${s.worktreePath ?? "(pending)"}`;
1147
1136
  var fmtWorker = (w) => {
1148
- const offline = w.alive === false ? " [offline]" : "";
1137
+ const offline = w.connected === false ? " [offline]" : "";
1149
1138
  return `W-${w.id} ${w.name} ${w.hostname}${offline}`;
1150
1139
  };
1140
+ var fmtInterval = (sec) => {
1141
+ if (sec % 86400 === 0) return `${sec / 86400}d`;
1142
+ if (sec % 3600 === 0) return `${sec / 3600}h`;
1143
+ if (sec % 60 === 0) return `${sec / 60}m`;
1144
+ return `${sec}s`;
1145
+ };
1146
+ var fmtLoop = (l) => {
1147
+ const label = l.name ? `${l.name} ` : "";
1148
+ const last = l.lastStatus ? ` last:${l.lastStatus}` : "";
1149
+ return `${l.id} [${l.enabled ? "on" : "off"}] ${label}every ${fmtInterval(l.intervalSec)}${last} "${l.message}"`;
1150
+ };
1151
1151
  var renderOne = (item, fmt, json) => json ? toJson(item) : fmt(item);
1152
1152
  var renderList = (items, fmt, json) => {
1153
1153
  if (json) return toJson(items);
@@ -1204,6 +1204,20 @@ var primeLogin = (baseUrl) => {
1204
1204
  );
1205
1205
  };
1206
1206
 
1207
+ // src/client/loops.ts
1208
+ var loopsClient = (baseUrl) => {
1209
+ const u = (p) => `${baseUrl}${p}`;
1210
+ return {
1211
+ create: (sessionId, input) => request(u(`/sessions/${sessionId}/loops`), { method: "POST", body: input }),
1212
+ listBySession: (sessionId) => request(u(`/sessions/${sessionId}/loops`), { method: "GET" }),
1213
+ get: (id) => request(u(`/loops/${id}`), { method: "GET" }),
1214
+ update: (id, input) => request(u(`/loops/${id}`), { method: "PATCH", body: input }),
1215
+ remove: async (id) => {
1216
+ await request(u(`/loops/${id}`), { method: "DELETE" });
1217
+ }
1218
+ };
1219
+ };
1220
+
1207
1221
  // src/client/projects.ts
1208
1222
  var projectClient = (baseUrl) => {
1209
1223
  const u = (p) => `${baseUrl}${p}`;
@@ -1379,6 +1393,7 @@ var clientFromBase = (baseUrl) => ({
1379
1393
  requirements: requirementClient(baseUrl),
1380
1394
  tasks: taskClient(baseUrl),
1381
1395
  sessions: sessionsClient(baseUrl),
1396
+ loops: loopsClient(baseUrl),
1382
1397
  workers: workersClient(baseUrl)
1383
1398
  });
1384
1399
  var BOUNDARY_EVENTS = /* @__PURE__ */ new Set([
@@ -1428,6 +1443,11 @@ var clientFor = (args) => {
1428
1443
  const auth = resolveAuth(process.env, fileToken);
1429
1444
  return createClient(baseUrl, auth && auth !== "cookie" ? auth : void 0);
1430
1445
  };
1446
+ var resolveBearer = () => {
1447
+ const fileToken = loadProjectConfigOrNull(projectConfigPath())?.worker?.apiToken;
1448
+ const auth = resolveAuth(process.env, fileToken);
1449
+ return auth && auth !== "cookie" ? auth.bearer : void 0;
1450
+ };
1431
1451
  var resolveProjectId = (args) => {
1432
1452
  if (args.project !== void 0 && args.project !== "") return Number(args.project);
1433
1453
  const cfg = loadProjectConfigOrNull(projectConfigPath());
@@ -1447,17 +1467,23 @@ var parseWorkerHandle = (handle) => {
1447
1467
  return m ? Number(m[1]) : null;
1448
1468
  };
1449
1469
  var splitCsv = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : void 0;
1470
+ var parseDuration = (s) => {
1471
+ const m = s.trim().match(/^(\d+)\s*([smhd]?)$/);
1472
+ if (!m) throw new Error(`bad interval "${s}" \u2014 use e.g. 90s, 30m, 2h, 1d`);
1473
+ const n = Number(m[1]);
1474
+ const mult = m[2] === "d" ? 86400 : m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1;
1475
+ return n * mult;
1476
+ };
1450
1477
 
1451
1478
  // src/commands/channel/close.ts
1452
1479
  var closeCommand = defineCommand({
1453
1480
  meta: { name: "close", description: "delete a channel (removes the room and all its messages)" },
1454
1481
  args: {
1455
1482
  channel: { type: "positional", required: true, description: "channel id" },
1456
- token: { type: "string", required: true, description: "channel token" },
1457
1483
  ...common
1458
1484
  },
1459
1485
  run: async ({ args }) => {
1460
- await channelClient(resolveBaseUrl(args.url)).destroy(args.channel, args.token);
1486
+ await channelClient(resolveBaseUrl(args.url)).destroy(args.channel);
1461
1487
  console.log(args.json ? toJson({ ok: true, deleted: args.channel }) : `closed ${args.channel}`);
1462
1488
  }
1463
1489
  });
@@ -1469,42 +1495,58 @@ var readStdin = async () => {
1469
1495
  return Buffer.concat(chunks).toString("utf8");
1470
1496
  };
1471
1497
  var renderRoster = (members) => members.length ? members.map((m) => ` ${m.name} (${m.kind})`).join("\n") : " (nobody online)";
1472
- var webLink = (url, channelId, token) => {
1498
+ var webLink = (url, channelId) => {
1473
1499
  const origin = url.replace(/\/api\/?$/, "");
1474
- return origin === url ? null : `${origin}/channel/${channelId}#token=${token}`;
1500
+ return origin === url ? null : `${origin}/channel/${channelId}`;
1475
1501
  };
1476
- var inviteBlock = (url, channelId, token) => {
1477
- const web = webLink(url, channelId, token);
1502
+ var inviteBlock = (url, channelId) => {
1503
+ const web = webLink(url, channelId);
1478
1504
  return [
1479
1505
  "\u2500\u2500 Share this to invite anyone into the room \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
1480
1506
  ...web ? [`Humans \u2014 open in a browser: ${web}`, ""] : [],
1481
1507
  "Agents \u2014 two curl steps:",
1482
- ` 1) see the room: curl -sS -H "authorization: Bearer ${token}" "${url}/channels/${channelId}"`,
1508
+ ` 1) see the room: curl -sS "${url}/channels/${channelId}"`,
1483
1509
  ` 2) read protocol: curl -sS "${url}/channels/help" (then join / listen / send, pick your own NAME)`,
1484
1510
  "",
1485
- `connection: url=${url} channel=${channelId} token=${token}`,
1511
+ `connection: url=${url} channel=${channelId}`,
1486
1512
  "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
1487
1513
  ].join("\n");
1488
1514
  };
1489
1515
 
1490
1516
  // src/commands/channel/create.ts
1491
1517
  var createCommand = defineCommand({
1492
- meta: { name: "create", description: "open a channel (chat room) and print an invite" },
1518
+ meta: {
1519
+ name: "create",
1520
+ description: "open a channel (chat room) in a workspace and print an invite"
1521
+ },
1493
1522
  args: {
1523
+ workspace: {
1524
+ type: "string",
1525
+ description: "owning workspace id (or the cwd .baton.json workspace)"
1526
+ },
1494
1527
  name: { type: "string", description: "room title (optional)" },
1495
- desc: { type: "string", description: "room self-description: purpose / topic / how to participate" },
1528
+ desc: {
1529
+ type: "string",
1530
+ description: "room self-description: purpose / topic / how to participate"
1531
+ },
1496
1532
  ...common
1497
1533
  },
1498
1534
  run: async ({ args }) => {
1499
1535
  const url = resolveBaseUrl(args.url);
1500
- const { channelId, token } = await channelClient(url).create(args.name, args.desc);
1536
+ const workspaceId = resolveWorkspaceId(args);
1537
+ const { channelId } = await channelClient(url).create(
1538
+ workspaceId,
1539
+ args.name,
1540
+ args.desc,
1541
+ resolveBearer()
1542
+ );
1501
1543
  if (args.json) {
1502
- console.log(toJson({ channelId, token, url, name: args.name, description: args.desc }));
1544
+ console.log(toJson({ channelId, url, workspaceId, name: args.name, description: args.desc }));
1503
1545
  return;
1504
1546
  }
1505
- console.log(`channel ${channelId} opened
1547
+ console.log(`channel ${channelId} opened in workspace ${workspaceId}
1506
1548
  `);
1507
- console.log(inviteBlock(url, channelId, token));
1549
+ console.log(inviteBlock(url, channelId));
1508
1550
  }
1509
1551
  });
1510
1552
 
@@ -1515,13 +1557,12 @@ var downloadCommand = defineCommand({
1515
1557
  args: {
1516
1558
  channel: { type: "positional", required: true, description: "channel id" },
1517
1559
  att: { type: "positional", required: true, description: "attachment id (from a message ref)" },
1518
- token: { type: "string", required: true, description: "channel token" },
1519
1560
  out: { type: "string", alias: "o", description: "output file path (default: ./<attId>)" },
1520
1561
  ...common
1521
1562
  },
1522
1563
  run: async ({ args }) => {
1523
1564
  const url = resolveBaseUrl(args.url);
1524
- const res = await channelClient(url).downloadAttachment(args.channel, args.token, args.att);
1565
+ const res = await channelClient(url).downloadAttachment(args.channel, args.att);
1525
1566
  const out = args.out ?? args.att;
1526
1567
  writeFileSync2(out, Buffer.from(await res.arrayBuffer()));
1527
1568
  console.log(`saved \u2192 ${out}`);
@@ -1533,12 +1574,11 @@ var aboutCommand = defineCommand({
1533
1574
  meta: { name: "about", description: "show a channel's manifest (description + online roster)" },
1534
1575
  args: {
1535
1576
  channel: { type: "positional", required: true, description: "channel id" },
1536
- token: { type: "string", required: true, description: "channel token" },
1537
1577
  ...common
1538
1578
  },
1539
1579
  run: async ({ args }) => {
1540
1580
  const url = resolveBaseUrl(args.url);
1541
- const m = await channelClient(url).manifest(args.channel, args.token);
1581
+ const m = await channelClient(url).manifest(args.channel);
1542
1582
  if (args.json) {
1543
1583
  console.log(toJson(m));
1544
1584
  return;
@@ -1553,7 +1593,7 @@ protocol: curl -sS "${url}${m.help}"`);
1553
1593
  }
1554
1594
  });
1555
1595
  var helpCommand = defineCommand({
1556
- meta: { name: "help", description: "print the channel protocol doc (no token needed)" },
1596
+ meta: { name: "help", description: "print the channel protocol doc" },
1557
1597
  args: { ...common },
1558
1598
  run: async ({ args }) => {
1559
1599
  console.log(await channelClient(resolveBaseUrl(args.url)).help());
@@ -1563,12 +1603,11 @@ var membersCommand = defineCommand({
1563
1603
  meta: { name: "members", description: "list the channel's online roster" },
1564
1604
  args: {
1565
1605
  channel: { type: "positional", required: true, description: "channel id" },
1566
- token: { type: "string", required: true, description: "channel token" },
1567
1606
  ...common
1568
1607
  },
1569
1608
  run: async ({ args }) => {
1570
1609
  const url = resolveBaseUrl(args.url);
1571
- const members = await channelClient(url).members(args.channel, args.token);
1610
+ const members = await channelClient(url).members(args.channel);
1572
1611
  console.log(args.json ? toJson(members) : renderRoster(members));
1573
1612
  }
1574
1613
  });
@@ -1576,7 +1615,6 @@ var updateCommand = defineCommand({
1576
1615
  meta: { name: "update", description: "update a channel's title / description (topic + rules)" },
1577
1616
  args: {
1578
1617
  channel: { type: "positional", required: true, description: "channel id" },
1579
- token: { type: "string", required: true, description: "channel token" },
1580
1618
  title: { type: "string", description: "new title" },
1581
1619
  desc: { type: "string", description: "new description (purpose / topic / rules)" },
1582
1620
  ...common
@@ -1585,7 +1623,7 @@ var updateCommand = defineCommand({
1585
1623
  if (args.title === void 0 && args.desc === void 0)
1586
1624
  throw new Error("nothing to update: pass --title and/or --desc");
1587
1625
  const url = resolveBaseUrl(args.url);
1588
- const ch = await channelClient(url).update(args.channel, args.token, {
1626
+ const ch = await channelClient(url).update(args.channel, {
1589
1627
  title: args.title,
1590
1628
  description: args.desc
1591
1629
  });
@@ -1672,7 +1710,7 @@ var runListen = async (o) => {
1672
1710
  file
1673
1711
  });
1674
1712
  };
1675
- const close = channelClient(o.url).listen(o.channel, o.token, {
1713
+ const close = channelClient(o.url).listen(o.channel, {
1676
1714
  since: o.since,
1677
1715
  for: o.for,
1678
1716
  as: o.from,
@@ -1699,8 +1737,10 @@ var listenCommand = defineCommand({
1699
1737
  },
1700
1738
  args: {
1701
1739
  channel: { type: "positional", required: true, description: "channel id" },
1702
- token: { type: "string", required: true, description: "channel token" },
1703
- from: { type: "string", description: "your name; your own messages skipped, presence kept fresh" },
1740
+ from: {
1741
+ type: "string",
1742
+ description: "your name; your own messages skipped, presence kept fresh"
1743
+ },
1704
1744
  since: { type: "string", description: "replay messages after this seq (default 0 = full)" },
1705
1745
  for: { type: "string", description: "only broadcasts + messages addressed to this name" },
1706
1746
  ...common
@@ -1709,7 +1749,6 @@ var listenCommand = defineCommand({
1709
1749
  await runListen({
1710
1750
  url: resolveBaseUrl(args.url),
1711
1751
  channel: args.channel,
1712
- token: args.token,
1713
1752
  from: args.from ?? "",
1714
1753
  since: args.since ? Number(args.since) : 0,
1715
1754
  for: args.for
@@ -1719,30 +1758,35 @@ var listenCommand = defineCommand({
1719
1758
 
1720
1759
  // src/commands/channel/join.ts
1721
1760
  var joinCommand = defineCommand({
1722
- meta: { name: "join", description: "join a channel (register presence); --listen to also stream" },
1761
+ meta: {
1762
+ name: "join",
1763
+ description: "join a channel (register presence); --listen to also stream"
1764
+ },
1723
1765
  args: {
1724
1766
  channel: { type: "positional", required: true, description: "channel id" },
1725
- token: { type: "string", required: true, description: "channel token" },
1726
1767
  from: { type: "string", required: true, description: "your participant name" },
1727
1768
  kind: { type: "string", description: "your kind: agent (default) | human" },
1728
- listen: { type: "boolean", description: "after joining, stream messages (background subscriber)" },
1769
+ listen: {
1770
+ type: "boolean",
1771
+ description: "after joining, stream messages (background subscriber)"
1772
+ },
1729
1773
  since: { type: "string", description: "with --listen: replay after this seq (default 0)" },
1730
1774
  ...common
1731
1775
  },
1732
1776
  run: async ({ args }) => {
1733
1777
  const url = resolveBaseUrl(args.url);
1734
1778
  const kind = args.kind === "human" ? "human" : "agent";
1735
- const members = await channelClient(url).join(args.channel, args.token, args.from, kind);
1779
+ const members = await channelClient(url).join(args.channel, args.from, kind);
1736
1780
  if (!args.listen) {
1737
1781
  if (args.json) console.log(toJson({ joined: args.from, members }));
1738
- else console.log(`joined as ${args.from} (${members.length} online)
1782
+ else
1783
+ console.log(`joined as ${args.from} (${members.length} online)
1739
1784
  ${renderRoster(members)}`);
1740
1785
  return;
1741
1786
  }
1742
1787
  await runListen({
1743
1788
  url,
1744
1789
  channel: args.channel,
1745
- token: args.token,
1746
1790
  from: args.from,
1747
1791
  since: args.since ? Number(args.since) : 0
1748
1792
  });
@@ -1760,7 +1804,6 @@ var readCommand = defineCommand({
1760
1804
  meta: { name: "read", description: "poll channel messages once (curl-style, since a cursor)" },
1761
1805
  args: {
1762
1806
  channel: { type: "positional", required: true, description: "channel id" },
1763
- token: { type: "string", required: true, description: "channel token" },
1764
1807
  since: { type: "string", description: "only messages after this seq (default 0)" },
1765
1808
  from: { type: "string", description: "skip messages from this name (no echo)" },
1766
1809
  for: { type: "string", description: "only broadcasts + messages addressed to this name" },
@@ -1768,7 +1811,7 @@ var readCommand = defineCommand({
1768
1811
  },
1769
1812
  run: async ({ args }) => {
1770
1813
  const url = resolveBaseUrl(args.url);
1771
- const msgs = await channelClient(url).read(args.channel, args.token, {
1814
+ const msgs = await channelClient(url).read(args.channel, {
1772
1815
  since: args.since ? Number(args.since) : 0,
1773
1816
  for: args.for
1774
1817
  });
@@ -1824,11 +1867,16 @@ var sendCommand = defineCommand({
1824
1867
  args: {
1825
1868
  channel: { type: "positional", required: true, description: "channel id" },
1826
1869
  text: { type: "positional", required: false, description: "message text (short one-liner)" },
1827
- token: { type: "string", required: true, description: "channel token" },
1828
1870
  from: { type: "string", description: "your participant name (default: peer)" },
1829
- to: { type: "string", description: "direct it at these names (comma-separated); omit = broadcast" },
1871
+ to: {
1872
+ type: "string",
1873
+ description: "direct it at these names (comma-separated); omit = broadcast"
1874
+ },
1830
1875
  file: { type: "string", description: "read message body from this file (for large content)" },
1831
- attach: { type: "string", description: "attach file(s) (comma-separated paths); uploaded + carried on the message" },
1876
+ attach: {
1877
+ type: "string",
1878
+ description: "attach file(s) (comma-separated paths); uploaded + carried on the message"
1879
+ },
1832
1880
  ...common
1833
1881
  },
1834
1882
  run: async ({ args }) => {
@@ -1840,7 +1888,7 @@ var sendCommand = defineCommand({
1840
1888
  const attachments = [];
1841
1889
  for (const path of attachPaths(args.attach)) {
1842
1890
  attachments.push(
1843
- await client.uploadAttachment(args.channel, args.token, {
1891
+ await client.uploadAttachment(args.channel, {
1844
1892
  filename: basename2(path),
1845
1893
  contentType: contentTypeForPath(path),
1846
1894
  body: await openAsBlob2(path, { type: contentTypeForPath(path) })
@@ -1848,8 +1896,10 @@ var sendCommand = defineCommand({
1848
1896
  );
1849
1897
  }
1850
1898
  if (!text && attachments.length === 0)
1851
- throw new Error("nothing to send: provide text, --file <path>, --attach <file>, or pipe stdin");
1852
- const msg = await client.send(args.channel, args.token, {
1899
+ throw new Error(
1900
+ "nothing to send: provide text, --file <path>, --attach <file>, or pipe stdin"
1901
+ );
1902
+ const msg = await client.send(args.channel, {
1853
1903
  from: args.from ?? "peer",
1854
1904
  text: text ?? "",
1855
1905
  to: splitCsv(args.to),
@@ -1945,6 +1995,163 @@ var init = defineCommand({
1945
1995
  }
1946
1996
  });
1947
1997
 
1998
+ // src/commands/session/shared.ts
1999
+ import { homedir } from "node:os";
2000
+ import { join as join3 } from "node:path";
2001
+ var defaultWorktreeDir = (env = process.env) => env.BATON_WORKTREE_DIR ?? join3(env.XDG_DATA_HOME ?? join3(env.HOME ?? homedir(), ".local/share"), "baton", "worktrees");
2002
+ var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
2003
+ var resolveSession = async (client, projectId, handle) => {
2004
+ const asInt = Number(handle);
2005
+ if (Number.isInteger(asInt) && asInt > 0) {
2006
+ const byId = await client.sessions.get(asInt).catch(() => null);
2007
+ if (byId) return { id: byId.id, name: byId.name };
2008
+ }
2009
+ const byName = await client.sessions.findByName(projectId, handle);
2010
+ if (byName) return { id: byName.id, name: byName.name };
2011
+ throw new Error(`session "${handle}" not found in project ${projectId}`);
2012
+ };
2013
+
2014
+ // src/commands/loop/create.ts
2015
+ var loopCreateCommand = defineCommand({
2016
+ meta: { name: "create", description: "create a recurring scheduled message on a session" },
2017
+ args: {
2018
+ session: { type: "positional", required: true, description: "session int id or name" },
2019
+ message: {
2020
+ type: "string",
2021
+ required: true,
2022
+ description: "message sent to the session each beat"
2023
+ },
2024
+ every: {
2025
+ type: "string",
2026
+ required: true,
2027
+ description: "interval: 90s / 30m / 2h / 1d (min 30s)"
2028
+ },
2029
+ name: { type: "string", description: "optional label" },
2030
+ off: { type: "boolean", description: "create paused (disabled)" },
2031
+ project: { type: "string", description: "project id (overrides .baton.json)" },
2032
+ ...common
2033
+ },
2034
+ run: async ({ args }) => {
2035
+ const c = clientFor(args);
2036
+ const session2 = await resolveSession(c, resolveProjectId(args), args.session);
2037
+ const loop2 = await c.loops.create(session2.id, {
2038
+ message: args.message,
2039
+ intervalSec: parseDuration(args.every),
2040
+ name: args.name,
2041
+ enabled: !args.off
2042
+ });
2043
+ console.log(renderOne(loop2, fmtLoop, Boolean(args.json)));
2044
+ }
2045
+ });
2046
+
2047
+ // src/commands/loop/disable.ts
2048
+ var loopDisableCommand = defineCommand({
2049
+ meta: { name: "disable", description: "disable (pause) a loop" },
2050
+ args: {
2051
+ loop: { type: "positional", required: true, description: "loop int id" },
2052
+ ...common
2053
+ },
2054
+ run: async ({ args }) => {
2055
+ const c = clientFor(args);
2056
+ const loop2 = await c.loops.update(Number(args.loop), { enabled: false });
2057
+ console.log(renderOne(loop2, fmtLoop, Boolean(args.json)));
2058
+ }
2059
+ });
2060
+
2061
+ // src/commands/loop/enable.ts
2062
+ var loopEnableCommand = defineCommand({
2063
+ meta: { name: "enable", description: "enable (resume) a loop" },
2064
+ args: {
2065
+ loop: { type: "positional", required: true, description: "loop int id" },
2066
+ ...common
2067
+ },
2068
+ run: async ({ args }) => {
2069
+ const c = clientFor(args);
2070
+ const loop2 = await c.loops.update(Number(args.loop), { enabled: true });
2071
+ console.log(renderOne(loop2, fmtLoop, Boolean(args.json)));
2072
+ }
2073
+ });
2074
+
2075
+ // src/commands/loop/get.ts
2076
+ var loopGetCommand = defineCommand({
2077
+ meta: { name: "get", description: "show one loop" },
2078
+ args: {
2079
+ loop: { type: "positional", required: true, description: "loop int id" },
2080
+ ...common
2081
+ },
2082
+ run: async ({ args }) => {
2083
+ const c = clientFor(args);
2084
+ const loop2 = await c.loops.get(Number(args.loop));
2085
+ console.log(renderOne(loop2, fmtLoop, Boolean(args.json)));
2086
+ }
2087
+ });
2088
+
2089
+ // src/commands/loop/ls.ts
2090
+ var loopLsCommand = defineCommand({
2091
+ meta: { name: "ls", description: "list a session's loops" },
2092
+ args: {
2093
+ session: { type: "positional", required: true, description: "session int id or name" },
2094
+ project: { type: "string", description: "project id (overrides .baton.json)" },
2095
+ ...common
2096
+ },
2097
+ run: async ({ args }) => {
2098
+ const c = clientFor(args);
2099
+ const session2 = await resolveSession(c, resolveProjectId(args), args.session);
2100
+ const loops = await c.loops.listBySession(session2.id);
2101
+ console.log(renderList(loops, fmtLoop, Boolean(args.json)));
2102
+ }
2103
+ });
2104
+
2105
+ // src/commands/loop/rm.ts
2106
+ var loopRmCommand = defineCommand({
2107
+ meta: { name: "rm", description: "delete a loop" },
2108
+ args: {
2109
+ loop: { type: "positional", required: true, description: "loop int id" },
2110
+ ...common
2111
+ },
2112
+ run: async ({ args }) => {
2113
+ const c = clientFor(args);
2114
+ const id = Number(args.loop);
2115
+ await c.loops.remove(id);
2116
+ console.log(removed("loop", id, Boolean(args.json)));
2117
+ }
2118
+ });
2119
+
2120
+ // src/commands/loop/set.ts
2121
+ var loopSetCommand = defineCommand({
2122
+ meta: { name: "set", description: "update a loop (message / interval / name)" },
2123
+ args: {
2124
+ loop: { type: "positional", required: true, description: "loop int id" },
2125
+ message: { type: "string", description: "new message" },
2126
+ every: { type: "string", description: "new interval: 90s / 30m / 2h / 1d" },
2127
+ name: { type: "string", description: "new label" },
2128
+ ...common
2129
+ },
2130
+ run: async ({ args }) => {
2131
+ const c = clientFor(args);
2132
+ const patch2 = {};
2133
+ if (args.message !== void 0) patch2.message = args.message;
2134
+ if (args.every !== void 0 && args.every !== "") patch2.intervalSec = parseDuration(args.every);
2135
+ if (args.name !== void 0) patch2.name = args.name;
2136
+ const loop2 = await c.loops.update(Number(args.loop), patch2);
2137
+ console.log(renderOne(loop2, fmtLoop, Boolean(args.json)));
2138
+ }
2139
+ });
2140
+
2141
+ // src/commands/loop.ts
2142
+ var loop = defineCommand({
2143
+ meta: { name: "loop", description: "recurring scheduled messages that wake a session" },
2144
+ subCommands: {
2145
+ create: loopCreateCommand,
2146
+ ls: loopLsCommand,
2147
+ get: loopGetCommand,
2148
+ set: loopSetCommand,
2149
+ enable: loopEnableCommand,
2150
+ disable: loopDisableCommand,
2151
+ rm: loopRmCommand
2152
+ }
2153
+ });
2154
+
1948
2155
  // src/commands/project.ts
1949
2156
  var createProject = (c, input, json) => c.projects.create(input).then((p) => renderOne(p, fmtProject, json));
1950
2157
  var listProjects = (c, workspaceId, json) => c.projects.listByWorkspace(workspaceId).then((ps) => renderList(ps, fmtProject, json));
@@ -2022,7 +2229,7 @@ var project = defineCommand({
2022
2229
  // src/commands/relay.ts
2023
2230
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
2024
2231
  import { tmpdir as tmpdir2 } from "node:os";
2025
- import { join as join3 } from "node:path";
2232
+ import { join as join4 } from "node:path";
2026
2233
 
2027
2234
  // src/client/relay.ts
2028
2235
  var relayClient = (baseUrl) => {
@@ -2150,11 +2357,11 @@ var listenCommand2 = defineCommand({
2150
2357
  process.stdout.write(`${JSON.stringify(o)}
2151
2358
  `);
2152
2359
  };
2153
- const spillDir = join3(tmpdir2(), "hotline", args.channel);
2360
+ const spillDir = join4(tmpdir2(), "hotline", args.channel);
2154
2361
  mkdirSync2(spillDir, { recursive: true });
2155
2362
  const surface = (m) => {
2156
2363
  const text = typeof m.text === "string" ? m.text : "";
2157
- const file = join3(spillDir, `${m.seq}.txt`);
2364
+ const file = join4(spillDir, `${m.seq}.txt`);
2158
2365
  writeFileSync4(file, text);
2159
2366
  const truncated = text.length > PREVIEW2;
2160
2367
  emit({
@@ -2308,22 +2515,6 @@ var requirement = defineCommand({
2308
2515
  }
2309
2516
  });
2310
2517
 
2311
- // src/commands/session/shared.ts
2312
- import { homedir } from "node:os";
2313
- import { join as join4 } from "node:path";
2314
- var defaultWorktreeDir = (env = process.env) => env.BATON_WORKTREE_DIR ?? join4(env.XDG_DATA_HOME ?? join4(env.HOME ?? homedir(), ".local/share"), "baton", "worktrees");
2315
- var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
2316
- var resolveSession = async (client, projectId, handle) => {
2317
- const asInt = Number(handle);
2318
- if (Number.isInteger(asInt) && asInt > 0) {
2319
- const byId = await client.sessions.get(asInt).catch(() => null);
2320
- if (byId) return { id: byId.id, name: byId.name };
2321
- }
2322
- const byName = await client.sessions.findByName(projectId, handle);
2323
- if (byName) return { id: byName.id, name: byName.name };
2324
- throw new Error(`session "${handle}" not found in project ${projectId}`);
2325
- };
2326
-
2327
2518
  // src/commands/session/abort.ts
2328
2519
  var sessionAbortCommand = defineCommand({
2329
2520
  meta: {
@@ -3361,6 +3552,17 @@ var removeWorktree = (repo, worktreePath) => {
3361
3552
  } catch {
3362
3553
  }
3363
3554
  };
3555
+ var restoreWorktree = (repo, worktreePath, sessionCode) => {
3556
+ spawnSync2("git", ["-C", repo, "worktree", "prune"], { stdio: "pipe" });
3557
+ mkdirSync3(dirname(worktreePath), { recursive: true });
3558
+ const branch = `baton/${sessionCode}`;
3559
+ const reuse = spawnSync2("git", ["-C", repo, "worktree", "add", worktreePath, branch], {
3560
+ stdio: "pipe",
3561
+ encoding: "utf8"
3562
+ });
3563
+ if (reuse.status === 0) return;
3564
+ createWorktree({ repo, worktreePath, sessionCode, base: repoHeadBranch(repo) });
3565
+ };
3364
3566
 
3365
3567
  // src/worker/watchdog.ts
3366
3568
  var stale = (okAt, now, thresholdMs) => now - okAt > thresholdMs;
@@ -3448,6 +3650,9 @@ var runWorkerDaemon = async (cfg, client, signal) => {
3448
3650
  });
3449
3651
  await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
3450
3652
  log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
3653
+ } else if (!existsSync5(worktreePath)) {
3654
+ restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8));
3655
+ log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
3451
3656
  }
3452
3657
  ensureExcluded(repo, PROJECT_CONFIG_NAME);
3453
3658
  saveProjectConfig(join7(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg));
@@ -3624,7 +3829,10 @@ var worker = defineCommand({
3624
3829
  "no baton server \u2014 pass --url <url> (e.g. https://baton.fmap.dev/api), set BATON_URL, or run `baton init`"
3625
3830
  );
3626
3831
  if (!args.json) console.log(`registering against ${server}`);
3627
- const c = createClient(server);
3832
+ const cfgPath = configPathFromArgs(args);
3833
+ const fileToken = loadProjectConfigOrNull(cfgPath)?.worker?.apiToken;
3834
+ const auth = resolveAuth(process.env, fileToken);
3835
+ const c = createClient(server, auth && auth !== "cookie" ? auth : void 0);
3628
3836
  const hostname = osHostname();
3629
3837
  const machineId = readOrCreateMachineId();
3630
3838
  const name = args.name ?? hostname;
@@ -3637,7 +3845,7 @@ var worker = defineCommand({
3637
3845
  hostname,
3638
3846
  machineId
3639
3847
  },
3640
- configPathFromArgs(args)
3848
+ cfgPath
3641
3849
  );
3642
3850
  if (args.json) {
3643
3851
  console.log(toJson({ ...out, configPath, machineIdPath: machineIdPath() }));
@@ -3832,6 +4040,7 @@ var main = defineCommand({
3832
4040
  init,
3833
4041
  worker,
3834
4042
  session,
4043
+ loop,
3835
4044
  relay,
3836
4045
  channel,
3837
4046
  workspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "baton CLI — register/run a worker, manage projects/requirements/tasks/sessions",