agent-yes 1.230.0 → 1.231.0-beta.561.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.
Files changed (60) hide show
  1. package/dist/{SUPPORTED_CLIS-Bzr_YYLu.js → SUPPORTED_CLIS-BjVtNw1b.js} +2 -2
  2. package/dist/{SUPPORTED_CLIS-NP2basqU.js → SUPPORTED_CLIS-DxzlLYfn.js} +3 -3
  3. package/dist/{agent-yes.config-0G11aPWe.js → agent-yes.config-BhjzWavn.js} +2 -2
  4. package/dist/{agentShare-BgzKKAFp.js → agentShare-D0eaibsl.js} +7 -7
  5. package/dist/{callback-CPEbx_V0.js → callback-15YcBPKg.js} +5 -5
  6. package/dist/callback-BaJEHrei.js +12 -0
  7. package/dist/{callbackCore-B3DNe_SP.js → callbackCore-CcbVa8r2.js} +1 -1
  8. package/dist/{callbackCore-BlNdDILy.js → callbackCore-Me97qn68.js} +1 -1
  9. package/dist/channels-Boi4aNiY.js +540 -0
  10. package/dist/channels-Bu4DZzHk.js +87 -0
  11. package/dist/channels.js +255 -0
  12. package/dist/cli.js +9 -9
  13. package/dist/{configShared-YKfS6NMe.js → configShared-BXz2vi4Q.js} +1 -1
  14. package/dist/{cwdConflictWarn-CKUsQ5_o.js → cwdConflictWarn-ArYYtOGy.js} +1 -1
  15. package/dist/{e2e-DB7BDdEA.js → e2e-CYlKpfxa.js} +2 -2
  16. package/dist/{expose-GWRXTGpj.js → expose-IjObc0ox.js} +1 -1
  17. package/dist/{forkNested-BWdv2mXC.js → forkNested-D11UZMk8.js} +1 -1
  18. package/dist/index.js +2 -2
  19. package/dist/{nodeRuntime-CgXWHe9p.js → nodeRuntime-BF_kAOwO.js} +1 -1
  20. package/dist/{notifyDaemon-D7rYBD3I.js → notifyDaemon-48Cn30WQ.js} +6 -6
  21. package/dist/{openBrowser-CSfI8zCG.js → openBrowser-BkfVWvPx.js} +1 -1
  22. package/dist/peer-BM805vTt.js +4 -0
  23. package/dist/peer-Cdzjr8Jd.js +366 -0
  24. package/dist/{remotes-ZqD0FHl_.js → remotes-BM3Zm8Wh.js} +3 -3
  25. package/dist/{remotes-BCgowsiu.js → remotes-CgXeqjac.js} +3 -3
  26. package/dist/{rustBinary-DhHjlhHV.js → rustBinary-Dc9gGCCs.js} +2 -2
  27. package/dist/{schedule-BOhtrVIR.js → schedule-uUROzgDn.js} +6 -6
  28. package/dist/{serve-TPasyukD.js → serve-GhS1eaFx.js} +28 -28
  29. package/dist/{setup-DE8lMboL.js → setup-Dyho5uVS.js} +3 -3
  30. package/dist/share-CDER9qNq.js +4 -0
  31. package/dist/{share-CrvxZO7o.js → share-D4K0JnHg.js} +3 -3
  32. package/dist/{spawnGate-CMlGqVN4.js → spawnGate-CRMR19Ql.js} +2 -2
  33. package/dist/{spawnGate-5jC6eiqk.js → spawnGate-DQRr_Gsy.js} +1 -1
  34. package/dist/store-Ba3SsoOx.js +211 -0
  35. package/dist/{subcommands-BPFUbAqp.js → subcommands--OoWVuAI.js} +25 -18
  36. package/dist/{subcommands-B-2o8r2B.js → subcommands-UDpU4Fs-.js} +5 -5
  37. package/dist/{todoCli-Cjhx3DqQ.js → todoCli-C6o6EP1r.js} +1 -1
  38. package/dist/{tray-C8VfnS7s.js → tray-DaNqCsZM.js} +1 -1
  39. package/dist/{ts-Cl1DluDM.js → ts-B8eWE6oN.js} +5 -5
  40. package/dist/{versionChecker-qZA8b3cE.js → versionChecker-DteYJH7s.js} +2 -2
  41. package/dist/{webrtcLink-CjckgIZe.js → webrtcLink-CEYNs0pt.js} +2 -2
  42. package/dist/{webrtcRemote-BMzEt9Cp.js → webrtcRemote-BhJlxnpJ.js} +3 -3
  43. package/dist/{workspaceConfig-De5tgjeO.js → workspaceConfig-DWLYPDdx.js} +1 -1
  44. package/dist/{ws-DVnuBJ1A.js → ws-CYxF4mRV.js} +7 -7
  45. package/package.json +11 -3
  46. package/ts/channels/browser.ts +287 -0
  47. package/ts/channels/hlc.ts +67 -0
  48. package/ts/channels/index.ts +10 -0
  49. package/ts/channels/link.ts +103 -0
  50. package/ts/channels/op.ts +89 -0
  51. package/ts/channels/peer.ts +468 -0
  52. package/ts/channels/store.browser.ts +42 -0
  53. package/ts/channels/store.node.ts +72 -0
  54. package/ts/channels/store.ts +170 -0
  55. package/ts/channels.spec.ts +176 -0
  56. package/ts/channels.ts +671 -0
  57. package/ts/share.ts +6 -2
  58. package/ts/subcommands.ts +8 -0
  59. package/dist/callback-du5QneP1.js +0 -12
  60. package/dist/share-ieE7D0C1.js +0 -4
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-Cl1DluDM.js";
1
+ import { t as CLIS_CONFIG } from "./ts-B8eWE6oN.js";
2
2
 
