@aopslabs/aops 0.2.0

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 (171) hide show
  1. package/LICENSE +19 -0
  2. package/NOTICE +7 -0
  3. package/README.md +203 -0
  4. package/assets/agent-assets/core/SKILL.md +49 -0
  5. package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
  6. package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
  7. package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
  8. package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
  9. package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
  10. package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
  11. package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
  12. package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
  13. package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
  14. package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
  15. package/assets/agent-assets/core/references/view/SKILL.md +58 -0
  16. package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
  17. package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
  18. package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
  19. package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
  20. package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
  21. package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
  22. package/assets/agent-assets/core/user-guides/docman.md +162 -0
  23. package/assets/agent-assets/core/user-guides/projectman.md +212 -0
  24. package/assets/agent-assets/core/user-guides/sys.md +94 -0
  25. package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
  26. package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
  27. package/assets/r6-remote-session-baseline.json +19 -0
  28. package/assets/skills/aops-install/SKILL.md +231 -0
  29. package/assets/skills/aops-install/agents/openai.yaml +4 -0
  30. package/dist/commands/activity.js +352 -0
  31. package/dist/commands/agent-profile.js +653 -0
  32. package/dist/commands/agent.js +710 -0
  33. package/dist/commands/agents-md.js +125 -0
  34. package/dist/commands/api.js +84 -0
  35. package/dist/commands/archive.js +873 -0
  36. package/dist/commands/artifact.js +535 -0
  37. package/dist/commands/assets.js +1414 -0
  38. package/dist/commands/auth/device-login.js +1 -0
  39. package/dist/commands/auth/legacy-export.js +177 -0
  40. package/dist/commands/auth/login.js +426 -0
  41. package/dist/commands/chat.js +1363 -0
  42. package/dist/commands/chatv3.js +1684 -0
  43. package/dist/commands/checkpoint.js +549 -0
  44. package/dist/commands/commercial-artifact.js +475 -0
  45. package/dist/commands/commercial-license.js +594 -0
  46. package/dist/commands/community-auth.js +62 -0
  47. package/dist/commands/community-cockpit.js +185 -0
  48. package/dist/commands/community-console.js +109 -0
  49. package/dist/commands/community-doctor.js +335 -0
  50. package/dist/commands/community-server.js +2062 -0
  51. package/dist/commands/community-setup.js +284 -0
  52. package/dist/commands/discuss.js +1448 -0
  53. package/dist/commands/doc.js +5219 -0
  54. package/dist/commands/experience.js +922 -0
  55. package/dist/commands/host.js +1127 -0
  56. package/dist/commands/init.js +118 -0
  57. package/dist/commands/memory.js +3129 -0
  58. package/dist/commands/mission.js +562 -0
  59. package/dist/commands/official-catalog.js +134 -0
  60. package/dist/commands/plan.js +84 -0
  61. package/dist/commands/playbook.js +415 -0
  62. package/dist/commands/pm/context.js +6 -0
  63. package/dist/commands/pm/index.js +701 -0
  64. package/dist/commands/pm/projectman.js +6266 -0
  65. package/dist/commands/project.js +1080 -0
  66. package/dist/commands/prompt.js +1024 -0
  67. package/dist/commands/repo-sync.js +1907 -0
  68. package/dist/commands/resource.js +527 -0
  69. package/dist/commands/skill.js +1211 -0
  70. package/dist/commands/start-disciplines.js +510 -0
  71. package/dist/commands/start.js +1343 -0
  72. package/dist/commands/target.js +309 -0
  73. package/dist/commands/version.js +42 -0
  74. package/dist/commands/view.js +1852 -0
  75. package/dist/launchers/cockpit.js +28 -0
  76. package/dist/launchers/options.js +74 -0
  77. package/dist/launchers/server.js +41 -0
  78. package/dist/lib/agent-assets/envelope.js +83 -0
  79. package/dist/lib/agent-assets/gateway.js +15 -0
  80. package/dist/lib/agent-assets/guards.js +23 -0
  81. package/dist/lib/agent-assets/hosted-discovery.js +148 -0
  82. package/dist/lib/agent-assets/hosted-package-input.js +154 -0
  83. package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
  84. package/dist/lib/agent-assets/native-fs.js +589 -0
  85. package/dist/lib/agent-assets/package-manifest.js +324 -0
  86. package/dist/lib/agent-assets/portable-path.js +67 -0
  87. package/dist/lib/agent-assets/release-input.js +177 -0
  88. package/dist/lib/agent-assets/roots.js +45 -0
  89. package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
  90. package/dist/lib/agent-assets/runtime-targets.js +50 -0
  91. package/dist/lib/agent-assets/store-reader.js +1212 -0
  92. package/dist/lib/agent-assets/store-types.js +1 -0
  93. package/dist/lib/agent-assets/store-writer.js +1484 -0
  94. package/dist/lib/agent-assets/types.js +1 -0
  95. package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
  96. package/dist/lib/commercial-final-readiness.js +504 -0
  97. package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
  98. package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
  99. package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
  100. package/dist/lib/community-client-contract.js +164 -0
  101. package/dist/lib/community-cockpit-lifecycle.js +601 -0
  102. package/dist/lib/community-command-abort.js +59 -0
  103. package/dist/lib/community-diagnostic.js +237 -0
  104. package/dist/lib/community-docker-adapter.js +498 -0
  105. package/dist/lib/community-home.js +31 -0
  106. package/dist/lib/community-instance-contract.js +148 -0
  107. package/dist/lib/community-instance-layout.js +97 -0
  108. package/dist/lib/community-lifecycle.js +1631 -0
  109. package/dist/lib/community-migration-snapshot.js +770 -0
  110. package/dist/lib/community-native-application-recovery.js +425 -0
  111. package/dist/lib/community-native-child.js +240 -0
  112. package/dist/lib/community-native-database-recovery.js +456 -0
  113. package/dist/lib/community-native-lifecycle.js +2458 -0
  114. package/dist/lib/community-native-migration.js +339 -0
  115. package/dist/lib/community-native-postgres.js +596 -0
  116. package/dist/lib/community-operation-journal.js +773 -0
  117. package/dist/lib/community-operation-lock.js +596 -0
  118. package/dist/lib/community-release-resolver.js +291 -0
  119. package/dist/lib/community-release-verifier.js +475 -0
  120. package/dist/lib/community-repo-discovery.js +252 -0
  121. package/dist/lib/community-setup-server-env.js +203 -0
  122. package/dist/lib/external-url.js +110 -0
  123. package/dist/lib/official-catalog-gateway.js +108 -0
  124. package/dist/lib/official-catalog.js +364 -0
  125. package/dist/lib/server-bootstrap.js +240 -0
  126. package/dist/lib/setup-agent-assets-bridge.js +176 -0
  127. package/dist/lib/setup-agent-assets-release.js +52 -0
  128. package/dist/lib/setup-external-postgres.js +108 -0
  129. package/dist/lib/setup-init-orchestrator.js +990 -0
  130. package/dist/lib/setup-install-guide.js +52 -0
  131. package/dist/lib/setup-installer-release.js +47 -0
  132. package/dist/lib/setup-local-postgres.js +329 -0
  133. package/dist/lib/setup-official-catalog-bridge.js +148 -0
  134. package/dist/lib/setup-readiness.js +888 -0
  135. package/dist/lib/tui-launcher.js +59 -0
  136. package/dist/main.js +150 -0
  137. package/dist/seeds/agents-md.js +54 -0
  138. package/dist/utils/agent-gateway.js +275 -0
  139. package/dist/utils/agents-md.js +281 -0
  140. package/dist/utils/api.js +255 -0
  141. package/dist/utils/chatv3-session-store.js +185 -0
  142. package/dist/utils/command.js +67 -0
  143. package/dist/utils/community-host-runtime.js +376 -0
  144. package/dist/utils/config.js +1015 -0
  145. package/dist/utils/discussion-workspace.js +552 -0
  146. package/dist/utils/experience-workspace.js +326 -0
  147. package/dist/utils/guide-paths.js +67 -0
  148. package/dist/utils/hosted-sugar.js +56 -0
  149. package/dist/utils/hosted-workspace.js +229 -0
  150. package/dist/utils/memory-workspace.js +537 -0
  151. package/dist/utils/operator-runtime.js +199 -0
  152. package/dist/utils/password-crypto.js +39 -0
  153. package/dist/utils/playbook-workspace.js +479 -0
  154. package/dist/utils/project-context.js +255 -0
  155. package/dist/utils/prompts.js +70 -0
  156. package/dist/utils/refresh-race.js +28 -0
  157. package/dist/utils/repo-config.js +109 -0
  158. package/dist/utils/repo-first-projectman.js +194 -0
  159. package/dist/utils/repo-first-storage.js +13 -0
  160. package/dist/utils/repo-first-view.js +305 -0
  161. package/dist/utils/session-guidance.js +278 -0
  162. package/dist/utils/session-state.js +137 -0
  163. package/dist/utils/table.js +39 -0
  164. package/dist/utils/transient-fs.js +49 -0
  165. package/dist/utils/workspace-root.js +1 -0
  166. package/launchers/aops-cockpit.ps1 +15 -0
  167. package/launchers/aops-cockpit.sh +16 -0
  168. package/launchers/aops-server.ps1 +15 -0
  169. package/launchers/aops-server.sh +16 -0
  170. package/native/tui/win32-x64/aops-tui.exe +0 -0
  171. package/package.json +81 -0
