toolroll 0.9.67 → 0.9.68

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 (236) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/acp-client.js +1 -3
  3. package/dist/agent-board.d.ts +0 -1
  4. package/dist/agent-board.js +0 -1
  5. package/dist/agent-fence.js +3 -0
  6. package/dist/browser/workspace.css +1 -1
  7. package/dist/browser/workspace.js +31 -29
  8. package/dist/builder.js +3 -26
  9. package/dist/canonical-json.d.ts +14 -0
  10. package/dist/canonical-json.js +36 -0
  11. package/dist/chat-actions.js +1 -2
  12. package/dist/chat-admission.d.ts +19 -0
  13. package/dist/chat-admission.js +22 -0
  14. package/dist/chat-channel.js +1 -1
  15. package/dist/chat-delivery.js +0 -1
  16. package/dist/chat-follow.d.ts +17 -0
  17. package/dist/chat-follow.js +32 -0
  18. package/dist/chat-messages.js +0 -1
  19. package/dist/chat-settings-page.d.ts +23 -0
  20. package/dist/chat-settings-page.js +41 -0
  21. package/dist/claim.js +3 -6
  22. package/dist/claude-print.d.ts +12 -0
  23. package/dist/claude-print.js +34 -0
  24. package/dist/cli-http.d.ts +0 -7
  25. package/dist/cli-http.js +0 -21
  26. package/dist/cli.d.ts +3 -10
  27. package/dist/cli.js +15 -24
  28. package/dist/coding-provider.js +2 -1
  29. package/dist/coding-workspace.js +1 -1
  30. package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
  31. package/dist/connect-cli.js +331 -0
  32. package/dist/containment.d.ts +0 -2
  33. package/dist/containment.js +2 -6
  34. package/dist/contracts/chat-actions.d.ts +0 -1
  35. package/dist/contracts/chat-content.d.ts +0 -4
  36. package/dist/contracts/chat-content.js +0 -2
  37. package/dist/contracts/chat-tables.d.ts +0 -1
  38. package/dist/contracts/cli.d.ts +0 -62
  39. package/dist/contracts/cli.js +0 -4
  40. package/dist/contracts/coding-workspace.js +1 -1
  41. package/dist/contracts/contract.js +1 -1
  42. package/dist/contracts/flow-card.d.ts +0 -1
  43. package/dist/contracts/flow-propose.d.ts +0 -1
  44. package/dist/contracts/flow-send.js +1 -1
  45. package/dist/contracts/gateway-tools.d.ts +0 -1
  46. package/dist/contracts/integration-metadata.js +1 -1
  47. package/dist/contracts/lead-tools.d.ts +0 -1
  48. package/dist/contracts/memory-pass.d.ts +0 -1
  49. package/dist/contracts/memory-pass.js +1 -1
  50. package/dist/contracts/plan.js +1 -1
  51. package/dist/contracts/project-knowledge.js +1 -1
  52. package/dist/contracts/project-skills.d.ts +0 -1
  53. package/dist/contracts/proof.js +1 -1
  54. package/dist/contracts/recipes.js +1 -1
  55. package/dist/contracts/scope.js +1 -1
  56. package/dist/contracts/scout-report.js +1 -1
  57. package/dist/contracts/teams-callback.d.ts +0 -1
  58. package/dist/converse.d.ts +0 -6
  59. package/dist/converse.js +0 -19
  60. package/dist/daemon.js +3 -2
  61. package/dist/decision.d.ts +1 -3
  62. package/dist/deprecations.d.ts +12 -4
  63. package/dist/deprecations.js +27 -4
  64. package/dist/desktop-update-recovery.js +1 -1
  65. package/dist/desktop-update.js +3 -2
  66. package/dist/discord-chat.js +4 -15
  67. package/dist/discord-settings.d.ts +2 -6
  68. package/dist/discord-settings.js +10 -38
  69. package/dist/discord.js +12 -23
  70. package/dist/evidence.d.ts +0 -1
  71. package/dist/evidence.js +0 -3
  72. package/dist/exec.d.ts +0 -9
  73. package/dist/exec.js +3 -81
  74. package/dist/flow-actions.js +0 -3
  75. package/dist/flow-draft.d.ts +1 -3
  76. package/dist/flow-draft.js +15 -28
  77. package/dist/flow-scripts.d.ts +1 -8
  78. package/dist/flow-scripts.js +0 -13
  79. package/dist/flow-send.js +1 -1
  80. package/dist/flow-share.d.ts +0 -2
  81. package/dist/flow-share.js +1 -6
  82. package/dist/flow-steps.js +1 -1
  83. package/dist/flow-triggers.d.ts +0 -1
  84. package/dist/flow-triggers.js +1 -2
  85. package/dist/flow-unattended.js +1 -12
  86. package/dist/flows.js +1 -1
  87. package/dist/health-cli.d.ts +2 -2
  88. package/dist/health-cli.js +1 -1
  89. package/dist/invoke.d.ts +0 -3
  90. package/dist/invoke.js +5 -3
  91. package/dist/is-record.d.ts +2 -0
  92. package/dist/is-record.js +2 -0
  93. package/dist/lead-doors.d.ts +1 -5
  94. package/dist/lead-doors.js +0 -4
  95. package/dist/lead-tools.js +3 -6
  96. package/dist/live-detail.d.ts +0 -1
  97. package/dist/live-detail.js +0 -6
  98. package/dist/live-terminal.js +2 -1
  99. package/dist/live.d.ts +0 -1
  100. package/dist/live.js +1 -2
  101. package/dist/mcp-core.js +0 -2
  102. package/dist/memory-pass.js +1 -1
  103. package/dist/model-catalog.js +1 -1
  104. package/dist/modes.js +1 -12
  105. package/dist/operate.js +383 -750
  106. package/dist/password-bearer.d.ts +0 -2
  107. package/dist/password-bearer.js +0 -4
  108. package/dist/peek-cli.js +2 -1
  109. package/dist/peek.d.ts +2 -26
  110. package/dist/peek.js +2 -168
  111. package/dist/phase-routing.d.ts +0 -4
  112. package/dist/phase-routing.js +2 -21
  113. package/dist/planner-source.d.ts +2 -1
  114. package/dist/planner-source.js +2 -12
  115. package/dist/planner.js +2 -14
  116. package/dist/process-tree.d.ts +0 -2
  117. package/dist/process-tree.js +0 -10
  118. package/dist/project-delete.d.ts +0 -2
  119. package/dist/project-delete.js +1 -3
  120. package/dist/project-learning.d.ts +0 -14
  121. package/dist/project-skills.d.ts +0 -1
  122. package/dist/project-skills.js +0 -14
  123. package/dist/project-tools.js +2 -10
  124. package/dist/provider.d.ts +0 -17
  125. package/dist/provider.js +0 -26
  126. package/dist/pulls.d.ts +0 -2
  127. package/dist/pulls.js +0 -9
  128. package/dist/release-adapters.d.ts +3 -0
  129. package/dist/release-adapters.js +15 -5
  130. package/dist/remote-exec.d.ts +3 -2
  131. package/dist/remote-exec.js +3 -2
  132. package/dist/repos.d.ts +0 -5
  133. package/dist/repos.js +2 -10
  134. package/dist/repository-context.d.ts +0 -2
  135. package/dist/repository-context.js +0 -4
  136. package/dist/restart-certification.d.ts +0 -2
  137. package/dist/restart-certification.js +1 -5
  138. package/dist/result-follow-ups.d.ts +1 -4
  139. package/dist/result-follow-ups.js +1 -6
  140. package/dist/result-review.d.ts +0 -2
  141. package/dist/result-review.js +0 -2
  142. package/dist/retention.d.ts +0 -1
  143. package/dist/retention.js +0 -1
  144. package/dist/review-context.d.ts +0 -1
  145. package/dist/review-context.js +1 -12
  146. package/dist/reviewer.d.ts +0 -16
  147. package/dist/reviewer.js +0 -18
  148. package/dist/run-closeout.js +2 -1
  149. package/dist/scope.js +1 -13
  150. package/dist/scout-browser.js +2 -1
  151. package/dist/serve.js +2 -2
  152. package/dist/server/console.css +5 -133
  153. package/dist/server/flows.js +2 -2
  154. package/dist/server/http.d.ts +2 -0
  155. package/dist/server/http.js +5 -0
  156. package/dist/server/pages.js +2 -2
  157. package/dist/server/people-tokens.js +3 -4
  158. package/dist/server/remote.js +1 -6
  159. package/dist/server/render-chat.d.ts +9 -2
  160. package/dist/server/render-chat.js +53 -7
  161. package/dist/server/render-results.d.ts +0 -6
  162. package/dist/server/render-results.js +3 -9
  163. package/dist/server/render-tasks.js +4 -5
  164. package/dist/server/route-table.d.ts +0 -4
  165. package/dist/server/route-table.js +0 -6
  166. package/dist/server/settings.js +23 -23
  167. package/dist/server/task-views.js +1 -1
  168. package/dist/server/tasks.js +3 -3
  169. package/dist/setup-guide.d.ts +2 -2
  170. package/dist/skills.d.ts +0 -6
  171. package/dist/skills.js +0 -8
  172. package/dist/slack-chat.d.ts +1 -7
  173. package/dist/slack-chat.js +5 -18
  174. package/dist/slack-settings.d.ts +2 -6
  175. package/dist/slack-settings.js +10 -38
  176. package/dist/slack.js +12 -23
  177. package/dist/sleep.d.ts +2 -0
  178. package/dist/sleep.js +2 -0
  179. package/dist/store.d.ts +0 -73
  180. package/dist/store.js +3 -177
  181. package/dist/structured-output.d.ts +0 -1
  182. package/dist/structured-output.js +0 -1
  183. package/dist/subagent-work.d.ts +0 -2
  184. package/dist/subagent-work.js +0 -2
  185. package/dist/subagents.d.ts +1 -3
  186. package/dist/subagents.js +20 -33
  187. package/dist/subscription-chat.d.ts +1 -3
  188. package/dist/subscription-chat.js +2 -3
  189. package/dist/task-sizing.d.ts +1 -4
  190. package/dist/task-sizing.js +3 -10
  191. package/dist/teams-chat.js +4 -11
  192. package/dist/teams.js +12 -19
  193. package/dist/telegram-lead.js +2 -2
  194. package/dist/telegram.js +1 -1
  195. package/dist/token-notices.d.ts +0 -11
  196. package/dist/token-notices.js +1 -5
  197. package/dist/tool-secret-store.d.ts +0 -3
  198. package/dist/tool-secret-store.js +2 -11
  199. package/dist/toolroll-update.d.ts +0 -2
  200. package/dist/toolroll-update.js +2 -2
  201. package/dist/verification-evidence.d.ts +0 -1
  202. package/dist/verification-evidence.js +0 -1
  203. package/dist/webhooks.d.ts +0 -1
  204. package/dist/webhooks.js +0 -1
  205. package/dist/workspace-ui.d.ts +0 -8
  206. package/dist/workspace-ui.js +1 -6
  207. package/package.json +1 -1
  208. package/dist/contracts/native-census.d.ts +0 -42
  209. package/dist/contracts/native-census.js +0 -36
  210. package/dist/process-recovery-anchor.d.ts +0 -75
  211. package/dist/process-recovery-anchor.js +0 -188
  212. package/dist/process-recovery-coalition.d.ts +0 -25
  213. package/dist/process-recovery-coalition.js +0 -105
  214. package/dist/process-recovery-eligibility.d.ts +0 -179
  215. package/dist/process-recovery-eligibility.js +0 -174
  216. package/dist/process-recovery-native.d.ts +0 -161
  217. package/dist/process-recovery-native.js +0 -595
  218. package/dist/process-recovery-provenance.d.ts +0 -163
  219. package/dist/process-recovery-provenance.js +0 -292
  220. package/dist/process-recovery-services.d.ts +0 -56
  221. package/dist/process-recovery-services.js +0 -191
  222. package/dist/process-recovery-settlement.d.ts +0 -32
  223. package/dist/process-recovery-settlement.js +0 -133
  224. package/dist/process-recovery.d.ts +0 -41
  225. package/dist/process-recovery.js +0 -91
  226. package/dist/session-brief.d.ts +0 -8
  227. package/dist/session-brief.js +0 -24
  228. package/dist/session-cli.d.ts +0 -42
  229. package/dist/session-cli.js +0 -271
  230. package/dist/session-contract.d.ts +0 -183
  231. package/dist/session-contract.js +0 -122
  232. package/dist/session-remote.d.ts +0 -19
  233. package/dist/session-remote.js +0 -86
  234. package/dist/session-service.d.ts +0 -22
  235. package/dist/session-service.js +0 -157
  236. package/dist/team-cli.js +0 -692
