opensteer 0.5.3 → 0.5.5
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/CHANGELOG.md +4 -0
- package/README.md +2 -6
- package/bin/opensteer.mjs +163 -38
- package/dist/{chunk-SXPIGCSD.js → chunk-XIH3WGPY.js} +99 -32
- package/dist/cli/server.cjs +141 -48
- package/dist/cli/server.js +43 -17
- package/dist/index.cjs +99 -32
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
health checks to remove startup races across concurrent commands.
|
|
15
15
|
- Added strict in-daemon request serialization for session commands, while
|
|
16
16
|
keeping `ping` out of the queue for reliable liveness checks.
|
|
17
|
+
- Breaking: CLI daemon routing is now scoped by canonical `cwd`
|
|
18
|
+
(`realpath(cwd)`) + logical session (`--session`/`OPENSTEER_SESSION`) rather
|
|
19
|
+
than machine-wide session id matching; the same logical session can run in
|
|
20
|
+
parallel across different directories.
|
|
17
21
|
- Breaking: removed legacy `ai` config from `OpensteerConfig`; use top-level `model` instead.
|
|
18
22
|
- Breaking: `OPENSTEER_AI_MODEL` is no longer supported; use `OPENSTEER_MODEL`.
|
|
19
23
|
- Breaking: `OPENSTEER_RUNTIME` is no longer supported; use `OPENSTEER_MODE`.
|
package/README.md
CHANGED
|
@@ -111,6 +111,8 @@ opensteer close --session demo
|
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
For non-interactive runs, set `OPENSTEER_SESSION` or `OPENSTEER_CLIENT_ID`.
|
|
114
|
+
Runtime daemon routing for `OPENSTEER_SESSION` is scoped by canonical `cwd`
|
|
115
|
+
(`realpath(cwd)`) + logical session id.
|
|
114
116
|
|
|
115
117
|
## For AI Agents
|
|
116
118
|
|
|
@@ -134,12 +136,6 @@ Install the Opensteer skill pack:
|
|
|
134
136
|
opensteer skills install
|
|
135
137
|
```
|
|
136
138
|
|
|
137
|
-
Fallback (direct upstream `skills` CLI):
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
npx skills add https://github.com/steerlabs/opensteer-skills --skill opensteer
|
|
141
|
-
```
|
|
142
|
-
|
|
143
139
|
Claude Code marketplace plugin:
|
|
144
140
|
|
|
145
141
|
```text
|
package/bin/opensteer.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
closeSync,
|
|
7
7
|
existsSync,
|
|
8
8
|
openSync,
|
|
9
|
+
realpathSync,
|
|
9
10
|
readFileSync,
|
|
10
11
|
readdirSync,
|
|
11
12
|
unlinkSync,
|
|
@@ -55,11 +56,13 @@ const RUNTIME_PREFIX = 'opensteer-'
|
|
|
55
56
|
const SOCKET_SUFFIX = '.sock'
|
|
56
57
|
const PID_SUFFIX = '.pid'
|
|
57
58
|
const LOCK_SUFFIX = '.lock'
|
|
59
|
+
const METADATA_SUFFIX = '.meta.json'
|
|
58
60
|
const CLIENT_BINDING_PREFIX = `${RUNTIME_PREFIX}client-`
|
|
59
61
|
const CLIENT_BINDING_SUFFIX = '.session'
|
|
60
62
|
const CLOSE_ALL_REQUEST = { id: 1, command: 'close', args: {} }
|
|
61
63
|
const PING_REQUEST = { id: 1, command: 'ping', args: {} }
|
|
62
64
|
const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
|
|
65
|
+
const RUNTIME_SESSION_PREFIX = 'sc-'
|
|
63
66
|
|
|
64
67
|
function getVersion() {
|
|
65
68
|
try {
|
|
@@ -214,7 +217,20 @@ function isInteractiveTerminal() {
|
|
|
214
217
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
215
218
|
}
|
|
216
219
|
|
|
217
|
-
function
|
|
220
|
+
function resolveScopeDir() {
|
|
221
|
+
const cwd = process.cwd()
|
|
222
|
+
try {
|
|
223
|
+
return realpathSync(cwd)
|
|
224
|
+
} catch {
|
|
225
|
+
return cwd
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildRuntimeSession(scopeDir, logicalSession) {
|
|
230
|
+
return `${RUNTIME_SESSION_PREFIX}${hashKey(`${scopeDir}:${logicalSession}`).slice(0, 24)}`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveSession(flags, scopeDir) {
|
|
218
234
|
if (flags.session !== undefined) {
|
|
219
235
|
if (flags.session === true) {
|
|
220
236
|
throw new Error('--session requires a session id value.')
|
|
@@ -244,7 +260,7 @@ function resolveSession(flags) {
|
|
|
244
260
|
process.env.OPENSTEER_CLIENT_ID.trim().length > 0
|
|
245
261
|
) {
|
|
246
262
|
const clientId = process.env.OPENSTEER_CLIENT_ID.trim()
|
|
247
|
-
const clientKey = `client:${
|
|
263
|
+
const clientKey = `client:${scopeDir}:${clientId}`
|
|
248
264
|
const bound = readClientBinding(clientKey)
|
|
249
265
|
if (bound) {
|
|
250
266
|
return { session: bound, source: 'client_binding' }
|
|
@@ -256,7 +272,7 @@ function resolveSession(flags) {
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
if (isInteractiveTerminal()) {
|
|
259
|
-
const ttyKey = `tty:${
|
|
275
|
+
const ttyKey = `tty:${scopeDir}:${process.ppid}`
|
|
260
276
|
const bound = readClientBinding(ttyKey)
|
|
261
277
|
if (bound) {
|
|
262
278
|
return { session: bound, source: 'tty_default' }
|
|
@@ -284,6 +300,10 @@ function getLockPath(session) {
|
|
|
284
300
|
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${LOCK_SUFFIX}`)
|
|
285
301
|
}
|
|
286
302
|
|
|
303
|
+
function getMetadataPath(session) {
|
|
304
|
+
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${METADATA_SUFFIX}`)
|
|
305
|
+
}
|
|
306
|
+
|
|
287
307
|
function buildRequest(command, flags, positional) {
|
|
288
308
|
const id = 1
|
|
289
309
|
const globalFlags = {}
|
|
@@ -430,20 +450,80 @@ function cleanStaleFiles(session, options = {}) {
|
|
|
430
450
|
unlinkSync(getPidPath(session))
|
|
431
451
|
} catch { }
|
|
432
452
|
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
unlinkSync(getMetadataPath(session))
|
|
456
|
+
} catch { }
|
|
433
457
|
}
|
|
434
458
|
|
|
435
|
-
function startServer(
|
|
459
|
+
function startServer(runtimeSession, logicalSession, scopeDir) {
|
|
436
460
|
const child = spawn('node', [SERVER_SCRIPT], {
|
|
437
461
|
detached: true,
|
|
438
462
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
439
463
|
env: {
|
|
440
464
|
...process.env,
|
|
441
|
-
OPENSTEER_SESSION:
|
|
465
|
+
OPENSTEER_SESSION: runtimeSession,
|
|
466
|
+
OPENSTEER_LOGICAL_SESSION: logicalSession,
|
|
467
|
+
OPENSTEER_SCOPE_DIR: scopeDir,
|
|
442
468
|
},
|
|
443
469
|
})
|
|
444
470
|
child.unref()
|
|
445
471
|
}
|
|
446
472
|
|
|
473
|
+
function readMetadata(session) {
|
|
474
|
+
const metadataPath = getMetadataPath(session)
|
|
475
|
+
if (!existsSync(metadataPath)) {
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const raw = JSON.parse(readFileSync(metadataPath, 'utf-8'))
|
|
481
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
482
|
+
return null
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
typeof raw.logicalSession !== 'string' ||
|
|
487
|
+
!raw.logicalSession.trim() ||
|
|
488
|
+
typeof raw.scopeDir !== 'string' ||
|
|
489
|
+
!raw.scopeDir.trim() ||
|
|
490
|
+
typeof raw.runtimeSession !== 'string' ||
|
|
491
|
+
!raw.runtimeSession.trim()
|
|
492
|
+
) {
|
|
493
|
+
return null
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
logicalSession: raw.logicalSession.trim(),
|
|
498
|
+
scopeDir: raw.scopeDir,
|
|
499
|
+
runtimeSession: raw.runtimeSession.trim(),
|
|
500
|
+
createdAt:
|
|
501
|
+
typeof raw.createdAt === 'number' ? raw.createdAt : undefined,
|
|
502
|
+
updatedAt:
|
|
503
|
+
typeof raw.updatedAt === 'number' ? raw.updatedAt : undefined,
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
return null
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function writeMetadata(runtimeSession, logicalSession, scopeDir) {
|
|
511
|
+
const metadataPath = getMetadataPath(runtimeSession)
|
|
512
|
+
const existing = readMetadata(runtimeSession)
|
|
513
|
+
const now = Date.now()
|
|
514
|
+
const payload = {
|
|
515
|
+
runtimeSession,
|
|
516
|
+
logicalSession,
|
|
517
|
+
scopeDir,
|
|
518
|
+
createdAt: existing?.createdAt ?? now,
|
|
519
|
+
updatedAt: now,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
writeFileSync(metadataPath, JSON.stringify(payload, null, 2))
|
|
524
|
+
} catch { }
|
|
525
|
+
}
|
|
526
|
+
|
|
447
527
|
function sendCommand(socketPath, request, timeoutMs = RESPONSE_TIMEOUT) {
|
|
448
528
|
return new Promise((resolve, reject) => {
|
|
449
529
|
const socket = connect(socketPath)
|
|
@@ -606,8 +686,14 @@ async function waitForServerReady(session, timeout) {
|
|
|
606
686
|
throw new Error(`Timed out waiting for server '${session}' to become healthy.`)
|
|
607
687
|
}
|
|
608
688
|
|
|
609
|
-
async function ensureServer(
|
|
610
|
-
|
|
689
|
+
async function ensureServer(context) {
|
|
690
|
+
const runtimeSession = context.runtimeSession
|
|
691
|
+
if (await isServerHealthy(runtimeSession)) {
|
|
692
|
+
writeMetadata(
|
|
693
|
+
runtimeSession,
|
|
694
|
+
context.logicalSession,
|
|
695
|
+
context.scopeDir
|
|
696
|
+
)
|
|
611
697
|
return
|
|
612
698
|
}
|
|
613
699
|
|
|
@@ -620,31 +706,45 @@ async function ensureServer(session) {
|
|
|
620
706
|
const deadline = Date.now() + CONNECT_TIMEOUT
|
|
621
707
|
|
|
622
708
|
while (Date.now() < deadline) {
|
|
623
|
-
if (await isServerHealthy(
|
|
709
|
+
if (await isServerHealthy(runtimeSession)) {
|
|
710
|
+
writeMetadata(
|
|
711
|
+
runtimeSession,
|
|
712
|
+
context.logicalSession,
|
|
713
|
+
context.scopeDir
|
|
714
|
+
)
|
|
624
715
|
return
|
|
625
716
|
}
|
|
626
717
|
|
|
627
|
-
const existingPid = readPid(getPidPath(
|
|
718
|
+
const existingPid = readPid(getPidPath(runtimeSession))
|
|
628
719
|
if (existingPid && isPidAlive(existingPid)) {
|
|
629
720
|
await sleep(POLL_INTERVAL)
|
|
630
721
|
continue
|
|
631
722
|
}
|
|
632
723
|
|
|
633
|
-
recoverStaleStartLock(
|
|
724
|
+
recoverStaleStartLock(runtimeSession)
|
|
634
725
|
|
|
635
|
-
if (acquireStartLock(
|
|
726
|
+
if (acquireStartLock(runtimeSession)) {
|
|
636
727
|
try {
|
|
637
|
-
if (!(await isServerHealthy(
|
|
638
|
-
startServer(
|
|
728
|
+
if (!(await isServerHealthy(runtimeSession))) {
|
|
729
|
+
startServer(
|
|
730
|
+
runtimeSession,
|
|
731
|
+
context.logicalSession,
|
|
732
|
+
context.scopeDir
|
|
733
|
+
)
|
|
639
734
|
}
|
|
640
735
|
|
|
641
736
|
await waitForServerReady(
|
|
642
|
-
|
|
737
|
+
runtimeSession,
|
|
643
738
|
Math.max(500, deadline - Date.now())
|
|
644
739
|
)
|
|
740
|
+
writeMetadata(
|
|
741
|
+
runtimeSession,
|
|
742
|
+
context.logicalSession,
|
|
743
|
+
context.scopeDir
|
|
744
|
+
)
|
|
645
745
|
return
|
|
646
746
|
} finally {
|
|
647
|
-
releaseStartLock(
|
|
747
|
+
releaseStartLock(runtimeSession)
|
|
648
748
|
}
|
|
649
749
|
}
|
|
650
750
|
|
|
@@ -652,7 +752,7 @@ async function ensureServer(session) {
|
|
|
652
752
|
}
|
|
653
753
|
|
|
654
754
|
throw new Error(
|
|
655
|
-
`Failed to start server for session '${
|
|
755
|
+
`Failed to start server for session '${context.logicalSession}' in cwd scope '${context.scopeDir}' within ${CONNECT_TIMEOUT}ms.`
|
|
656
756
|
)
|
|
657
757
|
}
|
|
658
758
|
|
|
@@ -665,24 +765,39 @@ function listSessions() {
|
|
|
665
765
|
continue
|
|
666
766
|
}
|
|
667
767
|
|
|
668
|
-
const
|
|
768
|
+
const runtimeSession = entry.slice(
|
|
669
769
|
RUNTIME_PREFIX.length,
|
|
670
770
|
entry.length - PID_SUFFIX.length
|
|
671
771
|
)
|
|
672
|
-
if (!
|
|
772
|
+
if (!runtimeSession) {
|
|
673
773
|
continue
|
|
674
774
|
}
|
|
675
775
|
|
|
676
776
|
const pid = readPid(join(tmpdir(), entry))
|
|
677
777
|
if (!pid || !isPidAlive(pid)) {
|
|
678
|
-
cleanStaleFiles(
|
|
778
|
+
cleanStaleFiles(runtimeSession)
|
|
679
779
|
continue
|
|
680
780
|
}
|
|
681
781
|
|
|
682
|
-
|
|
782
|
+
const metadata = readMetadata(runtimeSession)
|
|
783
|
+
sessions.push({
|
|
784
|
+
name: metadata?.logicalSession || runtimeSession,
|
|
785
|
+
logicalSession: metadata?.logicalSession || runtimeSession,
|
|
786
|
+
runtimeSession,
|
|
787
|
+
scopeDir: metadata?.scopeDir || null,
|
|
788
|
+
pid,
|
|
789
|
+
})
|
|
683
790
|
}
|
|
684
791
|
|
|
685
|
-
sessions.sort((a, b) =>
|
|
792
|
+
sessions.sort((a, b) => {
|
|
793
|
+
const scopeA = a.scopeDir || ''
|
|
794
|
+
const scopeB = b.scopeDir || ''
|
|
795
|
+
if (scopeA !== scopeB) {
|
|
796
|
+
return scopeA.localeCompare(scopeB)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return a.logicalSession.localeCompare(b.logicalSession)
|
|
800
|
+
})
|
|
686
801
|
return sessions
|
|
687
802
|
}
|
|
688
803
|
|
|
@@ -692,9 +807,9 @@ async function closeAllSessions() {
|
|
|
692
807
|
const failures = []
|
|
693
808
|
|
|
694
809
|
for (const session of sessions) {
|
|
695
|
-
const socketPath = getSocketPath(session.
|
|
810
|
+
const socketPath = getSocketPath(session.runtimeSession)
|
|
696
811
|
if (!existsSync(socketPath)) {
|
|
697
|
-
cleanStaleFiles(session.
|
|
812
|
+
cleanStaleFiles(session.runtimeSession)
|
|
698
813
|
continue
|
|
699
814
|
}
|
|
700
815
|
|
|
@@ -704,12 +819,12 @@ async function closeAllSessions() {
|
|
|
704
819
|
closed.push(session)
|
|
705
820
|
} else {
|
|
706
821
|
failures.push(
|
|
707
|
-
`${session.
|
|
822
|
+
`${session.logicalSession} (${session.scopeDir || 'unknown scope'}): ${response?.error || 'unknown close error'}`
|
|
708
823
|
)
|
|
709
824
|
}
|
|
710
825
|
} catch (err) {
|
|
711
826
|
failures.push(
|
|
712
|
-
`${session.
|
|
827
|
+
`${session.logicalSession} (${session.scopeDir || 'unknown scope'}): ${err instanceof Error ? err.message : String(err)}`
|
|
713
828
|
)
|
|
714
829
|
}
|
|
715
830
|
}
|
|
@@ -817,7 +932,7 @@ Navigation:
|
|
|
817
932
|
|
|
818
933
|
Sessions:
|
|
819
934
|
sessions List active session-scoped daemons
|
|
820
|
-
status Show resolved
|
|
935
|
+
status Show resolved logical/runtime session and session state
|
|
821
936
|
|
|
822
937
|
Observation:
|
|
823
938
|
snapshot [--mode action] Get page snapshot
|
|
@@ -868,7 +983,7 @@ Skills:
|
|
|
868
983
|
skills --help Show skills installer help
|
|
869
984
|
|
|
870
985
|
Global Flags:
|
|
871
|
-
--session <id>
|
|
986
|
+
--session <id> Logical session id (scoped by canonical cwd)
|
|
872
987
|
--name <namespace> Selector namespace for cache storage on 'open'
|
|
873
988
|
--headless Launch browser in headless mode
|
|
874
989
|
--connect-url <url> Connect to a running browser (e.g. http://localhost:9222)
|
|
@@ -881,7 +996,7 @@ Global Flags:
|
|
|
881
996
|
--version, -v Show version
|
|
882
997
|
|
|
883
998
|
Environment:
|
|
884
|
-
OPENSTEER_SESSION
|
|
999
|
+
OPENSTEER_SESSION Logical session id (equivalent to --session)
|
|
885
1000
|
OPENSTEER_CLIENT_ID Stable client identity for default session binding
|
|
886
1001
|
OPENSTEER_NAME Default selector namespace for 'open' when --name is omitted
|
|
887
1002
|
OPENSTEER_MODE Runtime routing: "local" (default) or "cloud"
|
|
@@ -925,27 +1040,37 @@ async function main() {
|
|
|
925
1040
|
|
|
926
1041
|
let resolvedSession
|
|
927
1042
|
let resolvedName
|
|
1043
|
+
const scopeDir = resolveScopeDir()
|
|
928
1044
|
try {
|
|
929
|
-
resolvedSession = resolveSession(flags)
|
|
1045
|
+
resolvedSession = resolveSession(flags, scopeDir)
|
|
930
1046
|
resolvedName = resolveName(flags, resolvedSession.session)
|
|
931
1047
|
} catch (err) {
|
|
932
1048
|
error(err instanceof Error ? err.message : 'Failed to resolve session')
|
|
933
1049
|
}
|
|
934
1050
|
|
|
935
|
-
const
|
|
1051
|
+
const logicalSession = resolvedSession.session
|
|
1052
|
+
const runtimeSession = buildRuntimeSession(scopeDir, logicalSession)
|
|
936
1053
|
const sessionSource = resolvedSession.source
|
|
937
1054
|
const name = resolvedName.name
|
|
938
1055
|
const nameSource = resolvedName.source
|
|
939
|
-
const socketPath = getSocketPath(
|
|
1056
|
+
const socketPath = getSocketPath(runtimeSession)
|
|
1057
|
+
const routingContext = {
|
|
1058
|
+
logicalSession,
|
|
1059
|
+
runtimeSession,
|
|
1060
|
+
scopeDir,
|
|
1061
|
+
}
|
|
940
1062
|
|
|
941
1063
|
if (command === 'status') {
|
|
942
1064
|
output({
|
|
943
1065
|
ok: true,
|
|
944
|
-
resolvedSession:
|
|
1066
|
+
resolvedSession: logicalSession,
|
|
1067
|
+
logicalSession,
|
|
1068
|
+
runtimeSession,
|
|
1069
|
+
scopeDir,
|
|
945
1070
|
sessionSource,
|
|
946
1071
|
resolvedName: name,
|
|
947
1072
|
nameSource,
|
|
948
|
-
serverRunning: await isServerHealthy(
|
|
1073
|
+
serverRunning: await isServerHealthy(runtimeSession),
|
|
949
1074
|
socketPath,
|
|
950
1075
|
sessions: listSessions(),
|
|
951
1076
|
})
|
|
@@ -961,20 +1086,20 @@ async function main() {
|
|
|
961
1086
|
request.args.name = name
|
|
962
1087
|
}
|
|
963
1088
|
|
|
964
|
-
if (!(await isServerHealthy(
|
|
1089
|
+
if (!(await isServerHealthy(runtimeSession))) {
|
|
965
1090
|
if (command !== 'open') {
|
|
966
1091
|
error(
|
|
967
|
-
`No server running for session '${
|
|
1092
|
+
`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.`
|
|
968
1093
|
)
|
|
969
1094
|
}
|
|
970
1095
|
|
|
971
1096
|
try {
|
|
972
|
-
await ensureServer(
|
|
1097
|
+
await ensureServer(routingContext)
|
|
973
1098
|
} catch (err) {
|
|
974
1099
|
error(
|
|
975
1100
|
err instanceof Error
|
|
976
1101
|
? err.message
|
|
977
|
-
: `Failed to start server for session '${
|
|
1102
|
+
: `Failed to start server for session '${logicalSession}' in cwd scope '${scopeDir}'.`
|
|
978
1103
|
)
|
|
979
1104
|
}
|
|
980
1105
|
}
|
|
@@ -992,7 +1117,7 @@ async function main() {
|
|
|
992
1117
|
error(
|
|
993
1118
|
formatTransportFailure(
|
|
994
1119
|
err,
|
|
995
|
-
`Failed to run '${command}' for session '${
|
|
1120
|
+
`Failed to run '${command}' for session '${logicalSession}' in cwd scope '${scopeDir}'`
|
|
996
1121
|
)
|
|
997
1122
|
)
|
|
998
1123
|
}
|
|
@@ -2999,8 +2999,56 @@ function applyCleaner(mode, html) {
|
|
|
2999
2999
|
return cleanForAction(html);
|
|
3000
3000
|
}
|
|
3001
3001
|
}
|
|
3002
|
+
function canonicalizeDuplicateNodeIds($) {
|
|
3003
|
+
const occurrencesByNodeId = /* @__PURE__ */ new Map();
|
|
3004
|
+
let order = 0;
|
|
3005
|
+
$("*").each(function() {
|
|
3006
|
+
const element = this;
|
|
3007
|
+
const nodeId = $(element).attr(OS_NODE_ID_ATTR);
|
|
3008
|
+
if (!nodeId) {
|
|
3009
|
+
order += 1;
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
const list = occurrencesByNodeId.get(nodeId) || [];
|
|
3013
|
+
list.push({
|
|
3014
|
+
element,
|
|
3015
|
+
order
|
|
3016
|
+
});
|
|
3017
|
+
occurrencesByNodeId.set(nodeId, list);
|
|
3018
|
+
order += 1;
|
|
3019
|
+
});
|
|
3020
|
+
for (const occurrences of occurrencesByNodeId.values()) {
|
|
3021
|
+
if (occurrences.length <= 1) continue;
|
|
3022
|
+
const canonical = pickCanonicalNodeIdOccurrence($, occurrences);
|
|
3023
|
+
for (const occurrence of occurrences) {
|
|
3024
|
+
if (occurrence.element === canonical.element) continue;
|
|
3025
|
+
$(occurrence.element).removeAttr(OS_NODE_ID_ATTR);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
function pickCanonicalNodeIdOccurrence($, occurrences) {
|
|
3030
|
+
let best = occurrences[0];
|
|
3031
|
+
let bestScore = scoreNodeIdOccurrence($, best.element);
|
|
3032
|
+
for (let i = 1; i < occurrences.length; i += 1) {
|
|
3033
|
+
const candidate = occurrences[i];
|
|
3034
|
+
const candidateScore = scoreNodeIdOccurrence($, candidate.element);
|
|
3035
|
+
if (candidateScore > bestScore || candidateScore === bestScore && candidate.order < best.order) {
|
|
3036
|
+
best = candidate;
|
|
3037
|
+
bestScore = candidateScore;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
return best;
|
|
3041
|
+
}
|
|
3042
|
+
function scoreNodeIdOccurrence($, element) {
|
|
3043
|
+
const el = $(element);
|
|
3044
|
+
const descendantCount = el.find("*").length;
|
|
3045
|
+
const normalizedTextLength = el.text().replace(/\s+/g, " ").trim().length;
|
|
3046
|
+
const attributeCount = Object.keys(el.attr() || {}).length;
|
|
3047
|
+
return descendantCount * 100 + normalizedTextLength * 10 + attributeCount;
|
|
3048
|
+
}
|
|
3002
3049
|
async function assignCounters(page, html, nodePaths, nodeMeta) {
|
|
3003
3050
|
const $ = cheerio3.load(html, { xmlMode: false });
|
|
3051
|
+
canonicalizeDuplicateNodeIds($);
|
|
3004
3052
|
const counterIndex = /* @__PURE__ */ new Map();
|
|
3005
3053
|
let nextCounter = 1;
|
|
3006
3054
|
const assignedByNodeId = /* @__PURE__ */ new Map();
|
|
@@ -3182,44 +3230,63 @@ function stripNodeIds(html) {
|
|
|
3182
3230
|
$(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
|
|
3183
3231
|
return $.html();
|
|
3184
3232
|
}
|
|
3233
|
+
function isLiveCounterSyncFailure(error) {
|
|
3234
|
+
if (!(error instanceof Error)) return false;
|
|
3235
|
+
return error.message.startsWith(
|
|
3236
|
+
"Failed to synchronize snapshot counters with the live DOM:"
|
|
3237
|
+
);
|
|
3238
|
+
}
|
|
3185
3239
|
async function prepareSnapshot(page, options = {}) {
|
|
3186
3240
|
const mode = options.mode ?? "action";
|
|
3187
3241
|
const withCounters = options.withCounters ?? true;
|
|
3188
3242
|
const shouldMarkInteractive = options.markInteractive ?? true;
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3243
|
+
const maxAttempts = withCounters ? 4 : 1;
|
|
3244
|
+
let lastCounterSyncError = null;
|
|
3245
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
3246
|
+
if (shouldMarkInteractive) {
|
|
3247
|
+
await markInteractiveElements(page);
|
|
3248
|
+
}
|
|
3249
|
+
const serialized = await serializePageHTML(page);
|
|
3250
|
+
const rawHtml = serialized.html;
|
|
3251
|
+
const processedHtml = rawHtml;
|
|
3252
|
+
const reducedHtml = applyCleaner(mode, processedHtml);
|
|
3253
|
+
let cleanedHtml = reducedHtml;
|
|
3254
|
+
let counterIndex = null;
|
|
3255
|
+
if (withCounters) {
|
|
3256
|
+
try {
|
|
3257
|
+
const counted = await assignCounters(
|
|
3258
|
+
page,
|
|
3259
|
+
reducedHtml,
|
|
3260
|
+
serialized.nodePaths,
|
|
3261
|
+
serialized.nodeMeta
|
|
3262
|
+
);
|
|
3263
|
+
cleanedHtml = counted.html;
|
|
3264
|
+
counterIndex = counted.counterIndex;
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
if (attempt < maxAttempts && isLiveCounterSyncFailure(error)) {
|
|
3267
|
+
lastCounterSyncError = error;
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
throw error;
|
|
3271
|
+
}
|
|
3272
|
+
} else {
|
|
3273
|
+
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
3274
|
+
}
|
|
3275
|
+
if (mode === "extraction") {
|
|
3276
|
+
const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
|
|
3277
|
+
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
3278
|
+
}
|
|
3279
|
+
return {
|
|
3280
|
+
mode,
|
|
3281
|
+
url: page.url(),
|
|
3282
|
+
rawHtml,
|
|
3283
|
+
processedHtml,
|
|
3201
3284
|
reducedHtml,
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
cleanedHtml = counted.html;
|
|
3206
|
-
counterIndex = counted.counterIndex;
|
|
3207
|
-
} else {
|
|
3208
|
-
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
3209
|
-
}
|
|
3210
|
-
if (mode === "extraction") {
|
|
3211
|
-
const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
|
|
3212
|
-
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
3285
|
+
cleanedHtml,
|
|
3286
|
+
counterIndex
|
|
3287
|
+
};
|
|
3213
3288
|
}
|
|
3214
|
-
|
|
3215
|
-
mode,
|
|
3216
|
-
url: page.url(),
|
|
3217
|
-
rawHtml,
|
|
3218
|
-
processedHtml,
|
|
3219
|
-
reducedHtml,
|
|
3220
|
-
cleanedHtml,
|
|
3221
|
-
counterIndex
|
|
3222
|
-
};
|
|
3289
|
+
throw lastCounterSyncError || new Error("Failed to prepare snapshot after retrying counter sync.");
|
|
3223
3290
|
}
|
|
3224
3291
|
|
|
3225
3292
|
// src/element-path/errors.ts
|
package/dist/cli/server.cjs
CHANGED
|
@@ -4219,8 +4219,56 @@ function applyCleaner(mode, html) {
|
|
|
4219
4219
|
return cleanForAction(html);
|
|
4220
4220
|
}
|
|
4221
4221
|
}
|
|
4222
|
+
function canonicalizeDuplicateNodeIds($) {
|
|
4223
|
+
const occurrencesByNodeId = /* @__PURE__ */ new Map();
|
|
4224
|
+
let order = 0;
|
|
4225
|
+
$("*").each(function() {
|
|
4226
|
+
const element = this;
|
|
4227
|
+
const nodeId = $(element).attr(OS_NODE_ID_ATTR);
|
|
4228
|
+
if (!nodeId) {
|
|
4229
|
+
order += 1;
|
|
4230
|
+
return;
|
|
4231
|
+
}
|
|
4232
|
+
const list = occurrencesByNodeId.get(nodeId) || [];
|
|
4233
|
+
list.push({
|
|
4234
|
+
element,
|
|
4235
|
+
order
|
|
4236
|
+
});
|
|
4237
|
+
occurrencesByNodeId.set(nodeId, list);
|
|
4238
|
+
order += 1;
|
|
4239
|
+
});
|
|
4240
|
+
for (const occurrences of occurrencesByNodeId.values()) {
|
|
4241
|
+
if (occurrences.length <= 1) continue;
|
|
4242
|
+
const canonical = pickCanonicalNodeIdOccurrence($, occurrences);
|
|
4243
|
+
for (const occurrence of occurrences) {
|
|
4244
|
+
if (occurrence.element === canonical.element) continue;
|
|
4245
|
+
$(occurrence.element).removeAttr(OS_NODE_ID_ATTR);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
function pickCanonicalNodeIdOccurrence($, occurrences) {
|
|
4250
|
+
let best = occurrences[0];
|
|
4251
|
+
let bestScore = scoreNodeIdOccurrence($, best.element);
|
|
4252
|
+
for (let i = 1; i < occurrences.length; i += 1) {
|
|
4253
|
+
const candidate = occurrences[i];
|
|
4254
|
+
const candidateScore = scoreNodeIdOccurrence($, candidate.element);
|
|
4255
|
+
if (candidateScore > bestScore || candidateScore === bestScore && candidate.order < best.order) {
|
|
4256
|
+
best = candidate;
|
|
4257
|
+
bestScore = candidateScore;
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
return best;
|
|
4261
|
+
}
|
|
4262
|
+
function scoreNodeIdOccurrence($, element) {
|
|
4263
|
+
const el = $(element);
|
|
4264
|
+
const descendantCount = el.find("*").length;
|
|
4265
|
+
const normalizedTextLength = el.text().replace(/\s+/g, " ").trim().length;
|
|
4266
|
+
const attributeCount = Object.keys(el.attr() || {}).length;
|
|
4267
|
+
return descendantCount * 100 + normalizedTextLength * 10 + attributeCount;
|
|
4268
|
+
}
|
|
4222
4269
|
async function assignCounters(page, html, nodePaths, nodeMeta) {
|
|
4223
4270
|
const $ = cheerio3.load(html, { xmlMode: false });
|
|
4271
|
+
canonicalizeDuplicateNodeIds($);
|
|
4224
4272
|
const counterIndex = /* @__PURE__ */ new Map();
|
|
4225
4273
|
let nextCounter = 1;
|
|
4226
4274
|
const assignedByNodeId = /* @__PURE__ */ new Map();
|
|
@@ -4402,44 +4450,63 @@ function stripNodeIds(html) {
|
|
|
4402
4450
|
$(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
|
|
4403
4451
|
return $.html();
|
|
4404
4452
|
}
|
|
4453
|
+
function isLiveCounterSyncFailure(error) {
|
|
4454
|
+
if (!(error instanceof Error)) return false;
|
|
4455
|
+
return error.message.startsWith(
|
|
4456
|
+
"Failed to synchronize snapshot counters with the live DOM:"
|
|
4457
|
+
);
|
|
4458
|
+
}
|
|
4405
4459
|
async function prepareSnapshot(page, options = {}) {
|
|
4406
4460
|
const mode = options.mode ?? "action";
|
|
4407
4461
|
const withCounters = options.withCounters ?? true;
|
|
4408
4462
|
const shouldMarkInteractive = options.markInteractive ?? true;
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4463
|
+
const maxAttempts = withCounters ? 4 : 1;
|
|
4464
|
+
let lastCounterSyncError = null;
|
|
4465
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
4466
|
+
if (shouldMarkInteractive) {
|
|
4467
|
+
await markInteractiveElements(page);
|
|
4468
|
+
}
|
|
4469
|
+
const serialized = await serializePageHTML(page);
|
|
4470
|
+
const rawHtml = serialized.html;
|
|
4471
|
+
const processedHtml = rawHtml;
|
|
4472
|
+
const reducedHtml = applyCleaner(mode, processedHtml);
|
|
4473
|
+
let cleanedHtml = reducedHtml;
|
|
4474
|
+
let counterIndex = null;
|
|
4475
|
+
if (withCounters) {
|
|
4476
|
+
try {
|
|
4477
|
+
const counted = await assignCounters(
|
|
4478
|
+
page,
|
|
4479
|
+
reducedHtml,
|
|
4480
|
+
serialized.nodePaths,
|
|
4481
|
+
serialized.nodeMeta
|
|
4482
|
+
);
|
|
4483
|
+
cleanedHtml = counted.html;
|
|
4484
|
+
counterIndex = counted.counterIndex;
|
|
4485
|
+
} catch (error) {
|
|
4486
|
+
if (attempt < maxAttempts && isLiveCounterSyncFailure(error)) {
|
|
4487
|
+
lastCounterSyncError = error;
|
|
4488
|
+
continue;
|
|
4489
|
+
}
|
|
4490
|
+
throw error;
|
|
4491
|
+
}
|
|
4492
|
+
} else {
|
|
4493
|
+
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
4494
|
+
}
|
|
4495
|
+
if (mode === "extraction") {
|
|
4496
|
+
const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
|
|
4497
|
+
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
4498
|
+
}
|
|
4499
|
+
return {
|
|
4500
|
+
mode,
|
|
4501
|
+
url: page.url(),
|
|
4502
|
+
rawHtml,
|
|
4503
|
+
processedHtml,
|
|
4421
4504
|
reducedHtml,
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
cleanedHtml = counted.html;
|
|
4426
|
-
counterIndex = counted.counterIndex;
|
|
4427
|
-
} else {
|
|
4428
|
-
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
4429
|
-
}
|
|
4430
|
-
if (mode === "extraction") {
|
|
4431
|
-
const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
|
|
4432
|
-
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
4505
|
+
cleanedHtml,
|
|
4506
|
+
counterIndex
|
|
4507
|
+
};
|
|
4433
4508
|
}
|
|
4434
|
-
|
|
4435
|
-
mode,
|
|
4436
|
-
url: page.url(),
|
|
4437
|
-
rawHtml,
|
|
4438
|
-
processedHtml,
|
|
4439
|
-
reducedHtml,
|
|
4440
|
-
cleanedHtml,
|
|
4441
|
-
counterIndex
|
|
4442
|
-
};
|
|
4509
|
+
throw lastCounterSyncError || new Error("Failed to prepare snapshot after retrying counter sync.");
|
|
4443
4510
|
}
|
|
4444
4511
|
|
|
4445
4512
|
// src/element-path/errors.ts
|
|
@@ -12538,6 +12605,9 @@ function getSocketPath(session2) {
|
|
|
12538
12605
|
function getPidPath(session2) {
|
|
12539
12606
|
return (0, import_path6.join)((0, import_os2.tmpdir)(), `${prefix(session2)}.pid`);
|
|
12540
12607
|
}
|
|
12608
|
+
function getMetadataPath(session2) {
|
|
12609
|
+
return (0, import_path6.join)((0, import_os2.tmpdir)(), `${prefix(session2)}.meta.json`);
|
|
12610
|
+
}
|
|
12541
12611
|
|
|
12542
12612
|
// src/cli/commands.ts
|
|
12543
12613
|
var import_promises2 = require("fs/promises");
|
|
@@ -12806,6 +12876,8 @@ if (!sessionEnv) {
|
|
|
12806
12876
|
process.exit(1);
|
|
12807
12877
|
}
|
|
12808
12878
|
var session = sessionEnv;
|
|
12879
|
+
var logicalSession = process.env.OPENSTEER_LOGICAL_SESSION?.trim() || session;
|
|
12880
|
+
var scopeDir = process.env.OPENSTEER_SCOPE_DIR?.trim() || process.cwd();
|
|
12809
12881
|
var socketPath = getSocketPath(session);
|
|
12810
12882
|
var pidPath = getPidPath(session);
|
|
12811
12883
|
function cleanup() {
|
|
@@ -12817,6 +12889,10 @@ function cleanup() {
|
|
|
12817
12889
|
(0, import_fs4.unlinkSync)(pidPath);
|
|
12818
12890
|
} catch {
|
|
12819
12891
|
}
|
|
12892
|
+
try {
|
|
12893
|
+
(0, import_fs4.unlinkSync)(getMetadataPath(session));
|
|
12894
|
+
} catch {
|
|
12895
|
+
}
|
|
12820
12896
|
}
|
|
12821
12897
|
function beginShutdown() {
|
|
12822
12898
|
if (shuttingDown) return;
|
|
@@ -12858,10 +12934,15 @@ async function handleRequest(request, socket) {
|
|
|
12858
12934
|
sendResponse(socket, {
|
|
12859
12935
|
id,
|
|
12860
12936
|
ok: false,
|
|
12861
|
-
error: `Session '${
|
|
12937
|
+
error: `Session '${logicalSession}' is shutting down.`,
|
|
12862
12938
|
errorInfo: {
|
|
12863
|
-
message: `Session '${
|
|
12864
|
-
code: "SESSION_SHUTTING_DOWN"
|
|
12939
|
+
message: `Session '${logicalSession}' is shutting down.`,
|
|
12940
|
+
code: "SESSION_SHUTTING_DOWN",
|
|
12941
|
+
details: {
|
|
12942
|
+
session: logicalSession,
|
|
12943
|
+
runtimeSession: session,
|
|
12944
|
+
scopeDir
|
|
12945
|
+
}
|
|
12865
12946
|
}
|
|
12866
12947
|
});
|
|
12867
12948
|
return;
|
|
@@ -12870,10 +12951,15 @@ async function handleRequest(request, socket) {
|
|
|
12870
12951
|
sendResponse(socket, {
|
|
12871
12952
|
id,
|
|
12872
12953
|
ok: false,
|
|
12873
|
-
error: `Session '${
|
|
12954
|
+
error: `Session '${logicalSession}' is shutting down. Retry your command.`,
|
|
12874
12955
|
errorInfo: {
|
|
12875
|
-
message: `Session '${
|
|
12876
|
-
code: "SESSION_SHUTTING_DOWN"
|
|
12956
|
+
message: `Session '${logicalSession}' is shutting down. Retry your command.`,
|
|
12957
|
+
code: "SESSION_SHUTTING_DOWN",
|
|
12958
|
+
details: {
|
|
12959
|
+
session: logicalSession,
|
|
12960
|
+
runtimeSession: session,
|
|
12961
|
+
scopeDir
|
|
12962
|
+
}
|
|
12877
12963
|
}
|
|
12878
12964
|
});
|
|
12879
12965
|
return;
|
|
@@ -12890,12 +12976,14 @@ async function handleRequest(request, socket) {
|
|
|
12890
12976
|
sendResponse(socket, {
|
|
12891
12977
|
id,
|
|
12892
12978
|
ok: false,
|
|
12893
|
-
error: `Session '${
|
|
12979
|
+
error: `Session '${logicalSession}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`,
|
|
12894
12980
|
errorInfo: {
|
|
12895
|
-
message: `Session '${
|
|
12981
|
+
message: `Session '${logicalSession}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`,
|
|
12896
12982
|
code: "SESSION_NAMESPACE_MISMATCH",
|
|
12897
12983
|
details: {
|
|
12898
|
-
session,
|
|
12984
|
+
session: logicalSession,
|
|
12985
|
+
runtimeSession: session,
|
|
12986
|
+
scopeDir,
|
|
12899
12987
|
activeNamespace: selectorNamespace,
|
|
12900
12988
|
requestedNamespace: requestedName
|
|
12901
12989
|
}
|
|
@@ -12904,9 +12992,9 @@ async function handleRequest(request, socket) {
|
|
|
12904
12992
|
return;
|
|
12905
12993
|
}
|
|
12906
12994
|
if (!selectorNamespace) {
|
|
12907
|
-
selectorNamespace = requestedName ??
|
|
12995
|
+
selectorNamespace = requestedName ?? logicalSession;
|
|
12908
12996
|
}
|
|
12909
|
-
const activeNamespace = selectorNamespace ??
|
|
12997
|
+
const activeNamespace = selectorNamespace ?? logicalSession;
|
|
12910
12998
|
if (instance && !launchPromise) {
|
|
12911
12999
|
try {
|
|
12912
13000
|
if (instance.page.isClosed()) {
|
|
@@ -12943,14 +13031,17 @@ async function handleRequest(request, socket) {
|
|
|
12943
13031
|
await launchPromise;
|
|
12944
13032
|
}
|
|
12945
13033
|
if (url) {
|
|
12946
|
-
await instance.
|
|
13034
|
+
await instance.goto(url);
|
|
12947
13035
|
}
|
|
12948
13036
|
sendResponse(socket, {
|
|
12949
13037
|
id,
|
|
12950
13038
|
ok: true,
|
|
12951
13039
|
result: {
|
|
12952
13040
|
url: instance.page.url(),
|
|
12953
|
-
session,
|
|
13041
|
+
session: logicalSession,
|
|
13042
|
+
logicalSession,
|
|
13043
|
+
runtimeSession: session,
|
|
13044
|
+
scopeDir,
|
|
12954
13045
|
name: activeNamespace,
|
|
12955
13046
|
cloudSessionId: instance.getCloudSessionId() ?? void 0,
|
|
12956
13047
|
cloudSessionUrl: instance.getCloudSessionUrl() ?? void 0
|
|
@@ -12992,12 +13083,14 @@ async function handleRequest(request, socket) {
|
|
|
12992
13083
|
sendResponse(socket, {
|
|
12993
13084
|
id,
|
|
12994
13085
|
ok: false,
|
|
12995
|
-
error: `No browser session in session '${
|
|
13086
|
+
error: `No browser session in session '${logicalSession}' (scope '${scopeDir}'). Call 'opensteer open --session ${logicalSession}' first, or use 'opensteer sessions' to list active sessions.`,
|
|
12996
13087
|
errorInfo: {
|
|
12997
|
-
message: `No browser session in session '${
|
|
13088
|
+
message: `No browser session in session '${logicalSession}' (scope '${scopeDir}'). Call 'opensteer open --session ${logicalSession}' first, or use 'opensteer sessions' to list active sessions.`,
|
|
12998
13089
|
code: "SESSION_NOT_OPEN",
|
|
12999
13090
|
details: {
|
|
13000
|
-
session
|
|
13091
|
+
session: logicalSession,
|
|
13092
|
+
runtimeSession: session,
|
|
13093
|
+
scopeDir
|
|
13001
13094
|
}
|
|
13002
13095
|
}
|
|
13003
13096
|
});
|
package/dist/cli/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Opensteer,
|
|
3
3
|
normalizeError
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-XIH3WGPY.js";
|
|
5
5
|
import "../chunk-3H5RRIMZ.js";
|
|
6
6
|
|
|
7
7
|
// src/cli/server.ts
|
|
@@ -20,6 +20,9 @@ function getSocketPath(session2) {
|
|
|
20
20
|
function getPidPath(session2) {
|
|
21
21
|
return join(tmpdir(), `${prefix(session2)}.pid`);
|
|
22
22
|
}
|
|
23
|
+
function getMetadataPath(session2) {
|
|
24
|
+
return join(tmpdir(), `${prefix(session2)}.meta.json`);
|
|
25
|
+
}
|
|
23
26
|
|
|
24
27
|
// src/cli/commands.ts
|
|
25
28
|
import { writeFile } from "fs/promises";
|
|
@@ -288,6 +291,8 @@ if (!sessionEnv) {
|
|
|
288
291
|
process.exit(1);
|
|
289
292
|
}
|
|
290
293
|
var session = sessionEnv;
|
|
294
|
+
var logicalSession = process.env.OPENSTEER_LOGICAL_SESSION?.trim() || session;
|
|
295
|
+
var scopeDir = process.env.OPENSTEER_SCOPE_DIR?.trim() || process.cwd();
|
|
291
296
|
var socketPath = getSocketPath(session);
|
|
292
297
|
var pidPath = getPidPath(session);
|
|
293
298
|
function cleanup() {
|
|
@@ -299,6 +304,10 @@ function cleanup() {
|
|
|
299
304
|
unlinkSync(pidPath);
|
|
300
305
|
} catch {
|
|
301
306
|
}
|
|
307
|
+
try {
|
|
308
|
+
unlinkSync(getMetadataPath(session));
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
302
311
|
}
|
|
303
312
|
function beginShutdown() {
|
|
304
313
|
if (shuttingDown) return;
|
|
@@ -340,10 +349,15 @@ async function handleRequest(request, socket) {
|
|
|
340
349
|
sendResponse(socket, {
|
|
341
350
|
id,
|
|
342
351
|
ok: false,
|
|
343
|
-
error: `Session '${
|
|
352
|
+
error: `Session '${logicalSession}' is shutting down.`,
|
|
344
353
|
errorInfo: {
|
|
345
|
-
message: `Session '${
|
|
346
|
-
code: "SESSION_SHUTTING_DOWN"
|
|
354
|
+
message: `Session '${logicalSession}' is shutting down.`,
|
|
355
|
+
code: "SESSION_SHUTTING_DOWN",
|
|
356
|
+
details: {
|
|
357
|
+
session: logicalSession,
|
|
358
|
+
runtimeSession: session,
|
|
359
|
+
scopeDir
|
|
360
|
+
}
|
|
347
361
|
}
|
|
348
362
|
});
|
|
349
363
|
return;
|
|
@@ -352,10 +366,15 @@ async function handleRequest(request, socket) {
|
|
|
352
366
|
sendResponse(socket, {
|
|
353
367
|
id,
|
|
354
368
|
ok: false,
|
|
355
|
-
error: `Session '${
|
|
369
|
+
error: `Session '${logicalSession}' is shutting down. Retry your command.`,
|
|
356
370
|
errorInfo: {
|
|
357
|
-
message: `Session '${
|
|
358
|
-
code: "SESSION_SHUTTING_DOWN"
|
|
371
|
+
message: `Session '${logicalSession}' is shutting down. Retry your command.`,
|
|
372
|
+
code: "SESSION_SHUTTING_DOWN",
|
|
373
|
+
details: {
|
|
374
|
+
session: logicalSession,
|
|
375
|
+
runtimeSession: session,
|
|
376
|
+
scopeDir
|
|
377
|
+
}
|
|
359
378
|
}
|
|
360
379
|
});
|
|
361
380
|
return;
|
|
@@ -372,12 +391,14 @@ async function handleRequest(request, socket) {
|
|
|
372
391
|
sendResponse(socket, {
|
|
373
392
|
id,
|
|
374
393
|
ok: false,
|
|
375
|
-
error: `Session '${
|
|
394
|
+
error: `Session '${logicalSession}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`,
|
|
376
395
|
errorInfo: {
|
|
377
|
-
message: `Session '${
|
|
396
|
+
message: `Session '${logicalSession}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`,
|
|
378
397
|
code: "SESSION_NAMESPACE_MISMATCH",
|
|
379
398
|
details: {
|
|
380
|
-
session,
|
|
399
|
+
session: logicalSession,
|
|
400
|
+
runtimeSession: session,
|
|
401
|
+
scopeDir,
|
|
381
402
|
activeNamespace: selectorNamespace,
|
|
382
403
|
requestedNamespace: requestedName
|
|
383
404
|
}
|
|
@@ -386,9 +407,9 @@ async function handleRequest(request, socket) {
|
|
|
386
407
|
return;
|
|
387
408
|
}
|
|
388
409
|
if (!selectorNamespace) {
|
|
389
|
-
selectorNamespace = requestedName ??
|
|
410
|
+
selectorNamespace = requestedName ?? logicalSession;
|
|
390
411
|
}
|
|
391
|
-
const activeNamespace = selectorNamespace ??
|
|
412
|
+
const activeNamespace = selectorNamespace ?? logicalSession;
|
|
392
413
|
if (instance && !launchPromise) {
|
|
393
414
|
try {
|
|
394
415
|
if (instance.page.isClosed()) {
|
|
@@ -425,14 +446,17 @@ async function handleRequest(request, socket) {
|
|
|
425
446
|
await launchPromise;
|
|
426
447
|
}
|
|
427
448
|
if (url) {
|
|
428
|
-
await instance.
|
|
449
|
+
await instance.goto(url);
|
|
429
450
|
}
|
|
430
451
|
sendResponse(socket, {
|
|
431
452
|
id,
|
|
432
453
|
ok: true,
|
|
433
454
|
result: {
|
|
434
455
|
url: instance.page.url(),
|
|
435
|
-
session,
|
|
456
|
+
session: logicalSession,
|
|
457
|
+
logicalSession,
|
|
458
|
+
runtimeSession: session,
|
|
459
|
+
scopeDir,
|
|
436
460
|
name: activeNamespace,
|
|
437
461
|
cloudSessionId: instance.getCloudSessionId() ?? void 0,
|
|
438
462
|
cloudSessionUrl: instance.getCloudSessionUrl() ?? void 0
|
|
@@ -474,12 +498,14 @@ async function handleRequest(request, socket) {
|
|
|
474
498
|
sendResponse(socket, {
|
|
475
499
|
id,
|
|
476
500
|
ok: false,
|
|
477
|
-
error: `No browser session in session '${
|
|
501
|
+
error: `No browser session in session '${logicalSession}' (scope '${scopeDir}'). Call 'opensteer open --session ${logicalSession}' first, or use 'opensteer sessions' to list active sessions.`,
|
|
478
502
|
errorInfo: {
|
|
479
|
-
message: `No browser session in session '${
|
|
503
|
+
message: `No browser session in session '${logicalSession}' (scope '${scopeDir}'). Call 'opensteer open --session ${logicalSession}' first, or use 'opensteer sessions' to list active sessions.`,
|
|
480
504
|
code: "SESSION_NOT_OPEN",
|
|
481
505
|
details: {
|
|
482
|
-
session
|
|
506
|
+
session: logicalSession,
|
|
507
|
+
runtimeSession: session,
|
|
508
|
+
scopeDir
|
|
483
509
|
}
|
|
484
510
|
}
|
|
485
511
|
});
|
package/dist/index.cjs
CHANGED
|
@@ -4303,8 +4303,56 @@ function applyCleaner(mode, html) {
|
|
|
4303
4303
|
return cleanForAction(html);
|
|
4304
4304
|
}
|
|
4305
4305
|
}
|
|
4306
|
+
function canonicalizeDuplicateNodeIds($) {
|
|
4307
|
+
const occurrencesByNodeId = /* @__PURE__ */ new Map();
|
|
4308
|
+
let order = 0;
|
|
4309
|
+
$("*").each(function() {
|
|
4310
|
+
const element = this;
|
|
4311
|
+
const nodeId = $(element).attr(OS_NODE_ID_ATTR);
|
|
4312
|
+
if (!nodeId) {
|
|
4313
|
+
order += 1;
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
const list = occurrencesByNodeId.get(nodeId) || [];
|
|
4317
|
+
list.push({
|
|
4318
|
+
element,
|
|
4319
|
+
order
|
|
4320
|
+
});
|
|
4321
|
+
occurrencesByNodeId.set(nodeId, list);
|
|
4322
|
+
order += 1;
|
|
4323
|
+
});
|
|
4324
|
+
for (const occurrences of occurrencesByNodeId.values()) {
|
|
4325
|
+
if (occurrences.length <= 1) continue;
|
|
4326
|
+
const canonical = pickCanonicalNodeIdOccurrence($, occurrences);
|
|
4327
|
+
for (const occurrence of occurrences) {
|
|
4328
|
+
if (occurrence.element === canonical.element) continue;
|
|
4329
|
+
$(occurrence.element).removeAttr(OS_NODE_ID_ATTR);
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
function pickCanonicalNodeIdOccurrence($, occurrences) {
|
|
4334
|
+
let best = occurrences[0];
|
|
4335
|
+
let bestScore = scoreNodeIdOccurrence($, best.element);
|
|
4336
|
+
for (let i = 1; i < occurrences.length; i += 1) {
|
|
4337
|
+
const candidate = occurrences[i];
|
|
4338
|
+
const candidateScore = scoreNodeIdOccurrence($, candidate.element);
|
|
4339
|
+
if (candidateScore > bestScore || candidateScore === bestScore && candidate.order < best.order) {
|
|
4340
|
+
best = candidate;
|
|
4341
|
+
bestScore = candidateScore;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
return best;
|
|
4345
|
+
}
|
|
4346
|
+
function scoreNodeIdOccurrence($, element) {
|
|
4347
|
+
const el = $(element);
|
|
4348
|
+
const descendantCount = el.find("*").length;
|
|
4349
|
+
const normalizedTextLength = el.text().replace(/\s+/g, " ").trim().length;
|
|
4350
|
+
const attributeCount = Object.keys(el.attr() || {}).length;
|
|
4351
|
+
return descendantCount * 100 + normalizedTextLength * 10 + attributeCount;
|
|
4352
|
+
}
|
|
4306
4353
|
async function assignCounters(page, html, nodePaths, nodeMeta) {
|
|
4307
4354
|
const $ = cheerio3.load(html, { xmlMode: false });
|
|
4355
|
+
canonicalizeDuplicateNodeIds($);
|
|
4308
4356
|
const counterIndex = /* @__PURE__ */ new Map();
|
|
4309
4357
|
let nextCounter = 1;
|
|
4310
4358
|
const assignedByNodeId = /* @__PURE__ */ new Map();
|
|
@@ -4486,44 +4534,63 @@ function stripNodeIds(html) {
|
|
|
4486
4534
|
$(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
|
|
4487
4535
|
return $.html();
|
|
4488
4536
|
}
|
|
4537
|
+
function isLiveCounterSyncFailure(error) {
|
|
4538
|
+
if (!(error instanceof Error)) return false;
|
|
4539
|
+
return error.message.startsWith(
|
|
4540
|
+
"Failed to synchronize snapshot counters with the live DOM:"
|
|
4541
|
+
);
|
|
4542
|
+
}
|
|
4489
4543
|
async function prepareSnapshot(page, options = {}) {
|
|
4490
4544
|
const mode = options.mode ?? "action";
|
|
4491
4545
|
const withCounters = options.withCounters ?? true;
|
|
4492
4546
|
const shouldMarkInteractive = options.markInteractive ?? true;
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4547
|
+
const maxAttempts = withCounters ? 4 : 1;
|
|
4548
|
+
let lastCounterSyncError = null;
|
|
4549
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
4550
|
+
if (shouldMarkInteractive) {
|
|
4551
|
+
await markInteractiveElements(page);
|
|
4552
|
+
}
|
|
4553
|
+
const serialized = await serializePageHTML(page);
|
|
4554
|
+
const rawHtml = serialized.html;
|
|
4555
|
+
const processedHtml = rawHtml;
|
|
4556
|
+
const reducedHtml = applyCleaner(mode, processedHtml);
|
|
4557
|
+
let cleanedHtml = reducedHtml;
|
|
4558
|
+
let counterIndex = null;
|
|
4559
|
+
if (withCounters) {
|
|
4560
|
+
try {
|
|
4561
|
+
const counted = await assignCounters(
|
|
4562
|
+
page,
|
|
4563
|
+
reducedHtml,
|
|
4564
|
+
serialized.nodePaths,
|
|
4565
|
+
serialized.nodeMeta
|
|
4566
|
+
);
|
|
4567
|
+
cleanedHtml = counted.html;
|
|
4568
|
+
counterIndex = counted.counterIndex;
|
|
4569
|
+
} catch (error) {
|
|
4570
|
+
if (attempt < maxAttempts && isLiveCounterSyncFailure(error)) {
|
|
4571
|
+
lastCounterSyncError = error;
|
|
4572
|
+
continue;
|
|
4573
|
+
}
|
|
4574
|
+
throw error;
|
|
4575
|
+
}
|
|
4576
|
+
} else {
|
|
4577
|
+
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
4578
|
+
}
|
|
4579
|
+
if (mode === "extraction") {
|
|
4580
|
+
const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
|
|
4581
|
+
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
4582
|
+
}
|
|
4583
|
+
return {
|
|
4584
|
+
mode,
|
|
4585
|
+
url: page.url(),
|
|
4586
|
+
rawHtml,
|
|
4587
|
+
processedHtml,
|
|
4505
4588
|
reducedHtml,
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
cleanedHtml = counted.html;
|
|
4510
|
-
counterIndex = counted.counterIndex;
|
|
4511
|
-
} else {
|
|
4512
|
-
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
4513
|
-
}
|
|
4514
|
-
if (mode === "extraction") {
|
|
4515
|
-
const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
|
|
4516
|
-
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
4589
|
+
cleanedHtml,
|
|
4590
|
+
counterIndex
|
|
4591
|
+
};
|
|
4517
4592
|
}
|
|
4518
|
-
|
|
4519
|
-
mode,
|
|
4520
|
-
url: page.url(),
|
|
4521
|
-
rawHtml,
|
|
4522
|
-
processedHtml,
|
|
4523
|
-
reducedHtml,
|
|
4524
|
-
cleanedHtml,
|
|
4525
|
-
counterIndex
|
|
4526
|
-
};
|
|
4593
|
+
throw lastCounterSyncError || new Error("Failed to prepare snapshot after retrying counter sync.");
|
|
4527
4594
|
}
|
|
4528
4595
|
|
|
4529
4596
|
// src/element-path/errors.ts
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opensteer",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Open-source browser automation SDK
|
|
3
|
+
"version": "0.5.5",
|
|
4
|
+
"description": "Open-source browser automation SDK and CLI that lets AI agents build complex scrapers directly in your codebase.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|