@oneworks/cli 0.1.0-alpha.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/channel.js +7 -0
  3. package/cli.js +5 -0
  4. package/mem.js +7 -0
  5. package/package.json +59 -0
  6. package/postinstall.js +75 -0
  7. package/src/AGENTS.md +169 -0
  8. package/src/channel-cli.ts +19 -0
  9. package/src/cli-argv.ts +27 -0
  10. package/src/cli.ts +63 -0
  11. package/src/commands/@core/adapter-option.ts +85 -0
  12. package/src/commands/@core/extra-options.ts +12 -0
  13. package/src/commands/@core/plugin-install.ts +1 -0
  14. package/src/commands/@core/plugin-source.ts +1 -0
  15. package/src/commands/accounts.ts +204 -0
  16. package/src/commands/adapter/prepare-selection.ts +181 -0
  17. package/src/commands/adapter/prepare.ts +104 -0
  18. package/src/commands/adapter.ts +48 -0
  19. package/src/commands/agent/actions.ts +176 -0
  20. package/src/commands/agent/runtime-store-commands.ts +56 -0
  21. package/src/commands/agent/runtime-store-events.ts +23 -0
  22. package/src/commands/agent/runtime-store-session.ts +170 -0
  23. package/src/commands/agent/runtime-store-shared.ts +139 -0
  24. package/src/commands/agent/runtime-store.ts +4 -0
  25. package/src/commands/agent.ts +81 -0
  26. package/src/commands/benchmark.ts +198 -0
  27. package/src/commands/channel.ts +594 -0
  28. package/src/commands/clear.ts +140 -0
  29. package/src/commands/config/actions.ts +196 -0
  30. package/src/commands/config/display-state.ts +108 -0
  31. package/src/commands/config/index.ts +135 -0
  32. package/src/commands/config/interactive.ts +121 -0
  33. package/src/commands/config/read-state.ts +56 -0
  34. package/src/commands/config/section-state.ts +109 -0
  35. package/src/commands/config/shared.ts +195 -0
  36. package/src/commands/kill.ts +41 -0
  37. package/src/commands/list.ts +224 -0
  38. package/src/commands/memory/context.ts +76 -0
  39. package/src/commands/memory/entries.ts +131 -0
  40. package/src/commands/memory/shared.ts +89 -0
  41. package/src/commands/memory/store.ts +69 -0
  42. package/src/commands/memory/target.ts +54 -0
  43. package/src/commands/memory.ts +97 -0
  44. package/src/commands/plugin.ts +62 -0
  45. package/src/commands/report-targets.ts +149 -0
  46. package/src/commands/report.ts +232 -0
  47. package/src/commands/run/adapter-cli-version.ts +65 -0
  48. package/src/commands/run/command.ts +982 -0
  49. package/src/commands/run/input-bridge.ts +108 -0
  50. package/src/commands/run/input-control.ts +112 -0
  51. package/src/commands/run/input-decision.ts +88 -0
  52. package/src/commands/run/options.ts +104 -0
  53. package/src/commands/run/output.ts +179 -0
  54. package/src/commands/run/permission-decision.ts +19 -0
  55. package/src/commands/run/permission-recovery.ts +194 -0
  56. package/src/commands/run/permission-state.ts +177 -0
  57. package/src/commands/run/print-idle-timeout.ts +47 -0
  58. package/src/commands/run/protocol-envelope.ts +111 -0
  59. package/src/commands/run/protocol-stdio.ts +71 -0
  60. package/src/commands/run/protocol.ts +391 -0
  61. package/src/commands/run/runtime-command-bridge.ts +190 -0
  62. package/src/commands/run/runtime-event-sink.ts +560 -0
  63. package/src/commands/run/session-exit-controller.ts +45 -0
  64. package/src/commands/run/types.ts +65 -0
  65. package/src/commands/run.ts +62 -0
  66. package/src/commands/session-control.ts +133 -0
  67. package/src/commands/skills/add-command.ts +88 -0
  68. package/src/commands/skills/install-command.ts +105 -0
  69. package/src/commands/skills/install.ts +216 -0
  70. package/src/commands/skills/progress.ts +126 -0
  71. package/src/commands/skills/publish-command.ts +85 -0
  72. package/src/commands/skills/register.ts +17 -0
  73. package/src/commands/skills/remove-command.ts +102 -0
  74. package/src/commands/skills/shared.ts +117 -0
  75. package/src/commands/skills/sync.ts +571 -0
  76. package/src/commands/skills/types.ts +33 -0
  77. package/src/commands/skills.ts +1 -0
  78. package/src/commands/stop.ts +41 -0
  79. package/src/config.ts +1 -0
  80. package/src/default-skill-plugin.ts +29 -0
  81. package/src/env.ts +1 -0
  82. package/src/hooks/plugins/index.ts +66 -0
  83. package/src/mem-cli.ts +19 -0
  84. package/src/session-cache.ts +250 -0
  85. package/src/session-permission-cache.ts +40 -0
  86. package/src/utils.ts +25 -0
  87. package/src/workspace.ts +12 -0