package/dist/team-cli.js DELETED
@@ -1,692 +0,0 @@
1
- import { constants, closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { dirname, join } from 'node:path';
4
- import { randomBytes } from 'node:crypto';
5
- import { createInterface } from 'node:readline/promises';
6
- import { setTimeout as delay } from 'node:timers/promises';
7
- import { envelopeJson } from './envelope.js';
8
- import { databasePath } from './store.js';
9
- import { configBase, namedPath } from './names.js';
10
- import { parseApiToken } from './api-tokens.js';
11
- import { deprecationWarning } from './deprecations.js';
12
- import { PASSWORD_PROFILE_WARNING } from './password-bearer.js';
13
- /** The central service's own lead verbs; every other `lead …` (token, say, or none) is the local lead's. */
14
- const CENTRAL_LEAD_ACTIONS = ['list', 'create', 'update', 'member', 'transfer'];
15
- /** D5: deprecated (deprecations.ts). Each still runs this release, hidden from help and the contract, with one warning on stderr. */
16
- export const TEAM_CLI_ACTIONS = ['connect', 'lead list', 'lead create', 'lead update', 'lead member', 'lead transfer', 'conversation list', 'conversation create', 'conversation show', 'conversation member', 'conversation edit', 'conversation withdraw', 'conversation read', 'conversation follow', 'conversation stop'];
17
- const HELP = `toolroll connect <HTTPS-origin> --as <account> --token-stdin
18
- Pipe your API token (so_…, from Settings → API tokens) or use --token-file <private-file>;
19
- --local-login uses your existing local sign-in for chat only.
20
- With an API token saved, other commands run on that server as you (--local runs here).
21
- Optional: --profile <name> (saved as the active profile).
22
- toolroll lead list|create|update|member|transfer [--profile <name>] [--json]
23
- create: --name <name> --instructions <text> --project <server-path> (repeatable)
24
- update: --lead <id> --revision <number> with --name, --instructions or --status
25
- member: --lead <id> --account <name> --role viewer|contributor|manager
26
- --active true|false --revision <lead-revision>
27
- transfer: --task <id> --lead <id> --revision <number>
28
- toolroll conversation list|create|show|member|edit|withdraw|read|follow|stop
29
- list: [--lead <id>]
30
- create: --lead <id> --title <title> --visibility private|team --project <server-path>
31
- member: --conversation <id> --account <name> --role viewer|contributor|manager
32
- --active true|false --revision <conversation-revision>
33
- edit/withdraw: --conversation <id> --message <id> --revision <message-revision> [--text <text>]
34
- read/stop: --conversation <id> --message <id>
35
- follow: --conversation <id> --enabled true|false (automatic summaries within authorized limits)
36
- toolroll chat --lead <id> --conversation <id> [--say <text>] [--follow] [--json]
37
- Without --say, a terminal opens interactive chat. --follow only reads updates.
38
- Enable paid chat with --authorize --terms-digest <digest> after reviewing its terms.
39
- --request-id <32-hex-id> identifies one message; after a lost response, inspect it
40
- with brief --conversation <id> --request-id <id>, without sending it again.
41
- toolroll brief --lead <id> --conversation <id> [--json]
42
-
43
- Saved profiles select the central service for chat and brief. Use --local for the
44
- existing local commands. Credentials stay in a private file, never in URLs or arguments.
45
- Reads and attachment do not start model work. Server permissions remain authoritative.`;
46
- export class UsageError extends Error {
47
- }
48
- /** Credentials are never sent to a URL with embedded authority, an API path, query, fragment or an insecure remote
49
- * host. Fetch cannot follow redirects. */
50
- export function serviceOrigin(raw) {
51
- let url;
52
- try {
53
- url = new URL(raw);
54
- }
55
- catch {
56
- throw new UsageError('The service origin must be HTTPS or loopback HTTP.');
57
- }
58
- if (url.username || url.password || url.search || url.hash || url.pathname !== '/')
59
- throw new UsageError('The service origin must contain only the origin, with no credentials, path, query or fragment.');
60
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))
61
- throw new UsageError('The service origin requires HTTPS except for loopback HTTP.');
62
- return url.origin;
63
- }
64
- /** Transport metadata stays local to the CLI; only an actual HTTP 429 permits a follower read retry. */
65
- const RETRY_AFTER_MS = Symbol('retry-after-ms');
66
- function retryDelay(header) {
67
- const raw = header?.trim() ?? '';
68
- const delay = /^\d+$/.test(raw) ? Number(raw) * 1000
69
- : /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), /.test(raw) ? Date.parse(raw) - Date.now() : NaN;
70
- return Number.isFinite(delay) && delay >= 0 ? Math.max(1000, Math.min(300_000, delay)) : 5000;
71
- }
72
- const value = (flags, name) => typeof flags[name] === 'string' ? flags[name] : '';
73
- const required = (flags, name) => { const result = value(flags, name); if (!result)
74
- throw new UsageError(`--${name} is required.`); return result; };
75
- const number = (flags, name) => { const result = required(flags, name); if (!/^\d+$/.test(result) || !Number.isSafeInteger(Number(result)))
76
- throw new UsageError(`--${name} must be a whole number.`); return Number(result); };
77
- const bool = (flags, name) => { const result = required(flags, name); if (!['true', 'false'].includes(result))
78
- throw new UsageError(`--${name} must be true or false.`); return result === 'true'; };
79
- const text = (raw) => raw.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
80
- const record = (input) => !!input && typeof input === 'object' && !Array.isArray(input);
81
- const strings = (input) => Array.isArray(input) && input.every(one => typeof one === 'string');
82
- const accountValid = (input) => !!input && input.length <= 200 && !/[\s:\x00-\x1f\x7f]/.test(input);
83
- const tokenValid = (input) => !!input && input.length <= 8192 && !/[\s\x00-\x1f\x7f]/.test(input);
84
- const profilePath = (options) => options.profileFile ?? namedPath(configBase(options.env ?? process.env, options.home ?? homedir()), ['remote', 'profiles.json']);
85
- /** Open the file itself without following a symlink; nonblocking also rejects FIFOs safely. */
86
- function privateFile(path, limit) {
87
- let fd;
88
- try {
89
- fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
90
- const stat = fstatSync(fd);
91
- if (!stat.isFile() || (stat.mode & 0o777) !== 0o600 || (process.getuid && stat.uid !== process.getuid()) || stat.size > limit)
92
- throw new Error();
93
- const bytes = readFileSync(fd);
94
- if (bytes.length > limit)
95
- throw new Error();
96
- return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
97
- }
98
- catch {
99
- throw new UsageError('The credential file must be an owned, private (chmod 600), regular UTF-8 file.');
100
- }
101
- finally {
102
- if (fd !== undefined)
103
- closeSync(fd);
104
- }
105
- }
106
- function profiles(options) {
107
- const path = profilePath(options);
108
- // lstat distinguishes a dangling symlink from a missing profile.
109
- try {
110
- lstatSync(path);
111
- }
112
- catch (error) {
113
- if (error.code === 'ENOENT')
114
- return null;
115
- throw new UsageError('The remote profile cannot be read.');
116
- }
117
- const raw = privateFile(path, 128 * 1024);
118
- try {
119
- const parsed = JSON.parse(raw);
120
- if (!record(parsed) || parsed.version !== 1 || typeof parsed.active !== 'string' || !record(parsed.profiles))
121
- throw new Error();
122
- for (const [name, one] of Object.entries(parsed.profiles)) {
123
- if (!/^[A-Za-z0-9_-]{1,64}$/.test(name) || !record(one) || typeof one.origin !== 'string' || serviceOrigin(one.origin) !== one.origin || typeof one.account !== 'string' || !accountValid(one.account) || typeof one.token !== 'string' || !tokenValid(one.token))
124
- throw new Error();
125
- }
126
- if (!Object.hasOwn(parsed.profiles, parsed.active))
127
- throw new Error();
128
- return parsed;
129
- }
130
- catch {
131
- throw new UsageError('The remote profile is invalid. Connect again using a private credential source.');
132
- }
133
- }
134
- function saveProfile(options, name, profile) {
135
- const path = profilePath(options), directory = dirname(path);
136
- const saved = profiles(options) ?? { version: 1, active: name, profiles: {} };
137
- mkdirSync(directory, { recursive: true, mode: 0o700 });
138
- const parent = lstatSync(directory);
139
- if (!parent.isDirectory() || (parent.mode & 0o777) !== 0o700 || (process.getuid && parent.uid !== process.getuid()))
140
- throw new UsageError('The remote profile directory must be owned and private (chmod 700).');
141
- saved.active = name;
142
- Object.defineProperty(saved.profiles, name, { value: profile, enumerable: true, configurable: true, writable: true });
143
- const temporary = join(directory, `.profile-${randomBytes(16).toString('hex')}`);
144
- let fd;
145
- try {
146
- fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
147
- writeFileSync(fd, `${JSON.stringify(saved)}\n`);
148
- fsyncSync(fd);
149
- closeSync(fd);
150
- fd = undefined;
151
- renameSync(temporary, path);
152
- fd = openSync(directory, constants.O_RDONLY);
153
- fsyncSync(fd);
154
- }
155
- finally {
156
- if (fd !== undefined)
157
- closeSync(fd);
158
- if (existsSync(temporary))
159
- unlinkSync(temporary);
160
- }
161
- }
162
- /** An API token (so_…) is sent bare; an older saved sign-in keeps its account:secret form. */
163
- export const bearerOf = (profile) => parseApiToken(profile.token) !== null ? `Bearer ${profile.token}` : `Bearer ${profile.account}:${profile.token}`;
164
- /** The saved central profile remote commands use: the named one (or the active one), and whether it holds an API token.
165
- * null when nothing is saved. Reads only the private profile file — never a local login or the database. */
166
- export function centralProfile(options, name) {
167
- const saved = profiles(options);
168
- if (saved === null)
169
- return null;
170
- const chosen = name ?? saved.active;
171
- if (!/^[A-Za-z0-9_-]{1,64}$/.test(chosen) || !Object.hasOwn(saved.profiles, chosen))
172
- throw new UsageError(`No saved connection named ${chosen}. Use connect with your API token first.`);
173
- const profile = saved.profiles[chosen];
174
- return { name: chosen, ...profile, apiToken: parseApiToken(profile.token) !== null };
175
- }
176
- async function stdinSecret() {
177
- if (process.stdin.isTTY)
178
- throw new UsageError('Pipe your sign-in secret into --token-stdin, or use --token-file.');
179
- const chunks = [];
180
- let size = 0;
181
- for await (const part of process.stdin) {
182
- const chunk = Buffer.from(part);
183
- size += chunk.length;
184
- if (size > 8192)
185
- throw new UsageError('The sign-in secret is too large.');
186
- chunks.push(chunk);
187
- }
188
- try {
189
- return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
190
- }
191
- catch {
192
- throw new UsageError('The sign-in secret must be UTF-8.');
193
- }
194
- }
195
- function parse(argv) {
196
- const command = argv[0] ?? '', action = ['lead', 'conversation'].includes(command) ? argv[1] ?? '' : '';
197
- const start = action ? 2 : 1;
198
- const common = ['profile', 'json', 'help'];
199
- const allowed = new Set([...common, ...(command === 'connect' ? ['as', 'token-stdin', 'token-file', 'local-login', 'login-file'] : command === 'chat' ? ['lead', 'conversation', 'say', 'follow', 'authorize', 'terms-digest', 'request-id'] : command === 'brief' ? ['lead', 'conversation', 'request-id'] : command === 'lead' ? action === 'list' ? [] : action === 'create' ? ['name', 'instructions', 'project'] : action === 'update' ? ['lead', 'revision', 'name', 'instructions', 'status'] : action === 'member' ? ['lead', 'account', 'role', 'active', 'revision'] : action === 'transfer' ? ['task', 'lead', 'revision'] : [] : action === 'list' ? ['lead'] : action === 'create' ? ['lead', 'title', 'visibility', 'project'] : action === 'member' ? ['conversation', 'account', 'role', 'active', 'revision'] : action === 'edit' ? ['conversation', 'message', 'revision', 'text'] : action === 'withdraw' ? ['conversation', 'message', 'revision'] : action === 'read' || action === 'stop' ? ['conversation', 'message'] : action === 'follow' ? ['conversation', 'enabled'] : ['conversation'])]);
200
- const switches = new Set(['json', 'help', 'token-stdin', 'local-login', 'follow', 'authorize']);
201
- const flags = {}, positions = [];
202
- for (let index = start; index < argv.length; index++) {
203
- const arg = argv[index];
204
- if (!arg.startsWith('--')) {
205
- positions.push(arg);
206
- continue;
207
- }
208
- const name = arg.slice(2);
209
- if (!allowed.has(name))
210
- throw new UsageError('Unsupported option. Use --help for this command; secrets must come from stdin or a private file.');
211
- if (Object.hasOwn(flags, name) && name !== 'project')
212
- throw new UsageError(`Use --${name} only once.`);
213
- if (switches.has(name))
214
- flags[name] = true;
215
- else {
216
- const next = argv[++index];
217
- if (!next || next.startsWith('--'))
218
- throw new UsageError(`--${name} needs a value.`);
219
- if (name === 'project')
220
- flags[name] = [...(Array.isArray(flags[name]) ? flags[name] : []), next];
221
- else
222
- flags[name] = next;
223
- }
224
- }
225
- if (positions.length !== (command === 'connect' ? 1 : 0) && !flags.help)
226
- throw new UsageError(command === 'connect' ? 'Name one service origin.' : 'Use named options for this command.');
227
- return { command, action, flags, positions };
228
- }
229
- function isMessage(one) {
230
- return record(one) && Number.isSafeInteger(one.id) && typeof one.author === 'string' && ['operator', 'assistant'].includes(String(one.role)) && typeof one.text === 'string' && ['queued', 'running', 'answered', 'failed', 'cancelled', 'uncertain'].includes(String(one.status)) && Number.isSafeInteger(one.revision) && typeof one.createdAt === 'string' && (one.requestId === null || typeof one.requestId === 'string') && (one.turnId === null || Number.isSafeInteger(one.turnId)) && (one.error === null || typeof one.error === 'string');
231
- }
232
- function isSnapshot(one) {
233
- if (!record(one) || !Array.isArray(one.leads) || !one.leads.every(lead => record(lead) && typeof lead.id === 'string' && typeof lead.name === 'string') || !Array.isArray(one.conversations) || !one.conversations.every(conversation => record(conversation) && typeof conversation.id === 'string' && typeof conversation.leadId === 'string' && typeof conversation.title === 'string') || !(one.selected === null || record(one.selected) && typeof one.selected.id === 'string' && typeof one.selected.leadId === 'string' && typeof one.selected.title === 'string') || !Array.isArray(one.participants) || !one.participants.every(participant => record(participant) && typeof participant.account === 'string' && ['viewer', 'contributor', 'manager'].includes(String(participant.role)) && typeof participant.active === 'boolean') || !Array.isArray(one.messages) || !one.messages.every(isMessage) || typeof one.canManage !== 'boolean' || typeof one.canSend !== 'boolean' || !Number.isSafeInteger(one.cursor) || typeof one.truncated !== 'boolean' || !strings(one.projects) || !strings(one.accounts))
234
- return false;
235
- if (one.chatAuthorization !== undefined) {
236
- const grant = one.chatAuthorization;
237
- if (!record(grant) || typeof grant.enabled !== 'boolean' || !(grant.provider === null || typeof grant.provider === 'string') || !(grant.model === null || typeof grant.model === 'string') || !Number.isSafeInteger(grant.dailyTurns) || ![grant.weeklyCeilingUsd, grant.conversationCeilingUsd].every(amount => amount === null || typeof amount === 'number' && Number.isFinite(amount) && amount >= 0) || typeof grant.termsDigest !== 'string')
238
- return false;
239
- }
240
- if (one.tasks !== undefined) {
241
- const tasks = one.tasks;
242
- if (!record(tasks) || !record(tasks.totals) || !['all', 'needs-you', 'running', 'completed'].every(key => Number.isSafeInteger(tasks.totals[key])) || !Array.isArray(tasks.items) || !tasks.items.every(task => record(task) && typeof task.rootId === 'string' && typeof task.activeTaskId === 'string' && typeof task.title === 'string' && record(task.status) && typeof task.status.label === 'string') || !(tasks.nextCursor === null || typeof tasks.nextCursor === 'string'))
243
- return false;
244
- }
245
- if (one.proposals !== undefined && (!Array.isArray(one.proposals) || !one.proposals.every(proposal => record(proposal) && Number.isSafeInteger(proposal.id) && typeof proposal.title === 'string' && typeof proposal.state === 'string' && typeof proposal.href === 'string')))
246
- return false;
247
- return true;
248
- }
249
- async function responseBody(response) {
250
- if (!response.body)
251
- throw new Error();
252
- const reader = response.body.getReader(), chunks = [];
253
- let size = 0;
254
- try {
255
- while (true) {
256
- const chunk = await reader.read();
257
- if (chunk.done)
258
- break;
259
- size += chunk.value.byteLength;
260
- if (size > 2 * 1024 * 1024) {
261
- await reader.cancel();
262
- throw new Error();
263
- }
264
- chunks.push(chunk.value);
265
- }
266
- }
267
- finally {
268
- reader.releaseLock();
269
- }
270
- return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
271
- }
272
- function failure(code, message, result) { return { version: 1, ok: false, code, message, ...(result === undefined ? {} : { result }) }; }
273
- async function perform(profile, request, options) {
274
- const reading = ['list', 'show'].includes(request.operation);
275
- const receipt = { operation: request.operation, ...(request.args.conversationId ? { conversationId: request.args.conversationId } : {}), ...(request.args.requestId ? { requestId: request.args.requestId } : {}) };
276
- const unconfirmed = () => failure(reading ? 'service-unavailable' : 'delivery-unconfirmed', reading ? 'The service response could not be read.' : 'The response could not be confirmed. Inspect saved messages before continuing; this request was not retried.', receipt);
277
- try {
278
- const response = await (options.fetch ?? fetch)(`${profile.origin}/api/team`, { method: 'POST', redirect: 'manual', credentials: 'omit', signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000), headers: { authorization: bearerOf(profile), 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(request) });
279
- if (response.status >= 300 && response.status < 400) {
280
- await response.body?.cancel();
281
- return unconfirmed();
282
- }
283
- if (response.status === 429) {
284
- await response.body?.cancel();
285
- return { ...failure('rate-limited', reading ? 'Request limit reached. Try again shortly.' : 'Request limit reached. Inspect saved messages before continuing; this request was not retried.', receipt), [RETRY_AFTER_MS]: retryDelay(response.headers.get('retry-after')) };
286
- }
287
- const payload = await responseBody(response);
288
- if (!record(payload) || payload.version !== 1 || typeof payload.ok !== 'boolean' || typeof payload.code !== 'string' || typeof payload.message !== 'string' || payload.snapshot !== undefined && !isSnapshot(payload.snapshot) || payload.ok && !response.ok)
289
- return unconfirmed();
290
- const reply = payload;
291
- if (reply.ok && reading && !reply.snapshot)
292
- return unconfirmed();
293
- if (reply.ok && request.args.conversationId && reply.snapshot && reply.snapshot.selected?.id !== request.args.conversationId)
294
- return unconfirmed();
295
- if (reply.ok && request.operation === 'send' && !reply.snapshot?.messages.some(message => message.requestId === request.args.requestId && message.role === 'operator' && message.author === profile.account && message.text === request.args.text))
296
- return unconfirmed();
297
- return request.operation === 'send'
298
- ? { ...reply, result: { ...(record(reply.result) ? reply.result : {}), requestId: request.args.requestId, conversationId: request.args.conversationId } }
299
- : reply;
300
- }
301
- catch {
302
- return unconfirmed();
303
- }
304
- }
305
- function terms(grant) {
306
- return `Paid chat: ${grant.provider ?? 'not configured'} / ${grant.model ?? 'not configured'}\nDaily turns: ${grant.dailyTurns}\nWeekly ceiling: ${grant.weeklyCeilingUsd === null ? 'not set' : `$${grant.weeklyCeilingUsd}`}\nConversation ceiling: ${grant.conversationCeilingUsd === null ? 'not set' : `$${grant.conversationCeilingUsd}`}\nTerms: ${grant.termsDigest}${grant.waitingReason ? `\n${grant.waitingReason}` : ''}`;
307
- }
308
- function render(reply, listing) {
309
- const lines = [reply.message], snapshot = reply.snapshot;
310
- if (snapshot && listing) {
311
- if (listing === 'leads')
312
- for (const lead of snapshot.leads)
313
- lines.push(`${lead.id} ${lead.name} · ${lead.status} · revision ${lead.revision}`);
314
- else
315
- for (const conversation of snapshot.conversations)
316
- lines.push(`${conversation.id} ${conversation.title} · lead ${conversation.leadId} · ${conversation.visibility} · revision ${conversation.revision}`);
317
- if (lines.length === 1)
318
- lines.push(listing === 'leads' ? 'No leads yet.' : 'No conversations yet.');
319
- return text(lines.join('\n'));
320
- }
321
- if (snapshot) {
322
- if (snapshot.selected)
323
- lines.push(`${snapshot.selected.title} · ${snapshot.selected.id} · revision ${snapshot.selected.revision}`, `Lead: ${snapshot.selected.leadId}`);
324
- else {
325
- for (const lead of snapshot.leads)
326
- lines.push(`${lead.id} ${lead.name}`);
327
- for (const conversation of snapshot.conversations)
328
- lines.push(`${conversation.id} ${conversation.title} (${conversation.leadId})`);
329
- }
330
- for (const message of snapshot.messages)
331
- lines.push(`${message.author} · ${message.status} · message ${message.id} · revision ${message.revision}${message.requestId ? ` · request ${message.requestId}` : ''}\n${message.text}${message.error ? `\n${message.error}` : ''}`);
332
- if (snapshot.tasks) {
333
- const totals = snapshot.tasks.totals;
334
- lines.push(`Work: ${totals.all} total · ${totals['needs-you']} need you · ${totals.running} running · ${totals.completed} complete`);
335
- for (const task of snapshot.tasks.items)
336
- lines.push(`${task.activeTaskId} ${task.status.label} ${task.title}`);
337
- if (snapshot.tasks.nextCursor)
338
- lines.push('More tasks are available in the browser.');
339
- }
340
- for (const proposal of snapshot.proposals ?? [])
341
- lines.push(`Proposal ${proposal.id} · ${proposal.state}: ${proposal.title}\n${proposal.href}`);
342
- if (snapshot.truncated)
343
- lines.push('Earlier messages are available in the browser.');
344
- }
345
- if (reply.code === 'grant-needed' && snapshot?.chatAuthorization)
346
- lines.push(terms(snapshot.chatAuthorization), 'Review these terms, then use --authorize --terms-digest with this exact digest.');
347
- if (record(reply.result)) {
348
- for (const [key, label] of [['leadId', 'Lead'], ['conversationId', 'Conversation'], ['taskId', 'Task'], ['messageId', 'Message'], ['revision', 'Revision'], ['requestId', 'Request']]) {
349
- if (typeof reply.result[key] === 'string' || typeof reply.result[key] === 'number')
350
- lines.push(`${label}: ${reply.result[key]}`);
351
- }
352
- }
353
- return text(lines.join('\n'));
354
- }
355
- /** null means preserve the existing local command path. Nothing here opens a database. */
356
- export async function maybeRunTeamCommand(argv, write, options = {}) {
357
- const command = argv[0];
358
- // `lead token` and `lead say` are local: the credential a lead agent acts with on this computer, and its voice. A bare
359
- // `lead` is the local lead's too ("lead" means only the lead); only the central service's verbs go on from here.
360
- if (command === 'lead' && (argv[1] === 'token' || argv[1] === 'say' || !argv.some(arg => arg === '--profile' || CENTRAL_LEAD_ACTIONS.includes(arg))))
361
- return null;
362
- if (!['connect', 'lead', 'conversation', 'chat', 'brief'].includes(command ?? '') && !argv.includes('--profile'))
363
- return null;
364
- const json = argv.includes('--json');
365
- let secret = '';
366
- const stderr = (line) => (options.stderr ?? (line => process.stderr.write(`${line}\n`)))(text(secret ? line.replaceAll(secret, '[redacted]') : line));
367
- const emit = (reply) => {
368
- const safe = JSON.parse(JSON.stringify(reply, (_key, one) => typeof one === 'string' && secret ? one.replaceAll(secret, '[redacted]') : one));
369
- const listing = argv[1] === 'list' ? command === 'lead' ? 'leads' : command === 'conversation' ? 'conversations' : undefined : undefined;
370
- write(json ? envelopeJson({ ...safe, command: `team ${command}` }) : render(safe, listing));
371
- };
372
- try {
373
- if (!['connect', 'lead', 'conversation', 'chat', 'brief'].includes(command ?? ''))
374
- throw new UsageError('--profile is supported by lead, conversation, chat and brief. No local command was run.');
375
- if ((command === 'chat' || command === 'brief') && argv.includes('--local')) {
376
- if (argv.some(arg => ['--profile', '--lead', '--conversation', '--authorize', '--terms-digest', '--request-id'].includes(arg)))
377
- throw new UsageError('--local cannot be combined with central team options.');
378
- return null;
379
- }
380
- const explicit = ['connect', 'lead', 'conversation'].includes(command) || argv.some(arg => ['--profile', '--lead', '--conversation', '--authorize', '--terms-digest', '--request-id'].includes(arg));
381
- // D5: the central team service is deprecated. It still runs this release; each use says so once, on stderr.
382
- const verb = command === 'lead' || command === 'conversation' ? `${command} ${argv.slice(1).find(arg => !arg.startsWith('--')) ?? ''}`.trim() : command === 'connect' ? command : `${command} --conversation`;
383
- const warn = () => stderr(deprecationWarning(verb, 'team'));
384
- if (explicit)
385
- warn();
386
- if (explicit && (argv.includes('--help') || ['lead', 'conversation'].includes(command) && argv.length === 1)) {
387
- write(HELP);
388
- return 0;
389
- }
390
- const saved = profiles(options);
391
- if (!explicit && !saved)
392
- return null;
393
- if (!explicit)
394
- warn();
395
- const parsed = parse(argv), { flags, action } = parsed;
396
- if (flags.help || ((command === 'lead' || command === 'conversation') && !action)) {
397
- write(HELP);
398
- return 0;
399
- }
400
- const profileName = value(flags, 'profile') || (command === 'connect' ? 'default' : saved?.active ?? 'default');
401
- if (!/^[A-Za-z0-9_-]{1,64}$/.test(profileName))
402
- throw new UsageError('Profile names use 1–64 letters, digits, underscores or hyphens.');
403
- if (command === 'connect') {
404
- let origin;
405
- try {
406
- origin = serviceOrigin(parsed.positions[0]);
407
- }
408
- catch {
409
- throw new UsageError('Use an HTTPS service origin (loopback HTTP is allowed), without credentials, paths, query or fragment.');
410
- }
411
- if (['token-stdin', 'token-file', 'local-login'].filter(name => flags[name]).length !== 1)
412
- throw new UsageError('Choose one credential source: --token-stdin, --token-file or --local-login.');
413
- if (flags['login-file'] && !flags['local-login'])
414
- throw new UsageError('--login-file requires --local-login.');
415
- let account = value(flags, 'as');
416
- if (flags['local-login']) {
417
- const login = privateFile(value(flags, 'login-file') || join(dirname(databasePath(options.env ?? process.env, options.home ?? homedir())), 'up-login.txt'), 8192).trim().split(/\s+/);
418
- if (login.length !== 2 || account && account !== login[0])
419
- throw new UsageError('The local sign-in does not match the requested account.');
420
- account = login[0];
421
- secret = login[1];
422
- }
423
- else
424
- secret = (flags['token-file'] ? privateFile(value(flags, 'token-file'), 8192) : await (options.readStdin ?? stdinSecret)()).trim();
425
- if (!accountValid(account) || !tokenValid(secret))
426
- throw new UsageError('Provide an account name and a valid sign-in secret from your selected source.');
427
- if (parseApiToken(secret) === null)
428
- stderr(PASSWORD_PROFILE_WARNING);
429
- const profile = { origin, account, token: secret }, checked = await perform(profile, { operation: 'list', args: {} }, options);
430
- if (!checked.ok) {
431
- emit(checked);
432
- return 1;
433
- }
434
- saveProfile(options, profileName, profile);
435
- emit({ version: 1, ok: true, code: 'connected', message: `Connected to ${origin} as ${account}.`, result: { profile: profileName, origin, account } });
436
- return 0;
437
- }
438
- const profile = saved?.profiles[profileName];
439
- if (!profile || !Object.hasOwn(saved.profiles, profileName))
440
- throw new UsageError('No saved connection. Use connect with your individual sign-in first.');
441
- secret = profile.token;
442
- if (parseApiToken(secret) === null)
443
- stderr(PASSWORD_PROFILE_WARNING);
444
- const call = (operation, args, signal) => perform(profile, { operation, args }, { ...options, ...(signal === undefined ? {} : { signal }) });
445
- if (command === 'chat' || command === 'brief')
446
- return await chat(command, flags, profile.account, call, emit, options, stderr);
447
- let operation, args = {};
448
- if (action === 'list') {
449
- operation = 'list';
450
- if (flags.lead)
451
- args.leadId = value(flags, 'lead');
452
- }
453
- else if (command === 'lead') {
454
- if (action === 'create') {
455
- operation = 'create-lead';
456
- args = { name: required(flags, 'name'), instructions: required(flags, 'instructions'), projects: flags.project ?? [] };
457
- }
458
- else if (action === 'update') {
459
- operation = 'update-lead';
460
- args = { leadId: required(flags, 'lead'), expectedRevision: number(flags, 'revision') };
461
- for (const name of ['name', 'instructions', 'status'])
462
- if (flags[name])
463
- args[name] = value(flags, name);
464
- }
465
- else if (action === 'member') {
466
- operation = 'member';
467
- args = { leadId: required(flags, 'lead'), account: required(flags, 'account'), role: required(flags, 'role'), active: bool(flags, 'active'), expectedRevision: number(flags, 'revision') };
468
- }
469
- else if (action === 'transfer') {
470
- operation = 'transfer';
471
- args = { leadId: required(flags, 'lead'), taskId: required(flags, 'task'), expectedRevision: number(flags, 'revision') };
472
- }
473
- else
474
- throw new UsageError('Choose lead list, create, update, member or transfer.');
475
- }
476
- else if (action === 'create') {
477
- operation = 'create-conversation';
478
- args = { leadId: required(flags, 'lead'), title: required(flags, 'title'), visibility: required(flags, 'visibility'), projects: flags.project ?? [] };
479
- }
480
- else {
481
- args.conversationId = required(flags, 'conversation');
482
- if (action === 'show')
483
- operation = 'show';
484
- else if (action === 'member') {
485
- operation = 'member';
486
- Object.assign(args, { account: required(flags, 'account'), role: required(flags, 'role'), active: bool(flags, 'active'), expectedRevision: number(flags, 'revision') });
487
- }
488
- else if (action === 'edit' || action === 'withdraw') {
489
- operation = action;
490
- Object.assign(args, { messageId: number(flags, 'message'), expectedRevision: number(flags, 'revision'), ...(action === 'edit' ? { text: required(flags, 'text') } : {}) });
491
- }
492
- else if (action === 'read' || action === 'stop') {
493
- operation = action;
494
- args.messageId = number(flags, 'message');
495
- }
496
- else if (action === 'follow') {
497
- operation = 'follow';
498
- args.enabled = bool(flags, 'enabled');
499
- }
500
- else
501
- throw new UsageError('Choose a conversation operation from --help.');
502
- }
503
- const reply = await call(operation, args);
504
- emit(reply);
505
- return reply.ok ? 0 : 1;
506
- }
507
- catch (error) {
508
- const reply = failure('usage', error instanceof UsageError ? error.message : 'The remote command could not be prepared. Check the private profile and credential files.');
509
- if (json)
510
- emit(reply);
511
- else
512
- stderr(text(reply.message));
513
- return 2;
514
- }
515
- }
516
- async function chat(command, flags, account, call, emit, options, stderr) {
517
- const conversationId = required(flags, 'conversation'), leadId = value(flags, 'lead'), requestId = value(flags, 'request-id');
518
- if (requestId && !/^[0-9a-f]{32}$/.test(requestId))
519
- throw new UsageError('--request-id must be 32 lowercase hexadecimal characters.');
520
- if (flags['terms-digest'] && !flags.authorize)
521
- throw new UsageError('--terms-digest requires --authorize.');
522
- if (command === 'chat' && requestId && !flags.say)
523
- throw new UsageError('Use brief --request-id to inspect a previous message.');
524
- const show = async (signal) => {
525
- const reply = await call('show', { conversationId }, signal);
526
- if (reply.ok && (!reply.snapshot || reply.snapshot.selected?.id !== conversationId || leadId && reply.snapshot.selected.leadId !== leadId))
527
- return failure('conversation-mismatch', 'The service returned a different conversation or lead. Nothing was sent.');
528
- return reply;
529
- };
530
- let current = await show();
531
- if (!current.ok) {
532
- emit(current);
533
- return 1;
534
- }
535
- if (command === 'brief') {
536
- if (requestId) {
537
- const message = current.snapshot.messages.find(one => one.requestId === requestId && one.author === account && one.role === 'operator');
538
- current = { ...current, result: { requestId, receipt: message ?? null, receiptStatus: message ? 'recorded' : 'not-in-current-page' }, message: message ? `Saved message ${message.id}: ${message.status}.` : 'This request is not in the current message page. Inspect the conversation before sending anything again.' };
539
- }
540
- emit(current);
541
- return 0;
542
- }
543
- const interactive = !flags.say && !flags.follow && !flags.json && (options.isTTY ?? process.stdin.isTTY ?? false);
544
- const controller = new AbortController();
545
- const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
546
- const readline = interactive && !options.readLine ? createInterface({ input: process.stdin, output: process.stderr, terminal: true }) : undefined;
547
- const interrupt = () => controller.abort();
548
- if (interactive || flags.follow)
549
- process.once('SIGINT', interrupt);
550
- const ask = options.readLine ?? (async (prompt) => { try {
551
- return await readline.question(prompt, { signal });
552
- }
553
- catch {
554
- return null;
555
- } });
556
- const observed = new Map();
557
- let observedView = '';
558
- const viewKey = (reply) => JSON.stringify(reply.snapshot && { selected: reply.snapshot.selected, participants: reply.snapshot.participants, tasks: reply.snapshot.tasks, proposals: reply.snapshot.proposals, authorization: reply.snapshot.chatAuthorization, canSend: reply.snapshot.canSend });
559
- const remember = (reply) => { observedView = viewKey(reply); for (const message of reply.snapshot?.messages ?? [])
560
- observed.set(message.id, JSON.stringify(message)); };
561
- const updated = (reply) => {
562
- const changes = reply.snapshot?.messages.filter(message => observed.get(message.id) !== JSON.stringify(message)) ?? [];
563
- if (changes.length || viewKey(reply) !== observedView) {
564
- emit({ ...reply, message: 'Conversation updated.', snapshot: { ...reply.snapshot, messages: changes } });
565
- remember(reply);
566
- }
567
- };
568
- const authorize = async (explicit) => {
569
- const grant = current.snapshot?.chatAuthorization;
570
- if (grant?.enabled)
571
- return true;
572
- if (!grant) {
573
- emit(failure('grant-unavailable', 'Paid chat terms are unavailable. Open this conversation in the browser.'));
574
- return false;
575
- }
576
- if (explicit) {
577
- if (value(flags, 'terms-digest') !== grant.termsDigest) {
578
- emit({ ...failure('grant-needed', 'Review the current paid chat terms before enabling chat.'), snapshot: current.snapshot });
579
- return false;
580
- }
581
- }
582
- else if (interactive) {
583
- stderr(text(terms(grant)));
584
- if ((await ask('Enable paid chat with these terms? [y/N] '))?.trim().toLowerCase() !== 'y')
585
- return false;
586
- }
587
- else {
588
- emit({ ...failure('grant-needed', 'Enable paid chat before sending a message.'), snapshot: current.snapshot });
589
- return false;
590
- }
591
- const reply = await call('authorize', { conversationId, termsDigest: grant.termsDigest });
592
- if (!reply.ok || !reply.snapshot?.chatAuthorization?.enabled) {
593
- emit(reply.ok ? failure('grant-unconfirmed', 'Chat authorization was not confirmed. Inspect the conversation before continuing.') : reply);
594
- return false;
595
- }
596
- current = reply;
597
- return true;
598
- };
599
- const send = async (message) => {
600
- if (message.length > 2000 || !message.trim())
601
- throw new UsageError('Messages must contain 1–2,000 characters.');
602
- // Re-read current permissions and terms before each explicit message. Attachment never authorizes.
603
- current = await show();
604
- if (!current.ok) {
605
- emit(current);
606
- return false;
607
- }
608
- if (!await authorize(!!flags.authorize))
609
- return false;
610
- if (!current.snapshot?.canSend) {
611
- emit(failure('forbidden', 'This account cannot send to the selected conversation.'));
612
- return false;
613
- }
614
- const id = requestId || randomBytes(16).toString('hex');
615
- const reply = await call('send', { conversationId, requestId: id, text: message.trim() });
616
- emit(reply);
617
- if (reply.ok) {
618
- current = reply;
619
- remember(reply);
620
- }
621
- return reply.ok;
622
- };
623
- let pollResult = 0;
624
- const poll = async () => {
625
- let waitMs = 2_000;
626
- while (!signal.aborted) {
627
- try {
628
- await (options.sleep ?? ((ms, abort) => delay(ms, undefined, { signal: abort })))(waitMs, signal);
629
- }
630
- catch {
631
- break;
632
- }
633
- if (signal.aborted)
634
- break;
635
- const next = await show(signal);
636
- if (signal.aborted)
637
- break;
638
- // Only this follower's read poll retries. Sends, authorizations and uncertain deliveries are never replayed.
639
- if (!next.ok && next[RETRY_AFTER_MS] !== undefined) {
640
- waitMs = next[RETRY_AFTER_MS];
641
- continue;
642
- }
643
- if (!next.ok) {
644
- emit(next);
645
- pollResult = 1;
646
- controller.abort();
647
- break;
648
- }
649
- waitMs = 2_000;
650
- current = next;
651
- updated(next);
652
- }
653
- };
654
- let polling;
655
- try {
656
- if (flags.say) {
657
- if (!await send(required(flags, 'say')))
658
- return 1;
659
- }
660
- else {
661
- if (flags.authorize && !await authorize(true))
662
- return 1;
663
- emit(current);
664
- remember(current);
665
- }
666
- if (flags.follow) {
667
- await poll();
668
- return pollResult;
669
- }
670
- if (!interactive)
671
- return 0;
672
- polling = poll();
673
- while (!signal.aborted) {
674
- const line = await ask('You> ');
675
- if (line === null || line.trim() === '/quit')
676
- break;
677
- if (!line.trim())
678
- continue;
679
- if (!await send(line)) {
680
- pollResult = 1;
681
- break;
682
- }
683
- }
684
- return pollResult;
685
- }
686
- finally {
687
- controller.abort();
688
- readline?.close();
689
- process.removeListener('SIGINT', interrupt);
690
- await polling;
691
- }
692
- }