3
3
  //#region ts/SUPPORTED_CLIS.ts
4
4
  const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
5
 
6
6
  //#endregion
7
7
  export { SUPPORTED_CLIS as t };
8
- //# sourceMappingURL=SUPPORTED_CLIS-Bzr_YYLu.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-BjVtNw1b.js.map
@@ -1,11 +1,11 @@
1
- import "./ts-Cl1DluDM.js";
1
+ import "./ts-B8eWE6oN.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-qZA8b3cE.js";
3
+ import "./versionChecker-DteYJH7s.js";
4
4
  import "./todoAutomation-x6guaQXc.js";
5
5
  import "./JsonlStore-CWOA3aDH.js";
6
6
  import "./globalPidIndex-Ca4sTr1b.js";
7
7
  import "./pidStore-Hjba3K6Q.js";
8
8
  import "./messageLog-zVGWLX7w.js";
9
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Bzr_YYLu.js";
9
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BjVtNw1b.js";
10
10
 
11
11
  export { SUPPORTED_CLIS };
@@ -1,5 +1,5 @@
1
1
  import { n as logger } from "./logger-CDIsZ-Pp.js";
2
- import { n as normalizeAgentYesConfig, t as loadSharedCliDefaults } from "./configShared-YKfS6NMe.js";
2
+ import { n as normalizeAgentYesConfig, t as loadSharedCliDefaults } from "./configShared-BXz2vi4Q.js";
3
3
  import os from "node:os";
