@reclaimprotocol/agent 0.1.24 → 0.1.25
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 +3 -2
- package/lib/cdp/claims.d.ts +21 -0
- package/lib/cdp/claims.js +129 -0
- package/lib/cdp/client.d.ts +0 -10
- package/lib/cdp/container.d.ts +85 -0
- package/lib/cdp/container.js +481 -0
- package/lib/live/tunnel.d.ts +6 -3
- package/lib/live/tunnel.js +57 -17
- package/lib/mcp/instructions.js +2 -1
- package/lib/mcp/start.js +58 -3
- package/lib/mcp/tools/agent/attach.d.ts +22 -2
- package/lib/mcp/tools/agent/attach.js +197 -55
- package/lib/mcp/tools/agent/build.d.ts +5 -0
- package/lib/mcp/tools/agent/build.js +9 -2
- package/lib/mcp/tools/agent/index.d.ts +1 -2
- package/lib/mcp/tools/agent/index.js +1 -1
- package/lib/mcp/tools/agent/live-view.d.ts +3 -7
- package/lib/mcp/tools/agent/live-view.js +74 -81
- package/lib/mcp/tools/agent/session.d.ts +14 -0
- package/lib/mcp/tools/agent/session.js +14 -8
- package/package.json +2 -2
- package/skill/SKILL.common.md +115 -38
- package/skill/builder-mode.md +9 -7
- package/skill/interception.md +3 -2
- package/skill/old-mode.md +10 -8
- package/src/cdp/claims.ts +153 -0
- package/src/cdp/client.ts +0 -8
- package/src/cdp/container.ts +593 -0
- package/src/live/tunnel.ts +71 -20
- package/src/mcp/instructions.ts +2 -1
- package/src/mcp/start.ts +65 -2
- package/src/mcp/tools/agent/attach.ts +229 -60
- package/src/mcp/tools/agent/build.ts +10 -2
- package/src/mcp/tools/agent/index.ts +6 -2
- package/src/mcp/tools/agent/live-view.ts +77 -93
- package/src/mcp/tools/agent/session.ts +14 -8
- package/lib/cdp/browser-proxy.d.ts +0 -78
- package/lib/cdp/browser-proxy.js +0 -186
- package/lib/live/page.d.ts +0 -20
- package/lib/live/page.js +0 -134
- package/lib/live/viewer.d.ts +0 -28
- package/lib/live/viewer.js +0 -232
- package/lib/mcp/tools/agent/proxy.d.ts +0 -3
- package/lib/mcp/tools/agent/proxy.js +0 -199
- package/src/live/page.ts +0 -145
- package/src/live/viewer.ts +0 -293
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which sessions are using which browser container.
|
|
3
|
+
*
|
|
4
|
+
* A container is worth sharing: about 2 GB of RAM and 7 to 17 seconds to start,
|
|
5
|
+
* so a developer running one session after another should get the same one back
|
|
6
|
+
* rather than pay for a new one each time.
|
|
7
|
+
*
|
|
8
|
+
* It can only be shared SEQUENTIALLY, and the profile is why. Every container
|
|
9
|
+
* mounts a host directory as Chromium's `user-data-dir`, and the runtime
|
|
10
|
+
* deletes `SingletonLock` on boot — so a second container on the same profile
|
|
11
|
+
* is a second writer on one cookie jar. Chromium does not support that, and the
|
|
12
|
+
* failure is silent corruption of the thing most worth keeping: the login.
|
|
13
|
+
*
|
|
14
|
+
* So each container gets a SLOT. A slot has one profile and at most one live
|
|
15
|
+
* claimant. A session adopts the first slot nobody holds; when the last claim
|
|
16
|
+
* on a slot goes, the container stops.
|
|
17
|
+
*
|
|
18
|
+
* State lives in a file because each MCP session is its own OS process, with
|
|
19
|
+
* no shared memory to count in. Claims are process ids, and liveness is a
|
|
20
|
+
* signal-0 check — the same test the orphan reaper uses. It fails the harmless
|
|
21
|
+
* way: a recycled pid keeps a container alive rather than pulling one out from
|
|
22
|
+
* under a running session.
|
|
23
|
+
*/
|
|
24
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { dirname, join } from 'node:path'
|
|
26
|
+
import { LOGGER } from '../logger.ts'
|
|
27
|
+
import { reclaimHomeDirPath } from '../paths.ts'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Slot → the claim tokens currently held on it.
|
|
31
|
+
*
|
|
32
|
+
* A token is `<pid>.<n>`, not a bare pid, because one session can legitimately
|
|
33
|
+
* hold a slot twice: re-attaching adopts the container it is already running,
|
|
34
|
+
* and for a moment both the new handle and the old one are outstanding. With
|
|
35
|
+
* bare pids those two collapse into one entry, so releasing the OLD handle
|
|
36
|
+
* stops the container the new one just adopted. The `<n>` keeps them distinct;
|
|
37
|
+
* the `<pid>` is what liveness is checked against.
|
|
38
|
+
*/
|
|
39
|
+
type ClaimFile = Record<string, string[]>
|
|
40
|
+
|
|
41
|
+
/** Distinguishes claims made by this same process. */
|
|
42
|
+
let claimSeq = 0
|
|
43
|
+
|
|
44
|
+
/** The pid a token belongs to, or 0 when it is malformed. */
|
|
45
|
+
function tokenPid(token: string): number {
|
|
46
|
+
const pid = Number(token.split('.')[0])
|
|
47
|
+
return Number.isInteger(pid) ? pid : 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function claimsPath(): string {
|
|
51
|
+
return join(reclaimHomeDirPath(), 'browser-runtime-claims.json')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function read(): ClaimFile {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(claimsPath(), 'utf8')) as ClaimFile
|
|
57
|
+
// Drop claims whose process is gone: that is what makes a slot free again
|
|
58
|
+
// after a session was killed without cleaning up.
|
|
59
|
+
const live: ClaimFile = {}
|
|
60
|
+
for(const [slot, tokens] of Object.entries(parsed)) {
|
|
61
|
+
const alive = (Array.isArray(tokens) ? tokens : [])
|
|
62
|
+
.filter((token) => processAlive(tokenPid(token)))
|
|
63
|
+
if(alive.length) {
|
|
64
|
+
live[slot] = alive
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return live
|
|
69
|
+
} catch{
|
|
70
|
+
// Missing or unreadable: nobody has claimed anything.
|
|
71
|
+
return {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function write(claims: ClaimFile): void {
|
|
76
|
+
const path = claimsPath()
|
|
77
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
78
|
+
// Write then rename, so a reader never sees a half-written file. Two sessions
|
|
79
|
+
// racing here means last-writer-wins on a file that is pruned on every read,
|
|
80
|
+
// which costs at most one redundant container.
|
|
81
|
+
const temp = `${path}.${process.pid}.tmp`
|
|
82
|
+
writeFileSync(temp, `${JSON.stringify(claims, null, '\t')}\n`)
|
|
83
|
+
renameSync(temp, path)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether a process is still running. Signal 0 checks without delivering. */
|
|
87
|
+
export function processAlive(pid: number): boolean {
|
|
88
|
+
if(!Number.isInteger(pid) || pid <= 0) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 0)
|
|
94
|
+
return true
|
|
95
|
+
} catch(err) {
|
|
96
|
+
// EPERM means it exists but belongs to another user — alive either way.
|
|
97
|
+
return (err as NodeJS.ErrnoException).code === 'EPERM'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Slots with at least one live claimant, so a caller can avoid them. */
|
|
102
|
+
export function busySlots(): Set<string> {
|
|
103
|
+
return new Set(Object.keys(read()))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Slots THIS process already holds.
|
|
108
|
+
*
|
|
109
|
+
* Its own claim must not push it onto a new slot: re-attaching in one session
|
|
110
|
+
* should come back to the browser it is already running, not start a second.
|
|
111
|
+
*/
|
|
112
|
+
export function ownSlots(): Set<string> {
|
|
113
|
+
const mine = new Set<string>()
|
|
114
|
+
for(const [slot, tokens] of Object.entries(read())) {
|
|
115
|
+
if(tokens.some((token) => tokenPid(token) === process.pid)) {
|
|
116
|
+
mine.add(slot)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return mine
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Take a claim on `slot`. Returns the token to release it with. */
|
|
124
|
+
export function claim(slot: string): string {
|
|
125
|
+
const token = `${process.pid}.${++claimSeq}`
|
|
126
|
+
const claims = read()
|
|
127
|
+
claims[slot] = [...claims[slot] ?? [], token]
|
|
128
|
+
write(claims)
|
|
129
|
+
LOGGER.debug({ slot, held: claims[slot] }, 'claimed a browser slot')
|
|
130
|
+
return token
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Give up this process's claim.
|
|
135
|
+
*
|
|
136
|
+
* Returns true when nobody is left, which is the caller's signal to stop the
|
|
137
|
+
* container. Returns false while another live session still holds it — stopping
|
|
138
|
+
* then would pull the browser out from under them.
|
|
139
|
+
*/
|
|
140
|
+
export function release(slot: string, token: string): boolean {
|
|
141
|
+
const claims = read()
|
|
142
|
+
const remaining = (claims[slot] ?? []).filter((held) => held !== token)
|
|
143
|
+
if(remaining.length) {
|
|
144
|
+
claims[slot] = remaining
|
|
145
|
+
write(claims)
|
|
146
|
+
LOGGER.debug({ slot, remaining }, 'released a claim; others still hold it')
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
delete claims[slot]
|
|
151
|
+
write(claims)
|
|
152
|
+
return true
|
|
153
|
+
}
|
package/src/cdp/client.ts
CHANGED
|
@@ -30,14 +30,6 @@ interface CdpTabEventMap {
|
|
|
30
30
|
headers: Record<string, string>
|
|
31
31
|
}
|
|
32
32
|
'Network.loadingFinished': { requestId: string }
|
|
33
|
-
/** Screencast frames, for the live view (`live/viewer.ts`). Each carries a
|
|
34
|
-
* `sessionId` that MUST be acked with `Page.screencastFrameAck`, or CDP
|
|
35
|
-
* stops sending. */
|
|
36
|
-
'Page.screencastFrame': {
|
|
37
|
-
data: string
|
|
38
|
-
sessionId: number
|
|
39
|
-
metadata?: { timestamp?: number }
|
|
40
|
-
}
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
export interface CdpTabClient {
|