@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.
- package/LICENSE +21 -0
- package/channel.js +7 -0
- package/cli.js +5 -0
- package/mem.js +7 -0
- package/package.json +59 -0
- package/postinstall.js +75 -0
- package/src/AGENTS.md +169 -0
- package/src/channel-cli.ts +19 -0
- package/src/cli-argv.ts +27 -0
- package/src/cli.ts +63 -0
- package/src/commands/@core/adapter-option.ts +85 -0
- package/src/commands/@core/extra-options.ts +12 -0
- package/src/commands/@core/plugin-install.ts +1 -0
- package/src/commands/@core/plugin-source.ts +1 -0
- package/src/commands/accounts.ts +204 -0
- package/src/commands/adapter/prepare-selection.ts +181 -0
- package/src/commands/adapter/prepare.ts +104 -0
- package/src/commands/adapter.ts +48 -0
- package/src/commands/agent/actions.ts +176 -0
- package/src/commands/agent/runtime-store-commands.ts +56 -0
- package/src/commands/agent/runtime-store-events.ts +23 -0
- package/src/commands/agent/runtime-store-session.ts +170 -0
- package/src/commands/agent/runtime-store-shared.ts +139 -0
- package/src/commands/agent/runtime-store.ts +4 -0
- package/src/commands/agent.ts +81 -0
- package/src/commands/benchmark.ts +198 -0
- package/src/commands/channel.ts +594 -0
- package/src/commands/clear.ts +140 -0
- package/src/commands/config/actions.ts +196 -0
- package/src/commands/config/display-state.ts +108 -0
- package/src/commands/config/index.ts +135 -0
- package/src/commands/config/interactive.ts +121 -0
- package/src/commands/config/read-state.ts +56 -0
- package/src/commands/config/section-state.ts +109 -0
- package/src/commands/config/shared.ts +195 -0
- package/src/commands/kill.ts +41 -0
- package/src/commands/list.ts +224 -0
- package/src/commands/memory/context.ts +76 -0
- package/src/commands/memory/entries.ts +131 -0
- package/src/commands/memory/shared.ts +89 -0
- package/src/commands/memory/store.ts +69 -0
- package/src/commands/memory/target.ts +54 -0
- package/src/commands/memory.ts +97 -0
- package/src/commands/plugin.ts +62 -0
- package/src/commands/report-targets.ts +149 -0
- package/src/commands/report.ts +232 -0
- package/src/commands/run/adapter-cli-version.ts +65 -0
- package/src/commands/run/command.ts +982 -0
- package/src/commands/run/input-bridge.ts +108 -0
- package/src/commands/run/input-control.ts +112 -0
- package/src/commands/run/input-decision.ts +88 -0
- package/src/commands/run/options.ts +104 -0
- package/src/commands/run/output.ts +179 -0
- package/src/commands/run/permission-decision.ts +19 -0
- package/src/commands/run/permission-recovery.ts +194 -0
- package/src/commands/run/permission-state.ts +177 -0
- package/src/commands/run/print-idle-timeout.ts +47 -0
- package/src/commands/run/protocol-envelope.ts +111 -0
- package/src/commands/run/protocol-stdio.ts +71 -0
- package/src/commands/run/protocol.ts +391 -0
- package/src/commands/run/runtime-command-bridge.ts +190 -0
- package/src/commands/run/runtime-event-sink.ts +560 -0
- package/src/commands/run/session-exit-controller.ts +45 -0
- package/src/commands/run/types.ts +65 -0
- package/src/commands/run.ts +62 -0
- package/src/commands/session-control.ts +133 -0
- package/src/commands/skills/add-command.ts +88 -0
- package/src/commands/skills/install-command.ts +105 -0
- package/src/commands/skills/install.ts +216 -0
- package/src/commands/skills/progress.ts +126 -0
- package/src/commands/skills/publish-command.ts +85 -0
- package/src/commands/skills/register.ts +17 -0
- package/src/commands/skills/remove-command.ts +102 -0
- package/src/commands/skills/shared.ts +117 -0
- package/src/commands/skills/sync.ts +571 -0
- package/src/commands/skills/types.ts +33 -0
- package/src/commands/skills.ts +1 -0
- package/src/commands/stop.ts +41 -0
- package/src/config.ts +1 -0
- package/src/default-skill-plugin.ts +29 -0
- package/src/env.ts +1 -0
- package/src/hooks/plugins/index.ts +66 -0
- package/src/mem-cli.ts +19 -0
- package/src/session-cache.ts +250 -0
- package/src/session-permission-cache.ts +40 -0
- package/src/utils.ts +25 -0
- 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
|
+
}
|