4
4
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
@@ -226,4 +226,4 @@ async function getDefaultConfig() {
226
226
 
227
227
  //#endregion
228
228
  export { agent_yes_config_default as default };
229
- //# sourceMappingURL=agent-yes.config-0G11aPWe.js.map
229
+ //# sourceMappingURL=agent-yes.config-BhjzWavn.js.map
@@ -1,12 +1,12 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
2
  import "./globalPidIndex-Ca4sTr1b.js";
3
3
  import "./messageLog-zVGWLX7w.js";
4
- import "./configShared-YKfS6NMe.js";
5
- import { N as resolveOne } from "./subcommands-BPFUbAqp.js";
6
- import "./e2e-DB7BDdEA.js";
7
- import "./webrtcLink-CjckgIZe.js";
8
- import "./remotes-ZqD0FHl_.js";
9
- import { r as startShare } from "./share-CrvxZO7o.js";
4
+ import "./e2e-CYlKpfxa.js";
5
+ import "./configShared-BXz2vi4Q.js";
6
+ import { N as resolveOne } from "./subcommands--OoWVuAI.js";
7
+ import "./webrtcLink-CEYNs0pt.js";
8
+ import "./remotes-BM3Zm8Wh.js";
9
+ import { a as startShare } from "./share-D4K0JnHg.js";
10
10
 
11
11
  //#region ts/agentShare.ts
12
12
  const MAX_SHARES = 8;
@@ -229,4 +229,4 @@ function transformEvent(rawEvent, agentId, forwarded) {
229
229
 
230
230
  //#endregion
231
231
  export { createScopedShare, listShares, revokeAllShares, revokeShare };
232
- //# sourceMappingURL=agentShare-BgzKKAFp.js.map
232
+ //# sourceMappingURL=agentShare-D0eaibsl.js.map
@@ -1,6 +1,6 @@
1
1
  import { t as agentYesHome } from "./agentYesHome-CtHb5b71.js";
2
- import { N as resolveOne } from "./subcommands-BPFUbAqp.js";
3
- import { a as mintCapability, o as parseExpires } from "./callbackCore-BlNdDILy.js";
2
+ import { N as resolveOne } from "./subcommands--OoWVuAI.js";
3
+ import { a as mintCapability, o as parseExpires } from "./callbackCore-Me97qn68.js";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
@@ -72,7 +72,7 @@ async function resolveBase(flag) {
72
72
  if (flag) return flag;
73
73
  if (process.env.AGENT_YES_CALLBACK_BASE) return process.env.AGENT_YES_CALLBACK_BASE;
74
74
  try {
75
- const { resolveLocalServeUrl } = await import("./serve-TPasyukD.js");
75
+ const { resolveLocalServeUrl } = await import("./serve-GhS1eaFx.js");
76
76
  const url = await resolveLocalServeUrl();
77
77
  if (url) return url;
78
78
  } catch {}
@@ -170,7 +170,7 @@ async function cmdCallback(rest) {
170
170
  createdAt: Date.now()
171
171
  };
172
172
  saveStore(all);
173
- const { buildSnippet } = await import("./callbackCore-B3DNe_SP.js");
173
+ const { buildSnippet } = await import("./callbackCore-CcbVa8r2.js");
174
174
  w(`callback ${id} → ${record.cli} #${record.pid} @ ${record.cwd}`);
175
175
  w(`expires: ${new Date(exp).toISOString()} (${expiresSpec})`);
176
176
  w(`endpoint: ${base.replace(/\/+$/, "")}/cb/${cap}`);
@@ -187,4 +187,4 @@ async function cmdCallback(rest) {
187
187
 
188
188
  //#endregion
189
189
  export { loadOrCreateCallbackSecret as i, isCallbackRevoked as n, loadCallbackSecretReadOnly as r, cmdCallback as t };
190
- //# sourceMappingURL=callback-CPEbx_V0.js.map
190
+ //# sourceMappingURL=callback-15YcBPKg.js.map
@@ -0,0 +1,12 @@
1
+ import "./logger-CDIsZ-Pp.js";
2
+ import "./globalPidIndex-Ca4sTr1b.js";
3
+ import "./messageLog-zVGWLX7w.js";
4
+ import "./e2e-CYlKpfxa.js";
5
+ import "./configShared-BXz2vi4Q.js";
6
+ import "./subcommands--OoWVuAI.js";
7
+ import "./webrtcLink-CEYNs0pt.js";
8
+ import "./remotes-BM3Zm8Wh.js";
9
+ import "./callbackCore-Me97qn68.js";
10
+ import { i as loadOrCreateCallbackSecret, n as isCallbackRevoked, r as loadCallbackSecretReadOnly, t as cmdCallback } from "./callback-15YcBPKg.js";
11
+
12
+ export { cmdCallback };
@@ -1,3 +1,3 @@
1
- import { a as mintCapability, i as frameVisitorMessage, n as MAX_EXPIRES_MS, o as parseExpires, r as buildSnippet, s as verifyCapability, t as MAX_CALLBACK_MSG_BYTES } from "./callbackCore-BlNdDILy.js";
1
+ import { a as mintCapability, i as frameVisitorMessage, n as MAX_EXPIRES_MS, o as parseExpires, r as buildSnippet, s as verifyCapability, t as MAX_CALLBACK_MSG_BYTES } from "./callbackCore-Me97qn68.js";
2
2
 
3
3
  export { buildSnippet };
@@ -140,4 +140,4 @@ function buildSnippet(opts) {
140
140
 
141
141
  //#endregion
142
142
  export { mintCapability as a, frameVisitorMessage as i, MAX_EXPIRES_MS as n, parseExpires as o, buildSnippet as r, verifyCapability as s, MAX_CALLBACK_MSG_BYTES as t };
143
- //# sourceMappingURL=callbackCore-BlNdDILy.js.map
143
+ //# sourceMappingURL=callbackCore-Me97qn68.js.map
@@ -0,0 +1,540 @@
1
+ import "./e2e-CYlKpfxa.js";
2
+ import { a as formatChannelWebLink, c as secretFromTopic, i as formatChannelLink, n as deriveChannelId, o as isChannelLink, r as deriveRoom, s as parseChannelLink } from "./channels-Bu4DZzHk.js";
3
+ import { a as renderThread, c as makeOp, f as hlcSend, n as maxHlc, o as sortOps, r as mergeOps, s as isValidOp } from "./store-Ba3SsoOx.js";
4
+ import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { randomBytes } from "crypto";
8
+
9
+ //#region ts/channels/store.node.ts
10
+ /** Rewrite (dedup + sort) once the file grows past this many raw lines. */
11
+ const COMPACT_AT_LINES = 4e3;
12
+ /** Path to a channel's jsonl replica under a project cwd. */
13
+ function channelFilePath(cwd, channelId) {
14
+ return path.join(cwd, ".agent-yes", `ch-${channelId}.jsonl`);
15
+ }
16
+ /** Read + parse a channel's ops, deduped and HLC-sorted. Missing file → []. */
17
+ async function readOps(cwd, channelId) {
18
+ let raw;
19
+ try {
20
+ raw = await readFile(channelFilePath(cwd, channelId), "utf-8");
21
+ } catch {
22
+ return [];
23
+ }
24
+ const byId = /* @__PURE__ */ new Map();
25
+ for (const line of raw.split("\n")) {
26
+ const t = line.trim();
27
+ if (!t) continue;
28
+ try {
29
+ const op = JSON.parse(t);
30
+ if (isValidOp(op) && !byId.has(op.id)) byId.set(op.id, op);
31
+ } catch {}
32
+ }
33
+ return sortOps([...byId.values()]);
34
+ }
35
+ /**
36
+ * Append `incoming` to a channel, deduping against what's already stored.
37
+ * Returns the ops that were genuinely new (so the daemon can rebroadcast just
38
+ * those). Opportunistically compacts when the file accumulates duplicate lines
39
+ * or grows large, keeping it bounded despite append-only writes.
40
+ */
41
+ async function appendOps(cwd, channelId, incoming) {
42
+ const valid = incoming.filter(isValidOp);
43
+ if (valid.length === 0) return [];
44
+ const file = channelFilePath(cwd, channelId);
45
+ await mkdir(path.dirname(file), { recursive: true });
46
+ const existing = await readOps(cwd, channelId);
47
+ const { added } = mergeOps(existing, valid);
48
+ if (added.length === 0) return [];
49
+ await appendFile(file, added.map((op) => JSON.stringify(op)).join("\n") + "\n");
50
+ if (existing.length + valid.length > COMPACT_AT_LINES) await writeFile(file, sortOps([...existing, ...added]).map((op) => JSON.stringify(op)).join("\n") + "\n");
51
+ return added;
52
+ }
53
+
54
+ //#endregion
55
+ //#region ts/channels.ts
56
+ /**
57
+ * `ay ch` — channels: local-first, end-to-end threads where AI agents and humans
58
+ * talk on a topic. No server ever stores a message; every participant keeps a
59
+ * full replica as an append-only CRDT log, cwd-scoped like the rest of
60
+ * agent-yes's per-project state:
61
+ *
62
+ * ay ch mk <topic> [--sighost H] [--name N] [--role agent|human] [--salt HEX]
63
+ * ay ch join <link> [--as <topic>] [--name N] [--role R]
64
+ * ay ch ls [--json]
65
+ * ay ch rm <topic>
66
+ * ay ch send <topic> <text…> [--name N] [--role R]
67
+ * ay ch read <topic> [-n N] [--json]
68
+ * ay ch head <topic> [-n N]
69
+ * ay ch tail <topic> [-n N] [-f]
70
+ * ay ch sync <topic> [--quiet] — hold the WebRTC mesh (live send/receive)
71
+ * ay ch pipe <topic> — sync + bridge stdin→send, inbound→stdout
72
+ *
73
+ * The read/write verbs are pure-local (they only touch the jsonl replica). Live
74
+ * delivery is a separate `sync` peer that holds the WebRTC mesh, appends inbound
75
+ * ops, and broadcasts newly-appeared local ops — so send/tail COMPOSE with a
76
+ * running sync through the shared replica file, no IPC/daemon. The browser side
77
+ * (Phase 3) joins the same mesh with its own peer, so agent↔human chat is truly
78
+ * peer-to-peer with no central bridge.
79
+ *
80
+ * The channels core (ts/channels/) is isomorphic and reused verbatim by the
81
+ * browser lib; this file is the Node CLI shell over it, mirroring ts/ws.ts.
82
+ */
83
+ const REG_SCHEMA = "ay-ch/v1";
84
+ /** An agent (AGENT_YES_PID set) defaults to role "agent"; a human shell to "human". */
85
+ function defaultRole() {
86
+ return process.env.AGENT_YES_PID ? "agent" : "human";
87
+ }
88
+ /** Default display name: $AY_CH_NAME, else the OS username, else "anon". */
89
+ function defaultName() {
90
+ if (process.env.AY_CH_NAME) return process.env.AY_CH_NAME;
91
+ try {
92
+ return os.userInfo().username || "anon";
93
+ } catch {
94
+ return "anon";
95
+ }
96
+ }
97
+ function validRole(v) {
98
+ if (v === "agent" || v === "human") return v;
99
+ throw new Error(`--role must be "agent" or "human"`);
100
+ }
101
+ function registryPath(cwd) {
102
+ return path.join(cwd, ".agent-yes", "channels.json");
103
+ }
104
+ async function readRegistry(cwd) {
105
+ try {
106
+ const reg = JSON.parse(await readFile(registryPath(cwd), "utf-8"));
107
+ if (reg && typeof reg === "object" && reg.channels) return reg;
108
+ } catch {}
109
+ return {
110
+ schema: REG_SCHEMA,
111
+ channels: {}
112
+ };
113
+ }
114
+ async function writeRegistry(cwd, reg) {
115
+ const file = registryPath(cwd);
116
+ await mkdir(path.dirname(file), { recursive: true });
117
+ await writeFile(file, JSON.stringify(reg, null, 2) + "\n");
118
+ await chmod(file, 384).catch(() => {});
119
+ }
120
+ /**
121
+ * Resolve a `<topic|link>` operand. A registered topic yields its full entry; a
122
+ * bare invite link yields just its channelId (read-only — sending needs an
123
+ * identity, i.e. `ay ch join` first). Anything else is an error.
124
+ */
125
+ async function resolveChannel(reg, arg) {
126
+ const entry = reg.channels[arg];
127
+ if (entry) return {
128
+ channelId: entry.channelId,
129
+ entry
130
+ };
131
+ if (isChannelLink(arg)) {
132
+ const link = parseChannelLink(arg);
133
+ if (link) return {
134
+ channelId: await deriveChannelId(link.s),
135
+ entry: null
136
+ };
137
+ }
138
+ throw new Error(`no channel "${arg}" — see 'ay ch ls', or 'ay ch join <link>'`);
139
+ }
140
+ /** One rendered thread line: `HH:MM:SS name(a): text 👍2`. Pure, for tests. */
141
+ function formatMessage(m) {
142
+ return `${new Date(m.ms).toISOString().slice(11, 19)} ${`${m.name}(${m.role[0]})`}: ${m.deleted ? "(deleted)" : m.text}${m.reactions.length ? " " + m.reactions.map((r) => `${r.emoji}${r.by.length > 1 ? r.by.length : ""}`).join(" ") : ""}`;
143
+ }
144
+ function parseFlags(args, known) {
145
+ const flags = {};
146
+ const positional = [];
147
+ for (let i = 0; i < args.length; i++) {
148
+ const a = args[i];
149
+ if (!a.startsWith("-") || a === "-") {
150
+ positional.push(a);
151
+ continue;
152
+ }
153
+ const isShortN = a === "-n";
154
+ const eq = a.indexOf("=");
155
+ const name = isShortN ? "limit" : eq === -1 ? a.replace(/^--?/, "") : a.slice(a.startsWith("--") ? 2 : 1, eq);
156
+ const kind = known[name];
157
+ if (!kind) throw new Error(`unknown flag ${a}`);
158
+ if (kind === "bool") {
159
+ if (eq !== -1) throw new Error(`--${name} takes no value`);
160
+ flags[name] = true;
161
+ } else {
162
+ const v = eq !== -1 ? a.slice(eq + 1) : args[++i];
163
+ if (v === void 0) throw new Error(`${a} requires a value`);
164
+ flags[name] = v;
165
+ }
166
+ }
167
+ return {
168
+ flags,
169
+ positional
170
+ };
171
+ }
172
+ function limitOf(flags) {
173
+ if (flags.limit === void 0) return void 0;
174
+ const n = Number(flags.limit);
175
+ if (!Number.isInteger(n) || n < 0) throw new Error(`-n/--limit must be a non-negative integer`);
176
+ return n;
177
+ }
178
+ async function cmdChMk(cwd, args) {
179
+ const { flags, positional } = parseFlags(args, {
180
+ sighost: "value",
181
+ name: "value",
182
+ role: "value",
183
+ salt: "value",
184
+ topic: "value"
185
+ });
186
+ const topic = positional[0];
187
+ if (!topic || positional.length > 1) throw new Error("usage: ay ch mk <name> [--topic <string>] [--sighost H] [--name N] [--role R] [--salt HEX]");
188
+ const reg = await readRegistry(cwd);
189
+ if (reg.channels[topic]) throw new Error(`channel "${topic}" already exists (ay ch rm ${topic} to replace)`);
190
+ const s = typeof flags.topic === "string" ? await secretFromTopic(flags.topic) : typeof flags.salt === "string" ? flags.salt : randomBytes(32).toString("hex");
191
+ const [channelId, room] = await Promise.all([deriveChannelId(s), deriveRoom(s)]);
192
+ const entry = {
193
+ topic,
194
+ channelId,
195
+ room,
196
+ sighost: (typeof flags.sighost === "string" ? flags.sighost : void 0) ?? "s.agent-yes.com",
197
+ s,
198
+ author: randomBytes(8).toString("hex"),
199
+ name: typeof flags.name === "string" ? flags.name : defaultName(),
200
+ role: flags.role ? validRole(flags.role) : defaultRole(),
201
+ createdAt: Date.now()
202
+ };
203
+ reg.channels[topic] = entry;
204
+ await writeRegistry(cwd, reg);
205
+ const link = formatChannelLink({
206
+ sighost: entry.sighost,
207
+ room,
208
+ s
209
+ });
210
+ process.stdout.write(`created channel "${topic}" (${entry.name}, ${entry.role})\n invite (CLI): ${link}\n invite (browser): ${formatChannelWebLink({
211
+ sighost: entry.sighost,
212
+ room,
213
+ s
214
+ })}\n\n share the invite; others join with: ay ch join '${link}'\n`);
215
+ return 0;
216
+ }
217
+ async function cmdChJoin(cwd, args) {
218
+ const { flags, positional } = parseFlags(args, {
219
+ as: "value",
220
+ name: "value",
221
+ role: "value"
222
+ });
223
+ const linkStr = positional[0];
224
+ if (!linkStr || positional.length > 1) throw new Error("usage: ay ch join <link> [--as <topic>] [--name N] [--role R]");
225
+ const link = parseChannelLink(linkStr);
226
+ if (!link) throw new Error(`not a channel invite link: ${linkStr}`);
227
+ const channelId = await deriveChannelId(link.s);
228
+ const topic = typeof flags.as === "string" ? flags.as : `ch-${link.room.slice(0, 10)}`;
229
+ const reg = await readRegistry(cwd);
230
+ const existing = reg.channels[topic];
231
+ if (existing && existing.channelId !== channelId) throw new Error(`topic "${topic}" is already bound to a different channel — pick another --as`);
232
+ const entry = existing ?? {
233
+ topic,
234
+ channelId,
235
+ room: link.room,
236
+ sighost: link.sighost,
237
+ s: link.s,
238
+ author: randomBytes(8).toString("hex"),
239
+ name: typeof flags.name === "string" ? flags.name : defaultName(),
240
+ role: flags.role ? validRole(flags.role) : defaultRole(),
241
+ createdAt: Date.now()
242
+ };
243
+ if (typeof flags.name === "string") entry.name = flags.name;
244
+ if (flags.role) entry.role = validRole(flags.role);
245
+ reg.channels[topic] = entry;
246
+ await writeRegistry(cwd, reg);
247
+ process.stdout.write(`joined channel "${topic}" (${entry.name}, ${entry.role})\n`);
248
+ return 0;
249
+ }
250
+ async function cmdChLs(cwd, args) {
251
+ const { flags, positional } = parseFlags(args, { json: "bool" });
252
+ if (positional.length) throw new Error("ay ch ls takes no positional args");
253
+ const reg = await readRegistry(cwd);
254
+ const topics = Object.keys(reg.channels).sort();
255
+ const rows = await Promise.all(topics.map(async (topic) => {
256
+ const e = reg.channels[topic];
257
+ const msgs = renderThread(await readOps(cwd, e.channelId));
258
+ const last = msgs.length ? msgs[msgs.length - 1] : null;
259
+ return {
260
+ topic,
261
+ channelId: e.channelId,
262
+ name: e.name,
263
+ role: e.role,
264
+ messages: msgs.length,
265
+ lastMs: last ? last.ms : null
266
+ };
267
+ }));
268
+ if (flags.json) {
269
+ process.stdout.write(JSON.stringify({
270
+ schema: REG_SCHEMA,
271
+ channels: rows
272
+ }, null, 2) + "\n");
273
+ return 0;
274
+ }
275
+ if (rows.length === 0) {
276
+ process.stderr.write(`no channels in ${cwd} — 'ay ch mk <topic>' or 'ay ch join <link>'\n`);
277
+ return 0;
278
+ }
279
+ const w = Math.max(5, ...rows.map((r) => r.topic.length));
280
+ process.stdout.write(`${"TOPIC".padEnd(w)} ${"MSGS".padStart(5)} LAST\n`);
281
+ for (const r of rows) {
282
+ const last = r.lastMs ? new Date(r.lastMs).toISOString().slice(0, 19).replace("T", " ") : "-";
283
+ process.stdout.write(`${r.topic.padEnd(w)} ${String(r.messages).padStart(5)} ${last}\n`);
284
+ }
285
+ return 0;
286
+ }
287
+ async function cmdChRm(cwd, args) {
288
+ const { positional } = parseFlags(args, {});
289
+ const topic = positional[0];
290
+ if (!topic || positional.length > 1) throw new Error("usage: ay ch rm <topic>");
291
+ const reg = await readRegistry(cwd);
292
+ const entry = reg.channels[topic];
293
+ if (!entry) throw new Error(`no channel "${topic}"`);
294
+ delete reg.channels[topic];
295
+ await writeRegistry(cwd, reg);
296
+ await rm(channelFilePath(cwd, entry.channelId), { force: true });
297
+ process.stdout.write(`removed channel "${topic}" (local replica deleted; peers unaffected)\n`);
298
+ return 0;
299
+ }
300
+ async function cmdChSend(cwd, args) {
301
+ const { flags, positional } = parseFlags(args, {
302
+ name: "value",
303
+ role: "value"
304
+ });
305
+ const topic = positional[0];
306
+ const text = positional.slice(1).join(" ");
307
+ if (!topic || !text) throw new Error("usage: ay ch send <topic> <text…>");
308
+ const { channelId, entry } = await resolveChannel(await readRegistry(cwd), topic);
309
+ if (!entry) throw new Error(`join "${topic}" before sending: ay ch join <link>`);
310
+ const hlc = hlcSend(maxHlc(await readOps(cwd, channelId)), Date.now(), entry.author);
311
+ await appendOps(cwd, channelId, [makeOp({
312
+ author: entry.author,
313
+ name: typeof flags.name === "string" ? flags.name : entry.name,
314
+ role: flags.role ? validRole(flags.role) : entry.role,
315
+ hlc,
316
+ kind: "msg",
317
+ body: text
318
+ })]);
319
+ return 0;
320
+ }
321
+ async function readAndRender(cwd, channelId) {
322
+ return renderThread(await readOps(cwd, channelId));
323
+ }
324
+ async function cmdChRead(cwd, args, mode) {
325
+ const { flags, positional } = parseFlags(args, {
326
+ limit: "value",
327
+ json: "bool",
328
+ follow: "bool",
329
+ f: "bool"
330
+ });
331
+ const topic = positional[0];
332
+ if (!topic || positional.length > 1) throw new Error(`usage: ay ch ${mode} <topic> [-n N]`);
333
+ const { channelId } = await resolveChannel(await readRegistry(cwd), topic);
334
+ const n = limitOf(flags);
335
+ let msgs = await readAndRender(cwd, channelId);
336
+ if (mode === "head") msgs = msgs.slice(0, n ?? 10);
337
+ else if (mode === "tail") msgs = msgs.slice(-(n ?? 96));
338
+ else if (n !== void 0) msgs = msgs.slice(-n);
339
+ if (flags.json) process.stdout.write(JSON.stringify(msgs, null, 2) + "\n");
340
+ else for (const m of msgs) process.stdout.write(formatMessage(m) + "\n");
341
+ if (mode === "tail" && (flags.follow || flags.f)) await followChannel(cwd, channelId, new Set(msgs.map((m) => m.id)));
342
+ return 0;
343
+ }
344
+ /**
345
+ * Follow a channel, printing messages as they appear. Phase 1 polls the local
346
+ * replica (fs.watch is unreliable in long-lived daemons — see the fswatch-dies
347
+ * note), so it surfaces same-cwd appends; Phase 2's daemon feeds it live peer
348
+ * traffic. Runs until SIGINT.
349
+ */
350
+ function followChannel(cwd, channelId, seen) {
351
+ return new Promise((resolve) => {
352
+ const timer = setInterval(async () => {
353
+ const msgs = await readAndRender(cwd, channelId);
354
+ for (const m of msgs) {
355
+ if (seen.has(m.id)) continue;
356
+ seen.add(m.id);
357
+ process.stdout.write(formatMessage(m) + "\n");
358
+ }
359
+ }, 500);
360
+ const stop = () => {
361
+ clearInterval(timer);
362
+ resolve();
363
+ };
364
+ process.once("SIGINT", stop);
365
+ process.once("SIGTERM", stop);
366
+ });
367
+ }
368
+ /** Store adapter the mesh peer drives (persist + dedup, returns new ops). */
369
+ function nodeStore(cwd, channelId) {
370
+ return {
371
+ all: () => readOps(cwd, channelId),
372
+ append: (ops) => appendOps(cwd, channelId, ops)
373
+ };
374
+ }
375
+ /** One inbound op as a line (only messages are surfaced live). */
376
+ function printOp(op) {
377
+ if (op.kind !== "msg") return;
378
+ const t = new Date(Number(op.hlc.split(".")[0])).toISOString().slice(11, 19);
379
+ process.stdout.write(`${t} ${op.name}(${op.role[0]}): ${op.body ?? ""}\n`);
380
+ }
381
+ /**
382
+ * Hold the WebRTC mesh for a channel: append inbound ops to the local replica and
383
+ * broadcast newly-appeared LOCAL ops (e.g. from a separate `ay ch send`) to peers.
384
+ * All coordination is through the jsonl file — no IPC — so `send`/`tail` compose
385
+ * with a running `sync` unchanged. Resolves when stopped (SIGINT/SIGTERM) or when
386
+ * `extraStdin` (pipe mode) drives it. Returns the started peer + a stop fn.
387
+ */
388
+ async function runMesh(cwd, entry, opts) {
389
+ const { ChannelPeer } = await import("./peer-BM805vTt.js");
390
+ const { importRTC, getIceServers } = await import("./share-CDER9qNq.js");
391
+ const rtc = await importRTC();
392
+ const seen = new Set((await readOps(cwd, entry.channelId)).map((o) => o.id));
393
+ const peer = new ChannelPeer({
394
+ room: entry.room,
395
+ sighost: entry.sighost,
396
+ s: entry.s,
397
+ rtc,
398
+ iceServers: getIceServers,
399
+ store: nodeStore(cwd, entry.channelId),
400
+ onOp: (op) => {
401
+ seen.add(op.id);
402
+ opts.onInbound?.(op);
403
+ if (!opts.quiet) printOp(op);
404
+ },
405
+ onPeers: (n) => process.stderr.write(`[ch] peers: ${n}\n`)
406
+ });
407
+ await peer.start();
408
+ const poll = setInterval(() => {
409
+ (async () => {
410
+ for (const op of await readOps(cwd, entry.channelId)) {
411
+ if (seen.has(op.id)) continue;
412
+ seen.add(op.id);
413
+ await peer.publish(op);
414
+ }
415
+ })();
416
+ }, 500);
417
+ let resolveDone;
418
+ const done = new Promise((r) => resolveDone = r);
419
+ const stop = () => {
420
+ clearInterval(poll);
421
+ peer.close();
422
+ resolveDone();
423
+ };
424
+ process.once("SIGINT", stop);
425
+ process.once("SIGTERM", stop);
426
+ return {
427
+ stop,
428
+ done,
429
+ peer
430
+ };
431
+ }
432
+ async function cmdChSync(cwd, args) {
433
+ const { flags, positional } = parseFlags(args, { quiet: "bool" });
434
+ const topic = positional[0];
435
+ if (!topic || positional.length > 1) throw new Error("usage: ay ch sync <topic> [--quiet]");
436
+ const { entry } = await resolveChannel(await readRegistry(cwd), topic);
437
+ if (!entry) throw new Error(`join "${topic}" before syncing: ay ch join <link>`);
438
+ process.stderr.write(`syncing "${topic}" over the mesh — Ctrl-C to stop\n`);
439
+ const { done } = await runMesh(cwd, entry, { quiet: !!flags.quiet });
440
+ await done;
441
+ return 0;
442
+ }
443
+ async function cmdChPipe(cwd, args) {
444
+ const { positional } = parseFlags(args, {});
445
+ const topic = positional[0];
446
+ if (!topic || positional.length > 1) throw new Error("usage: ay ch pipe <topic>");
447
+ const { entry } = await resolveChannel(await readRegistry(cwd), topic);
448
+ if (!entry) throw new Error(`join "${topic}" before piping: ay ch join <link>`);
449
+ const { done } = await runMesh(cwd, entry, {});
450
+ const rl = (await import("readline")).createInterface({ input: process.stdin });
451
+ rl.on("line", (line) => {
452
+ const text = line.trimEnd();
453
+ if (!text) return;
454
+ (async () => {
455
+ const hlc = hlcSend(maxHlc(await readOps(cwd, entry.channelId)), Date.now(), entry.author);
456
+ await appendOps(cwd, entry.channelId, [makeOp({
457
+ author: entry.author,
458
+ name: entry.name,
459
+ role: entry.role,
460
+ hlc,
461
+ kind: "msg",
462
+ body: text
463
+ })]);
464
+ })();
465
+ });
466
+ await done;
467
+ rl.close();
468
+ return 0;
469
+ }
470
+ async function cmdChEmbed(cwd, args) {
471
+ const { flags, positional } = parseFlags(args, { host: "value" });
472
+ const topic = positional[0];
473
+ if (!topic || positional.length > 1) throw new Error("usage: ay ch embed <topic> [--host <console-host>]");
474
+ const { entry } = await resolveChannel(await readRegistry(cwd), topic);
475
+ if (!entry) throw new Error(`join "${topic}" before embedding: ay ch join <link>`);
476
+ const consoleHost = typeof flags.host === "string" ? flags.host : "agent-yes.com";
477
+ const link = formatChannelLink({
478
+ sighost: entry.sighost,
479
+ room: entry.room,
480
+ s: entry.s
481
+ });
482
+ process.stdout.write(`<!-- ay channel: ${topic} — anyone who can read this snippet can join -->\n<script type="module">\n import AyChannel from "https://${consoleHost}/w/channels.js";\n new AyChannel(${JSON.stringify(link)}).mount();\n<\/script>\n`);
483
+ return 0;
484
+ }
485
+ /** The page bookmarklet: derive a channel from the page URL and mount the widget. */
486
+ function buildBookmarklet(host, sighost) {
487
+ return `javascript:(async()=>{try{const{AyChannel}=await import('https://${host}/w/channels.js');const t=prompt('ay channel — topic (same topic = same room):',location.href);if(!t)return;let n=localStorage.getItem('ay29ch-name');if(!n){n=prompt('Your name:','guest')||'guest';localStorage.setItem('ay29ch-name',n);}const ch=await AyChannel.fromTopic(t,{sighost:'${sighost}',name:n});ch.mount(document.body,{open:true});}catch(e){alert('ay channel could not load — this page\\'s CSP may block it (works on your own apps, localhost, blogs, docs).\\n'+(e&&e.message||e));}})()`;
488
+ }
489
+ async function cmdChBookmarklet(_cwd, args) {
490
+ const { flags, positional } = parseFlags(args, {
491
+ host: "value",
492
+ sighost: "value"
493
+ });
494
+ if (positional.length) throw new Error("usage: ay ch bookmarklet [--host H] [--sighost H]");
495
+ const host = typeof flags.host === "string" ? flags.host : "agent-yes.com";
496
+ const sighost = typeof flags.sighost === "string" ? flags.sighost : "s.agent-yes.com";
497
+ process.stdout.write(buildBookmarklet(host, sighost) + "\n");
498
+ process.stderr.write("\n Make a new bookmark and paste the line above as its URL. Click it on any\n page to drop an ay-channel chat window joined to that page's URL (edit the\n prompt to change the topic). Same topic = same room, no invite needed.\n Heads-up: strict site CSP (GitHub/Google/…) blocks it; it works on your own\n apps, localhost, blogs, and docs.\n");
499
+ return 0;
500
+ }
501
+ function chHelp() {
502
+ process.stdout.write("ay ch - local-first E2E channels for AI ↔ humans (per-cwd, no server storage)\n\n ay ch mk <topic> [--name N] [--role R] create a channel, print an invite link\n ay ch join <link> [--as <topic>] join a channel from an invite link\n ay ch ls [--json] list channels in this project (+message counts)\n ay ch rm <topic> delete the local replica (peers unaffected)\n ay ch send <topic> <text…> post a message\n ay ch read <topic> [-n N] [--json] print the thread (last N)\n ay ch head <topic> [-n N] first N messages\n ay ch tail <topic> [-n N] [-f] last N (96), -f to follow\n ay ch sync <topic> [--quiet] hold the WebRTC mesh: live send/receive (Ctrl-C to stop)\n ay ch pipe <topic> sync + bridge stdin→send and inbound→stdout\n ay ch embed <topic> [--host H] print an HTML snippet embedding a floating chat widget\n ay ch bookmarklet [--host H] print a bookmarklet: any page joins a channel by its URL\n\n URL-topic: 'ay ch mk <name> --topic <string>' derives the SAME channel a\n bookmarklet on that URL joins (topic = public membership)\n identity: --name defaults to $AY_CH_NAME/OS user; --role to agent (in an agent) or human\n storage: <cwd>/.agent-yes/ch-<id>.jsonl (a full CRDT replica; cwd-scoped)\n live: run 'ay ch sync <topic>' (foreground/backgrounded) to join the mesh; then\n 'ay ch send/tail' compose with it through the shared replica file — no daemon\n");
503
+ return 0;
504
+ }
505
+ /** `ay ch <sub> …` dispatcher (called from runSubcommand). */
506
+ async function cmdCh(args) {
507
+ const cwd = process.cwd();
508
+ const sub = args[0];
509
+ const rest = args.slice(1);
510
+ switch (sub) {
511
+ case "mk":
512
+ case "new":
513
+ case "create": return cmdChMk(cwd, rest);
514
+ case "join": return cmdChJoin(cwd, rest);
515
+ case "ls":
516
+ case "list": return cmdChLs(cwd, rest);
517
+ case "rm":
518
+ case "remove": return cmdChRm(cwd, rest);
519
+ case "send": return cmdChSend(cwd, rest);
520
+ case "read": return cmdChRead(cwd, rest, "read");
521
+ case "head": return cmdChRead(cwd, rest, "head");
522
+ case "tail": return cmdChRead(cwd, rest, "tail");
523
+ case "sync": return cmdChSync(cwd, rest);
524
+ case "pipe": return cmdChPipe(cwd, rest);
525
+ case "embed": return cmdChEmbed(cwd, rest);
526
+ case "bookmarklet": return cmdChBookmarklet(cwd, rest);
527
+ case void 0:
528
+ case "help":
529
+ case "--help":
530
+ case "-h": return chHelp();
531
+ default:
532
+ process.stderr.write(`ay ch: unknown subcommand "${sub}"\n\n`);
533
+ chHelp();
534
+ return 1;
535
+ }
536
+ }
537
+
538
+ //#endregion
539
+ export { cmdCh };
540
+ //# sourceMappingURL=channels-Boi4aNiY.js.map