opensteer 0.6.13 → 0.7.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/README.md +256 -184
- package/dist/chunk-PQYA6IX2.js +32571 -0
- package/dist/chunk-PQYA6IX2.js.map +1 -0
- package/dist/cli/bin.cjs +38201 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +5612 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/index.cjs +31309 -16009
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4440 -670
- package/dist/index.d.ts +4440 -670
- package/dist/index.js +438 -378
- package/dist/index.js.map +1 -0
- package/package.json +56 -62
- package/skills/README.md +21 -20
- package/skills/opensteer/SKILL.md +60 -194
- package/skills/opensteer/references/cli-reference.md +69 -113
- package/skills/opensteer/references/request-workflow.md +81 -0
- package/skills/opensteer/references/sdk-reference.md +101 -154
- package/CHANGELOG.md +0 -75
- package/bin/opensteer.mjs +0 -1423
- package/dist/browser-profile-client-CGXc0-P9.d.cts +0 -228
- package/dist/browser-profile-client-DHLzMf-K.d.ts +0 -228
- package/dist/chunk-2ES46WCO.js +0 -1437
- package/dist/chunk-3H5RRIMZ.js +0 -69
- package/dist/chunk-AVXUMEDG.js +0 -62
- package/dist/chunk-DN3GI5CH.js +0 -57
- package/dist/chunk-FAHE5DB2.js +0 -190
- package/dist/chunk-HBTSQ2V4.js +0 -15259
- package/dist/chunk-K5CL76MG.js +0 -81
- package/dist/chunk-U724TBY6.js +0 -1262
- package/dist/chunk-ZRCFF546.js +0 -77
- package/dist/cli/auth.cjs +0 -2022
- package/dist/cli/auth.d.cts +0 -114
- package/dist/cli/auth.d.ts +0 -114
- package/dist/cli/auth.js +0 -15
- package/dist/cli/local-profile.cjs +0 -197
- package/dist/cli/local-profile.d.cts +0 -18
- package/dist/cli/local-profile.d.ts +0 -18
- package/dist/cli/local-profile.js +0 -97
- package/dist/cli/profile.cjs +0 -18548
- package/dist/cli/profile.d.cts +0 -79
- package/dist/cli/profile.d.ts +0 -79
- package/dist/cli/profile.js +0 -1328
- package/dist/cli/server.cjs +0 -17232
- package/dist/cli/server.d.cts +0 -2
- package/dist/cli/server.d.ts +0 -2
- package/dist/cli/server.js +0 -977
- package/dist/cli/skills-installer.cjs +0 -230
- package/dist/cli/skills-installer.d.cts +0 -28
- package/dist/cli/skills-installer.d.ts +0 -28
- package/dist/cli/skills-installer.js +0 -201
- package/dist/extractor-4Q3TFZJB.js +0 -8
- package/dist/resolver-MGN64KCP.js +0 -7
- package/dist/types-Cr10igF3.d.cts +0 -345
- package/dist/types-Cr10igF3.d.ts +0 -345
- package/skills/electron/SKILL.md +0 -87
- package/skills/electron/references/opensteer-electron-recipes.md +0 -88
- package/skills/electron/references/opensteer-electron-workflow.md +0 -85
- package/skills/opensteer/references/examples.md +0 -118
package/bin/opensteer.mjs
DELETED
|
@@ -1,1423 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { createHash } from 'crypto'
|
|
4
|
-
import { spawn } from 'child_process'
|
|
5
|
-
import {
|
|
6
|
-
closeSync,
|
|
7
|
-
existsSync,
|
|
8
|
-
openSync,
|
|
9
|
-
realpathSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
readdirSync,
|
|
12
|
-
unlinkSync,
|
|
13
|
-
writeFileSync,
|
|
14
|
-
} from 'fs'
|
|
15
|
-
import { connect } from 'net'
|
|
16
|
-
import { tmpdir } from 'os'
|
|
17
|
-
import { dirname, join } from 'path'
|
|
18
|
-
import { fileURLToPath, pathToFileURL } from 'url'
|
|
19
|
-
|
|
20
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
21
|
-
const SERVER_SCRIPT = join(__dirname, '..', 'dist', 'cli', 'server.js')
|
|
22
|
-
const SKILLS_INSTALLER_SCRIPT = join(
|
|
23
|
-
__dirname,
|
|
24
|
-
'..',
|
|
25
|
-
'dist',
|
|
26
|
-
'cli',
|
|
27
|
-
'skills-installer.js'
|
|
28
|
-
)
|
|
29
|
-
const PROFILE_CLI_SCRIPT = join(__dirname, '..', 'dist', 'cli', 'profile.js')
|
|
30
|
-
const LOCAL_PROFILE_CLI_SCRIPT = join(
|
|
31
|
-
__dirname,
|
|
32
|
-
'..',
|
|
33
|
-
'dist',
|
|
34
|
-
'cli',
|
|
35
|
-
'local-profile.js'
|
|
36
|
-
)
|
|
37
|
-
const AUTH_CLI_SCRIPT = join(__dirname, '..', 'dist', 'cli', 'auth.js')
|
|
38
|
-
const SKILLS_HELP_TEXT = `Usage: opensteer skills <install|add> [options]
|
|
39
|
-
|
|
40
|
-
Installs the first-party Opensteer skill using the upstream "skills" CLI.
|
|
41
|
-
|
|
42
|
-
Commands:
|
|
43
|
-
install Install the opensteer skill
|
|
44
|
-
add Alias for install
|
|
45
|
-
|
|
46
|
-
Supported Options:
|
|
47
|
-
-a, --agent <agents...> Target specific agent(s)
|
|
48
|
-
-g, --global Install globally
|
|
49
|
-
-y, --yes Skip confirmations
|
|
50
|
-
--copy Copy files instead of symlinking
|
|
51
|
-
--all Install to all agents
|
|
52
|
-
-h, --help Show this help
|
|
53
|
-
|
|
54
|
-
Examples:
|
|
55
|
-
opensteer skills install
|
|
56
|
-
opensteer skills add --agent codex --global --yes
|
|
57
|
-
opensteer skills install --all --yes
|
|
58
|
-
`
|
|
59
|
-
const PROFILE_HELP_TEXT = `Usage: opensteer profile <command> [options]
|
|
60
|
-
|
|
61
|
-
Manage cloud browser profiles and sync local cookie state into cloud profiles.
|
|
62
|
-
|
|
63
|
-
Commands:
|
|
64
|
-
list
|
|
65
|
-
create --name <name>
|
|
66
|
-
sync
|
|
67
|
-
|
|
68
|
-
Run "opensteer profile --help" after building for full command details.
|
|
69
|
-
`
|
|
70
|
-
const LOCAL_PROFILE_HELP_TEXT = `Usage: opensteer local-profile <command> [options]
|
|
71
|
-
|
|
72
|
-
Inspect local Chrome profiles for real-browser mode.
|
|
73
|
-
|
|
74
|
-
Commands:
|
|
75
|
-
list
|
|
76
|
-
|
|
77
|
-
Run "opensteer local-profile --help" after building for full command details.
|
|
78
|
-
`
|
|
79
|
-
const AUTH_HELP_TEXT = `Usage: opensteer auth <command> [options]
|
|
80
|
-
|
|
81
|
-
Authenticate Opensteer CLI with Opensteer Cloud.
|
|
82
|
-
|
|
83
|
-
Commands:
|
|
84
|
-
login
|
|
85
|
-
status
|
|
86
|
-
logout
|
|
87
|
-
|
|
88
|
-
Run "opensteer auth --help" after building for full command details.
|
|
89
|
-
`
|
|
90
|
-
|
|
91
|
-
const CONNECT_TIMEOUT = 15000
|
|
92
|
-
const POLL_INTERVAL = 100
|
|
93
|
-
const RESPONSE_TIMEOUT = 120000
|
|
94
|
-
const HEALTH_TIMEOUT = 1500
|
|
95
|
-
const RUNTIME_PREFIX = 'opensteer-'
|
|
96
|
-
const SOCKET_SUFFIX = '.sock'
|
|
97
|
-
const PID_SUFFIX = '.pid'
|
|
98
|
-
const LOCK_SUFFIX = '.lock'
|
|
99
|
-
const METADATA_SUFFIX = '.meta.json'
|
|
100
|
-
const CLIENT_BINDING_PREFIX = `${RUNTIME_PREFIX}client-`
|
|
101
|
-
const CLIENT_BINDING_SUFFIX = '.session'
|
|
102
|
-
const CLOSE_ALL_REQUEST = { id: 1, command: 'close', args: {} }
|
|
103
|
-
const PING_REQUEST = { id: 1, command: 'ping', args: {} }
|
|
104
|
-
const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
|
|
105
|
-
const RUNTIME_SESSION_PREFIX = 'sc-'
|
|
106
|
-
const BOOLEAN_FLAGS = new Set(['all', 'headless', 'headed', 'json'])
|
|
107
|
-
|
|
108
|
-
function getVersion() {
|
|
109
|
-
try {
|
|
110
|
-
const pkgPath = join(__dirname, '..', 'package.json')
|
|
111
|
-
return JSON.parse(readFileSync(pkgPath, 'utf-8')).version
|
|
112
|
-
} catch {
|
|
113
|
-
return 'unknown'
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function parseArgs(argv) {
|
|
118
|
-
const args = argv.slice(2)
|
|
119
|
-
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
120
|
-
printHelp()
|
|
121
|
-
process.exit(0)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (args[0] === '--version' || args[0] === '-v') {
|
|
125
|
-
console.log(getVersion())
|
|
126
|
-
process.exit(0)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const command = args[0]
|
|
130
|
-
const flags = {}
|
|
131
|
-
const positional = []
|
|
132
|
-
|
|
133
|
-
for (let i = 1; i < args.length; i++) {
|
|
134
|
-
const arg = args[i]
|
|
135
|
-
if (arg.startsWith('--')) {
|
|
136
|
-
const key = arg.slice(2)
|
|
137
|
-
const next = args[i + 1]
|
|
138
|
-
if (
|
|
139
|
-
BOOLEAN_FLAGS.has(key) &&
|
|
140
|
-
next !== undefined &&
|
|
141
|
-
next !== 'true' &&
|
|
142
|
-
next !== 'false'
|
|
143
|
-
) {
|
|
144
|
-
flags[key] = true
|
|
145
|
-
} else if (next !== undefined && !next.startsWith('--')) {
|
|
146
|
-
flags[key] = parseValue(next)
|
|
147
|
-
i++
|
|
148
|
-
} else {
|
|
149
|
-
flags[key] = true
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
positional.push(arg)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return { command, flags, positional }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function parseValue(str) {
|
|
160
|
-
if (str === 'true') return true
|
|
161
|
-
if (str === 'false') return false
|
|
162
|
-
const num = Number(str)
|
|
163
|
-
if (!Number.isNaN(num) && str.trim() !== '') return num
|
|
164
|
-
return str
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function sanitizeNamespace(value) {
|
|
168
|
-
const trimmed = String(value || '').trim()
|
|
169
|
-
if (!trimmed || trimmed === '.' || trimmed === '..') {
|
|
170
|
-
return 'default'
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const replaced = trimmed.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
174
|
-
const collapsed = replaced.replace(/_+/g, '_')
|
|
175
|
-
const bounded = collapsed.replace(/^_+|_+$/g, '')
|
|
176
|
-
|
|
177
|
-
return bounded || 'default'
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function isValidSessionId(value) {
|
|
181
|
-
return SESSION_ID_PATTERN.test(value)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function validateSessionId(rawValue, label) {
|
|
185
|
-
const value = String(rawValue ?? '').trim()
|
|
186
|
-
if (!value) {
|
|
187
|
-
throw new Error(`${label} cannot be empty.`)
|
|
188
|
-
}
|
|
189
|
-
if (!isValidSessionId(value)) {
|
|
190
|
-
throw new Error(
|
|
191
|
-
`${label} "${value}" is invalid. Use only letters, numbers, underscores, and hyphens.`
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
return value
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function resolveName(flags, session) {
|
|
198
|
-
if (flags.name !== undefined) {
|
|
199
|
-
if (flags.name === true) {
|
|
200
|
-
throw new Error('--name requires a namespace value.')
|
|
201
|
-
}
|
|
202
|
-
const raw = String(flags.name).trim()
|
|
203
|
-
if (raw.length > 0) {
|
|
204
|
-
return { name: sanitizeNamespace(raw), source: 'flag' }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
typeof process.env.OPENSTEER_NAME === 'string' &&
|
|
210
|
-
process.env.OPENSTEER_NAME.trim().length > 0
|
|
211
|
-
) {
|
|
212
|
-
return {
|
|
213
|
-
name: sanitizeNamespace(process.env.OPENSTEER_NAME),
|
|
214
|
-
source: 'env',
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return { name: sanitizeNamespace(session), source: 'session' }
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function hashKey(value) {
|
|
222
|
-
return createHash('sha256').update(value).digest('hex')
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function getClientBindingPath(clientKey) {
|
|
226
|
-
return join(
|
|
227
|
-
tmpdir(),
|
|
228
|
-
`${CLIENT_BINDING_PREFIX}${hashKey(clientKey).slice(0, 24)}${CLIENT_BINDING_SUFFIX}`
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function readClientBinding(clientKey) {
|
|
233
|
-
const bindingPath = getClientBindingPath(clientKey)
|
|
234
|
-
if (!existsSync(bindingPath)) {
|
|
235
|
-
return null
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const rawSession = readFileSync(bindingPath, 'utf-8').trim()
|
|
240
|
-
if (!rawSession) {
|
|
241
|
-
unlinkSync(bindingPath)
|
|
242
|
-
return null
|
|
243
|
-
}
|
|
244
|
-
if (!isValidSessionId(rawSession)) {
|
|
245
|
-
unlinkSync(bindingPath)
|
|
246
|
-
return null
|
|
247
|
-
}
|
|
248
|
-
return rawSession
|
|
249
|
-
} catch {
|
|
250
|
-
return null
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function writeClientBinding(clientKey, session) {
|
|
255
|
-
try {
|
|
256
|
-
writeFileSync(getClientBindingPath(clientKey), session)
|
|
257
|
-
} catch { /* best-effort */ }
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function createDefaultSessionId(prefix, clientKey) {
|
|
261
|
-
return `${prefix}-${hashKey(clientKey).slice(0, 12)}`
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function isInteractiveTerminal() {
|
|
265
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function resolveScopeDir() {
|
|
269
|
-
const cwd = process.cwd()
|
|
270
|
-
try {
|
|
271
|
-
return realpathSync(cwd)
|
|
272
|
-
} catch {
|
|
273
|
-
return cwd
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function buildRuntimeSession(scopeDir, logicalSession) {
|
|
278
|
-
return `${RUNTIME_SESSION_PREFIX}${hashKey(`${scopeDir}:${logicalSession}`).slice(0, 24)}`
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function resolveSession(flags, scopeDir) {
|
|
282
|
-
if (flags.session !== undefined) {
|
|
283
|
-
if (flags.session === true) {
|
|
284
|
-
throw new Error('--session requires a session id value.')
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
session: validateSessionId(flags.session, 'Session id'),
|
|
289
|
-
source: 'flag',
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
typeof process.env.OPENSTEER_SESSION === 'string' &&
|
|
295
|
-
process.env.OPENSTEER_SESSION.trim().length > 0
|
|
296
|
-
) {
|
|
297
|
-
return {
|
|
298
|
-
session: validateSessionId(
|
|
299
|
-
process.env.OPENSTEER_SESSION,
|
|
300
|
-
'OPENSTEER_SESSION'
|
|
301
|
-
),
|
|
302
|
-
source: 'env',
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (
|
|
307
|
-
typeof process.env.OPENSTEER_CLIENT_ID === 'string' &&
|
|
308
|
-
process.env.OPENSTEER_CLIENT_ID.trim().length > 0
|
|
309
|
-
) {
|
|
310
|
-
const clientId = process.env.OPENSTEER_CLIENT_ID.trim()
|
|
311
|
-
const clientKey = `client:${scopeDir}:${clientId}`
|
|
312
|
-
const bound = readClientBinding(clientKey)
|
|
313
|
-
if (bound) {
|
|
314
|
-
return { session: bound, source: 'client_binding' }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const created = createDefaultSessionId('client', clientKey)
|
|
318
|
-
writeClientBinding(clientKey, created)
|
|
319
|
-
return { session: created, source: 'client_binding' }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (isInteractiveTerminal()) {
|
|
323
|
-
const ttyKey = `tty:${scopeDir}:${process.ppid}`
|
|
324
|
-
const bound = readClientBinding(ttyKey)
|
|
325
|
-
if (bound) {
|
|
326
|
-
return { session: bound, source: 'tty_default' }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const created = createDefaultSessionId('tty', ttyKey)
|
|
330
|
-
writeClientBinding(ttyKey, created)
|
|
331
|
-
return { session: created, source: 'tty_default' }
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
throw new Error(
|
|
335
|
-
'No session resolved for this non-interactive command. Set OPENSTEER_SESSION or OPENSTEER_CLIENT_ID, or pass --session <id>.'
|
|
336
|
-
)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function getSocketPath(session) {
|
|
340
|
-
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${SOCKET_SUFFIX}`)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function getPidPath(session) {
|
|
344
|
-
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${PID_SUFFIX}`)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function getLockPath(session) {
|
|
348
|
-
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${LOCK_SUFFIX}`)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function getMetadataPath(session) {
|
|
352
|
-
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${METADATA_SUFFIX}`)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function buildRequest(command, flags, positional) {
|
|
356
|
-
const id = 1
|
|
357
|
-
const globalFlags = {}
|
|
358
|
-
for (const key of [
|
|
359
|
-
'headless',
|
|
360
|
-
'json',
|
|
361
|
-
'browser',
|
|
362
|
-
'profile',
|
|
363
|
-
'cdp-url',
|
|
364
|
-
'user-data-dir',
|
|
365
|
-
'browser-path',
|
|
366
|
-
'cloud-profile-id',
|
|
367
|
-
'cloud-profile-reuse-if-active',
|
|
368
|
-
'cursor',
|
|
369
|
-
]) {
|
|
370
|
-
if (key in flags) {
|
|
371
|
-
globalFlags[key] = flags[key]
|
|
372
|
-
delete flags[key]
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const args = { ...globalFlags, ...flags }
|
|
377
|
-
if ('headed' in flags) {
|
|
378
|
-
const headed = flags.headed === false ? false : Boolean(flags.headed)
|
|
379
|
-
args.headless = args.headless ?? !headed
|
|
380
|
-
delete args.headed
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
switch (command) {
|
|
384
|
-
case 'open':
|
|
385
|
-
case 'navigate':
|
|
386
|
-
args.url = positional[0] || args.url
|
|
387
|
-
break
|
|
388
|
-
|
|
389
|
-
case 'click':
|
|
390
|
-
case 'dblclick':
|
|
391
|
-
case 'rightclick':
|
|
392
|
-
case 'hover':
|
|
393
|
-
case 'select':
|
|
394
|
-
case 'scroll':
|
|
395
|
-
case 'get-text':
|
|
396
|
-
case 'get-value':
|
|
397
|
-
case 'get-attrs':
|
|
398
|
-
if (positional[0] !== undefined && args.element === undefined) {
|
|
399
|
-
args.element = Number(positional[0])
|
|
400
|
-
}
|
|
401
|
-
break
|
|
402
|
-
|
|
403
|
-
case 'input': {
|
|
404
|
-
// input 12 "text" or input "text" --element 12
|
|
405
|
-
if (positional.length >= 2) {
|
|
406
|
-
const first = Number(positional[0])
|
|
407
|
-
if (!Number.isNaN(first)) {
|
|
408
|
-
args.element = args.element ?? first
|
|
409
|
-
args.text = args.text ?? positional[1]
|
|
410
|
-
} else {
|
|
411
|
-
args.text = args.text ?? positional[0]
|
|
412
|
-
args.element = args.element ?? Number(positional[1])
|
|
413
|
-
}
|
|
414
|
-
} else if (positional.length === 1) {
|
|
415
|
-
args.text = args.text ?? positional[0]
|
|
416
|
-
}
|
|
417
|
-
break
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
case 'press':
|
|
421
|
-
args.key = positional[0] || args.key
|
|
422
|
-
break
|
|
423
|
-
|
|
424
|
-
case 'type':
|
|
425
|
-
args.text = positional[0] || args.text
|
|
426
|
-
break
|
|
427
|
-
|
|
428
|
-
case 'get-html':
|
|
429
|
-
if (positional[0] && !args.selector) {
|
|
430
|
-
args.selector = positional[0]
|
|
431
|
-
}
|
|
432
|
-
break
|
|
433
|
-
|
|
434
|
-
case 'tab-new':
|
|
435
|
-
args.url = positional[0] || args.url
|
|
436
|
-
break
|
|
437
|
-
|
|
438
|
-
case 'tab-switch':
|
|
439
|
-
case 'tab-close':
|
|
440
|
-
args.index =
|
|
441
|
-
positional[0] !== undefined ? Number(positional[0]) : args.index
|
|
442
|
-
break
|
|
443
|
-
|
|
444
|
-
case 'cookies-export':
|
|
445
|
-
case 'cookies-import':
|
|
446
|
-
case 'screenshot':
|
|
447
|
-
args.file = positional[0] || args.file
|
|
448
|
-
break
|
|
449
|
-
|
|
450
|
-
case 'eval':
|
|
451
|
-
args.expression = positional[0] || args.expression
|
|
452
|
-
break
|
|
453
|
-
|
|
454
|
-
case 'wait-for':
|
|
455
|
-
args.text = positional[0] || args.text
|
|
456
|
-
break
|
|
457
|
-
|
|
458
|
-
case 'wait-selector':
|
|
459
|
-
args.selector = positional[0] || args.selector
|
|
460
|
-
break
|
|
461
|
-
|
|
462
|
-
case 'extract':
|
|
463
|
-
if (positional[0] && !args.schema) {
|
|
464
|
-
try {
|
|
465
|
-
args.schema = JSON.parse(positional[0])
|
|
466
|
-
} catch {
|
|
467
|
-
error(`Invalid JSON schema: ${positional[0]}`)
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
break
|
|
471
|
-
|
|
472
|
-
case 'snapshot':
|
|
473
|
-
args.mode = positional[0] || args.mode
|
|
474
|
-
break
|
|
475
|
-
|
|
476
|
-
case 'cursor':
|
|
477
|
-
args.mode = positional[0] || args.mode || 'status'
|
|
478
|
-
break
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
return { id, command, args }
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function readPid(pidPath) {
|
|
485
|
-
if (!existsSync(pidPath)) {
|
|
486
|
-
return null
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const parsed = Number.parseInt(readFileSync(pidPath, 'utf-8').trim(), 10)
|
|
490
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
491
|
-
return null
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return parsed
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function isPidAlive(pid) {
|
|
498
|
-
try {
|
|
499
|
-
process.kill(pid, 0)
|
|
500
|
-
return true
|
|
501
|
-
} catch {
|
|
502
|
-
return false
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function cleanStaleFiles(session, options = {}) {
|
|
507
|
-
const removeSocket = options.removeSocket !== false
|
|
508
|
-
const removePid = options.removePid !== false
|
|
509
|
-
|
|
510
|
-
if (removeSocket) {
|
|
511
|
-
try {
|
|
512
|
-
unlinkSync(getSocketPath(session))
|
|
513
|
-
} catch { }
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (removePid) {
|
|
517
|
-
try {
|
|
518
|
-
unlinkSync(getPidPath(session))
|
|
519
|
-
} catch { }
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
unlinkSync(getMetadataPath(session))
|
|
524
|
-
} catch { }
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function startServer(runtimeSession, logicalSession, scopeDir) {
|
|
528
|
-
const child = spawn('node', [SERVER_SCRIPT], {
|
|
529
|
-
detached: true,
|
|
530
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
531
|
-
env: {
|
|
532
|
-
...process.env,
|
|
533
|
-
OPENSTEER_SESSION: runtimeSession,
|
|
534
|
-
OPENSTEER_LOGICAL_SESSION: logicalSession,
|
|
535
|
-
OPENSTEER_SCOPE_DIR: scopeDir,
|
|
536
|
-
},
|
|
537
|
-
})
|
|
538
|
-
child.unref()
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function readMetadata(session) {
|
|
542
|
-
const metadataPath = getMetadataPath(session)
|
|
543
|
-
if (!existsSync(metadataPath)) {
|
|
544
|
-
return null
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
try {
|
|
548
|
-
const raw = JSON.parse(readFileSync(metadataPath, 'utf-8'))
|
|
549
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
550
|
-
return null
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
typeof raw.logicalSession !== 'string' ||
|
|
555
|
-
!raw.logicalSession.trim() ||
|
|
556
|
-
typeof raw.scopeDir !== 'string' ||
|
|
557
|
-
!raw.scopeDir.trim() ||
|
|
558
|
-
typeof raw.runtimeSession !== 'string' ||
|
|
559
|
-
!raw.runtimeSession.trim()
|
|
560
|
-
) {
|
|
561
|
-
return null
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return {
|
|
565
|
-
logicalSession: raw.logicalSession.trim(),
|
|
566
|
-
scopeDir: raw.scopeDir,
|
|
567
|
-
runtimeSession: raw.runtimeSession.trim(),
|
|
568
|
-
createdAt:
|
|
569
|
-
typeof raw.createdAt === 'number' ? raw.createdAt : undefined,
|
|
570
|
-
updatedAt:
|
|
571
|
-
typeof raw.updatedAt === 'number' ? raw.updatedAt : undefined,
|
|
572
|
-
}
|
|
573
|
-
} catch {
|
|
574
|
-
return null
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function writeMetadata(runtimeSession, logicalSession, scopeDir) {
|
|
579
|
-
const metadataPath = getMetadataPath(runtimeSession)
|
|
580
|
-
const existing = readMetadata(runtimeSession)
|
|
581
|
-
const now = Date.now()
|
|
582
|
-
const payload = {
|
|
583
|
-
runtimeSession,
|
|
584
|
-
logicalSession,
|
|
585
|
-
scopeDir,
|
|
586
|
-
createdAt: existing?.createdAt ?? now,
|
|
587
|
-
updatedAt: now,
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
try {
|
|
591
|
-
writeFileSync(metadataPath, JSON.stringify(payload, null, 2))
|
|
592
|
-
} catch { }
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function sendCommand(socketPath, request, timeoutMs = RESPONSE_TIMEOUT) {
|
|
596
|
-
return new Promise((resolve, reject) => {
|
|
597
|
-
const socket = connect(socketPath)
|
|
598
|
-
let buffer = ''
|
|
599
|
-
let settled = false
|
|
600
|
-
|
|
601
|
-
const timer = setTimeout(() => {
|
|
602
|
-
if (!settled) {
|
|
603
|
-
settled = true
|
|
604
|
-
socket.destroy()
|
|
605
|
-
reject(new Error('Response timeout'))
|
|
606
|
-
}
|
|
607
|
-
}, timeoutMs)
|
|
608
|
-
|
|
609
|
-
socket.on('connect', () => {
|
|
610
|
-
socket.write(JSON.stringify(request) + '\n')
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
socket.on('data', (chunk) => {
|
|
614
|
-
buffer += chunk.toString()
|
|
615
|
-
const idx = buffer.indexOf('\n')
|
|
616
|
-
if (idx !== -1) {
|
|
617
|
-
const line = buffer.slice(0, idx)
|
|
618
|
-
clearTimeout(timer)
|
|
619
|
-
settled = true
|
|
620
|
-
socket.end()
|
|
621
|
-
try {
|
|
622
|
-
resolve(JSON.parse(line))
|
|
623
|
-
} catch {
|
|
624
|
-
reject(new Error('Invalid JSON response from server'))
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
socket.on('error', (err) => {
|
|
630
|
-
if (!settled) {
|
|
631
|
-
clearTimeout(timer)
|
|
632
|
-
settled = true
|
|
633
|
-
reject(err)
|
|
634
|
-
}
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
socket.on('close', () => {
|
|
638
|
-
if (!settled) {
|
|
639
|
-
clearTimeout(timer)
|
|
640
|
-
settled = true
|
|
641
|
-
reject(new Error('Connection closed before response'))
|
|
642
|
-
}
|
|
643
|
-
})
|
|
644
|
-
})
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
async function pingServer(session) {
|
|
648
|
-
const socketPath = getSocketPath(session)
|
|
649
|
-
if (!existsSync(socketPath)) return false
|
|
650
|
-
|
|
651
|
-
try {
|
|
652
|
-
const response = await sendCommand(socketPath, PING_REQUEST, HEALTH_TIMEOUT)
|
|
653
|
-
return Boolean(response?.ok && response?.result?.pong)
|
|
654
|
-
} catch {
|
|
655
|
-
return false
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
async function isServerHealthy(session) {
|
|
660
|
-
const pid = readPid(getPidPath(session))
|
|
661
|
-
const pidAlive = pid ? isPidAlive(pid) : false
|
|
662
|
-
const socketExists = existsSync(getSocketPath(session))
|
|
663
|
-
|
|
664
|
-
if (pid && !pidAlive) {
|
|
665
|
-
cleanStaleFiles(session)
|
|
666
|
-
return false
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (!socketExists) {
|
|
670
|
-
return false
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const healthy = await pingServer(session)
|
|
674
|
-
if (healthy) {
|
|
675
|
-
return true
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (!pid) {
|
|
679
|
-
cleanStaleFiles(session, { removeSocket: true, removePid: false })
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return false
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function acquireStartLock(session) {
|
|
686
|
-
const lockPath = getLockPath(session)
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
const fd = openSync(lockPath, 'wx')
|
|
690
|
-
writeFileSync(
|
|
691
|
-
fd,
|
|
692
|
-
JSON.stringify({
|
|
693
|
-
pid: process.pid,
|
|
694
|
-
createdAt: Date.now(),
|
|
695
|
-
})
|
|
696
|
-
)
|
|
697
|
-
closeSync(fd)
|
|
698
|
-
return true
|
|
699
|
-
} catch {
|
|
700
|
-
return false
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function releaseStartLock(session) {
|
|
705
|
-
try {
|
|
706
|
-
unlinkSync(getLockPath(session))
|
|
707
|
-
} catch { /* best-effort */ }
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function recoverStaleStartLock(session) {
|
|
711
|
-
const lockPath = getLockPath(session)
|
|
712
|
-
if (!existsSync(lockPath)) {
|
|
713
|
-
return false
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
try {
|
|
717
|
-
const raw = readFileSync(lockPath, 'utf-8')
|
|
718
|
-
const parsed = JSON.parse(raw)
|
|
719
|
-
const pid =
|
|
720
|
-
parsed && Number.isInteger(parsed.pid) && parsed.pid > 0
|
|
721
|
-
? parsed.pid
|
|
722
|
-
: null
|
|
723
|
-
|
|
724
|
-
if (!pid || !isPidAlive(pid)) {
|
|
725
|
-
unlinkSync(lockPath)
|
|
726
|
-
return true
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
return false
|
|
730
|
-
} catch {
|
|
731
|
-
try {
|
|
732
|
-
unlinkSync(lockPath)
|
|
733
|
-
return true
|
|
734
|
-
} catch {
|
|
735
|
-
return false
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
async function sleep(ms) {
|
|
741
|
-
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
async function waitForServerReady(session, timeout) {
|
|
745
|
-
const start = Date.now()
|
|
746
|
-
|
|
747
|
-
while (Date.now() - start <= timeout) {
|
|
748
|
-
if (await isServerHealthy(session)) {
|
|
749
|
-
return
|
|
750
|
-
}
|
|
751
|
-
await sleep(POLL_INTERVAL)
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
throw new Error(`Timed out waiting for server '${session}' to become healthy.`)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
async function ensureServer(context) {
|
|
758
|
-
const runtimeSession = context.runtimeSession
|
|
759
|
-
if (await isServerHealthy(runtimeSession)) {
|
|
760
|
-
writeMetadata(
|
|
761
|
-
runtimeSession,
|
|
762
|
-
context.logicalSession,
|
|
763
|
-
context.scopeDir
|
|
764
|
-
)
|
|
765
|
-
return
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (!existsSync(SERVER_SCRIPT)) {
|
|
769
|
-
throw new Error(
|
|
770
|
-
`Server script not found: ${SERVER_SCRIPT}. Run the build script first.`
|
|
771
|
-
)
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const deadline = Date.now() + CONNECT_TIMEOUT
|
|
775
|
-
|
|
776
|
-
while (Date.now() < deadline) {
|
|
777
|
-
if (await isServerHealthy(runtimeSession)) {
|
|
778
|
-
writeMetadata(
|
|
779
|
-
runtimeSession,
|
|
780
|
-
context.logicalSession,
|
|
781
|
-
context.scopeDir
|
|
782
|
-
)
|
|
783
|
-
return
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const existingPid = readPid(getPidPath(runtimeSession))
|
|
787
|
-
if (existingPid && isPidAlive(existingPid)) {
|
|
788
|
-
await sleep(POLL_INTERVAL)
|
|
789
|
-
continue
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
recoverStaleStartLock(runtimeSession)
|
|
793
|
-
|
|
794
|
-
if (acquireStartLock(runtimeSession)) {
|
|
795
|
-
try {
|
|
796
|
-
if (!(await isServerHealthy(runtimeSession))) {
|
|
797
|
-
startServer(
|
|
798
|
-
runtimeSession,
|
|
799
|
-
context.logicalSession,
|
|
800
|
-
context.scopeDir
|
|
801
|
-
)
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
await waitForServerReady(
|
|
805
|
-
runtimeSession,
|
|
806
|
-
Math.max(500, deadline - Date.now())
|
|
807
|
-
)
|
|
808
|
-
writeMetadata(
|
|
809
|
-
runtimeSession,
|
|
810
|
-
context.logicalSession,
|
|
811
|
-
context.scopeDir
|
|
812
|
-
)
|
|
813
|
-
return
|
|
814
|
-
} finally {
|
|
815
|
-
releaseStartLock(runtimeSession)
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
await sleep(POLL_INTERVAL)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
throw new Error(
|
|
823
|
-
`Failed to start server for session '${context.logicalSession}' in cwd scope '${context.scopeDir}' within ${CONNECT_TIMEOUT}ms.`
|
|
824
|
-
)
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function listSessions() {
|
|
828
|
-
const sessions = []
|
|
829
|
-
const entries = readdirSync(tmpdir())
|
|
830
|
-
|
|
831
|
-
for (const entry of entries) {
|
|
832
|
-
if (!entry.startsWith(RUNTIME_PREFIX) || !entry.endsWith(PID_SUFFIX)) {
|
|
833
|
-
continue
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const runtimeSession = entry.slice(
|
|
837
|
-
RUNTIME_PREFIX.length,
|
|
838
|
-
entry.length - PID_SUFFIX.length
|
|
839
|
-
)
|
|
840
|
-
if (!runtimeSession) {
|
|
841
|
-
continue
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
const pid = readPid(join(tmpdir(), entry))
|
|
845
|
-
if (!pid || !isPidAlive(pid)) {
|
|
846
|
-
cleanStaleFiles(runtimeSession)
|
|
847
|
-
continue
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const metadata = readMetadata(runtimeSession)
|
|
851
|
-
sessions.push({
|
|
852
|
-
name: metadata?.logicalSession || runtimeSession,
|
|
853
|
-
logicalSession: metadata?.logicalSession || runtimeSession,
|
|
854
|
-
runtimeSession,
|
|
855
|
-
scopeDir: metadata?.scopeDir || null,
|
|
856
|
-
pid,
|
|
857
|
-
})
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
sessions.sort((a, b) => {
|
|
861
|
-
const scopeA = a.scopeDir || ''
|
|
862
|
-
const scopeB = b.scopeDir || ''
|
|
863
|
-
if (scopeA !== scopeB) {
|
|
864
|
-
return scopeA.localeCompare(scopeB)
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return a.logicalSession.localeCompare(b.logicalSession)
|
|
868
|
-
})
|
|
869
|
-
return sessions
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
async function closeAllSessions() {
|
|
873
|
-
const sessions = listSessions()
|
|
874
|
-
const closed = []
|
|
875
|
-
const failures = []
|
|
876
|
-
|
|
877
|
-
for (const session of sessions) {
|
|
878
|
-
const socketPath = getSocketPath(session.runtimeSession)
|
|
879
|
-
if (!existsSync(socketPath)) {
|
|
880
|
-
cleanStaleFiles(session.runtimeSession)
|
|
881
|
-
continue
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
const response = await sendCommand(socketPath, CLOSE_ALL_REQUEST)
|
|
886
|
-
if (response && response.ok === true) {
|
|
887
|
-
closed.push(session)
|
|
888
|
-
} else {
|
|
889
|
-
failures.push(
|
|
890
|
-
`${session.logicalSession} (${session.scopeDir || 'unknown scope'}): ${response?.error || 'unknown close error'}`
|
|
891
|
-
)
|
|
892
|
-
}
|
|
893
|
-
} catch (err) {
|
|
894
|
-
failures.push(
|
|
895
|
-
`${session.logicalSession} (${session.scopeDir || 'unknown scope'}): ${err instanceof Error ? err.message : String(err)}`
|
|
896
|
-
)
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (failures.length > 0) {
|
|
901
|
-
throw new Error(`Failed to close sessions: ${failures.join('; ')}`)
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
return closed
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function output(data) {
|
|
908
|
-
process.stdout.write(JSON.stringify(data) + '\n')
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
function error(msg) {
|
|
912
|
-
process.stderr.write(JSON.stringify({ ok: false, error: msg }) + '\n')
|
|
913
|
-
process.exit(1)
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function normalizeFailedResponse(response) {
|
|
917
|
-
const info = toObject(response?.errorInfo)
|
|
918
|
-
|
|
919
|
-
let message = 'Request failed.'
|
|
920
|
-
if (typeof info?.message === 'string' && info.message.trim()) {
|
|
921
|
-
message = info.message.trim()
|
|
922
|
-
} else if (typeof response?.error === 'string' && response.error.trim()) {
|
|
923
|
-
message = response.error.trim()
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
return {
|
|
927
|
-
ok: false,
|
|
928
|
-
error: message,
|
|
929
|
-
...(info && typeof info.code === 'string' && info.code.trim()
|
|
930
|
-
? { code: info.code.trim() }
|
|
931
|
-
: {}),
|
|
932
|
-
...(toObject(info?.details)
|
|
933
|
-
? { details: info.details }
|
|
934
|
-
: {}),
|
|
935
|
-
...(info ? { errorInfo: info } : {}),
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function formatTransportFailure(error, context) {
|
|
940
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
941
|
-
return `${context}: ${message}`
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function toObject(value) {
|
|
945
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
946
|
-
return value
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function isSkillsHelpRequest(args) {
|
|
950
|
-
if (args.length === 0) return true
|
|
951
|
-
|
|
952
|
-
const [subcommand, ...rest] = args
|
|
953
|
-
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
954
|
-
return true
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
if (subcommand !== 'install' && subcommand !== 'add') {
|
|
958
|
-
return false
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
return rest.includes('--help') || rest.includes('-h')
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function printSkillsHelp() {
|
|
965
|
-
process.stdout.write(SKILLS_HELP_TEXT)
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
async function runSkillsSubcommand(args) {
|
|
969
|
-
if (isSkillsHelpRequest(args)) {
|
|
970
|
-
printSkillsHelp()
|
|
971
|
-
return
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (!existsSync(SKILLS_INSTALLER_SCRIPT)) {
|
|
975
|
-
throw new Error(
|
|
976
|
-
`Skills installer module not found: ${SKILLS_INSTALLER_SCRIPT}. Run the build script first.`
|
|
977
|
-
)
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const moduleUrl = pathToFileURL(SKILLS_INSTALLER_SCRIPT).href
|
|
981
|
-
const { runOpensteerSkillsInstaller } = await import(moduleUrl)
|
|
982
|
-
|
|
983
|
-
const exitCode = await runOpensteerSkillsInstaller(args)
|
|
984
|
-
if (exitCode !== 0) {
|
|
985
|
-
process.exit(exitCode)
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
async function runProfileSubcommand(args) {
|
|
990
|
-
if (isProfileHelpRequest(args)) {
|
|
991
|
-
process.stdout.write(PROFILE_HELP_TEXT)
|
|
992
|
-
return
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
if (!existsSync(PROFILE_CLI_SCRIPT)) {
|
|
996
|
-
throw new Error(
|
|
997
|
-
`Profile CLI module was not found at "${PROFILE_CLI_SCRIPT}". Run "npm run build" to generate dist artifacts.`
|
|
998
|
-
)
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
const moduleUrl = pathToFileURL(PROFILE_CLI_SCRIPT).href
|
|
1002
|
-
const { runOpensteerProfileCli } = await import(moduleUrl)
|
|
1003
|
-
const exitCode = await runOpensteerProfileCli(args)
|
|
1004
|
-
if (exitCode !== 0) {
|
|
1005
|
-
process.exit(exitCode)
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
async function runLocalProfileSubcommand(args) {
|
|
1010
|
-
if (
|
|
1011
|
-
args.length === 0 ||
|
|
1012
|
-
args.includes('--help') ||
|
|
1013
|
-
args.includes('-h')
|
|
1014
|
-
) {
|
|
1015
|
-
process.stdout.write(LOCAL_PROFILE_HELP_TEXT)
|
|
1016
|
-
return
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (!existsSync(LOCAL_PROFILE_CLI_SCRIPT)) {
|
|
1020
|
-
throw new Error(
|
|
1021
|
-
`Local profile CLI module was not found at "${LOCAL_PROFILE_CLI_SCRIPT}". Run "npm run build" to generate dist artifacts.`
|
|
1022
|
-
)
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const moduleUrl = pathToFileURL(LOCAL_PROFILE_CLI_SCRIPT).href
|
|
1026
|
-
const { runOpensteerLocalProfileCli } = await import(moduleUrl)
|
|
1027
|
-
const exitCode = await runOpensteerLocalProfileCli(args)
|
|
1028
|
-
if (exitCode !== 0) {
|
|
1029
|
-
process.exit(exitCode)
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
async function runAuthSubcommand(args) {
|
|
1034
|
-
if (isAuthHelpRequest(args)) {
|
|
1035
|
-
process.stdout.write(AUTH_HELP_TEXT)
|
|
1036
|
-
return
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (!existsSync(AUTH_CLI_SCRIPT)) {
|
|
1040
|
-
throw new Error(
|
|
1041
|
-
`Auth CLI module was not found at "${AUTH_CLI_SCRIPT}". Run "npm run build" to generate dist artifacts.`
|
|
1042
|
-
)
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
const moduleUrl = pathToFileURL(AUTH_CLI_SCRIPT).href
|
|
1046
|
-
const { runOpensteerAuthCli } = await import(moduleUrl)
|
|
1047
|
-
const exitCode = await runOpensteerAuthCli(args)
|
|
1048
|
-
if (exitCode !== 0) {
|
|
1049
|
-
process.exit(exitCode)
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
function isAuthHelpRequest(args) {
|
|
1054
|
-
if (args.length === 0) return true
|
|
1055
|
-
const [subcommand, ...rest] = args
|
|
1056
|
-
if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
|
|
1057
|
-
return true
|
|
1058
|
-
}
|
|
1059
|
-
return rest.includes('--help') || rest.includes('-h')
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
async function ensureOpenCloudCredentials(flags, scopeDir) {
|
|
1063
|
-
if (!existsSync(AUTH_CLI_SCRIPT)) {
|
|
1064
|
-
throw new Error(
|
|
1065
|
-
`Auth CLI module was not found at "${AUTH_CLI_SCRIPT}". Run "npm run build" to generate dist artifacts.`
|
|
1066
|
-
)
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
const apiKeyFlag =
|
|
1070
|
-
typeof flags['api-key'] === 'string' ? flags['api-key'] : undefined
|
|
1071
|
-
const accessTokenFlag =
|
|
1072
|
-
typeof flags['access-token'] === 'string'
|
|
1073
|
-
? flags['access-token']
|
|
1074
|
-
: undefined
|
|
1075
|
-
|
|
1076
|
-
const moduleUrl = pathToFileURL(AUTH_CLI_SCRIPT).href
|
|
1077
|
-
const { ensureCloudCredentialsForOpenCommand } = await import(moduleUrl)
|
|
1078
|
-
return await ensureCloudCredentialsForOpenCommand({
|
|
1079
|
-
scopeDir,
|
|
1080
|
-
env: process.env,
|
|
1081
|
-
apiKeyFlag,
|
|
1082
|
-
accessTokenFlag,
|
|
1083
|
-
interactive: isInteractiveTerminal(),
|
|
1084
|
-
writeProgress: (message) => process.stderr.write(message),
|
|
1085
|
-
writeStderr: (message) => process.stderr.write(message),
|
|
1086
|
-
})
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function isProfileHelpRequest(args) {
|
|
1090
|
-
if (args.length === 0) return true
|
|
1091
|
-
const [subcommand, ...rest] = args
|
|
1092
|
-
if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
|
|
1093
|
-
return true
|
|
1094
|
-
}
|
|
1095
|
-
return rest.includes('--help') || rest.includes('-h')
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function buildOpenCloudAuthPayload(auth) {
|
|
1099
|
-
if (!auth) {
|
|
1100
|
-
return null
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
return {
|
|
1104
|
-
...(auth.kind === 'access-token'
|
|
1105
|
-
? { accessToken: auth.token }
|
|
1106
|
-
: { apiKey: auth.token }),
|
|
1107
|
-
baseUrl: auth.baseUrl,
|
|
1108
|
-
authScheme: auth.authScheme,
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function printHelp() {
|
|
1113
|
-
console.log(`Usage: opensteer <command> [options]
|
|
1114
|
-
|
|
1115
|
-
Navigation:
|
|
1116
|
-
open <url> Open browser and navigate to URL
|
|
1117
|
-
navigate <url> Navigate and wait for visual stability
|
|
1118
|
-
back Go back
|
|
1119
|
-
forward Go forward
|
|
1120
|
-
reload Reload page
|
|
1121
|
-
close Close browser and server
|
|
1122
|
-
close --all Close all active session-scoped servers
|
|
1123
|
-
|
|
1124
|
-
Sessions:
|
|
1125
|
-
sessions List active session-scoped daemons
|
|
1126
|
-
status Show resolved logical/runtime session and session state
|
|
1127
|
-
|
|
1128
|
-
Observation:
|
|
1129
|
-
snapshot [--mode action] Get page snapshot
|
|
1130
|
-
state Get page URL, title, and snapshot
|
|
1131
|
-
cursor [on|off|status] Configure/query cursor preview mode
|
|
1132
|
-
screenshot [file] Take screenshot
|
|
1133
|
-
|
|
1134
|
-
Actions:
|
|
1135
|
-
click [element] Click element
|
|
1136
|
-
dblclick [element] Double-click element
|
|
1137
|
-
rightclick [element] Right-click element
|
|
1138
|
-
hover [element] Hover over element
|
|
1139
|
-
input [element] <text> Input text into element
|
|
1140
|
-
select [element] Select option from dropdown
|
|
1141
|
-
scroll [element] Scroll page or element
|
|
1142
|
-
|
|
1143
|
-
Keyboard:
|
|
1144
|
-
press <key> Press key
|
|
1145
|
-
type <text> Type text into focused element
|
|
1146
|
-
|
|
1147
|
-
Element Info:
|
|
1148
|
-
get-text [element] Get element text
|
|
1149
|
-
get-value [element] Get element value
|
|
1150
|
-
get-attrs [element] Get element attributes
|
|
1151
|
-
get-html [selector] Get page or element HTML
|
|
1152
|
-
|
|
1153
|
-
Tabs:
|
|
1154
|
-
tabs List tabs
|
|
1155
|
-
tab-new [url] Open new tab
|
|
1156
|
-
tab-switch <index> Switch to tab
|
|
1157
|
-
tab-close [index] Close tab
|
|
1158
|
-
|
|
1159
|
-
Cookies:
|
|
1160
|
-
cookies [--url] Get cookies
|
|
1161
|
-
cookie-set Set cookie (--name, --value, ...)
|
|
1162
|
-
cookies-clear Clear all cookies
|
|
1163
|
-
cookies-export <file> Export cookies to file
|
|
1164
|
-
cookies-import <file> Import cookies from file
|
|
1165
|
-
|
|
1166
|
-
Utility:
|
|
1167
|
-
eval <expression> Evaluate JavaScript
|
|
1168
|
-
wait-for <text> Wait for text to appear
|
|
1169
|
-
wait-selector <selector> Wait for selector
|
|
1170
|
-
extract <schema-json> Extract structured data
|
|
1171
|
-
|
|
1172
|
-
Skills:
|
|
1173
|
-
skills install [options] Install Opensteer skill pack for supported agents
|
|
1174
|
-
skills add [options] Alias for "skills install"
|
|
1175
|
-
skills --help Show skills installer help
|
|
1176
|
-
profile <command> Manage cloud browser profiles and cookie sync
|
|
1177
|
-
local-profile <command> Inspect local Chrome profiles for real-browser mode
|
|
1178
|
-
auth <command> Manage cloud login credentials (login/status/logout)
|
|
1179
|
-
login Alias for "auth login"
|
|
1180
|
-
logout Alias for "auth logout"
|
|
1181
|
-
|
|
1182
|
-
Global Flags:
|
|
1183
|
-
--session <id> Logical session id (scoped by canonical cwd)
|
|
1184
|
-
--name <namespace> Selector namespace for cache storage on 'open'
|
|
1185
|
-
--headless Launch chromium mode in headless mode
|
|
1186
|
-
--browser <mode> Browser mode: chromium or real
|
|
1187
|
-
--profile <name> Browser profile directory name for real-browser mode
|
|
1188
|
-
--headed Launch real-browser mode with a visible window
|
|
1189
|
-
--cdp-url <url> Connect to a running browser (e.g. http://localhost:9222)
|
|
1190
|
-
--user-data-dir <path> Browser user-data root for real-browser mode
|
|
1191
|
-
--browser-path <path> Override Chrome executable path for real-browser mode
|
|
1192
|
-
--cloud-profile-id <id> Launch cloud session with a specific browser profile
|
|
1193
|
-
--cloud-profile-reuse-if-active <true|false>
|
|
1194
|
-
Reuse active cloud session for that browser profile
|
|
1195
|
-
--cursor <true|false> Enable/disable cursor preview for the session
|
|
1196
|
-
--element <N> Target element by counter
|
|
1197
|
-
--selector <css> Target element by CSS selector
|
|
1198
|
-
--description <text> Description for selector persistence
|
|
1199
|
-
--help Show this help
|
|
1200
|
-
--version, -v Show version
|
|
1201
|
-
|
|
1202
|
-
Environment:
|
|
1203
|
-
OPENSTEER_SESSION Logical session id (equivalent to --session)
|
|
1204
|
-
OPENSTEER_CLIENT_ID Stable client identity for default session binding
|
|
1205
|
-
OPENSTEER_NAME Default selector namespace for 'open' when --name is omitted
|
|
1206
|
-
OPENSTEER_CURSOR Default cursor enablement (SDK + CLI session bootstrap)
|
|
1207
|
-
OPENSTEER_MODE Runtime routing: "local" (default) or "cloud"
|
|
1208
|
-
OPENSTEER_API_KEY Cloud API key credential
|
|
1209
|
-
OPENSTEER_ACCESS_TOKEN Cloud bearer access token credential
|
|
1210
|
-
OPENSTEER_BASE_URL Override cloud control-plane base URL
|
|
1211
|
-
OPENSTEER_AUTH_SCHEME Cloud auth scheme: api-key (default) or bearer
|
|
1212
|
-
OPENSTEER_REMOTE_ANNOUNCE Cloud session announcement policy: always (default), off, tty
|
|
1213
|
-
OPENSTEER_BROWSER Local browser mode: chromium or real
|
|
1214
|
-
OPENSTEER_CDP_URL Connect to a running browser (e.g. http://localhost:9222)
|
|
1215
|
-
OPENSTEER_USER_DATA_DIR Browser user-data root for real-browser mode
|
|
1216
|
-
OPENSTEER_PROFILE_DIRECTORY Browser profile directory for real-browser mode
|
|
1217
|
-
`)
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
async function main() {
|
|
1221
|
-
const rawArgs = process.argv.slice(2)
|
|
1222
|
-
if (rawArgs[0] === 'skills') {
|
|
1223
|
-
try {
|
|
1224
|
-
await runSkillsSubcommand(rawArgs.slice(1))
|
|
1225
|
-
} catch (err) {
|
|
1226
|
-
const message =
|
|
1227
|
-
err instanceof Error ? err.message : 'Failed to run skills command'
|
|
1228
|
-
process.stderr.write(`${message}\n`)
|
|
1229
|
-
process.exit(1)
|
|
1230
|
-
}
|
|
1231
|
-
return
|
|
1232
|
-
}
|
|
1233
|
-
if (rawArgs[0] === 'auth') {
|
|
1234
|
-
try {
|
|
1235
|
-
await runAuthSubcommand(rawArgs.slice(1))
|
|
1236
|
-
} catch (err) {
|
|
1237
|
-
const message =
|
|
1238
|
-
err instanceof Error ? err.message : 'Failed to run auth command'
|
|
1239
|
-
process.stderr.write(`${message}\n`)
|
|
1240
|
-
process.exit(1)
|
|
1241
|
-
}
|
|
1242
|
-
return
|
|
1243
|
-
}
|
|
1244
|
-
if (rawArgs[0] === 'login') {
|
|
1245
|
-
try {
|
|
1246
|
-
await runAuthSubcommand(['login', ...rawArgs.slice(1)])
|
|
1247
|
-
} catch (err) {
|
|
1248
|
-
const message =
|
|
1249
|
-
err instanceof Error ? err.message : 'Failed to run login command'
|
|
1250
|
-
process.stderr.write(`${message}\n`)
|
|
1251
|
-
process.exit(1)
|
|
1252
|
-
}
|
|
1253
|
-
return
|
|
1254
|
-
}
|
|
1255
|
-
if (rawArgs[0] === 'logout') {
|
|
1256
|
-
try {
|
|
1257
|
-
await runAuthSubcommand(['logout', ...rawArgs.slice(1)])
|
|
1258
|
-
} catch (err) {
|
|
1259
|
-
const message =
|
|
1260
|
-
err instanceof Error ? err.message : 'Failed to run logout command'
|
|
1261
|
-
process.stderr.write(`${message}\n`)
|
|
1262
|
-
process.exit(1)
|
|
1263
|
-
}
|
|
1264
|
-
return
|
|
1265
|
-
}
|
|
1266
|
-
if (rawArgs[0] === 'profile') {
|
|
1267
|
-
try {
|
|
1268
|
-
await runProfileSubcommand(rawArgs.slice(1))
|
|
1269
|
-
} catch (err) {
|
|
1270
|
-
const message =
|
|
1271
|
-
err instanceof Error ? err.message : 'Failed to run profile command'
|
|
1272
|
-
process.stderr.write(`${message}\n`)
|
|
1273
|
-
process.exit(1)
|
|
1274
|
-
}
|
|
1275
|
-
return
|
|
1276
|
-
}
|
|
1277
|
-
if (rawArgs[0] === 'local-profile') {
|
|
1278
|
-
try {
|
|
1279
|
-
await runLocalProfileSubcommand(rawArgs.slice(1))
|
|
1280
|
-
} catch (err) {
|
|
1281
|
-
const message =
|
|
1282
|
-
err instanceof Error
|
|
1283
|
-
? err.message
|
|
1284
|
-
: 'Failed to run local-profile command'
|
|
1285
|
-
process.stderr.write(`${message}\n`)
|
|
1286
|
-
process.exit(1)
|
|
1287
|
-
}
|
|
1288
|
-
return
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
const scopeDir = resolveScopeDir()
|
|
1292
|
-
|
|
1293
|
-
const { command, flags, positional } = parseArgs(process.argv)
|
|
1294
|
-
|
|
1295
|
-
if (
|
|
1296
|
-
flags['connect-url'] !== undefined ||
|
|
1297
|
-
flags.channel !== undefined ||
|
|
1298
|
-
flags['profile-dir'] !== undefined
|
|
1299
|
-
) {
|
|
1300
|
-
error(
|
|
1301
|
-
'--connect-url, --channel, and --profile-dir are no longer supported. Use --cdp-url, --browser real, --profile, --user-data-dir, and --browser-path instead.'
|
|
1302
|
-
)
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
if (command === 'sessions') {
|
|
1306
|
-
output({ ok: true, sessions: listSessions() })
|
|
1307
|
-
return
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
if (command === 'close' && flags.all === true) {
|
|
1311
|
-
try {
|
|
1312
|
-
const closed = await closeAllSessions()
|
|
1313
|
-
output({ ok: true, closed })
|
|
1314
|
-
} catch (err) {
|
|
1315
|
-
error(err instanceof Error ? err.message : 'Failed to close sessions')
|
|
1316
|
-
}
|
|
1317
|
-
return
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
let openCloudAuth = null
|
|
1321
|
-
if (command === 'open') {
|
|
1322
|
-
try {
|
|
1323
|
-
openCloudAuth = await ensureOpenCloudCredentials(flags, scopeDir)
|
|
1324
|
-
} catch (err) {
|
|
1325
|
-
error(
|
|
1326
|
-
err instanceof Error
|
|
1327
|
-
? err.message
|
|
1328
|
-
: 'Failed to resolve cloud authentication for open command.'
|
|
1329
|
-
)
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
let resolvedSession
|
|
1334
|
-
let resolvedName
|
|
1335
|
-
try {
|
|
1336
|
-
resolvedSession = resolveSession(flags, scopeDir)
|
|
1337
|
-
resolvedName = resolveName(flags, resolvedSession.session)
|
|
1338
|
-
} catch (err) {
|
|
1339
|
-
error(err instanceof Error ? err.message : 'Failed to resolve session')
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const logicalSession = resolvedSession.session
|
|
1343
|
-
const runtimeSession = buildRuntimeSession(scopeDir, logicalSession)
|
|
1344
|
-
const sessionSource = resolvedSession.source
|
|
1345
|
-
const name = resolvedName.name
|
|
1346
|
-
const nameSource = resolvedName.source
|
|
1347
|
-
const socketPath = getSocketPath(runtimeSession)
|
|
1348
|
-
const routingContext = {
|
|
1349
|
-
logicalSession,
|
|
1350
|
-
runtimeSession,
|
|
1351
|
-
scopeDir,
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
if (command === 'status') {
|
|
1355
|
-
output({
|
|
1356
|
-
ok: true,
|
|
1357
|
-
resolvedSession: logicalSession,
|
|
1358
|
-
logicalSession,
|
|
1359
|
-
runtimeSession,
|
|
1360
|
-
scopeDir,
|
|
1361
|
-
sessionSource,
|
|
1362
|
-
resolvedName: name,
|
|
1363
|
-
nameSource,
|
|
1364
|
-
serverRunning: await isServerHealthy(runtimeSession),
|
|
1365
|
-
socketPath,
|
|
1366
|
-
sessions: listSessions(),
|
|
1367
|
-
})
|
|
1368
|
-
return
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
delete flags.name
|
|
1372
|
-
delete flags.session
|
|
1373
|
-
delete flags.all
|
|
1374
|
-
delete flags['api-key']
|
|
1375
|
-
delete flags['access-token']
|
|
1376
|
-
|
|
1377
|
-
const request = buildRequest(command, flags, positional)
|
|
1378
|
-
if (command === 'open') {
|
|
1379
|
-
request.args.name = name
|
|
1380
|
-
const cloudAuthPayload = buildOpenCloudAuthPayload(openCloudAuth)
|
|
1381
|
-
if (cloudAuthPayload) {
|
|
1382
|
-
request.args['cloud-auth'] = cloudAuthPayload
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
if (!(await isServerHealthy(runtimeSession))) {
|
|
1387
|
-
if (command !== 'open') {
|
|
1388
|
-
error(
|
|
1389
|
-
`No server running for session '${logicalSession}' in cwd scope '${scopeDir}' (resolved from ${sessionSource}). Run 'opensteer open' first or use 'opensteer sessions' to see active sessions.`
|
|
1390
|
-
)
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
try {
|
|
1394
|
-
await ensureServer(routingContext)
|
|
1395
|
-
} catch (err) {
|
|
1396
|
-
error(
|
|
1397
|
-
err instanceof Error
|
|
1398
|
-
? err.message
|
|
1399
|
-
: `Failed to start server for session '${logicalSession}' in cwd scope '${scopeDir}'.`
|
|
1400
|
-
)
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
try {
|
|
1405
|
-
const response = await sendCommand(socketPath, request)
|
|
1406
|
-
|
|
1407
|
-
if (response.ok) {
|
|
1408
|
-
output({ ok: true, ...response.result })
|
|
1409
|
-
} else {
|
|
1410
|
-
process.stderr.write(JSON.stringify(normalizeFailedResponse(response)) + '\n')
|
|
1411
|
-
process.exit(1)
|
|
1412
|
-
}
|
|
1413
|
-
} catch (err) {
|
|
1414
|
-
error(
|
|
1415
|
-
formatTransportFailure(
|
|
1416
|
-
err,
|
|
1417
|
-
`Failed to run '${command}' for session '${logicalSession}' in cwd scope '${scopeDir}'`
|
|
1418
|
-
)
|
|
1419
|
-
)
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
main()
|