@@ -0,0 +1,1684 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { Command } from 'commander';
3
+ import { logSuccess } from '@aopslabs/xf-cli-ui';
4
+ import { Chatv3Client, deriveKek, importEpochKey, MemoryKeyStore, parseInvite, unwrapEpochKey, } from '@aopslabs/domain-product-client-chatv3';
5
+ import { resolveCliApiBaseUrl } from '../utils/api.js';
6
+ import { applyCommonOptions, compactPayload, normalizeNonEmpty } from '../utils/command.js';
7
+ import { buildOperatorCookbook } from '../utils/hosted-sugar.js';
8
+ import { deleteChatv3Session, getChatv3SessionRecord, listChatv3SessionRecords, loadChatv3Session, resolveChatv3SessionStorePath, saveChatv3Session, summarizeChatv3Session, } from '../utils/chatv3-session-store.js';
9
+ const AGENT_BOOTSTRAP_STEPS = [
10
+ 'Read channel and active-room rules before acting.',
11
+ 'Use chat for coordination and wake; use PM for task/review truth.',
12
+ 'If PM/RR/doc/discuss refs are present, read those canonical records before changing code.',
13
+ 'Send a short ACK/status message after joining or consuming a directive.',
14
+ 'Mark delivered/read after reading messages so other members see cursor progress.',
15
+ 'Do not paste secrets, member tokens, or invite fragments into chat messages.',
16
+ ];
17
+ const SUMMARY_SOURCE_MESSAGE_POLICY = 'For summarization only; DO NOT persist verbatim to memory.';
18
+ const DEFAULT_CHATV3_SPACE_SLUG = 'default';
19
+ const DEFAULT_CHATV3_CHANNEL_MODE = 'server-encrypted';
20
+ const CHATV3_INVITE_SECRET_WARNING = 'Invite contains secrets; share securely and do not paste it into public logs.';
21
+ const CHATV3_COMMAND_TREE = {
22
+ channel: new Set(['create', 'delete', 'purge-before']),
23
+ channels: null,
24
+ join: null,
25
+ send: null,
26
+ read: null,
27
+ listen: null,
28
+ leave: null,
29
+ binding: new Set(['add', 'list', 'remove']),
30
+ member: new Set(['list', 'remove']),
31
+ presence: new Set(['list', 'set']),
32
+ room: new Set(['brief', 'summary']),
33
+ session: new Set(['list', 'get', 'forget']),
34
+ help: null,
35
+ };
36
+ function toInteger(value, label) {
37
+ if (value === undefined)
38
+ return 0;
39
+ if (typeof value === 'number' && Number.isInteger(value))
40
+ return value;
41
+ const normalized = normalizeNonEmpty(value);
42
+ if (!normalized)
43
+ return 0;
44
+ const parsed = Number.parseInt(normalized, 10);
45
+ if (!Number.isInteger(parsed) || parsed < 0)
46
+ throw new Error(`${label} must be a non-negative integer.`);
47
+ return parsed;
48
+ }
49
+ function toPositiveInteger(value, label, fallback) {
50
+ if (value === undefined)
51
+ return fallback;
52
+ const parsed = toInteger(value, label);
53
+ if (parsed <= 0)
54
+ throw new Error(`${label} must be a positive integer.`);
55
+ return parsed;
56
+ }
57
+ function sleep(ms) {
58
+ return new Promise((resolve) => setTimeout(resolve, ms));
59
+ }
60
+ export function guardChatv3UnknownSubcommand(argv) {
61
+ const index = argv.indexOf('chatv3');
62
+ if (index < 0)
63
+ return;
64
+ const top = argv[index + 1];
65
+ if (!top || top.startsWith('-'))
66
+ return;
67
+ const nested = CHATV3_COMMAND_TREE[top];
68
+ if (nested === undefined) {
69
+ throw new Error(`unknown chatv3 command '${top}'. Run aops-cli chatv3 --help.`);
70
+ }
71
+ if (nested === null)
72
+ return;
73
+ const child = argv[index + 2];
74
+ if (!child || child.startsWith('-'))
75
+ return;
76
+ if (child === 'help')
77
+ return;
78
+ if (!nested.has(child)) {
79
+ throw new Error(`unknown chatv3 ${top} command '${child}'. Run aops-cli chatv3 ${top} --help.`);
80
+ }
81
+ }
82
+ function readTextInput(value, label) {
83
+ const normalized = normalizeNonEmpty(value);
84
+ if (!normalized)
85
+ throw new Error(`Provide ${label}.`);
86
+ if (normalized.startsWith('@'))
87
+ return readFileSync(normalized.slice(1).trim(), 'utf8');
88
+ return normalized;
89
+ }
90
+ function requireField(value, label) {
91
+ const normalized = normalizeNonEmpty(value);
92
+ if (!normalized)
93
+ throw new Error(`Provide ${label}.`);
94
+ return normalized;
95
+ }
96
+ function resolveChatv3ServerBaseUrl(options) {
97
+ return resolveCliApiBaseUrl(options.apiBaseUrl);
98
+ }
99
+ function normalizeChatv3Slug(value, label) {
100
+ const slug = requireField(value, label);
101
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
102
+ throw new Error(`${label} must be lowercase kebab-case: /^[a-z0-9][a-z0-9-]*$/.`);
103
+ }
104
+ return slug;
105
+ }
106
+ function slugifyChatv3Slug(value) {
107
+ return value
108
+ .trim()
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9]+/g, '-')
111
+ .replace(/^-+|-+$/g, '')
112
+ .slice(0, 120);
113
+ }
114
+ function resolveChannelSlug(title, explicitSlug) {
115
+ if (explicitSlug !== undefined)
116
+ return normalizeChatv3Slug(explicitSlug, '--slug <slug>');
117
+ const slug = slugifyChatv3Slug(title);
118
+ if (!slug)
119
+ throw new Error('Provide --slug <slug>; the title cannot be converted to a lowercase kebab-case slug.');
120
+ return slug;
121
+ }
122
+ function resolveSpaceSlug(value) {
123
+ return normalizeNonEmpty(value) ? normalizeChatv3Slug(value, '--space <slug>') : DEFAULT_CHATV3_SPACE_SLUG;
124
+ }
125
+ function normalizeChannelMode(value) {
126
+ const mode = normalizeNonEmpty(value) ?? DEFAULT_CHATV3_CHANNEL_MODE;
127
+ if (mode !== 'server-encrypted' && mode !== 'e2e') {
128
+ throw new Error('--mode must be one of: server-encrypted, e2e.');
129
+ }
130
+ return mode;
131
+ }
132
+ function normalizeChannelStatus(value) {
133
+ const status = normalizeNonEmpty(value);
134
+ if (!status)
135
+ return undefined;
136
+ if (status !== 'active' && status !== 'archived') {
137
+ throw new Error('--status must be one of: active, archived.');
138
+ }
139
+ return status;
140
+ }
141
+ function looksUuid(value) {
142
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
143
+ }
144
+ function normalizeIsoDateTime(value, label) {
145
+ const raw = requireField(value, label);
146
+ if (Number.isNaN(Date.parse(raw)))
147
+ throw new Error(`${label} must be an ISO date/time.`);
148
+ return raw;
149
+ }
150
+ function slugifySessionId(value) {
151
+ return value
152
+ .trim()
153
+ .toLowerCase()
154
+ .replace(/[^a-z0-9._-]+/g, '-')
155
+ .replace(/^-+|-+$/g, '')
156
+ .slice(0, 80) || 'chatv3-session';
157
+ }
158
+ function sessionIdFor(handle, channelId, explicit) {
159
+ return slugifySessionId(normalizeNonEmpty(explicit) ?? `${handle}-${channelId.slice(0, 8)}`);
160
+ }
161
+ function roomToSessionRoom(room) {
162
+ return {
163
+ id: room.id,
164
+ slug: room.slug,
165
+ title: room.title,
166
+ currentEpoch: room.currentEpoch,
167
+ purpose: 'purpose' in room ? room.purpose : undefined,
168
+ guidanceMarkdown: 'guidanceMarkdown' in room ? room.guidanceMarkdown : undefined,
169
+ status: 'status' in room ? room.status : undefined,
170
+ };
171
+ }
172
+ function normalizePurpose(value) {
173
+ if (typeof value !== 'string')
174
+ return null;
175
+ const trimmed = value.trim();
176
+ return trimmed ? trimmed : null;
177
+ }
178
+ function buildAgentGuidance(params) {
179
+ return compactPayload({
180
+ sourceField: 'guidanceMarkdown',
181
+ channel: compactPayload({
182
+ id: params.channel.id,
183
+ slug: params.channel.slug,
184
+ title: params.channel.title,
185
+ purpose: normalizePurpose(params.channel.purpose),
186
+ rules: normalizePurpose(params.channel.guidanceMarkdown),
187
+ }),
188
+ rooms: params.rooms.map((room) => compactPayload({
189
+ id: room.id,
190
+ slug: room.slug,
191
+ title: room.title,
192
+ purpose: 'purpose' in room ? normalizePurpose(room.purpose) : null,
193
+ rules: 'guidanceMarkdown' in room ? normalizePurpose(room.guidanceMarkdown) : null,
194
+ })),
195
+ bootstrap: AGENT_BOOTSTRAP_STEPS,
196
+ });
197
+ }
198
+ function summarizeChatv3Binding(binding) {
199
+ return compactPayload({
200
+ id: binding.id,
201
+ channelId: binding.channelId,
202
+ roomId: binding.roomId ?? undefined,
203
+ bindingType: binding.bindingType,
204
+ refId: binding.refId ?? undefined,
205
+ uri: binding.uri ?? undefined,
206
+ title: binding.title ?? undefined,
207
+ note: binding.note ?? undefined,
208
+ createdBy: binding.createdBy ?? undefined,
209
+ createdAt: binding.createdAt ?? undefined,
210
+ });
211
+ }
212
+ function bindingLabel(binding) {
213
+ return normalizeNonEmpty(binding.title)
214
+ ?? normalizeNonEmpty(binding.refId)
215
+ ?? normalizeNonEmpty(binding.uri)
216
+ ?? binding.id;
217
+ }
218
+ function buildRecommendedNextReads(bindings) {
219
+ return bindings.map((binding, index) => compactPayload({
220
+ index: index + 1,
221
+ type: binding.bindingType,
222
+ refId: binding.refId ?? undefined,
223
+ uri: binding.uri ?? undefined,
224
+ title: binding.title ?? undefined,
225
+ note: binding.note ?? undefined,
226
+ }));
227
+ }
228
+ function senderHandleForMessage(message, members) {
229
+ const raw = message;
230
+ const senderMemberId = normalizeNonEmpty(raw.senderMemberId);
231
+ if (!senderMemberId)
232
+ return undefined;
233
+ return members.find((member) => member.id === senderMemberId)?.handle;
234
+ }
235
+ function oneLine(value) {
236
+ return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
237
+ }
238
+ function summarizeRoomSourceMessages(messages, members) {
239
+ return messages.map((message) => {
240
+ const raw = message;
241
+ return compactPayload({
242
+ seq: message.seq,
243
+ createdAt: normalizeNonEmpty(raw.createdAt),
244
+ kind: normalizeNonEmpty(raw.kind),
245
+ senderMemberId: normalizeNonEmpty(raw.senderMemberId),
246
+ senderHandle: senderHandleForMessage(message, members),
247
+ text: oneLine(raw.text),
248
+ policy: SUMMARY_SOURCE_MESSAGE_POLICY,
249
+ });
250
+ });
251
+ }
252
+ function seqRangeForRoom(room, afterSeq, explicitLatestSeq) {
253
+ const latestSeq = explicitLatestSeq ?? room.lastSeq ?? afterSeq;
254
+ return {
255
+ afterSeq,
256
+ latestSeq,
257
+ nextAfterSeq: latestSeq,
258
+ messageCountEstimate: Math.max(0, latestSeq - afterSeq),
259
+ };
260
+ }
261
+ function buildRoomContextMarkdown(params) {
262
+ const lines = [
263
+ `# ${params.title}`,
264
+ '',
265
+ `Channel: ${params.runtime.channel.title || params.runtime.channel.slug} (${params.runtime.channel.slug})`,
266
+ `Room: ${params.room.title || params.room.slug} (${params.room.slug})`,
267
+ `Room id: ${params.room.id}`,
268
+ `Seq range: after ${params.seqRange.afterSeq ?? 0}, latest ${params.seqRange.latestSeq ?? 0}`,
269
+ ];
270
+ if (params.forAgent)
271
+ lines.push(`Audience: ${params.forAgent}`);
272
+ const channelGuidance = normalizeNonEmpty(params.runtime.channel.guidanceMarkdown);
273
+ const roomGuidance = normalizeNonEmpty(params.room.guidanceMarkdown);
274
+ if (!params.summaryMode && (channelGuidance || roomGuidance)) {
275
+ lines.push('', '## Guidance');
276
+ if (channelGuidance)
277
+ lines.push(channelGuidance);
278
+ if (roomGuidance && roomGuidance !== channelGuidance)
279
+ lines.push(roomGuidance);
280
+ }
281
+ lines.push('', '## Members');
282
+ if (params.members.length === 0) {
283
+ lines.push('- (none listed)');
284
+ }
285
+ else {
286
+ params.members.forEach((member) => {
287
+ lines.push(`- ${member.handle} (${member.roleKey}, ${member.status})`);
288
+ });
289
+ }
290
+ lines.push('', '## Presence');
291
+ if (params.presence.length === 0) {
292
+ lines.push('- (none listed)');
293
+ }
294
+ else {
295
+ params.presence.forEach((entry) => {
296
+ const member = params.members.find((candidate) => candidate.id === entry.memberId);
297
+ const handle = member?.handle ?? entry.memberId;
298
+ const note = normalizeNonEmpty(entry.note);
299
+ lines.push(`- ${handle}: ${entry.state}${entry.expired ? ' expired' : ''}${note ? ` - ${note}` : ''}`);
300
+ });
301
+ }
302
+ lines.push('', '## Bindings');
303
+ if (params.bindings.length === 0) {
304
+ lines.push('- (none listed)');
305
+ }
306
+ else {
307
+ params.bindings.forEach((binding) => {
308
+ const label = bindingLabel(binding);
309
+ const uri = normalizeNonEmpty(binding.uri);
310
+ const note = normalizeNonEmpty(binding.note);
311
+ lines.push(`- ${binding.bindingType}: ${label}${uri && uri !== label ? ` <${uri}>` : ''}${note ? ` - ${note}` : ''}`);
312
+ });
313
+ }
314
+ lines.push('', '## Cursors');
315
+ if (params.receipts.length === 0) {
316
+ lines.push('- (none listed)');
317
+ }
318
+ else {
319
+ params.receipts.forEach((receipt) => {
320
+ lines.push(`- ${receipt.handle}: read ${receipt.lastReadSeq}, delivered ${receipt.deliveredSeq}, ack ${receipt.ackSeq}`);
321
+ });
322
+ }
323
+ lines.push('', '## Recommended Next Reads');
324
+ const nextReads = buildRecommendedNextReads(params.bindings);
325
+ if (nextReads.length === 0) {
326
+ lines.push('- (none listed)');
327
+ }
328
+ else {
329
+ nextReads.forEach((read) => {
330
+ const label = normalizeNonEmpty(read.title) ?? normalizeNonEmpty(read.refId) ?? normalizeNonEmpty(read.uri) ?? '(unlabeled)';
331
+ lines.push(`- ${read.type}: ${label}`);
332
+ });
333
+ }
334
+ if (params.summaryMode) {
335
+ lines.push('', '## Source Messages For Summarization');
336
+ lines.push(`Policy: ${SUMMARY_SOURCE_MESSAGE_POLICY}`);
337
+ if (!params.sourceMessages || params.sourceMessages.length === 0) {
338
+ lines.push('- (none in seq range)');
339
+ }
340
+ else {
341
+ params.sourceMessages.forEach((message) => {
342
+ const seq = message.seq ?? '?';
343
+ const sender = normalizeNonEmpty(message.senderHandle) ?? normalizeNonEmpty(message.senderMemberId) ?? 'unknown';
344
+ const createdAt = normalizeNonEmpty(message.createdAt);
345
+ const kind = normalizeNonEmpty(message.kind) ?? 'message';
346
+ const text = oneLine(message.text) || '(empty)';
347
+ lines.push(`- seq ${seq}${createdAt ? ` @ ${createdAt}` : ''} | ${sender} | ${kind}: ${text}`);
348
+ });
349
+ }
350
+ lines.push('', 'Narrative digest slot: write an abstractive NARRATIVE-DIGEST from the source messages, refs, and seq range; never store source messages verbatim in memory.');
351
+ }
352
+ else {
353
+ lines.push('', 'Use this room brief as orientation. Follow PM/RR/doc/discuss refs in bindings before acting; chat remains coordination only.');
354
+ }
355
+ return `${lines.join('\n')}\n`;
356
+ }
357
+ function buildJoinSummary(params) {
358
+ const activeRoom = params.rooms.find((room) => room.id === params.activeRoomId) ??
359
+ params.rooms.find((room) => room.slug === 'general') ??
360
+ params.rooms[0];
361
+ const channelLabel = params.channel.title || params.channel.slug;
362
+ const roomLabel = activeRoom?.title || activeRoom?.slug || 'room';
363
+ return {
364
+ text: `Joined ${channelLabel} / ${roomLabel} - ${params.rooms.length} room${params.rooms.length === 1 ? '' : 's'}, ${params.memberCount ?? 0} member${params.memberCount === 1 ? '' : 's'}, ${params.initialMessageCount ?? 0} recent message${params.initialMessageCount === 1 ? '' : 's'}.`,
365
+ channel: { id: params.channel.id, slug: params.channel.slug, title: params.channel.title },
366
+ activeRoom: activeRoom ? { id: activeRoom.id, slug: activeRoom.slug, title: activeRoom.title } : null,
367
+ roomCount: params.rooms.length,
368
+ memberCount: params.memberCount ?? null,
369
+ initialMessageCount: params.initialMessageCount ?? null,
370
+ };
371
+ }
372
+ function summarizeChannelMine(row) {
373
+ const channel = row.channel;
374
+ const membershipStatus = row.membership?.status ?? null;
375
+ const archived = channel.status === 'archived' || Boolean(channel.archivedAt);
376
+ const locked = archived || (membershipStatus !== null && membershipStatus !== 'active');
377
+ return compactPayload({
378
+ id: channel.id,
379
+ slug: channel.slug,
380
+ title: channel.title,
381
+ status: channel.status,
382
+ archived,
383
+ locked,
384
+ generalRoomId: channel.generalRoomId,
385
+ updatedAt: channel.updatedAt,
386
+ archivedAt: channel.archivedAt,
387
+ isOwner: row.isOwner,
388
+ canDelete: row.canDelete,
389
+ membership: row.membership ? compactPayload({
390
+ id: row.membership.id,
391
+ handle: row.membership.handle,
392
+ roleKey: row.membership.roleKey,
393
+ status: row.membership.status,
394
+ joinedAt: row.membership.joinedAt,
395
+ removedAt: row.membership.removedAt,
396
+ }) : null,
397
+ modeStatus: row.modeStatus,
398
+ });
399
+ }
400
+ function summarizeChannelDeleteTarget(channel) {
401
+ return compactPayload({
402
+ id: channel.id,
403
+ slug: channel.slug,
404
+ title: channel.title,
405
+ status: channel.status,
406
+ spaceId: channel.spaceId,
407
+ encryptionMode: channel.encryptionMode,
408
+ generalRoomId: channel.generalRoomId,
409
+ createdAt: channel.createdAt,
410
+ updatedAt: channel.updatedAt,
411
+ archivedAt: channel.archivedAt,
412
+ archived: channel.status === 'archived' || Boolean(channel.archivedAt),
413
+ });
414
+ }
415
+ function buildEnvelope(params) {
416
+ const resolvedContext = compactPayload({
417
+ serverBaseUrl: params.serverBaseUrl,
418
+ storePath: resolveChatv3SessionStorePath(params.options.storePath),
419
+ });
420
+ if (params.options.json) {
421
+ console.log(JSON.stringify(compactPayload({
422
+ command: params.command,
423
+ surface: 'chatv3-product-client-v1',
424
+ resolvedContext,
425
+ input: params.input,
426
+ artifacts: params.artifacts,
427
+ result: params.result,
428
+ }), null, 2));
429
+ return;
430
+ }
431
+ logSuccess(`${params.command} completed.`);
432
+ console.log(JSON.stringify(params.result, null, 2));
433
+ }
434
+ async function hydrateRoomEpochs(runtime, room) {
435
+ if (!runtime.secrets.wrapSecret) {
436
+ if (runtime.channel.encryptionMode === 'server-encrypted')
437
+ return;
438
+ throw new Error(`ChatV3 session "${runtime.record.id}" has no wrapSecret for e2e channel ${runtime.channel.id}.`);
439
+ }
440
+ const epochs = await runtime.client.http.get(`/rooms/${room.id}/epochs`);
441
+ for (const epoch of epochs) {
442
+ const kek = await deriveKek(runtime.secrets.wrapSecret, {
443
+ tenantId: runtime.channel.tenantId,
444
+ spaceId: runtime.channel.spaceId,
445
+ keyId: runtime.record.keyId,
446
+ epoch: epoch.epoch,
447
+ });
448
+ const epochKey = await unwrapEpochKey(kek, epoch.wrappedKeyBlob);
449
+ await runtime.keyStore.setEpochKey(epoch.roomId, epoch.epoch, epochKey);
450
+ }
451
+ }
452
+ async function hydrateStoredServerEpochKeys(runtime) {
453
+ for (const epoch of runtime.secrets.epochKeys ?? []) {
454
+ await runtime.keyStore.setEpochKey(epoch.roomId, epoch.epoch, await importEpochKey(epoch.rawEpochKey));
455
+ }
456
+ }
457
+ function sessionEpochKeysFromServer(keys) {
458
+ if (!keys || keys.length === 0)
459
+ return undefined;
460
+ return keys.map((key) => compactPayload({
461
+ roomId: key.roomId,
462
+ epoch: key.epoch,
463
+ rawEpochKey: key.rawEpochKey,
464
+ cipherSuite: key.cipherSuite,
465
+ keyId: key.keyId,
466
+ }));
467
+ }
468
+ async function refreshServerEpochKeys(runtime) {
469
+ if (runtime.channel.encryptionMode !== 'server-encrypted')
470
+ return;
471
+ const bundle = await runtime.client.epochKeys(runtime.channel.id, { tenantId: runtime.channel.tenantId });
472
+ await Promise.all(bundle.keys.map(async (epoch) => {
473
+ await runtime.keyStore.setEpochKey(epoch.roomId, epoch.epoch, await importEpochKey(epoch.rawEpochKey));
474
+ }));
475
+ runtime.secrets.epochKeys = sessionEpochKeysFromServer(bundle.keys);
476
+ }
477
+ async function persistRuntimeSnapshot(runtime, activeRoomId) {
478
+ const plain = {
479
+ id: runtime.record.id,
480
+ serverBaseUrl: runtime.client.http.url('/').replace(/\/api\/chatv3\/v1\/$/, ''),
481
+ handle: runtime.record.handle,
482
+ channel: {
483
+ id: runtime.channel.id,
484
+ tenantId: runtime.channel.tenantId,
485
+ spaceId: runtime.channel.spaceId,
486
+ slug: runtime.channel.slug,
487
+ title: runtime.channel.title,
488
+ purpose: runtime.channel.purpose,
489
+ guidanceMarkdown: runtime.channel.guidanceMarkdown,
490
+ encryptionMode: runtime.channel.encryptionMode,
491
+ },
492
+ keyId: runtime.record.keyId,
493
+ activeRoomId: activeRoomId ?? runtime.record.activeRoomId ?? runtime.rooms[0]?.id,
494
+ rooms: runtime.rooms.map(roomToSessionRoom),
495
+ createdAt: runtime.record.createdAt,
496
+ };
497
+ runtime.record = await saveChatv3Session(plain, runtime.secrets, { storePath: runtime.storePath });
498
+ }
499
+ async function loadRuntime(options, sessionId) {
500
+ const storePath = resolveChatv3SessionStorePath(options.storePath);
501
+ const loaded = await loadChatv3Session(sessionId, { storePath });
502
+ const serverBaseUrl = normalizeNonEmpty(options.apiBaseUrl) ?? loaded.record.serverBaseUrl;
503
+ const keyStore = new MemoryKeyStore();
504
+ if (loaded.secrets.wrapSecret) {
505
+ await keyStore.setWrapSecret(loaded.record.channel.id, loaded.secrets.wrapSecret);
506
+ }
507
+ const client = new Chatv3Client({
508
+ serverBaseUrl,
509
+ memberToken: loaded.secrets.memberToken,
510
+ keyStore,
511
+ });
512
+ const channel = await client.getChannel(loaded.record.channel.id);
513
+ const rooms = await client.listRooms(channel.id, { status: 'active' }).catch(() => loaded.record.rooms.map((room) => ({
514
+ ...room,
515
+ tenantId: channel.tenantId,
516
+ channelId: channel.id,
517
+ kind: 'session',
518
+ purpose: null,
519
+ status: room.status ?? 'active',
520
+ lastSeq: 0,
521
+ lastMessageAt: null,
522
+ createdBy: null,
523
+ updatedBy: null,
524
+ createdAt: loaded.record.createdAt,
525
+ updatedAt: loaded.record.updatedAt,
526
+ archivedAt: null,
527
+ })));
528
+ const runtime = {
529
+ ...loaded,
530
+ client,
531
+ keyStore,
532
+ channel,
533
+ rooms,
534
+ storePath,
535
+ };
536
+ await hydrateStoredServerEpochKeys(runtime);
537
+ if (channel.encryptionMode === 'server-encrypted') {
538
+ await refreshServerEpochKeys(runtime);
539
+ }
540
+ else {
541
+ for (const room of rooms) {
542
+ await hydrateRoomEpochs(runtime, room);
543
+ }
544
+ }
545
+ await persistRuntimeSnapshot(runtime);
546
+ return runtime;
547
+ }
548
+ function channelMatchesSelector(channel, selector) {
549
+ return channel.id === selector || channel.slug === selector || (selector.length >= 8 && channel.id.startsWith(selector));
550
+ }
551
+ function resolveRoom(runtime, selector) {
552
+ const normalized = normalizeNonEmpty(selector);
553
+ let room;
554
+ if (normalized) {
555
+ const matches = runtime.rooms.filter((candidate) => candidate.id === normalized || candidate.slug === normalized);
556
+ if (matches.length > 1) {
557
+ throw new Error(`Room selector "${normalized}" matched multiple rooms in channel ${runtime.channel.slug}; use a room id.`);
558
+ }
559
+ room = matches[0];
560
+ }
561
+ else {
562
+ room =
563
+ runtime.rooms.find((candidate) => candidate.id === runtime.record.activeRoomId) ??
564
+ runtime.rooms.find((candidate) => candidate.slug === 'general') ??
565
+ runtime.rooms[0];
566
+ }
567
+ if (!room)
568
+ throw new Error(normalized ? `Room not found in session: ${normalized}` : 'Session has no active rooms.');
569
+ return room;
570
+ }
571
+ async function loadRuntimeForChannelGuard(options) {
572
+ const explicitSessionId = normalizeNonEmpty(options.session);
573
+ const channelSelector = normalizeNonEmpty(options.channel);
574
+ if (explicitSessionId) {
575
+ const runtime = await loadRuntime(options, explicitSessionId);
576
+ if (channelSelector && !channelMatchesSelector(runtime.channel, channelSelector)) {
577
+ throw new Error(`Session "${explicitSessionId}" is bound to channel ${runtime.channel.slug} (${runtime.channel.id}); refusing --channel ${channelSelector}.`);
578
+ }
579
+ return { sessionId: explicitSessionId, runtime };
580
+ }
581
+ if (!channelSelector)
582
+ throw new Error('Provide --session <id> or --channel <channel-id-or-slug>.');
583
+ const storePath = resolveChatv3SessionStorePath(options.storePath);
584
+ const matches = (await listChatv3SessionRecords({ storePath }))
585
+ .filter((record) => channelMatchesSelector(record.channel, channelSelector));
586
+ if (matches.length === 0)
587
+ throw new Error(`No local ChatV3 session found for channel ${channelSelector}. Provide --session <id>.`);
588
+ if (matches.length > 1) {
589
+ const ids = matches.map((record) => `${record.id}=${record.channel.slug}(${record.channel.id})`).join(', ');
590
+ throw new Error(`Multiple local ChatV3 sessions match channel ${channelSelector}: ${ids}. Provide --session <id>.`);
591
+ }
592
+ const sessionId = matches[0].id;
593
+ return { sessionId, runtime: await loadRuntime(options, sessionId) };
594
+ }
595
+ async function listChatv3Bindings(runtime, roomId) {
596
+ const q = new URLSearchParams();
597
+ if (roomId)
598
+ q.set('roomId', roomId);
599
+ const qs = q.toString();
600
+ return runtime.client.http.get(`/channels/${runtime.channel.id}/bindings${qs ? `?${qs}` : ''}`);
601
+ }
602
+ async function resolveChannelListSpaceId(client, spaceSlugOrId) {
603
+ if (!spaceSlugOrId)
604
+ return {};
605
+ const space = normalizeChatv3Slug(spaceSlugOrId, '--space <slug>');
606
+ if (looksUuid(space))
607
+ return { spaceId: space, spaceSlug: space };
608
+ const spaces = await client.listSpaces({ limit: 500 });
609
+ const row = spaces.find((entry) => entry.slug === space);
610
+ if (!row)
611
+ throw new Error(`No ChatV3 space found for --space ${space}.`);
612
+ return { spaceId: row.id, spaceSlug: row.slug };
613
+ }
614
+ async function listChannelsForDeleteLookup(client) {
615
+ const rows = await Promise.all([
616
+ client.listChannels({ status: 'active', limit: 500 }),
617
+ client.listChannels({ status: 'archived', limit: 500 }),
618
+ ]);
619
+ const byId = new Map();
620
+ rows.flat().forEach((channel) => byId.set(channel.id, channel));
621
+ return [...byId.values()];
622
+ }
623
+ function assertConfirmSlugMatches(target, confirmSlug) {
624
+ if (target.slug !== confirmSlug) {
625
+ throw new Error(`--confirm-slug ${confirmSlug} does not match target channel slug ${target.slug}. Nothing was deleted.`);
626
+ }
627
+ }
628
+ async function resolveChannelDeleteTarget(params) {
629
+ const selector = params.selector;
630
+ if (params.runtime) {
631
+ const channel = params.runtime.channel;
632
+ if (selector !== channel.id && selector !== channel.slug) {
633
+ throw new Error(`--channel ${selector} does not match session channel ${channel.slug} (${channel.id}). Nothing was deleted.`);
634
+ }
635
+ return channel;
636
+ }
637
+ if (looksUuid(selector)) {
638
+ const known = (await listChannelsForDeleteLookup(params.client).catch(() => []))
639
+ .find((channel) => channel.id === selector);
640
+ return known ?? { id: selector, slug: '' };
641
+ }
642
+ const slug = normalizeChatv3Slug(selector, '--channel <id|slug>');
643
+ const matches = (await listChannelsForDeleteLookup(params.client)).filter((channel) => channel.slug === slug);
644
+ if (matches.length === 0) {
645
+ throw new Error(`No ChatV3 channel found for slug ${slug}. Pass a channel id or use --session for a local session-bound delete.`);
646
+ }
647
+ if (matches.length > 1) {
648
+ throw new Error(`Channel slug ${slug} matches ${matches.length} channels across spaces. Pass a channel id or use --session.`);
649
+ }
650
+ return matches[0];
651
+ }
652
+ export async function runChatv3ChannelCreate(options = {}) {
653
+ const title = requireField(options.title, '--title <title>');
654
+ const handle = requireField(options.handle, '--handle <handle>');
655
+ const slug = resolveChannelSlug(title, normalizeNonEmpty(options.slug));
656
+ const spaceSlug = resolveSpaceSlug(options.space);
657
+ const mode = normalizeChannelMode(options.mode);
658
+ const serverBaseUrl = resolveChatv3ServerBaseUrl(options);
659
+ const storePath = resolveChatv3SessionStorePath(options.storePath);
660
+ const explicitSessionId = normalizeNonEmpty(options.session);
661
+ if (options.saveSession && explicitSessionId && options.force !== true) {
662
+ const existing = await getChatv3SessionRecord(explicitSessionId, { storePath });
663
+ if (existing) {
664
+ throw new Error(`Refusing to overwrite existing ChatV3 session "${explicitSessionId}" bound to channel ${existing.channel.slug} (${existing.channel.id}). Rerun with --force to replace it.`);
665
+ }
666
+ }
667
+ const client = new Chatv3Client({ serverBaseUrl, accessToken: options.accessToken });
668
+ const space = await client.ensureSpace(spaceSlug, spaceSlug);
669
+ const created = await client.createChannel({
670
+ space,
671
+ slug,
672
+ title,
673
+ handle,
674
+ encryptionMode: mode,
675
+ });
676
+ const channelMode = created.channel.encryptionMode ?? mode;
677
+ const parsedInvite = parseInvite(created.invite);
678
+ const activeRoom = created.generalRoom;
679
+ const sessionId = sessionIdFor(handle, created.channel.id, explicitSessionId);
680
+ const memberToken = client.http.memberToken;
681
+ if (!memberToken)
682
+ throw new Error('ChatV3 channel create did not return a member token.');
683
+ let record = null;
684
+ if (options.saveSession) {
685
+ if (options.force !== true) {
686
+ const existing = await getChatv3SessionRecord(sessionId, { storePath });
687
+ if (existing) {
688
+ throw new Error(`Refusing to overwrite ChatV3 session "${sessionId}" bound to channel ${existing.channel.slug} (${existing.channel.id}). Rerun with --force to replace it.`);
689
+ }
690
+ }
691
+ const epochKeys = channelMode === 'server-encrypted'
692
+ ? sessionEpochKeysFromServer((await client.epochKeys(created.channel.id, { tenantId: created.channel.tenantId })).keys)
693
+ : undefined;
694
+ const saved = await saveChatv3Session({
695
+ id: sessionId,
696
+ serverBaseUrl,
697
+ handle,
698
+ channel: {
699
+ id: created.channel.id,
700
+ tenantId: created.channel.tenantId,
701
+ spaceId: created.channel.spaceId,
702
+ slug: created.channel.slug,
703
+ title: created.channel.title ?? title,
704
+ purpose: created.channel.purpose ?? null,
705
+ guidanceMarkdown: created.channel.guidanceMarkdown ?? null,
706
+ encryptionMode: channelMode,
707
+ },
708
+ keyId: parsedInvite.keyId,
709
+ activeRoomId: activeRoom.id,
710
+ rooms: [roomToSessionRoom(activeRoom)],
711
+ }, {
712
+ memberToken,
713
+ wrapSecret: parsedInvite.mode === 'server-encrypted' ? undefined : parsedInvite.wrapSecret,
714
+ epochKeys,
715
+ }, { storePath });
716
+ record = summarizeChatv3Session(saved);
717
+ }
718
+ const channelForGuidance = {
719
+ id: created.channel.id,
720
+ slug: created.channel.slug,
721
+ title: created.channel.title ?? title,
722
+ purpose: created.channel.purpose ?? null,
723
+ guidanceMarkdown: created.channel.guidanceMarkdown ?? null,
724
+ };
725
+ buildEnvelope({
726
+ options,
727
+ command: 'chatv3.channel.create',
728
+ serverBaseUrl,
729
+ input: {
730
+ title,
731
+ slug,
732
+ space: spaceSlug,
733
+ mode,
734
+ handle,
735
+ session: sessionId,
736
+ saveSession: options.saveSession === true,
737
+ },
738
+ artifacts: compactPayload({ sessionId, channelId: created.channel.id, activeRoomId: activeRoom.id }),
739
+ result: {
740
+ created: true,
741
+ saved: options.saveSession === true,
742
+ warning: CHATV3_INVITE_SECRET_WARNING,
743
+ invite: created.invite,
744
+ channel: compactPayload({
745
+ id: created.channel.id,
746
+ tenantId: created.channel.tenantId,
747
+ spaceId: created.channel.spaceId,
748
+ slug: created.channel.slug,
749
+ title: created.channel.title ?? title,
750
+ encryptionMode: channelMode,
751
+ }),
752
+ activeRoom: { id: activeRoom.id, slug: activeRoom.slug, title: activeRoom.title },
753
+ session: record,
754
+ guidance: buildAgentGuidance({ channel: channelForGuidance, rooms: [activeRoom] }),
755
+ },
756
+ });
757
+ }
758
+ export async function runChatv3ChannelDelete(options = {}) {
759
+ const selector = requireField(options.channel, '--channel <id|slug>');
760
+ const confirmSlug = normalizeChatv3Slug(options.confirmSlug, '--confirm-slug <slug>');
761
+ const sessionId = normalizeNonEmpty(options.session);
762
+ const runtime = sessionId ? await loadRuntime(options, sessionId) : undefined;
763
+ const serverBaseUrl = runtime?.client.http.url('/').replace(/\/api\/chatv3\/v1\/$/, '') ?? resolveChatv3ServerBaseUrl(options);
764
+ const client = runtime?.client ?? new Chatv3Client({ serverBaseUrl, accessToken: options.accessToken });
765
+ const target = await resolveChannelDeleteTarget({ selector, runtime, client });
766
+ if (!target.slug) {
767
+ target.slug = confirmSlug;
768
+ }
769
+ assertConfirmSlugMatches(target, confirmSlug);
770
+ const deleted = await client.deleteChannel(target.id, { confirmSlug });
771
+ buildEnvelope({
772
+ options,
773
+ command: 'chatv3.channel.delete',
774
+ serverBaseUrl,
775
+ input: compactPayload({
776
+ session: sessionId,
777
+ channel: selector,
778
+ confirmSlug,
779
+ }),
780
+ artifacts: compactPayload({ sessionId, channelId: target.id }),
781
+ result: {
782
+ deleted: deleted.deleted === true,
783
+ destructive: true,
784
+ confirmSlug,
785
+ whatWasDeleted: [summarizeChannelDeleteTarget(target)],
786
+ },
787
+ });
788
+ }
789
+ export async function runChatv3ChannelPurgeBefore(options = {}) {
790
+ const beforeDate = normalizeIsoDateTime(options.before, '--before <ISO>');
791
+ const serverBaseUrl = resolveChatv3ServerBaseUrl(options);
792
+ const client = new Chatv3Client({ serverBaseUrl, accessToken: options.accessToken });
793
+ const dryRun = options.confirm !== true;
794
+ const result = await client.purgeChannelsBefore({
795
+ beforeDate,
796
+ dryRun,
797
+ confirm: options.confirm === true ? true : undefined,
798
+ });
799
+ const candidates = result.candidates.map(summarizeChannelDeleteTarget);
800
+ buildEnvelope({
801
+ options,
802
+ command: 'chatv3.channel.purge-before',
803
+ serverBaseUrl,
804
+ input: {
805
+ before: beforeDate,
806
+ dryRun,
807
+ confirm: options.confirm === true,
808
+ },
809
+ result: {
810
+ beforeDate: result.beforeDate,
811
+ dryRun: result.dryRun,
812
+ applied: result.applied,
813
+ candidateCount: result.candidateCount,
814
+ deletedCount: result.deletedCount,
815
+ willDeleteCount: dryRun ? result.candidateCount : 0,
816
+ whatWillBeDeleted: dryRun ? candidates : [],
817
+ whatWasDeleted: result.applied ? candidates : [],
818
+ },
819
+ });
820
+ }
821
+ export async function runChatv3Channels(options = {}) {
822
+ const serverBaseUrl = resolveChatv3ServerBaseUrl(options);
823
+ const client = new Chatv3Client({ serverBaseUrl, accessToken: options.accessToken });
824
+ const status = normalizeChannelStatus(options.status);
825
+ const spaceFilter = await resolveChannelListSpaceId(client, normalizeNonEmpty(options.space));
826
+ const rows = await client.listMyChannels({
827
+ ...(spaceFilter.spaceId ? { spaceId: spaceFilter.spaceId } : {}),
828
+ ...(status ? { status } : {}),
829
+ });
830
+ const channels = rows.map(summarizeChannelMine);
831
+ buildEnvelope({
832
+ options,
833
+ command: 'chatv3.channels',
834
+ serverBaseUrl,
835
+ input: compactPayload({
836
+ space: spaceFilter.spaceSlug ?? normalizeNonEmpty(options.space),
837
+ spaceId: spaceFilter.spaceId,
838
+ status,
839
+ }),
840
+ result: {
841
+ count: channels.length,
842
+ channels,
843
+ },
844
+ });
845
+ }
846
+ export async function runChatv3Join(invite, options = {}) {
847
+ const parsed = parseInvite(invite);
848
+ const handle = normalizeNonEmpty(options.handle);
849
+ if (!handle)
850
+ throw new Error('Provide --handle <agent-handle>.');
851
+ const serverBaseUrl = normalizeNonEmpty(options.apiBaseUrl) ?? parsed.serverBaseUrl;
852
+ const sessionId = sessionIdFor(handle, parsed.channelId, options.session);
853
+ const storePath = resolveChatv3SessionStorePath(options.storePath);
854
+ if (options.saveSession && options.force !== true) {
855
+ const existing = await getChatv3SessionRecord(sessionId, { storePath });
856
+ if (existing?.channel.id === parsed.channelId) {
857
+ buildEnvelope({
858
+ options,
859
+ command: 'chatv3.join',
860
+ serverBaseUrl: existing.serverBaseUrl,
861
+ input: { invite: 'redacted', handle, session: sessionId, saveSession: true, mode: parsed.mode ?? 'e2e' },
862
+ artifacts: { sessionId, channelId: parsed.channelId },
863
+ result: {
864
+ joined: false,
865
+ reusedSession: true,
866
+ summary: buildJoinSummary({
867
+ channel: {
868
+ id: existing.channel.id,
869
+ slug: existing.channel.slug,
870
+ title: existing.channel.title ?? existing.channel.slug,
871
+ },
872
+ rooms: existing.rooms,
873
+ activeRoomId: existing.activeRoomId,
874
+ }),
875
+ session: summarizeChatv3Session(existing),
876
+ guidance: buildAgentGuidance({
877
+ channel: {
878
+ id: existing.channel.id,
879
+ slug: existing.channel.slug,
880
+ title: existing.channel.title ?? existing.channel.slug,
881
+ purpose: existing.channel.purpose ?? null,
882
+ guidanceMarkdown: existing.channel.guidanceMarkdown ?? null,
883
+ },
884
+ rooms: existing.rooms,
885
+ }),
886
+ },
887
+ });
888
+ return;
889
+ }
890
+ if (existing) {
891
+ throw new Error(`Refusing to overwrite ChatV3 session "${sessionId}" bound to channel ${existing.channel.slug} (${existing.channel.id}) with invite channel ${parsed.channelId}. Use a channel-qualified --session id or rerun with --force to replace it.`);
892
+ }
893
+ }
894
+ const client = new Chatv3Client({ serverBaseUrl });
895
+ const joined = await client.joinFromInvite({ ...parsed, serverBaseUrl }, handle);
896
+ const channel = await client.getChannel(parsed.channelId);
897
+ const rooms = await client.listRooms(channel.id, { status: 'active' }).catch(() => joined.rooms);
898
+ const activeRoom = rooms.find((room) => room.slug === 'general') ?? rooms[0];
899
+ const members = await client.listMembers(channel.id, { status: 'active' }).catch(() => []);
900
+ const initialMessages = activeRoom ? await client.readText(activeRoom, 0, 1).catch(() => []) : [];
901
+ const memberToken = client.http.memberToken;
902
+ if (!memberToken)
903
+ throw new Error('ChatV3 join did not return a member token.');
904
+ const channelMode = channel.encryptionMode ?? parsed.mode;
905
+ const epochKeys = channelMode === 'server-encrypted'
906
+ ? sessionEpochKeysFromServer((await client.epochKeys(channel.id, { tenantId: channel.tenantId })).keys)
907
+ : sessionEpochKeysFromServer(joined.epochKeys);
908
+ let record = null;
909
+ if (options.saveSession) {
910
+ const saved = await saveChatv3Session({
911
+ id: sessionId,
912
+ serverBaseUrl,
913
+ handle,
914
+ channel: {
915
+ id: channel.id,
916
+ tenantId: channel.tenantId,
917
+ spaceId: channel.spaceId,
918
+ slug: channel.slug,
919
+ title: channel.title,
920
+ purpose: channel.purpose,
921
+ guidanceMarkdown: channel.guidanceMarkdown,
922
+ encryptionMode: channel.encryptionMode,
923
+ },
924
+ keyId: parsed.keyId,
925
+ activeRoomId: activeRoom?.id,
926
+ rooms: rooms.map(roomToSessionRoom),
927
+ }, {
928
+ memberToken,
929
+ wrapSecret: parsed.mode === 'server-encrypted' ? undefined : parsed.wrapSecret,
930
+ epochKeys,
931
+ }, { storePath });
932
+ record = summarizeChatv3Session(saved);
933
+ }
934
+ buildEnvelope({
935
+ options,
936
+ command: 'chatv3.join',
937
+ serverBaseUrl,
938
+ input: { invite: 'redacted', handle, session: sessionId, saveSession: options.saveSession === true, mode: parsed.mode ?? 'e2e' },
939
+ artifacts: compactPayload({ sessionId, channelId: channel.id, activeRoomId: rooms[0]?.id }),
940
+ result: {
941
+ joined: true,
942
+ saved: options.saveSession === true,
943
+ summary: buildJoinSummary({
944
+ channel,
945
+ rooms,
946
+ activeRoomId: activeRoom?.id,
947
+ memberCount: members.length,
948
+ initialMessageCount: initialMessages.length,
949
+ }),
950
+ session: record,
951
+ rooms: rooms.map(roomToSessionRoom),
952
+ guidance: buildAgentGuidance({ channel, rooms }),
953
+ },
954
+ });
955
+ }
956
+ export async function runChatv3Send(messageArg, options = {}) {
957
+ const sessionId = normalizeNonEmpty(options.session);
958
+ if (!sessionId)
959
+ throw new Error('Provide --session <id>.');
960
+ const text = readTextInput(normalizeNonEmpty(options.text) ?? messageArg, '<message> or --text');
961
+ const runtime = await loadRuntime(options, sessionId);
962
+ const room = resolveRoom(runtime, options.room);
963
+ const sent = await runtime.client.sendText(room, text, normalizeNonEmpty(options.kind) ?? 'message');
964
+ let delivered = null;
965
+ let read = null;
966
+ if (options.markDelivered)
967
+ delivered = await runtime.client.markDelivered(room.id, sent.seq);
968
+ if (options.markRead)
969
+ read = await runtime.client.markRead(room.id, sent.seq);
970
+ await persistRuntimeSnapshot(runtime, room.id);
971
+ buildEnvelope({
972
+ options,
973
+ command: 'chatv3.send',
974
+ serverBaseUrl: runtime.record.serverBaseUrl,
975
+ input: {
976
+ session: sessionId,
977
+ room: options.room ?? room.slug,
978
+ text: 'redacted',
979
+ kind: normalizeNonEmpty(options.kind) ?? 'message',
980
+ markDelivered: options.markDelivered === true,
981
+ markRead: options.markRead === true,
982
+ },
983
+ artifacts: { sessionId, channelId: runtime.channel.id, roomId: room.id, seq: String(sent.seq) },
984
+ result: compactPayload({
985
+ message: sent,
986
+ delivered,
987
+ read,
988
+ guidance: buildAgentGuidance({ channel: runtime.channel, rooms: runtime.rooms }),
989
+ }),
990
+ });
991
+ }
992
+ export async function runChatv3Read(options = {}) {
993
+ const sessionId = normalizeNonEmpty(options.session);
994
+ if (!sessionId)
995
+ throw new Error('Provide --session <id>.');
996
+ const afterSeq = toInteger(options.afterSeq, '--after-seq');
997
+ const limit = options.limit === undefined ? undefined : toInteger(options.limit, '--limit');
998
+ const runtime = await loadRuntime(options, sessionId);
999
+ const room = resolveRoom(runtime, options.room);
1000
+ const messages = await runtime.client.readText(room, afterSeq, limit);
1001
+ const topSeq = messages.reduce((max, message) => Math.max(max, message.seq), afterSeq);
1002
+ let delivered = null;
1003
+ let read = null;
1004
+ if (topSeq > afterSeq && options.markDelivered)
1005
+ delivered = await runtime.client.markDelivered(room.id, topSeq);
1006
+ if (topSeq > afterSeq && options.markRead)
1007
+ read = await runtime.client.markRead(room.id, topSeq);
1008
+ await persistRuntimeSnapshot(runtime, room.id);
1009
+ buildEnvelope({
1010
+ options,
1011
+ command: 'chatv3.read',
1012
+ serverBaseUrl: runtime.record.serverBaseUrl,
1013
+ input: {
1014
+ session: sessionId,
1015
+ room: options.room ?? room.slug,
1016
+ afterSeq,
1017
+ limit,
1018
+ markDelivered: options.markDelivered === true,
1019
+ markRead: options.markRead === true,
1020
+ },
1021
+ artifacts: { sessionId, channelId: runtime.channel.id, roomId: room.id },
1022
+ result: {
1023
+ messages,
1024
+ messageCount: messages.length,
1025
+ latestSeq: topSeq,
1026
+ caughtUp: messages.length === 0,
1027
+ delivered,
1028
+ read,
1029
+ guidance: buildAgentGuidance({ channel: runtime.channel, rooms: runtime.rooms }),
1030
+ },
1031
+ });
1032
+ }
1033
+ export async function runChatv3Listen(options = {}) {
1034
+ const sessionId = normalizeNonEmpty(options.session);
1035
+ if (!sessionId)
1036
+ throw new Error('Provide --session <id>.');
1037
+ const afterSeq = toInteger(options.afterSeq, '--after-seq');
1038
+ const limit = options.limit === undefined ? undefined : toInteger(options.limit, '--limit');
1039
+ const timeoutSec = toPositiveInteger(options.timeoutSec, '--timeout-sec', 60);
1040
+ const intervalSec = toPositiveInteger(options.intervalSec, '--interval-sec', 5);
1041
+ const deadline = Date.now() + timeoutSec * 1000;
1042
+ const runtime = await loadRuntime(options, sessionId);
1043
+ const room = resolveRoom(runtime, options.room);
1044
+ let attempts = 0;
1045
+ let messages = [];
1046
+ while (Date.now() <= deadline) {
1047
+ attempts += 1;
1048
+ messages = await runtime.client.readText(room, afterSeq, limit);
1049
+ if (messages.length > 0)
1050
+ break;
1051
+ const remainingMs = deadline - Date.now();
1052
+ if (remainingMs <= 0)
1053
+ break;
1054
+ await sleep(Math.min(intervalSec * 1000, remainingMs));
1055
+ }
1056
+ const topSeq = messages.reduce((max, message) => Math.max(max, message.seq), afterSeq);
1057
+ let delivered = null;
1058
+ let read = null;
1059
+ if (topSeq > afterSeq && options.markDelivered)
1060
+ delivered = await runtime.client.markDelivered(room.id, topSeq);
1061
+ if (topSeq > afterSeq && options.markRead)
1062
+ read = await runtime.client.markRead(room.id, topSeq);
1063
+ await persistRuntimeSnapshot(runtime, room.id);
1064
+ if (messages.length === 0)
1065
+ process.exitCode = 22;
1066
+ buildEnvelope({
1067
+ options,
1068
+ command: 'chatv3.listen',
1069
+ serverBaseUrl: runtime.record.serverBaseUrl,
1070
+ input: {
1071
+ session: sessionId,
1072
+ room: options.room ?? room.slug,
1073
+ afterSeq,
1074
+ limit,
1075
+ timeoutSec,
1076
+ intervalSec,
1077
+ markDelivered: options.markDelivered === true,
1078
+ markRead: options.markRead === true,
1079
+ },
1080
+ artifacts: { sessionId, channelId: runtime.channel.id, roomId: room.id },
1081
+ result: {
1082
+ status: messages.length > 0 ? 'messages' : 'timeout',
1083
+ messages,
1084
+ messageCount: messages.length,
1085
+ latestSeq: topSeq,
1086
+ caughtUp: messages.length === 0,
1087
+ attempts,
1088
+ delivered,
1089
+ read,
1090
+ guidance: buildAgentGuidance({ channel: runtime.channel, rooms: runtime.rooms }),
1091
+ },
1092
+ });
1093
+ }
1094
+ export async function runChatv3MemberList(options = {}) {
1095
+ const sessionId = normalizeNonEmpty(options.session);
1096
+ if (!sessionId)
1097
+ throw new Error('Provide --session <id>.');
1098
+ const limit = options.limit === undefined ? undefined : toInteger(options.limit, '--limit');
1099
+ const runtime = await loadRuntime(options, sessionId);
1100
+ const members = await runtime.client.listMembers(runtime.channel.id, { status: options.status, limit });
1101
+ buildEnvelope({
1102
+ options,
1103
+ command: 'chatv3.member.list',
1104
+ serverBaseUrl: runtime.record.serverBaseUrl,
1105
+ input: compactPayload({ session: sessionId, status: options.status, limit }),
1106
+ artifacts: { sessionId, channelId: runtime.channel.id },
1107
+ result: {
1108
+ members,
1109
+ memberCount: members.length,
1110
+ guidance: buildAgentGuidance({ channel: runtime.channel, rooms: runtime.rooms }),
1111
+ },
1112
+ });
1113
+ }
1114
+ export async function runChatv3MemberRemove(options = {}) {
1115
+ const memberId = requireField(options.member, '--member');
1116
+ const { sessionId, runtime } = await loadRuntimeForChannelGuard(options);
1117
+ const beforeMembers = await runtime.client.listMembers(runtime.channel.id).catch(() => []);
1118
+ const target = beforeMembers.find((member) => member.id === memberId);
1119
+ if (target && target.channelId !== runtime.channel.id) {
1120
+ throw new Error(`Member ${memberId} belongs to channel ${target.channelId}; refusing to remove from ${runtime.channel.id}.`);
1121
+ }
1122
+ const member = await runtime.client.updateMember(memberId, { status: 'removed' });
1123
+ if (member.channelId !== runtime.channel.id) {
1124
+ throw new Error(`Server returned member ${member.id} from channel ${member.channelId}; expected ${runtime.channel.id}.`);
1125
+ }
1126
+ const members = await runtime.client.listMembers(runtime.channel.id).catch(() => beforeMembers.map((entry) => (entry.id === memberId ? member : entry)));
1127
+ await persistRuntimeSnapshot(runtime);
1128
+ buildEnvelope({
1129
+ options,
1130
+ command: 'chatv3.member.remove',
1131
+ serverBaseUrl: runtime.record.serverBaseUrl,
1132
+ input: compactPayload({ session: sessionId, channel: options.channel, member: memberId }),
1133
+ artifacts: { sessionId, channelId: runtime.channel.id, memberId },
1134
+ result: {
1135
+ removed: true,
1136
+ member,
1137
+ members,
1138
+ memberCount: members.length,
1139
+ guidance: buildAgentGuidance({ channel: runtime.channel, rooms: runtime.rooms }),
1140
+ },
1141
+ });
1142
+ }
1143
+ export async function runChatv3PresenceList(options = {}) {
1144
+ const sessionId = normalizeNonEmpty(options.session);
1145
+ if (!sessionId)
1146
+ throw new Error('Provide --session <id>.');
1147
+ const runtime = await loadRuntime(options, sessionId);
1148
+ const room = resolveRoom(runtime, options.room);
1149
+ const presence = await runtime.client.listPresence(room.id);
1150
+ buildEnvelope({
1151
+ options,
1152
+ command: 'chatv3.presence.list',
1153
+ serverBaseUrl: runtime.record.serverBaseUrl,
1154
+ input: { session: sessionId, room: options.room ?? room.slug },
1155
+ artifacts: { sessionId, channelId: runtime.channel.id, roomId: room.id },
1156
+ result: {
1157
+ presence,
1158
+ presenceCount: presence.length,
1159
+ },
1160
+ });
1161
+ }
1162
+ export async function runChatv3PresenceSet(options = {}) {
1163
+ const sessionId = normalizeNonEmpty(options.session);
1164
+ if (!sessionId)
1165
+ throw new Error('Provide --session <id>.');
1166
+ const ttlSec = options.ttlSec === undefined ? undefined : toInteger(options.ttlSec, '--ttl-sec');
1167
+ const runtime = await loadRuntime(options, sessionId);
1168
+ const room = resolveRoom(runtime, options.room);
1169
+ const presence = await runtime.client.setPresence(room.id, {
1170
+ state: options.state,
1171
+ note: normalizeNonEmpty(options.note),
1172
+ ttlSec,
1173
+ });
1174
+ buildEnvelope({
1175
+ options,
1176
+ command: 'chatv3.presence.set',
1177
+ serverBaseUrl: runtime.record.serverBaseUrl,
1178
+ input: compactPayload({ session: sessionId, room: options.room ?? room.slug, state: options.state, note: options.note, ttlSec }),
1179
+ artifacts: { sessionId, channelId: runtime.channel.id, roomId: room.id },
1180
+ result: { presence },
1181
+ });
1182
+ }
1183
+ export async function runChatv3BindingAdd(options = {}) {
1184
+ const sessionId = normalizeNonEmpty(options.session);
1185
+ if (!sessionId)
1186
+ throw new Error('Provide --session <id>.');
1187
+ const runtime = await loadRuntime(options, sessionId);
1188
+ const room = options.channelOnly ? undefined : resolveRoom(runtime, options.room);
1189
+ const input = compactPayload({
1190
+ channelId: runtime.channel.id,
1191
+ roomId: room?.id,
1192
+ bindingType: requireField(options.bindingType, '--binding-type'),
1193
+ refId: normalizeNonEmpty(options.refId),
1194
+ uri: normalizeNonEmpty(options.uri),
1195
+ title: normalizeNonEmpty(options.title),
1196
+ note: normalizeNonEmpty(options.note),
1197
+ });
1198
+ const binding = await runtime.client.http.post(`/channels/${runtime.channel.id}/bindings`, input);
1199
+ await persistRuntimeSnapshot(runtime, room?.id);
1200
+ buildEnvelope({
1201
+ options,
1202
+ command: 'chatv3.binding.add',
1203
+ serverBaseUrl: runtime.record.serverBaseUrl,
1204
+ input: compactPayload({
1205
+ session: sessionId,
1206
+ room: room ? (options.room ?? room.slug) : undefined,
1207
+ channelOnly: options.channelOnly === true,
1208
+ bindingType: input.bindingType,
1209
+ refId: input.refId,
1210
+ uri: input.uri,
1211
+ title: input.title,
1212
+ note: input.note,
1213
+ }),
1214
+ artifacts: compactPayload({ sessionId, channelId: runtime.channel.id, roomId: room?.id, bindingId: binding.id }),
1215
+ result: {
1216
+ binding: summarizeChatv3Binding(binding),
1217
+ guidance: buildAgentGuidance({ channel: runtime.channel, rooms: runtime.rooms }),
1218
+ },
1219
+ });
1220
+ }
1221
+ export async function runChatv3BindingList(options = {}) {
1222
+ const sessionId = normalizeNonEmpty(options.session);
1223
+ if (!sessionId)
1224
+ throw new Error('Provide --session <id>.');
1225
+ const runtime = await loadRuntime(options, sessionId);
1226
+ const room = options.allRooms ? undefined : resolveRoom(runtime, options.room);
1227
+ const bindings = await listChatv3Bindings(runtime, room?.id);
1228
+ await persistRuntimeSnapshot(runtime, room?.id);
1229
+ buildEnvelope({
1230
+ options,
1231
+ command: 'chatv3.binding.list',
1232
+ serverBaseUrl: runtime.record.serverBaseUrl,
1233
+ input: compactPayload({
1234
+ session: sessionId,
1235
+ room: room ? (options.room ?? room.slug) : undefined,
1236
+ allRooms: options.allRooms === true,
1237
+ }),
1238
+ artifacts: compactPayload({ sessionId, channelId: runtime.channel.id, roomId: room?.id }),
1239
+ result: {
1240
+ bindings: bindings.map(summarizeChatv3Binding),
1241
+ bindingCount: bindings.length,
1242
+ recommendedNextReads: buildRecommendedNextReads(bindings),
1243
+ guidance: buildAgentGuidance({ channel: runtime.channel, rooms: runtime.rooms }),
1244
+ },
1245
+ });
1246
+ }
1247
+ export async function runChatv3BindingRemove(options = {}) {
1248
+ const sessionId = normalizeNonEmpty(options.session);
1249
+ if (!sessionId)
1250
+ throw new Error('Provide --session <id>.');
1251
+ const bindingId = requireField(options.id, '--id');
1252
+ const runtime = await loadRuntime(options, sessionId);
1253
+ const removed = await runtime.client.http.request('DELETE', `/bindings/${bindingId}`, { bindingId });
1254
+ await persistRuntimeSnapshot(runtime);
1255
+ buildEnvelope({
1256
+ options,
1257
+ command: 'chatv3.binding.remove',
1258
+ serverBaseUrl: runtime.record.serverBaseUrl,
1259
+ input: { session: sessionId, id: bindingId },
1260
+ artifacts: { sessionId, channelId: runtime.channel.id, bindingId },
1261
+ result: { removed },
1262
+ });
1263
+ }
1264
+ async function buildChatv3RoomContext(options, explicitLatestSeq) {
1265
+ const sessionId = normalizeNonEmpty(options.session);
1266
+ if (!sessionId)
1267
+ throw new Error('Provide --session <id>.');
1268
+ const afterSeq = toInteger(options.afterSeq, '--after-seq');
1269
+ const runtime = await loadRuntime(options, sessionId);
1270
+ const room = resolveRoom(runtime, options.room);
1271
+ const [members, presence, receipts, bindings] = await Promise.all([
1272
+ runtime.client.listMembers(runtime.channel.id, { status: 'active' }),
1273
+ runtime.client.listPresence(room.id),
1274
+ runtime.client.getReceipts(room.id),
1275
+ listChatv3Bindings(runtime, room.id),
1276
+ ]);
1277
+ await persistRuntimeSnapshot(runtime, room.id);
1278
+ return {
1279
+ sessionId,
1280
+ runtime,
1281
+ room,
1282
+ afterSeq,
1283
+ seqRange: seqRangeForRoom(room, afterSeq, explicitLatestSeq),
1284
+ members,
1285
+ presence,
1286
+ receipts,
1287
+ bindings,
1288
+ };
1289
+ }
1290
+ export async function runChatv3RoomBrief(options = {}) {
1291
+ const context = await buildChatv3RoomContext(options);
1292
+ const forAgent = normalizeNonEmpty(options.for);
1293
+ const briefMarkdown = buildRoomContextMarkdown({
1294
+ title: `ChatV3 Room Brief: ${context.room.title || context.room.slug}`,
1295
+ runtime: context.runtime,
1296
+ room: context.room,
1297
+ forAgent,
1298
+ seqRange: context.seqRange,
1299
+ members: context.members,
1300
+ presence: context.presence,
1301
+ receipts: context.receipts,
1302
+ bindings: context.bindings,
1303
+ });
1304
+ const result = {
1305
+ briefMarkdown,
1306
+ room: roomToSessionRoom(context.room),
1307
+ seqRange: context.seqRange,
1308
+ members: context.members,
1309
+ presence: context.presence,
1310
+ receipts: context.receipts,
1311
+ bindings: context.bindings.map(summarizeChatv3Binding),
1312
+ recommendedNextReads: buildRecommendedNextReads(context.bindings),
1313
+ transcriptPolicy: 'Raw message transcript is not included in the room brief.',
1314
+ };
1315
+ buildEnvelope({
1316
+ options,
1317
+ command: 'chatv3.room.brief',
1318
+ serverBaseUrl: context.runtime.record.serverBaseUrl,
1319
+ input: compactPayload({
1320
+ session: context.sessionId,
1321
+ room: options.room ?? context.room.slug,
1322
+ for: forAgent,
1323
+ afterSeq: context.afterSeq,
1324
+ }),
1325
+ artifacts: { sessionId: context.sessionId, channelId: context.runtime.channel.id, roomId: context.room.id },
1326
+ result,
1327
+ });
1328
+ }
1329
+ export async function runChatv3RoomSummary(options = {}) {
1330
+ const latestSeq = options.latestSeq === undefined ? undefined : toInteger(options.latestSeq, '--latest-seq');
1331
+ const context = await buildChatv3RoomContext(options, latestSeq);
1332
+ const latestSeqValue = Number(context.seqRange.latestSeq ?? context.afterSeq);
1333
+ const sourceMessages = summarizeRoomSourceMessages((await context.runtime.client.readText(context.room, context.afterSeq))
1334
+ .filter((message) => message.seq > context.afterSeq && message.seq <= latestSeqValue), context.members);
1335
+ const summaryMarkdown = buildRoomContextMarkdown({
1336
+ title: `ChatV3 Room Summary: ${context.room.title || context.room.slug}`,
1337
+ runtime: context.runtime,
1338
+ room: context.room,
1339
+ forAgent: normalizeNonEmpty(options.for),
1340
+ seqRange: context.seqRange,
1341
+ members: context.members,
1342
+ presence: context.presence,
1343
+ receipts: context.receipts,
1344
+ bindings: context.bindings,
1345
+ sourceMessages,
1346
+ summaryMode: true,
1347
+ });
1348
+ const sourceRef = compactPayload({
1349
+ type: 'chatv3.room',
1350
+ channelId: context.runtime.channel.id,
1351
+ channelSlug: context.runtime.channel.slug,
1352
+ roomId: context.room.id,
1353
+ roomSlug: context.room.slug,
1354
+ seqRange: context.seqRange,
1355
+ });
1356
+ const nextReadRefs = buildRecommendedNextReads(context.bindings);
1357
+ const memoryArgs = [
1358
+ 'mem',
1359
+ 'summary',
1360
+ '--subject',
1361
+ 'project',
1362
+ '--content',
1363
+ '@<narrative-digest.md>',
1364
+ '--source-ref',
1365
+ JSON.stringify(sourceRef),
1366
+ '--tag',
1367
+ 'chatv3:room-summary',
1368
+ '--tag',
1369
+ `chatv3:room:${context.room.slug}`,
1370
+ '--apply',
1371
+ '--json',
1372
+ ];
1373
+ for (const ref of nextReadRefs) {
1374
+ memoryArgs.splice(memoryArgs.length - 2, 0, '--next-read-ref', JSON.stringify(ref));
1375
+ }
1376
+ buildEnvelope({
1377
+ options,
1378
+ command: 'chatv3.room.summary',
1379
+ serverBaseUrl: context.runtime.record.serverBaseUrl,
1380
+ input: compactPayload({
1381
+ session: context.sessionId,
1382
+ room: options.room ?? context.room.slug,
1383
+ afterSeq: context.afterSeq,
1384
+ latestSeq: context.seqRange.latestSeq,
1385
+ }),
1386
+ artifacts: { sessionId: context.sessionId, channelId: context.runtime.channel.id, roomId: context.room.id },
1387
+ result: {
1388
+ summaryMarkdown,
1389
+ sourceRef,
1390
+ nextReadRefs,
1391
+ sourceMessages,
1392
+ sourceMessagePolicy: SUMMARY_SOURCE_MESSAGE_POLICY,
1393
+ transcriptPolicy: 'Source messages are exposed only for agent summarization; write only the abstractive narrative digest, refs, and seq range to memory.',
1394
+ memoryWrite: {
1395
+ command: 'aops-cli',
1396
+ args: memoryArgs,
1397
+ slots: {
1398
+ 'NARRATIVE-DIGEST': 'Agent-authored abstractive digest from result.sourceMessages, sourceRef, and nextReadRefs. Do not paste sourceMessages verbatim.',
1399
+ },
1400
+ sourceInputPath: 'result.sourceMessages',
1401
+ },
1402
+ },
1403
+ });
1404
+ }
1405
+ export async function runChatv3Leave(options = {}) {
1406
+ const sessionId = normalizeNonEmpty(options.session);
1407
+ if (!sessionId)
1408
+ throw new Error('Provide --session <id>.');
1409
+ const runtime = await loadRuntime(options, sessionId);
1410
+ const members = await runtime.client.listMembers(runtime.channel.id, { status: 'active' });
1411
+ const self = members.find((member) => member.handle === runtime.record.handle);
1412
+ if (!self)
1413
+ throw new Error(`Active member for handle ${runtime.record.handle} not found.`);
1414
+ const member = await runtime.client.updateMember(self.id, { status: 'removed' });
1415
+ const forgotten = options.keepSession === true ? false : await deleteChatv3Session(sessionId, { storePath: runtime.storePath });
1416
+ buildEnvelope({
1417
+ options,
1418
+ command: 'chatv3.leave',
1419
+ serverBaseUrl: runtime.record.serverBaseUrl,
1420
+ input: { session: sessionId, keepSession: options.keepSession === true },
1421
+ artifacts: { sessionId, channelId: runtime.channel.id, memberId: self.id },
1422
+ result: {
1423
+ left: true,
1424
+ member,
1425
+ sessionForgotten: forgotten,
1426
+ },
1427
+ });
1428
+ }
1429
+ export async function runChatv3SessionList(options = {}) {
1430
+ const sessions = await listChatv3SessionRecords({ storePath: options.storePath });
1431
+ buildEnvelope({
1432
+ options,
1433
+ command: 'chatv3.session.list',
1434
+ input: {},
1435
+ artifacts: {},
1436
+ result: {
1437
+ sessions: sessions.map(summarizeChatv3Session),
1438
+ },
1439
+ });
1440
+ }
1441
+ export async function runChatv3SessionGet(options = {}) {
1442
+ const sessionId = normalizeNonEmpty(options.session);
1443
+ if (!sessionId)
1444
+ throw new Error('Provide --session <id>.');
1445
+ const record = await getChatv3SessionRecord(sessionId, { storePath: options.storePath });
1446
+ if (!record)
1447
+ throw new Error(`ChatV3 session not found: ${sessionId}`);
1448
+ buildEnvelope({
1449
+ options,
1450
+ command: 'chatv3.session.get',
1451
+ serverBaseUrl: record.serverBaseUrl,
1452
+ input: { session: sessionId },
1453
+ artifacts: { sessionId, channelId: record.channel.id },
1454
+ result: {
1455
+ session: summarizeChatv3Session(record),
1456
+ guidance: buildAgentGuidance({
1457
+ channel: {
1458
+ id: record.channel.id,
1459
+ slug: record.channel.slug,
1460
+ title: record.channel.title ?? record.channel.slug,
1461
+ purpose: record.channel.purpose ?? null,
1462
+ guidanceMarkdown: record.channel.guidanceMarkdown ?? null,
1463
+ },
1464
+ rooms: record.rooms,
1465
+ }),
1466
+ },
1467
+ });
1468
+ }
1469
+ export async function runChatv3SessionForget(options = {}) {
1470
+ const sessionId = normalizeNonEmpty(options.session);
1471
+ if (!sessionId)
1472
+ throw new Error('Provide --session <id>.');
1473
+ const deleted = await deleteChatv3Session(sessionId, { storePath: options.storePath });
1474
+ buildEnvelope({
1475
+ options,
1476
+ command: 'chatv3.session.forget',
1477
+ input: { session: sessionId },
1478
+ artifacts: { sessionId },
1479
+ result: { deleted },
1480
+ });
1481
+ }
1482
+ function applyChatv3Options(cmd) {
1483
+ return applyCommonOptions(cmd.option('--store-path <path>', 'Session store path (default: AOPS_CHATV3_SESSION_STORE_PATH or ~/.aops/chatv3-sessions.json)'), { withAuth: false });
1484
+ }
1485
+ export function makeChatv3Command() {
1486
+ const cmd = new Command('chatv3')
1487
+ .description('ChatV3 encrypted product-channel CLI for agent sessions with bootstrap guidance; separate from hosted `aops-cli chat` coordination rooms');
1488
+ applyChatv3Options(cmd.command('join')
1489
+ .argument('<invite>', 'chv3://join/... invite string')
1490
+ .requiredOption('--handle <handle>', 'Agent handle to join with')
1491
+ .option('--session <id>', 'Local session id; default <handle>-<channel-prefix>')
1492
+ .option('--save-session', 'Persist encrypted member token plus mode-specific key material locally')
1493
+ .option('--force', 'Join again even if the target local session already exists')
1494
+ .addHelpText('after', '\nNote: join uses the server URL embedded in the invite unless --api-base-url explicitly overrides it.\n')).action(async (invite, options) => {
1495
+ await runChatv3Join(invite, options);
1496
+ });
1497
+ const channel = cmd.command('channel').description('ChatV3 channel lifecycle helpers');
1498
+ applyChatv3Options(channel.command('create')
1499
+ .description('Create a ChatV3 channel and print the invite once; invite contains secrets')
1500
+ .requiredOption('--title <title>', 'Channel title')
1501
+ .requiredOption('--handle <handle>', 'Creator agent handle')
1502
+ .option('--slug <slug>', 'Channel slug; default kebab-case title')
1503
+ .option('--space <slug>', `Space slug; default ${DEFAULT_CHATV3_SPACE_SLUG}`)
1504
+ .option('--mode <server-encrypted|e2e>', `Encryption mode; default ${DEFAULT_CHATV3_CHANNEL_MODE}`)
1505
+ .option('--session <id>', 'Local session id to save when --save-session is set; default <handle>-<channel-prefix>')
1506
+ .option('--save-session', 'Persist creator member token plus mode-specific key material locally')
1507
+ .option('--force', 'Replace an existing local session when --save-session is set')
1508
+ .addHelpText('after', `\nWarning: the returned invite contains secrets. Share it securely and do not paste it into public logs.\n`)).action(async (options) => {
1509
+ await runChatv3ChannelCreate(options);
1510
+ });
1511
+ applyChatv3Options(channel.command('delete')
1512
+ .description('Hard-delete one ChatV3 channel after confirm-slug guard')
1513
+ .requiredOption('--channel <idOrSlug>', 'Channel id or unique slug to delete')
1514
+ .requiredOption('--confirm-slug <slug>', 'Must exactly match the target channel slug')
1515
+ .option('--session <id>', 'Use a local session-bound owner/operator member token and guard --channel against it')).action(async (options) => {
1516
+ await runChatv3ChannelDelete(options);
1517
+ });
1518
+ applyChatv3Options(channel.command('purge-before')
1519
+ .description('Preview or apply admin cleanup for channels created before an ISO cutoff')
1520
+ .requiredOption('--before <ISO>', 'ISO cutoff; channels created before this timestamp are candidates')
1521
+ .option('--confirm', 'Apply deletion; omitted means dry-run preview only')).action(async (options) => {
1522
+ await runChatv3ChannelPurgeBefore(options);
1523
+ });
1524
+ applyChatv3Options(cmd.command('channels')
1525
+ .description('List channels owned by or joined by the verified AuthV2 principal')
1526
+ .option('--space <slug>', 'Filter by ChatV3 space slug')
1527
+ .option('--status <active|archived>', 'Filter by channel status', 'active')).action(async (options) => {
1528
+ await runChatv3Channels(options);
1529
+ });
1530
+ applyChatv3Options(cmd.command('send')
1531
+ .argument('[message]', 'Message text; use --text @file.md for file input')
1532
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1533
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1534
+ .option('--text <textOrFile>', 'Message text or @file.md')
1535
+ .option('--kind <kind>', 'Message kind', 'message')
1536
+ .option('--mark-delivered', 'Advance this member delivered cursor to the sent seq')
1537
+ .option('--mark-read', 'Advance this member read cursor to the sent seq')).action(async (message, options) => {
1538
+ await runChatv3Send(message, options);
1539
+ });
1540
+ applyChatv3Options(cmd.command('read')
1541
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1542
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1543
+ .option('--after-seq <seq>', 'Read messages after this seq', '0')
1544
+ .option('--limit <n>', 'Optional message page limit')
1545
+ .option('--mark-delivered', 'Advance delivered cursor to the highest returned seq')
1546
+ .option('--mark-read', 'Advance read cursor to the highest returned seq')).action(async (options) => {
1547
+ await runChatv3Read(options);
1548
+ });
1549
+ applyChatv3Options(cmd.command('listen')
1550
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1551
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1552
+ .option('--after-seq <seq>', 'Poll for messages after this seq', '0')
1553
+ .option('--limit <n>', 'Optional message page limit')
1554
+ .option('--timeout-sec <n>', 'Wait timeout in seconds; exit 22 on timeout', '60')
1555
+ .option('--interval-sec <n>', 'Polling interval in seconds', '5')
1556
+ .option('--mark-delivered', 'Advance delivered cursor to the highest returned seq')
1557
+ .option('--mark-read', 'Advance read cursor to the highest returned seq')).action(async (options) => {
1558
+ await runChatv3Listen(options);
1559
+ });
1560
+ const binding = cmd.command('binding').description('ChatV3 loose channel/room reference bindings');
1561
+ applyChatv3Options(binding.command('add')
1562
+ .description('Attach a loose external reference to the active room or channel')
1563
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1564
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1565
+ .option('--channel-only', 'Attach at channel level instead of the active room')
1566
+ .requiredOption('--binding-type <type>', 'Binding type, such as projectman.review-request, repo.url, docman.document, agentspace.discussion-topic')
1567
+ .option('--ref-id <id>', 'Bound reference id/slug')
1568
+ .option('--uri <uri>', 'Bound reference URI')
1569
+ .option('--title <text>', 'Binding title')
1570
+ .option('--note <text>', 'Binding note')).action(async (options) => {
1571
+ await runChatv3BindingAdd(options);
1572
+ });
1573
+ applyChatv3Options(binding.command('list')
1574
+ .description('List loose references for the active room by default')
1575
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1576
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1577
+ .option('--all-rooms', 'List channel-level and all room bindings instead of filtering to one room')).action(async (options) => {
1578
+ await runChatv3BindingList(options);
1579
+ });
1580
+ applyChatv3Options(binding.command('remove')
1581
+ .description('Remove a loose ChatV3 binding')
1582
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1583
+ .requiredOption('--id <id>', 'Binding id')).action(async (options) => {
1584
+ await runChatv3BindingRemove(options);
1585
+ });
1586
+ const room = cmd.command('room').description('ChatV3 room orientation helpers');
1587
+ applyChatv3Options(room.command('brief')
1588
+ .description('Build a paste-ready room brief from guidance, bindings, members, presence, and cursor refs')
1589
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1590
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1591
+ .option('--for <agent>', 'Agent handle/id the brief is for')
1592
+ .option('--after-seq <seq>', 'Cursor baseline for the seq range', '0')).action(async (options) => {
1593
+ await runChatv3RoomBrief(options);
1594
+ });
1595
+ applyChatv3Options(room.command('summary')
1596
+ .description('Build a room summary pack with source messages for agent-composed memory digest')
1597
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1598
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1599
+ .option('--for <agent>', 'Agent handle/id the summary is for')
1600
+ .option('--after-seq <seq>', 'Cursor baseline for the seq range', '0')
1601
+ .option('--latest-seq <seq>', 'Explicit latest seq override; default room lastSeq')).action(async (options) => {
1602
+ await runChatv3RoomSummary(options);
1603
+ });
1604
+ const member = cmd.command('member').description('ChatV3 channel member roster');
1605
+ applyChatv3Options(member.command('list')
1606
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1607
+ .option('--status <active|removed>', 'Member status filter')
1608
+ .option('--limit <n>', 'Optional member page limit')).action(async (options) => {
1609
+ await runChatv3MemberList(options);
1610
+ });
1611
+ applyChatv3Options(member.command('remove')
1612
+ .description('Remove a member from a ChatV3 channel using an owner/operator session')
1613
+ .option('--session <id>', 'Local ChatV3 session id; required unless --channel matches exactly one local session')
1614
+ .option('--channel <idOrSlug>', 'Expected channel id/slug; required when --session is omitted and used as a guard when provided')
1615
+ .requiredOption('--member <id>', 'Member id to remove')).action(async (options) => {
1616
+ await runChatv3MemberRemove(options);
1617
+ });
1618
+ const presence = cmd.command('presence').description('ChatV3 room presence');
1619
+ applyChatv3Options(presence.command('list')
1620
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1621
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')).action(async (options) => {
1622
+ await runChatv3PresenceList(options);
1623
+ });
1624
+ applyChatv3Options(presence.command('set')
1625
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1626
+ .option('--room <slugOrId>', 'Room slug or id; default active/general room')
1627
+ .option('--state <state>', 'Presence state: active, idle, working, reviewing, blocked, offline')
1628
+ .option('--note <text>', 'Short presence note')
1629
+ .option('--ttl-sec <n>', 'Presence TTL in seconds')).action(async (options) => {
1630
+ await runChatv3PresenceSet(options);
1631
+ });
1632
+ applyChatv3Options(cmd.command('leave')
1633
+ .requiredOption('--session <id>', 'Local ChatV3 session id')
1634
+ .option('--keep-session', 'Do not delete the local encrypted session after leaving')).action(async (options) => {
1635
+ await runChatv3Leave(options);
1636
+ });
1637
+ const session = cmd.command('session').description('Local ChatV3 agent session records');
1638
+ applyChatv3Options(session.command('list')).action(async (options) => {
1639
+ await runChatv3SessionList(options);
1640
+ });
1641
+ applyChatv3Options(session.command('get').requiredOption('--session <id>', 'Local ChatV3 session id')).action(async (options) => {
1642
+ await runChatv3SessionGet(options);
1643
+ });
1644
+ applyChatv3Options(session.command('forget').requiredOption('--session <id>', 'Local ChatV3 session id')).action(async (options) => {
1645
+ await runChatv3SessionForget(options);
1646
+ });
1647
+ cmd.addHelpText('afterAll', buildOperatorCookbook({
1648
+ examples: [
1649
+ 'aops-cli chatv3 channel create --title "Round 2" --handle codex --space default --mode server-encrypted --save-session --json',
1650
+ 'aops-cli chatv3 channel delete --channel <id-or-slug> --confirm-slug <slug> --json',
1651
+ 'aops-cli chatv3 channel purge-before --before 2026-07-01T00:00:00.000Z --json',
1652
+ 'aops-cli chatv3 channel purge-before --before 2026-07-01T00:00:00.000Z --confirm --json',
1653
+ 'aops-cli chatv3 channels --space default --status active --json',
1654
+ 'aops-cli chatv3 join "<invite>" --handle codex --session codex --save-session --json',
1655
+ 'aops-cli chatv3 send --session codex --room general "Hazirim." --mark-delivered --mark-read --json',
1656
+ 'aops-cli chatv3 read --session codex --room general --after-seq 0 --mark-delivered --mark-read --json',
1657
+ 'aops-cli chatv3 listen --session codex --room general --after-seq 12 --timeout-sec 60 --json',
1658
+ 'aops-cli chatv3 binding add --session codex --room general --binding-type projectman.review-request --ref-id <rr-id> --title "Slice review" --json',
1659
+ 'aops-cli chatv3 binding list --session codex --room general --json',
1660
+ 'aops-cli chatv3 room brief --session codex --room general --for claude --json',
1661
+ 'aops-cli chatv3 room summary --session codex --room general --after-seq 40 --json',
1662
+ 'aops-cli chatv3 member list --session codex --json',
1663
+ 'aops-cli chatv3 member remove --session codex --channel <channel-id> --member <member-id> --json',
1664
+ 'aops-cli chatv3 presence set --session codex --room general --state working --note "reviewing RR" --json',
1665
+ 'aops-cli chatv3 leave --session codex --json',
1666
+ 'aops-cli chatv3 session list --json',
1667
+ ],
1668
+ notes: [
1669
+ '`aops-cli chatv3` talks to ChatV3 product channels/rooms. `aops-cli chat` remains the hosted AOPS coordination/wake room surface.',
1670
+ 'Join/send/read/session JSON includes result.guidance with channel/room rules plus agent bootstrap steps from the dedicated guidanceMarkdown field.',
1671
+ 'listen exits 0 when messages are found and 22 on timeout. read/listen always include messages:[], messageCount, latestSeq, and caughtUp.',
1672
+ 'binding add/list/remove stores loose channel/room refs only; room brief includes refs and cursors.',
1673
+ 'room summary includes source messages tagged for summarization only plus a NARRATIVE-DIGEST memory recipe; do not persist source messages verbatim.',
1674
+ 'channel create prints the invite string exactly in result.invite; it contains secrets and must be shared securely.',
1675
+ 'channel delete always requires --confirm-slug and prints whatWasDeleted; a mismatched slug fails before the delete call when the channel can be resolved.',
1676
+ 'channel purge-before is dry-run by default and prints whatWillBeDeleted; --confirm applies deletion and prints whatWasDeleted.',
1677
+ 'channels uses the verified AuthV2 principal (trusted-local loopback principal on local hosts) and includes modeStatus.encryptionMode plus locked/archived markers.',
1678
+ 'join uses the server URL embedded in the invite unless --api-base-url explicitly overrides it.',
1679
+ 'Session store persists memberToken and either e2e wrapSecret or server-encrypted epoch keys encrypted at rest.',
1680
+ 'Invite strings contain secrets; command JSON redacts them from output.',
1681
+ ],
1682
+ }));
1683
+ return cmd;
1684
+ }