@@ -0,0 +1,594 @@
1
+ /* eslint-disable max-lines -- Channel send CLI parsing and request dispatch stay colocated. */
2
+ import { Buffer } from 'node:buffer'
3
+ import { readFileSync } from 'node:fs'
4
+ import process from 'node:process'
5
+
6
+ import type { Command } from 'commander'
7
+
8
+ import { MAX_CHANNEL_TEXT_MESSAGE_LENGTH, countChannelTextMessageCharacters } from '@oneworks/core/channel'
9
+ import type { ChannelTextMention } from '@oneworks/core/channel'
10
+ import {
11
+ filterChannelEmojiRegistryEntries,
12
+ findChannelEmojiRegistryEntry,
13
+ formatChannelEmojiRegistryEntries,
14
+ listChannelEmojiRegistryEntries,
15
+ sortChannelEmojiRegistryEntriesByRecent,
16
+ upsertChannelEmojiRegistryEntry
17
+ } from '@oneworks/utils'
18
+
19
+ import { resolveContext as resolveMemoryContext } from './memory/context'
20
+
21
+ export interface ChannelCommandOptions {
22
+ cwd?: string
23
+ env?: NodeJS.ProcessEnv
24
+ fetch?: typeof globalThis.fetch
25
+ }
26
+
27
+ interface ParsedChannelCommand {
28
+ atAll?: boolean
29
+ atIds?: string[]
30
+ ats?: string
31
+ channelKey?: string
32
+ command: 'send'
33
+ cwd?: string
34
+ lineBreakToken?: string
35
+ message: unknown
36
+ receiveId?: string
37
+ receiveIdType?: string
38
+ server?: string
39
+ }
40
+
41
+ interface ParsedEmojiCommand {
42
+ action: 'annotate' | 'get' | 'list' | 'save' | 'send'
43
+ aliases?: string[]
44
+ channelKey?: string
45
+ emojiMd5?: string
46
+ emojiSize?: string
47
+ id?: string
48
+ label?: string
49
+ limit?: number
50
+ metadata?: Record<string, string>
51
+ note?: string
52
+ platform?: string
53
+ query?: string
54
+ receiveId?: string
55
+ receiveIdType?: string
56
+ recent?: boolean
57
+ sendable?: boolean
58
+ server?: string
59
+ tags?: string[]
60
+ }
61
+
62
+ const CHANNEL_CONTEXT_PATH_ENV = '__ONEWORKS_PROJECT_CHANNEL_CONTEXT_PATH__'
63
+ const CHANNEL_KEY_ENV = '__ONEWORKS_PROJECT_CHANNEL_KEY__'
64
+ const CHANNEL_ID_ENV = '__ONEWORKS_PROJECT_CHANNEL_ID__'
65
+ const SESSION_ID_ENV = '__ONEWORKS_PROJECT_SESSION_ID__'
66
+ const DEFAULT_LINE_BREAK_TOKEN = '⏎'
67
+ const ESCAPED_LINE_BREAK_RE = /\\r\\n|\\n|\\r/gu
68
+
69
+ const normalizeLineBreaks = (value: string, lineBreakToken?: string) => {
70
+ const token = lineBreakToken?.trim()
71
+ const marked = token == null || token === '' ? value : value.split(token).join('\n')
72
+ return marked.replace(ESCAPED_LINE_BREAK_RE, '\n')
73
+ }
74
+
75
+ const trimNonEmpty = (value: unknown) => {
76
+ if (typeof value !== 'string') return undefined
77
+ const trimmed = value.trim()
78
+ return trimmed === '' ? undefined : trimmed
79
+ }
80
+
81
+ const isRecord = (value: unknown): value is Record<string, unknown> => (
82
+ value != null && typeof value === 'object' && !Array.isArray(value)
83
+ )
84
+
85
+ const readContext = (env: NodeJS.ProcessEnv) => {
86
+ const contextPath = trimNonEmpty(env[CHANNEL_CONTEXT_PATH_ENV])
87
+ if (contextPath == null) return {}
88
+
89
+ try {
90
+ const parsed = JSON.parse(readFileSync(contextPath, 'utf8')) as unknown
91
+ return isRecord(parsed) ? parsed : {}
92
+ } catch {
93
+ return {}
94
+ }
95
+ }
96
+
97
+ const optionValueNames = new Set([
98
+ '--at',
99
+ '--ats',
100
+ '--channel',
101
+ '--channel-key',
102
+ '--cwd',
103
+ '--line-break-token',
104
+ '--newline-token',
105
+ '--receive-id',
106
+ '--receive-id-type',
107
+ '--server',
108
+ '--to'
109
+ ])
110
+ const booleanOptions = new Set(['--br'])
111
+
112
+ type StringOptionKey = 'channelKey' | 'cwd' | 'receiveId' | 'receiveIdType' | 'server'
113
+
114
+ const optionAliases: Record<string, StringOptionKey> = {
115
+ '--channel': 'channelKey',
116
+ '--channel-key': 'channelKey',
117
+ '--cwd': 'cwd',
118
+ '--receive-id': 'receiveId',
119
+ '--receive-id-type': 'receiveIdType',
120
+ '--server': 'server',
121
+ '--to': 'receiveId'
122
+ }
123
+
124
+ const emojiOptionValueNames = new Set([
125
+ '--alias',
126
+ '--channel',
127
+ '--channel-key',
128
+ '--emoji-md5',
129
+ '--emoji-size',
130
+ '--id',
131
+ '--label',
132
+ '--limit',
133
+ '--meta',
134
+ '--note',
135
+ '--platform',
136
+ '--query',
137
+ '--receive-id',
138
+ '--receive-id-type',
139
+ '--server',
140
+ '--tag',
141
+ '--to'
142
+ ])
143
+ const emojiBooleanOptions = new Set(['--recent', '--sendable'])
144
+
145
+ const isEmojiCommand = (argv: string[]) => argv[0] === 'emoji' || argv[1] === 'emoji'
146
+
147
+ const parseEmojiArgs = (argv: string[]): ParsedEmojiCommand => {
148
+ const channelKey = argv[0] === 'emoji' ? undefined : argv[0]
149
+ const args = argv[0] === 'emoji' ? argv.slice(1) : argv.slice(2)
150
+ const action = args[0]
151
+ if (action !== 'list' && action !== 'get' && action !== 'save' && action !== 'annotate' && action !== 'send') {
152
+ throw new Error('Usage: oneworks channel [channelKey] emoji <list|get|save|annotate|send> [id]')
153
+ }
154
+
155
+ const positionals: string[] = []
156
+ const options: ParsedEmojiCommand = { action, channelKey }
157
+ for (let index = 1; index < args.length; index += 1) {
158
+ const arg = args[index]
159
+ if (emojiBooleanOptions.has(arg)) {
160
+ if (arg === '--recent') {
161
+ options.recent = true
162
+ } else if (arg === '--sendable') {
163
+ options.sendable = true
164
+ }
165
+ continue
166
+ }
167
+ if (emojiOptionValueNames.has(arg)) {
168
+ const value = args[index + 1]
169
+ if (value == null) {
170
+ throw new Error(`Missing value for ${arg}.`)
171
+ }
172
+ if (arg === '--alias') {
173
+ options.aliases = [...(options.aliases ?? []), value]
174
+ } else if (arg === '--channel' || arg === '--channel-key') {
175
+ options.channelKey = value
176
+ } else if (arg === '--emoji-md5') {
177
+ options.emojiMd5 = value
178
+ } else if (arg === '--emoji-size') {
179
+ options.emojiSize = value
180
+ } else if (arg === '--id') {
181
+ options.id = value
182
+ } else if (arg === '--label') {
183
+ options.label = value
184
+ } else if (arg === '--limit') {
185
+ const limit = Number.parseInt(value, 10)
186
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new Error('--limit must be a positive integer.')
187
+ options.limit = limit
188
+ } else if (arg === '--meta') {
189
+ const separator = value.indexOf('=')
190
+ if (separator < 1) throw new Error('--meta expects key=value.')
191
+ const key = value.slice(0, separator).trim()
192
+ if (key === '') throw new Error('--meta expects key=value.')
193
+ options.metadata = {
194
+ ...(options.metadata ?? {}),
195
+ [key]: value.slice(separator + 1).trim()
196
+ }
197
+ } else if (arg === '--note') {
198
+ options.note = value
199
+ } else if (arg === '--platform') {
200
+ options.platform = value
201
+ } else if (arg === '--query') {
202
+ options.query = value
203
+ } else if (arg === '--receive-id' || arg === '--to') {
204
+ options.receiveId = value
205
+ } else if (arg === '--receive-id-type') {
206
+ options.receiveIdType = value
207
+ } else if (arg === '--server') {
208
+ options.server = value
209
+ } else if (arg === '--tag') {
210
+ options.tags = [...(options.tags ?? []), value]
211
+ }
212
+ index += 1
213
+ continue
214
+ }
215
+ positionals.push(arg)
216
+ }
217
+
218
+ return {
219
+ ...options,
220
+ id: options.id ?? positionals[0]
221
+ }
222
+ }
223
+
224
+ const parseArgs = (argv: string[]): Omit<ParsedChannelCommand, 'message'> & { contentParts: string[] } => {
225
+ const positionals: string[] = []
226
+ const options: Partial<ParsedChannelCommand> = {}
227
+
228
+ for (let index = 0; index < argv.length; index += 1) {
229
+ const arg = argv[index]
230
+ if (arg === '--at-all') {
231
+ options.atAll = true
232
+ continue
233
+ }
234
+ if (booleanOptions.has(arg)) {
235
+ if (arg === '--br') {
236
+ options.lineBreakToken = DEFAULT_LINE_BREAK_TOKEN
237
+ }
238
+ continue
239
+ }
240
+ if (optionValueNames.has(arg)) {
241
+ const value = argv[index + 1]
242
+ if (value == null) {
243
+ throw new Error(`Missing value for ${arg}.`)
244
+ }
245
+ if (arg === '--at') {
246
+ options.atIds = [...(options.atIds ?? []), value]
247
+ } else if (arg === '--ats') {
248
+ options.ats = value
249
+ } else if (arg === '--line-break-token' || arg === '--newline-token') {
250
+ options.lineBreakToken = value
251
+ } else {
252
+ const optionKey = optionAliases[arg]
253
+ if (optionKey != null) {
254
+ options[optionKey] = value
255
+ }
256
+ }
257
+ index += 1
258
+ continue
259
+ }
260
+ positionals.push(arg)
261
+ }
262
+
263
+ if (positionals[0] === 'send') {
264
+ return {
265
+ ...options,
266
+ command: 'send',
267
+ contentParts: positionals.slice(1)
268
+ }
269
+ }
270
+
271
+ if (positionals[1] === 'send') {
272
+ return {
273
+ ...options,
274
+ channelKey: options.channelKey ?? positionals[0],
275
+ command: 'send',
276
+ contentParts: positionals.slice(2)
277
+ }
278
+ }
279
+
280
+ throw new Error('Usage: oneworks channel [channelKey] send <text|payload>')
281
+ }
282
+
283
+ const toMention = (id: string): ChannelTextMention | undefined => {
284
+ const normalized = trimNonEmpty(id)
285
+ if (normalized == null) return undefined
286
+ return {
287
+ id: normalized,
288
+ platform: 'wechat',
289
+ type: normalized === 'notify@all' ? 'all' : 'user'
290
+ }
291
+ }
292
+
293
+ const resolveMentions = (parsed: Omit<ParsedChannelCommand, 'message'>) => {
294
+ const atsMentions = trimNonEmpty(parsed.ats)
295
+ ?.split(',')
296
+ .map(toMention)
297
+ .filter((item): item is ChannelTextMention => item != null) ?? []
298
+ const directMentions = (parsed.atIds ?? [])
299
+ .map(toMention)
300
+ .filter((item): item is ChannelTextMention => item != null)
301
+ const allMention = parsed.atAll === true
302
+ ? [toMention('notify@all')].filter((item): item is ChannelTextMention => item != null)
303
+ : []
304
+ const mentions = [...atsMentions, ...directMentions, ...allMention]
305
+ return mentions.length === 0 ? undefined : mentions
306
+ }
307
+
308
+ const readStdin = async () => {
309
+ if (process.stdin.isTTY) return ''
310
+ const chunks: Uint8Array[] = []
311
+ for await (const chunk of process.stdin) {
312
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
313
+ }
314
+ return Buffer.concat(chunks).toString('utf8')
315
+ }
316
+
317
+ const parseLooseObject = (raw: string) => {
318
+ const body = raw.replace(/^\s*\{\s*/u, '').replace(/\s*\}\s*$/u, '')
319
+ const entries = body
320
+ .split(',')
321
+ .map(part => part.trim())
322
+ .filter(Boolean)
323
+ const result: Record<string, string> = {}
324
+
325
+ for (const entry of entries) {
326
+ const separator = entry.indexOf(':')
327
+ if (separator < 0) continue
328
+ const key = entry.slice(0, separator).trim().replace(/^['"]|['"]$/gu, '')
329
+ const value = entry.slice(separator + 1).trim().replace(/^['"]|['"]$/gu, '')
330
+ if (key !== '') {
331
+ result[key] = value
332
+ }
333
+ }
334
+
335
+ return Object.keys(result).length === 0 ? undefined : result
336
+ }
337
+
338
+ const normalizeParsedMessage = (message: unknown, lineBreakToken?: string): unknown => {
339
+ if (typeof message === 'string') return normalizeLineBreaks(message, lineBreakToken)
340
+ if (!isRecord(message)) return message
341
+ const next = { ...message }
342
+ if (typeof next.text === 'string') next.text = normalizeLineBreaks(next.text, lineBreakToken)
343
+ if (typeof next.content === 'string') next.content = normalizeLineBreaks(next.content, lineBreakToken)
344
+ return next
345
+ }
346
+
347
+ const parseMessage = async (contentParts: string[], options: { lineBreakToken?: string } = {}) => {
348
+ const raw = (contentParts.join(' ') || await readStdin()).trim()
349
+ if (raw === '') {
350
+ throw new Error('Missing message content.')
351
+ }
352
+
353
+ if (raw.startsWith('{') && raw.endsWith('}')) {
354
+ try {
355
+ return normalizeParsedMessage(JSON.parse(raw) as unknown, options.lineBreakToken)
356
+ } catch {
357
+ const loose = parseLooseObject(raw)
358
+ if (loose != null) return normalizeParsedMessage(loose, options.lineBreakToken)
359
+ throw new Error('Object payload must be valid JSON or key:value pairs.')
360
+ }
361
+ }
362
+
363
+ return normalizeLineBreaks(raw, options.lineBreakToken)
364
+ }
365
+
366
+ const normalizeServerHost = (host: string) => {
367
+ const normalized = host.trim()
368
+ if (normalized === '' || normalized === '0.0.0.0' || normalized === '::') {
369
+ return '127.0.0.1'
370
+ }
371
+ return normalized
372
+ }
373
+
374
+ const resolveServerBaseUrl = (input: { env: NodeJS.ProcessEnv; server?: string }) => {
375
+ const explicit = trimNonEmpty(input.server)
376
+ if (explicit != null) return explicit.replace(/\/+$/u, '')
377
+
378
+ const envBase = trimNonEmpty(input.env.__ONEWORKS_PROJECT_SERVER_BASE_URL__)
379
+ if (envBase != null) return envBase.replace(/\/+$/u, '')
380
+
381
+ const host = normalizeServerHost(trimNonEmpty(input.env.__ONEWORKS_PROJECT_SERVER_HOST__) ?? '127.0.0.1')
382
+ const port = trimNonEmpty(input.env.__ONEWORKS_PROJECT_SERVER_PORT__) ?? '8787'
383
+ return `http://${host}:${port}`
384
+ }
385
+
386
+ const normalizeApiResponse = async (response: Response) => {
387
+ const text = await response.text()
388
+ const parsed = text === ''
389
+ ? {}
390
+ : JSON.parse(text) as unknown
391
+
392
+ if (!response.ok) {
393
+ if (isRecord(parsed) && isRecord(parsed.error) && typeof parsed.error.message === 'string') {
394
+ throw new Error(parsed.error.message)
395
+ }
396
+ if (isRecord(parsed) && typeof parsed.message === 'string') {
397
+ throw new Error(parsed.message)
398
+ }
399
+ throw new Error(`Channel send failed: HTTP ${response.status}`)
400
+ }
401
+
402
+ if (isRecord(parsed) && parsed.success === true) {
403
+ return isRecord(parsed.data) ? parsed.data : {}
404
+ }
405
+ return isRecord(parsed) ? parsed : {}
406
+ }
407
+
408
+ const resolveTextMessageContent = (message: unknown) => {
409
+ if (typeof message === 'string') return message
410
+ if (!isRecord(message)) return undefined
411
+
412
+ const type = trimNonEmpty(message.type)?.toLowerCase()
413
+ if (type != null && type !== 'text') return undefined
414
+
415
+ return trimNonEmpty(message.text) ?? trimNonEmpty(message.content)
416
+ }
417
+
418
+ const assertTextMessageLength = (message: unknown) => {
419
+ const text = resolveTextMessageContent(message)
420
+ if (text == null) return
421
+
422
+ const length = countChannelTextMessageCharacters(text)
423
+ if (length > MAX_CHANNEL_TEXT_MESSAGE_LENGTH) {
424
+ throw new Error(
425
+ `Channel text messages must be ${MAX_CHANNEL_TEXT_MESSAGE_LENGTH} characters or fewer; got ${length}. ` +
426
+ 'Shorten the visible reply or send an emoji/file instead.'
427
+ )
428
+ }
429
+ }
430
+
431
+ const resolveEmojiRegistryContext = (input: {
432
+ cwd: string
433
+ env: NodeJS.ProcessEnv
434
+ platform?: string
435
+ }) => {
436
+ const context = resolveMemoryContext({
437
+ channel: input.platform,
438
+ cwd: input.cwd,
439
+ env: input.env
440
+ })
441
+ const platform = trimNonEmpty(input.platform) ?? context.channelType
442
+ if (platform == null) {
443
+ throw new Error('Missing emoji platform. Pass --platform or run from a channel session context.')
444
+ }
445
+ return {
446
+ platform,
447
+ root: context.root
448
+ }
449
+ }
450
+
451
+ const runEmojiCommand = async (
452
+ argv: string[],
453
+ options: ChannelCommandOptions
454
+ ): Promise<string> => {
455
+ const env = options.env ?? process.env
456
+ const cwd = options.cwd ?? process.cwd()
457
+ const parsed = parseEmojiArgs(argv)
458
+ const { platform, root } = resolveEmojiRegistryContext({ cwd, env, platform: parsed.platform })
459
+
460
+ if (parsed.action === 'list') {
461
+ const entries = filterChannelEmojiRegistryEntries(
462
+ await listChannelEmojiRegistryEntries(root, platform),
463
+ {
464
+ query: parsed.query,
465
+ sendable: parsed.sendable,
466
+ tags: parsed.tags
467
+ }
468
+ )
469
+ const ordered = parsed.recent === true ? sortChannelEmojiRegistryEntriesByRecent(entries) : entries
470
+ return formatChannelEmojiRegistryEntries(parsed.limit == null ? ordered : ordered.slice(0, parsed.limit))
471
+ }
472
+
473
+ const id = trimNonEmpty(parsed.id)
474
+ if (id == null) {
475
+ throw new Error(`Emoji ${parsed.action} requires an id.`)
476
+ }
477
+
478
+ if (parsed.action === 'get') {
479
+ const emoji = await findChannelEmojiRegistryEntry(root, { id, platform })
480
+ if (emoji == null) throw new Error(`Emoji "${id}" was not found in the ${platform} registry.`)
481
+ return JSON.stringify(emoji, null, 2)
482
+ }
483
+
484
+ if (parsed.action === 'save' || parsed.action === 'annotate') {
485
+ const emojiMd5 = trimNonEmpty(parsed.emojiMd5)
486
+ const emojiSize = trimNonEmpty(parsed.emojiSize)
487
+ const emoji = await upsertChannelEmojiRegistryEntry(root, {
488
+ id,
489
+ platform,
490
+ ...(parsed.aliases == null || parsed.aliases.length === 0 ? {} : { aliases: parsed.aliases }),
491
+ ...(trimNonEmpty(parsed.label) == null ? {} : { label: trimNonEmpty(parsed.label) }),
492
+ ...(trimNonEmpty(parsed.note) == null ? {} : { note: trimNonEmpty(parsed.note) }),
493
+ ...(parsed.tags == null || parsed.tags.length === 0 ? {} : { tags: parsed.tags }),
494
+ metadata: {
495
+ ...(parsed.metadata ?? {}),
496
+ ...(emojiMd5 == null ? {} : { emojiMd5 }),
497
+ ...(emojiSize == null ? {} : { emojiSize })
498
+ }
499
+ })
500
+ return `Emoji saved: ${emoji.platform}:${emoji.id}`
501
+ }
502
+
503
+ return await runChannelCommand([
504
+ ...(parsed.channelKey == null ? [] : [parsed.channelKey]),
505
+ 'send',
506
+ ...(parsed.receiveId == null ? [] : ['--to', parsed.receiveId]),
507
+ ...(parsed.receiveIdType == null ? [] : ['--receive-id-type', parsed.receiveIdType]),
508
+ ...(parsed.server == null ? [] : ['--server', parsed.server]),
509
+ JSON.stringify({ type: 'emoji', id, platform })
510
+ ], options)
511
+ }
512
+
513
+ export const runChannelCommand = async (
514
+ argv: string[],
515
+ options: ChannelCommandOptions = {}
516
+ ): Promise<string> => {
517
+ if (isEmojiCommand(argv)) {
518
+ return await runEmojiCommand(argv, options)
519
+ }
520
+
521
+ const env = options.env ?? process.env
522
+ const cwd = options.cwd ?? process.cwd()
523
+ const context = readContext(env)
524
+ const parsed = parseArgs(argv)
525
+ const message = await parseMessage(parsed.contentParts, { lineBreakToken: parsed.lineBreakToken })
526
+ assertTextMessageLength(message)
527
+ const mentions = resolveMentions(parsed)
528
+ const channelKey = trimNonEmpty(parsed.channelKey) ??
529
+ trimNonEmpty(context.channelKey) ??
530
+ trimNonEmpty(env[CHANNEL_KEY_ENV])
531
+ const receiveId = trimNonEmpty(parsed.receiveId) ??
532
+ trimNonEmpty(context.replyReceiveId) ??
533
+ trimNonEmpty(context.channelId) ??
534
+ trimNonEmpty(env[CHANNEL_ID_ENV])
535
+ const receiveIdType = trimNonEmpty(parsed.receiveIdType) ??
536
+ trimNonEmpty(context.replyReceiveIdType) ??
537
+ 'chat_id'
538
+ const sessionId = trimNonEmpty(context.sessionId) ?? trimNonEmpty(env[SESSION_ID_ENV])
539
+
540
+ if (channelKey == null) {
541
+ throw new Error('Missing channel key. Pass a channel key or run from a channel session context.')
542
+ }
543
+
544
+ const fetchImpl = options.fetch ?? globalThis.fetch
545
+ const response = await fetchImpl(
546
+ `${resolveServerBaseUrl({ env, server: parsed.server })}/api/channels/${encodeURIComponent(channelKey)}/send`,
547
+ {
548
+ method: 'POST',
549
+ headers: { 'content-type': 'application/json' },
550
+ body: JSON.stringify({
551
+ cwd: parsed.cwd ?? cwd,
552
+ message,
553
+ ...(mentions == null ? {} : { mentions }),
554
+ receiveId,
555
+ receiveIdType,
556
+ ...(sessionId == null ? {} : { sessionId })
557
+ })
558
+ }
559
+ )
560
+ const data = await normalizeApiResponse(response)
561
+ const type = typeof data.type === 'string' ? data.type : 'message'
562
+ const messageId = typeof data.messageId === 'string' ? data.messageId : undefined
563
+
564
+ return [
565
+ `Sent ${type} message through channel ${channelKey}.`,
566
+ messageId == null ? undefined : `messageId: ${messageId}`
567
+ ].filter(Boolean).join('\n')
568
+ }
569
+
570
+ const printResult = (output: string) => {
571
+ if (output === '') return
572
+ process.stdout.write(output.endsWith('\n') ? output : `${output}\n`)
573
+ }
574
+
575
+ export const registerChannelSubcommands = (command: Command) => {
576
+ command
577
+ .allowUnknownOption()
578
+ .allowExcessArguments()
579
+ .argument('[args...]')
580
+ .description('Send messages through OneWorks channels from agent sessions')
581
+ .action(async (args: string[]) => {
582
+ printResult(await runChannelCommand(args))
583
+ })
584
+
585
+ return command
586
+ }
587
+
588
+ export const registerChannelCommand = (program: Command) => {
589
+ registerChannelSubcommands(
590
+ program
591
+ .command('channel')
592
+ .description('Send messages through OneWorks channels from agent sessions')
593
+ )
594
+ }