browse 0.9.1 → 0.9.2-alpha-ad9825f
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 +1 -2
- package/dist/commands/cloud/contexts/add.js +45 -0
- package/dist/commands/cloud/contexts/create.js +27 -2
- package/dist/commands/cloud/contexts/delete.js +29 -5
- package/dist/commands/cloud/contexts/get.js +12 -4
- package/dist/commands/cloud/contexts/list.js +48 -0
- package/dist/commands/cloud/contexts/update.js +11 -3
- package/dist/commands/cloud/sessions/create.js +8 -2
- package/dist/commands/open.js +1 -0
- package/dist/lib/cloud/contexts-resolve.js +23 -0
- package/dist/lib/cloud/contexts-store.js +209 -0
- package/dist/lib/driver/command-cli.js +8 -2
- package/dist/lib/driver/commands/snapshot.js +0 -8
- package/dist/lib/driver/commands/types.js +0 -1
- package/dist/lib/driver/daemon/client.js +3 -0
- package/dist/lib/driver/daemon/forwarded-env.js +39 -0
- package/dist/lib/driver/daemon/protocol.js +3 -0
- package/dist/lib/driver/daemon/server.js +2 -0
- package/dist/lib/driver/doctor.js +34 -1
- package/dist/lib/driver/flags.js +6 -0
- package/dist/lib/driver/mode.js +24 -0
- package/dist/lib/driver/remote.disabled.js +7 -0
- package/dist/lib/driver/remote.js +24 -4
- package/dist/lib/driver/session-manager.js +55 -4
- package/dist/lib/identity.js +1 -1
- package/oclif.manifest.json +501 -112
- package/package.json +2 -2
- package/skills/browse/SKILL.md +21 -6
- package/dist/commands/refs.js +0 -13
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ browse back # Navigate backward
|
|
|
84
84
|
browse forward # Navigate forward
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
### Snapshot
|
|
87
|
+
### Snapshot
|
|
88
88
|
|
|
89
89
|
The accessibility snapshot is the recommended way for agents to discover elements. It prints a tree of refs like `@0-12` that the element commands accept directly.
|
|
90
90
|
|
|
@@ -93,7 +93,6 @@ browse snapshot # Accessibility snapshot + cached refs
|
|
|
93
93
|
browse snapshot --compact # Tree only, no ref maps
|
|
94
94
|
browse snapshot --filter submit # Filter lines by text or /regex/, keeping ancestors
|
|
95
95
|
browse snapshot --max-depth 4 # Trim output deeper than this depth
|
|
96
|
-
browse refs # Show refs cached from the last snapshot
|
|
97
96
|
```
|
|
98
97
|
|
|
99
98
|
### Element actions
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Args, Flags } from "@oclif/core";
|
|
2
|
+
import { contextNameRequirement, getContextAlias, isValidContextName, saveContextAlias, } from "../../../lib/cloud/contexts-store.js";
|
|
3
|
+
import { fail } from "../../../lib/errors.js";
|
|
4
|
+
import { outputJson } from "../../../lib/output.js";
|
|
5
|
+
import { BrowseCommand } from "../../../base.js";
|
|
6
|
+
export default class ContextsAdd extends BrowseCommand {
|
|
7
|
+
static description = "Save a local name for an existing Browserbase context ID so you can reuse it by name (e.g. an ID a teammate shared or one created on another device).";
|
|
8
|
+
static examples = [
|
|
9
|
+
"browse cloud contexts add github 45ed525f-63a5-490d-b4c4-853f50643b90",
|
|
10
|
+
"browse cloud contexts add github <new-id> --force",
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
name: Args.string({ required: true, description: "Local name to save." }),
|
|
14
|
+
id: Args.string({
|
|
15
|
+
required: true,
|
|
16
|
+
description: "Existing Browserbase context ID.",
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
static flags = {
|
|
20
|
+
force: Flags.boolean({
|
|
21
|
+
description: "Overwrite the name if it is already saved locally.",
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { args, flags } = await this.parse(ContextsAdd);
|
|
26
|
+
if (!isValidContextName(args.name)) {
|
|
27
|
+
fail(`Invalid context name "${args.name}". ${contextNameRequirement()}`);
|
|
28
|
+
}
|
|
29
|
+
// Normalize before storing so whitespace-padded input isn't saved and later
|
|
30
|
+
// resolved as a bogus context id.
|
|
31
|
+
const id = args.id.trim();
|
|
32
|
+
if (id.length === 0) {
|
|
33
|
+
fail("Context ID cannot be empty.");
|
|
34
|
+
}
|
|
35
|
+
if (!flags.force && (await getContextAlias(args.name))) {
|
|
36
|
+
fail(`A context named "${args.name}" already exists locally. Pass --force to overwrite, ` +
|
|
37
|
+
`or remove it with "browse cloud contexts delete ${args.name}".`);
|
|
38
|
+
}
|
|
39
|
+
await saveContextAlias(args.name, {
|
|
40
|
+
id,
|
|
41
|
+
createdAt: new Date().toISOString(),
|
|
42
|
+
});
|
|
43
|
+
outputJson({ name: args.name, id });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
2
|
import { createBrowserbaseClient, outputJson, resolveBody, withBrowserbaseApi, } from "../../../lib/cloud/api.js";
|
|
3
|
+
import { contextNameRequirement, getContextAlias, isValidContextName, saveContextAlias, } from "../../../lib/cloud/contexts-store.js";
|
|
3
4
|
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
5
|
+
import { fail } from "../../../lib/errors.js";
|
|
4
6
|
import { BrowseCommand } from "../../../base.js";
|
|
5
7
|
export default class ContextsCreate extends BrowseCommand {
|
|
6
|
-
static description = "Create a Browserbase context.";
|
|
8
|
+
static description = "Create a Browserbase context. Pass --name to save a local alias you can reuse instead of the context ID.";
|
|
7
9
|
static examples = [
|
|
8
10
|
"browse cloud contexts create",
|
|
11
|
+
"browse cloud contexts create --name github",
|
|
9
12
|
`browse cloud contexts create --body '{"region":"us-west-2"}'`,
|
|
10
13
|
`echo '{"region":"us-west-2"}' | browse cloud contexts create --stdin`,
|
|
11
14
|
];
|
|
12
15
|
static flags = {
|
|
13
16
|
...apiCommonFlags,
|
|
17
|
+
name: Flags.string({
|
|
18
|
+
description: "Save a local alias for the new context so you can reuse it by name.",
|
|
19
|
+
helpValue: "<name>",
|
|
20
|
+
}),
|
|
14
21
|
body: Flags.string({
|
|
15
22
|
description: "Optional JSON request body.",
|
|
16
23
|
helpValue: "<body>",
|
|
@@ -21,10 +28,28 @@ export default class ContextsCreate extends BrowseCommand {
|
|
|
21
28
|
};
|
|
22
29
|
async run() {
|
|
23
30
|
const { flags } = await this.parse(ContextsCreate);
|
|
31
|
+
const name = flags.name;
|
|
32
|
+
if (name !== undefined) {
|
|
33
|
+
if (!isValidContextName(name)) {
|
|
34
|
+
fail(`Invalid context name "${name}". ${contextNameRequirement()}`);
|
|
35
|
+
}
|
|
36
|
+
if (await getContextAlias(name)) {
|
|
37
|
+
fail(`A context named "${name}" already exists locally. Choose another name or remove it with "browse cloud contexts delete ${name}".`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
24
40
|
await withBrowserbaseApi("contexts", async () => {
|
|
25
41
|
const client = createBrowserbaseClient(toApiOptions(flags));
|
|
26
42
|
const body = await resolveBody({ body: flags.body, stdin: flags.stdin });
|
|
27
|
-
|
|
43
|
+
const context = await client.contexts.create(body);
|
|
44
|
+
if (name !== undefined && context.id) {
|
|
45
|
+
await saveContextAlias(name, {
|
|
46
|
+
id: context.id,
|
|
47
|
+
createdAt: new Date().toISOString(),
|
|
48
|
+
});
|
|
49
|
+
outputJson({ ...context, name });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
outputJson(context);
|
|
28
53
|
});
|
|
29
54
|
}
|
|
30
55
|
}
|
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
import { Args } from "@oclif/core";
|
|
2
2
|
import { outputJson, requestBrowserbase } from "../../../lib/cloud/api.js";
|
|
3
|
+
import { resolveContextRefOrFail } from "../../../lib/cloud/contexts-resolve.js";
|
|
4
|
+
import { removeContextAliasesById } from "../../../lib/cloud/contexts-store.js";
|
|
3
5
|
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
6
|
import { BrowseCommand } from "../../../base.js";
|
|
5
7
|
export default class ContextsDelete extends BrowseCommand {
|
|
6
|
-
static description = "Delete a Browserbase context.";
|
|
7
|
-
static examples = [
|
|
8
|
+
static description = "Delete a Browserbase context by ID or saved name.";
|
|
9
|
+
static examples = [
|
|
10
|
+
"browse cloud contexts delete <context-id>",
|
|
11
|
+
"browse cloud contexts delete github",
|
|
12
|
+
];
|
|
8
13
|
static args = {
|
|
9
|
-
id: Args.string({
|
|
14
|
+
id: Args.string({
|
|
15
|
+
required: true,
|
|
16
|
+
description: "Context ID or saved name.",
|
|
17
|
+
}),
|
|
10
18
|
};
|
|
11
19
|
static flags = { ...apiCommonFlags };
|
|
12
20
|
async run() {
|
|
13
21
|
const { args, flags } = await this.parse(ContextsDelete);
|
|
14
|
-
await
|
|
22
|
+
const id = await resolveContextRefOrFail(args.id);
|
|
23
|
+
await requestBrowserbase(toApiOptions(flags), `/v1/contexts/${id}`, {
|
|
15
24
|
method: "DELETE",
|
|
16
25
|
headers: {
|
|
17
26
|
Accept: "*/*",
|
|
18
27
|
},
|
|
19
28
|
});
|
|
20
|
-
|
|
29
|
+
// Keep the local name map consistent: drop any aliases pointing at the
|
|
30
|
+
// now-deleted context (whether the user passed a name or a raw id). The
|
|
31
|
+
// remote delete already succeeded, so a local cleanup failure must not turn
|
|
32
|
+
// the command into a false negative — best-effort, never throws.
|
|
33
|
+
let removed;
|
|
34
|
+
try {
|
|
35
|
+
removed = await removeContextAliasesById(id);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
removed = [];
|
|
39
|
+
}
|
|
40
|
+
outputJson({
|
|
41
|
+
ok: true,
|
|
42
|
+
id,
|
|
43
|
+
...(removed.length ? { removedAliases: removed } : {}),
|
|
44
|
+
});
|
|
21
45
|
}
|
|
22
46
|
}
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import { Args } from "@oclif/core";
|
|
2
2
|
import { createBrowserbaseClient, outputJson, withBrowserbaseApi, } from "../../../lib/cloud/api.js";
|
|
3
|
+
import { resolveContextRefOrFail } from "../../../lib/cloud/contexts-resolve.js";
|
|
3
4
|
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
5
|
import { BrowseCommand } from "../../../base.js";
|
|
5
6
|
export default class ContextsGet extends BrowseCommand {
|
|
6
|
-
static description = "Get a Browserbase context by ID.";
|
|
7
|
-
static examples = [
|
|
7
|
+
static description = "Get a Browserbase context by ID or saved name.";
|
|
8
|
+
static examples = [
|
|
9
|
+
"browse cloud contexts get <context-id>",
|
|
10
|
+
"browse cloud contexts get github",
|
|
11
|
+
];
|
|
8
12
|
static args = {
|
|
9
|
-
id: Args.string({
|
|
13
|
+
id: Args.string({
|
|
14
|
+
required: true,
|
|
15
|
+
description: "Context ID or saved name.",
|
|
16
|
+
}),
|
|
10
17
|
};
|
|
11
18
|
static flags = { ...apiCommonFlags };
|
|
12
19
|
async run() {
|
|
13
20
|
const { args, flags } = await this.parse(ContextsGet);
|
|
21
|
+
const id = await resolveContextRefOrFail(args.id);
|
|
14
22
|
await withBrowserbaseApi("contexts", async () => {
|
|
15
23
|
const client = createBrowserbaseClient(toApiOptions(flags));
|
|
16
|
-
outputJson(await client.contexts.retrieve(
|
|
24
|
+
outputJson(await client.contexts.retrieve(id));
|
|
17
25
|
});
|
|
18
26
|
}
|
|
19
27
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { outputJson } from "../../../lib/cloud/api.js";
|
|
2
|
+
import { listContextAliases, } from "../../../lib/cloud/contexts-store.js";
|
|
3
|
+
import { BrowseCommand } from "../../../base.js";
|
|
4
|
+
import { formatId, formatUtcDateTime, outputFormatFlags, outputTable, resolveOutputFormat, } from "../../../lib/output.js";
|
|
5
|
+
export default class ContextsList extends BrowseCommand {
|
|
6
|
+
static description = "List Browserbase contexts you have saved locally with a name.";
|
|
7
|
+
static examples = [
|
|
8
|
+
"browse cloud contexts list",
|
|
9
|
+
"browse cloud contexts list --json",
|
|
10
|
+
];
|
|
11
|
+
static flags = {
|
|
12
|
+
...outputFormatFlags,
|
|
13
|
+
};
|
|
14
|
+
async run() {
|
|
15
|
+
const { flags } = await this.parse(ContextsList);
|
|
16
|
+
const contexts = await listContextAliases();
|
|
17
|
+
if (resolveOutputFormat(flags) === "json") {
|
|
18
|
+
// Wrap in a named key to match `templates list` / `skills list` so the
|
|
19
|
+
// JSON shape is consistent and machine-readable across list commands.
|
|
20
|
+
outputJson({ contexts });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (contexts.length === 0) {
|
|
24
|
+
console.log("No saved contexts. Create one with: browse cloud contexts create --name <name>");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
outputContextsTable(contexts, { wide: flags.wide });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function outputContextsTable(contexts, options) {
|
|
31
|
+
outputTable(contexts, [
|
|
32
|
+
{
|
|
33
|
+
header: "Name",
|
|
34
|
+
maxWidth: 24,
|
|
35
|
+
value: (context) => context.name,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
header: "ID",
|
|
39
|
+
maxWidth: 12,
|
|
40
|
+
value: (context) => formatId(context.id, options.wide),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
header: "Created",
|
|
44
|
+
maxWidth: 17,
|
|
45
|
+
value: (context) => formatUtcDateTime(context.createdAt),
|
|
46
|
+
},
|
|
47
|
+
], { wide: options.wide });
|
|
48
|
+
}
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { Args } from "@oclif/core";
|
|
2
2
|
import { outputJson, requestBrowserbaseJson } from "../../../lib/cloud/api.js";
|
|
3
|
+
import { resolveContextRefOrFail } from "../../../lib/cloud/contexts-resolve.js";
|
|
3
4
|
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
5
|
import { BrowseCommand } from "../../../base.js";
|
|
5
6
|
export default class ContextsUpdate extends BrowseCommand {
|
|
6
7
|
static description = "Refresh the upload URL for a Browserbase context.";
|
|
7
|
-
static examples = [
|
|
8
|
+
static examples = [
|
|
9
|
+
"browse cloud contexts update <context-id>",
|
|
10
|
+
"browse cloud contexts update github",
|
|
11
|
+
];
|
|
8
12
|
static args = {
|
|
9
|
-
id: Args.string({
|
|
13
|
+
id: Args.string({
|
|
14
|
+
required: true,
|
|
15
|
+
description: "Context ID or saved name.",
|
|
16
|
+
}),
|
|
10
17
|
};
|
|
11
18
|
static flags = { ...apiCommonFlags };
|
|
12
19
|
async run() {
|
|
13
20
|
const { args, flags } = await this.parse(ContextsUpdate);
|
|
14
|
-
|
|
21
|
+
const id = await resolveContextRefOrFail(args.id);
|
|
22
|
+
outputJson(await requestBrowserbaseJson(toApiOptions(flags), `/v1/contexts/${id}`, {
|
|
15
23
|
method: "PUT",
|
|
16
24
|
}));
|
|
17
25
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
2
|
import { createBrowserbaseClient, deepMerge, outputJson, resolveBody, withBrowserbaseApi, } from "../../../lib/cloud/api.js";
|
|
3
|
+
import { resolveContextRefOrFail } from "../../../lib/cloud/contexts-resolve.js";
|
|
3
4
|
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
5
|
import { fail } from "../../../lib/errors.js";
|
|
5
6
|
import { getCliVersion, resolveInstallId, toMetadataValue, } from "../../../lib/identity.js";
|
|
@@ -137,8 +138,8 @@ export default class SessionsCreate extends BrowseCommand {
|
|
|
137
138
|
helpValue: "<seconds>",
|
|
138
139
|
}),
|
|
139
140
|
"context-id": Flags.string({
|
|
140
|
-
description: "Browserbase context ID for persistent state.",
|
|
141
|
-
helpValue: "<id>",
|
|
141
|
+
description: "Browserbase context ID, or a name saved with 'contexts create --name', for persistent state.",
|
|
142
|
+
helpValue: "<id|name>",
|
|
142
143
|
}),
|
|
143
144
|
persist: Flags.boolean({
|
|
144
145
|
description: "Persist context changes after session ends.",
|
|
@@ -162,6 +163,11 @@ export default class SessionsCreate extends BrowseCommand {
|
|
|
162
163
|
};
|
|
163
164
|
async run() {
|
|
164
165
|
const { flags } = await this.parse(SessionsCreate);
|
|
166
|
+
// Allow --context-id to be a locally-saved name; resolve it to a real id.
|
|
167
|
+
// A context id passes through; an unknown name fails with a helpful message.
|
|
168
|
+
if (flags["context-id"]) {
|
|
169
|
+
flags["context-id"] = await resolveContextRefOrFail(flags["context-id"]);
|
|
170
|
+
}
|
|
165
171
|
await withBrowserbaseApi("sessions", async () => {
|
|
166
172
|
const client = createBrowserbaseClient(toApiOptions(flags));
|
|
167
173
|
const jsonBody = await resolveBody({
|
package/dist/commands/open.js
CHANGED
|
@@ -7,6 +7,7 @@ export default class Open extends BrowseCommand {
|
|
|
7
7
|
"browse open https://example.com",
|
|
8
8
|
"browse open https://example.com --local --headed",
|
|
9
9
|
"browse open https://example.com --remote",
|
|
10
|
+
"browse open https://example.com --remote --verified --proxies",
|
|
10
11
|
"browse open https://example.com --auto-connect",
|
|
11
12
|
"browse open https://example.com --cdp 9222",
|
|
12
13
|
"browse open https://example.com --cdp ws://127.0.0.1:9222/devtools/browser/<id> --target-id <target-id>",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fail } from "../errors.js";
|
|
2
|
+
import { resolveContextRefDetailed } from "./contexts-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a context name-or-id for a command. A saved name or a context id
|
|
5
|
+
* resolves to its id. When a ref is neither but is close to a saved name, we
|
|
6
|
+
* treat it as a typo and fail with a "did you mean?" hint instead of sending a
|
|
7
|
+
* bogus id. Otherwise we pass the ref through unchanged so raw ids of any shape
|
|
8
|
+
* still reach the API — preserving raw-id compatibility.
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveContextRefOrFail(ref) {
|
|
11
|
+
const { id, suggestions } = await resolveContextRefDetailed(ref);
|
|
12
|
+
if (id !== null) {
|
|
13
|
+
return id;
|
|
14
|
+
}
|
|
15
|
+
if (suggestions.length === 0) {
|
|
16
|
+
// Not a known name and not close to one — could be a raw id we don't
|
|
17
|
+
// recognize the shape of. Let the API be the judge rather than blocking it.
|
|
18
|
+
return ref;
|
|
19
|
+
}
|
|
20
|
+
return fail(`No saved context named "${ref}". Did you mean: ${suggestions.join(", ")}? ` +
|
|
21
|
+
`Pass a Browserbase context ID, save one with "browse cloud contexts create --name <name>", ` +
|
|
22
|
+
`or list saved names with "browse cloud contexts list".`);
|
|
23
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { distance } from "fastest-levenshtein";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { resolveConfigDir } from "../identity.js";
|
|
5
|
+
/**
|
|
6
|
+
* Local name -> Browserbase context-id map.
|
|
7
|
+
*
|
|
8
|
+
* Browserbase contexts are identified only by an opaque id and the platform has
|
|
9
|
+
* no server-side list endpoint, so to give contexts memorable names (e.g.
|
|
10
|
+
* `github`, `gmail`) we keep a small map on the local device. It lives next to
|
|
11
|
+
* the CLI's other state at `(XDG_CONFIG_HOME||~/.config)/browserbase/contexts.json`
|
|
12
|
+
* (honoring `BROWSERBASE_CONFIG_DIR`). This is purely a client-side convenience:
|
|
13
|
+
* the ids it stores are the same ids the API already returns, and a missing or
|
|
14
|
+
* corrupt file degrades to "no saved contexts" rather than an error.
|
|
15
|
+
*/
|
|
16
|
+
const STORE_VERSION = 1;
|
|
17
|
+
const MAX_NAME_LENGTH = 64;
|
|
18
|
+
// A name must start with an alphanumeric and may then contain letters, digits,
|
|
19
|
+
// dots, dashes, and underscores. Keeps names shell- and filename-friendly and
|
|
20
|
+
// unambiguous against opaque context ids.
|
|
21
|
+
const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
22
|
+
// Browserbase context ids are UUIDs. A name must not be UUID-shaped, otherwise a
|
|
23
|
+
// saved alias could shadow a real id and break raw-id passthrough in resolution.
|
|
24
|
+
const CONTEXT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
25
|
+
export function looksLikeContextId(value) {
|
|
26
|
+
return CONTEXT_ID_PATTERN.test(value);
|
|
27
|
+
}
|
|
28
|
+
export function isValidContextName(name) {
|
|
29
|
+
return (name.length > 0 &&
|
|
30
|
+
name.length <= MAX_NAME_LENGTH &&
|
|
31
|
+
NAME_PATTERN.test(name) &&
|
|
32
|
+
// Disallow id-shaped names so a name can never shadow a raw context id.
|
|
33
|
+
!looksLikeContextId(name));
|
|
34
|
+
}
|
|
35
|
+
export function contextNameRequirement() {
|
|
36
|
+
return `Context names must be 1-${MAX_NAME_LENGTH} characters, start with a letter or number, contain only letters, numbers, dots, dashes, or underscores, and not look like a context ID.`;
|
|
37
|
+
}
|
|
38
|
+
export function contextsStorePath(env = process.env) {
|
|
39
|
+
return join(resolveConfigDir(env), "contexts.json");
|
|
40
|
+
}
|
|
41
|
+
function emptyStore() {
|
|
42
|
+
// Null-prototype map so a ref like "toString" or "constructor" can never read
|
|
43
|
+
// an inherited Object.prototype member as if it were a saved alias.
|
|
44
|
+
return {
|
|
45
|
+
version: STORE_VERSION,
|
|
46
|
+
contexts: Object.create(null),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function readStore(env = process.env) {
|
|
50
|
+
let raw;
|
|
51
|
+
try {
|
|
52
|
+
raw = await readFile(contextsStorePath(env), "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return emptyStore();
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (!parsed ||
|
|
60
|
+
typeof parsed !== "object" ||
|
|
61
|
+
typeof parsed.contexts !== "object" ||
|
|
62
|
+
parsed.contexts === null) {
|
|
63
|
+
return emptyStore();
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
version: STORE_VERSION,
|
|
67
|
+
contexts: sanitizeContexts(parsed.contexts),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Corrupt file: treat as empty rather than crashing the command.
|
|
72
|
+
return emptyStore();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Keep only well-formed entries so a hand-edited or partially-written
|
|
77
|
+
* `contexts.json` degrades gracefully (a malformed entry is dropped rather than
|
|
78
|
+
* trusted as a `ContextAlias` and breaking resolution or cleanup).
|
|
79
|
+
*/
|
|
80
|
+
function sanitizeContexts(raw) {
|
|
81
|
+
// Null-prototype so prototype keys can't be read back as aliases (see emptyStore).
|
|
82
|
+
const result = Object.create(null);
|
|
83
|
+
if (!raw || typeof raw !== "object") {
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
87
|
+
// Validate the key too: a hand-edited file could carry an id-shaped or
|
|
88
|
+
// otherwise invalid name that would shadow a raw id, so hold disk entries to
|
|
89
|
+
// the same rule as `create --name`.
|
|
90
|
+
if (!isValidContextName(name)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!value || typeof value !== "object") {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const entry = value;
|
|
97
|
+
if (typeof entry.id !== "string" || entry.id.length === 0) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
result[name] = {
|
|
101
|
+
id: entry.id,
|
|
102
|
+
createdAt: typeof entry.createdAt === "string" ? entry.createdAt : "",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
async function writeStore(store, env = process.env) {
|
|
108
|
+
const path = contextsStorePath(env);
|
|
109
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
110
|
+
// Write to a temp file at 0600 then atomically rename over the target. The
|
|
111
|
+
// rename means a reader never sees a half-written file, and the final file
|
|
112
|
+
// always inherits the temp file's 0600 perms even if an older, more permissive
|
|
113
|
+
// contexts.json already existed (writeFile's `mode` only applies on create).
|
|
114
|
+
// For this single-user local config, simultaneous writers remain
|
|
115
|
+
// last-writer-wins; cross-process locking isn't warranted here.
|
|
116
|
+
const tempPath = `${path}.${process.pid}.tmp`;
|
|
117
|
+
await writeFile(tempPath, `${JSON.stringify(store, null, 2)}\n`, {
|
|
118
|
+
encoding: "utf8",
|
|
119
|
+
mode: 0o600,
|
|
120
|
+
});
|
|
121
|
+
await rename(tempPath, path);
|
|
122
|
+
}
|
|
123
|
+
export async function listContextAliases(env = process.env) {
|
|
124
|
+
const store = await readStore(env);
|
|
125
|
+
return Object.entries(store.contexts)
|
|
126
|
+
.map(([name, alias]) => ({ name, ...alias }))
|
|
127
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
128
|
+
}
|
|
129
|
+
export async function getContextAlias(name, env = process.env) {
|
|
130
|
+
const store = await readStore(env);
|
|
131
|
+
return store.contexts[name];
|
|
132
|
+
}
|
|
133
|
+
export async function saveContextAlias(name, alias, env = process.env) {
|
|
134
|
+
const store = await readStore(env);
|
|
135
|
+
store.contexts[name] = alias;
|
|
136
|
+
await writeStore(store, env);
|
|
137
|
+
}
|
|
138
|
+
export async function removeContextAlias(name, env = process.env) {
|
|
139
|
+
const store = await readStore(env);
|
|
140
|
+
if (!(name in store.contexts)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
delete store.contexts[name];
|
|
144
|
+
await writeStore(store, env);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Drop any saved aliases that point at a given context id. Used after a delete
|
|
149
|
+
* so the local map never references a context that no longer exists, regardless
|
|
150
|
+
* of whether the user deleted it by name or by raw id. Returns the names pruned.
|
|
151
|
+
*/
|
|
152
|
+
export async function removeContextAliasesById(id, env = process.env) {
|
|
153
|
+
const store = await readStore(env);
|
|
154
|
+
const removed = Object.entries(store.contexts)
|
|
155
|
+
.filter(([, alias]) => alias.id === id)
|
|
156
|
+
.map(([name]) => name);
|
|
157
|
+
if (removed.length === 0) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
for (const name of removed) {
|
|
161
|
+
delete store.contexts[name];
|
|
162
|
+
}
|
|
163
|
+
await writeStore(store, env);
|
|
164
|
+
return removed;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a context reference that may be a locally-saved name or a raw
|
|
168
|
+
* Browserbase context id. If `ref` matches a saved name, returns its id;
|
|
169
|
+
* otherwise returns `ref` unchanged (assumed to already be a context id). Never
|
|
170
|
+
* throws — an unknown ref simply passes through to the API.
|
|
171
|
+
*/
|
|
172
|
+
export async function resolveContextRef(ref, env = process.env) {
|
|
173
|
+
const alias = await getContextAlias(ref, env);
|
|
174
|
+
return alias ? alias.id : ref;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Like `resolveContextRef`, but distinguishes "this is an unknown name" from a
|
|
178
|
+
* real id so callers can show a friendly error instead of letting a typo'd name
|
|
179
|
+
* hit the API as a bogus id. A ref resolves when it matches a saved name or is
|
|
180
|
+
* shaped like a context id (UUID); anything else returns `id: null` plus the
|
|
181
|
+
* closest saved names.
|
|
182
|
+
*/
|
|
183
|
+
export async function resolveContextRefDetailed(ref, env = process.env) {
|
|
184
|
+
const store = await readStore(env);
|
|
185
|
+
const alias = store.contexts[ref];
|
|
186
|
+
if (alias) {
|
|
187
|
+
return { id: alias.id, suggestions: [] };
|
|
188
|
+
}
|
|
189
|
+
if (looksLikeContextId(ref)) {
|
|
190
|
+
return { id: ref, suggestions: [] };
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
id: null,
|
|
194
|
+
suggestions: closeContextNameMatches(ref, Object.keys(store.contexts)),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Saved names within a small edit distance of `ref`, nearest first, for typo
|
|
199
|
+
* hints. The threshold scales with name length so short names aren't matched too
|
|
200
|
+
* loosely.
|
|
201
|
+
*/
|
|
202
|
+
export function closeContextNameMatches(ref, names, limit = 3) {
|
|
203
|
+
return names
|
|
204
|
+
.map((name) => ({ name, d: distance(ref, name) }))
|
|
205
|
+
.filter(({ name, d }) => d <= Math.max(2, Math.floor(name.length / 3)))
|
|
206
|
+
.sort((a, b) => a.d - b.d || a.name.localeCompare(b.name))
|
|
207
|
+
.slice(0, limit)
|
|
208
|
+
.map((match) => match.name);
|
|
209
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
|
-
import { autoConnectFlag, cdpFlag, chromeArgFlag, headedFlag, headlessFlag, ignoreDefaultChromeArgFlag, localFlag, noDefaultChromeArgsFlag, remoteFlag, sessionFlag, sessionName, targetIdFlag, } from "./flags.js";
|
|
2
|
+
import { autoConnectFlag, cdpFlag, chromeArgFlag, headedFlag, headlessFlag, ignoreDefaultChromeArgFlag, localFlag, noDefaultChromeArgsFlag, proxiesFlag, remoteFlag, sessionFlag, sessionName, targetIdFlag, verifiedFlag, } from "./flags.js";
|
|
3
3
|
import { getDriverStatus } from "./daemon/client.js";
|
|
4
4
|
import { hasChromeArgFlags, resolveConnectionTarget, } from "./mode.js";
|
|
5
5
|
import { outputJson } from "../output.js";
|
|
@@ -13,9 +13,11 @@ export const driverCommandFlags = {
|
|
|
13
13
|
"ignore-default-chrome-arg": ignoreDefaultChromeArgFlag,
|
|
14
14
|
local: localFlag,
|
|
15
15
|
"no-default-chrome-args": noDefaultChromeArgsFlag,
|
|
16
|
+
proxies: proxiesFlag,
|
|
16
17
|
remote: remoteFlag,
|
|
17
18
|
session: sessionFlag,
|
|
18
19
|
"target-id": targetIdFlag,
|
|
20
|
+
verified: verifiedFlag,
|
|
19
21
|
};
|
|
20
22
|
export const waitUntilFlag = Flags.string({
|
|
21
23
|
default: "load",
|
|
@@ -58,7 +60,9 @@ export function hasExplicitDriverTarget(flags) {
|
|
|
58
60
|
hasChromeArgFlags(flags) ||
|
|
59
61
|
flags["target-id"] ||
|
|
60
62
|
flags.headed ||
|
|
61
|
-
flags.headless
|
|
63
|
+
flags.headless ||
|
|
64
|
+
flags.verified ||
|
|
65
|
+
flags.proxies);
|
|
62
66
|
}
|
|
63
67
|
function hasModeOnlyFlag(flags) {
|
|
64
68
|
return Boolean((flags.local || flags.remote) &&
|
|
@@ -68,6 +72,8 @@ function hasModeOnlyFlag(flags) {
|
|
|
68
72
|
!flags["target-id"] &&
|
|
69
73
|
!flags.headed &&
|
|
70
74
|
!flags.headless &&
|
|
75
|
+
!flags.verified &&
|
|
76
|
+
!flags.proxies &&
|
|
71
77
|
flags.local !== flags.remote);
|
|
72
78
|
}
|
|
73
79
|
function targetMatchesRequestedMode(target, flags) {
|
|
@@ -29,12 +29,4 @@ export const snapshotHandlers = {
|
|
|
29
29
|
xpathMap: snapshot.xpathMap,
|
|
30
30
|
};
|
|
31
31
|
},
|
|
32
|
-
async refs(manager) {
|
|
33
|
-
const refMaps = manager.getRefMaps();
|
|
34
|
-
return {
|
|
35
|
-
count: Object.keys(refMaps.xpathMap).length,
|
|
36
|
-
urlMap: refMaps.urlMap,
|
|
37
|
-
xpathMap: refMaps.xpathMap,
|
|
38
|
-
};
|
|
39
|
-
},
|
|
40
32
|
};
|
|
@@ -4,6 +4,7 @@ import net from "node:net";
|
|
|
4
4
|
import { CommandFailure, fail } from "../../errors.js";
|
|
5
5
|
import { targetsCompatible } from "../mode.js";
|
|
6
6
|
import { cleanupDaemonFiles, ensureRuntimeDir, getLockPath, getPidPath, getSocketPath, PRIVATE_FILE_MODE, } from "./paths.js";
|
|
7
|
+
import { collectForwardedEnv } from "./forwarded-env.js";
|
|
7
8
|
import { isProcessAlive } from "./process.js";
|
|
8
9
|
import { ResponseSchema } from "./protocol.js";
|
|
9
10
|
export async function ensureDriverDaemon({ session, target, }) {
|
|
@@ -36,6 +37,7 @@ export async function ensureDriverDaemon({ session, target, }) {
|
|
|
36
37
|
export async function openViaDaemon(session, url, options = {}) {
|
|
37
38
|
return sendDriverRequest(session, {
|
|
38
39
|
...options,
|
|
40
|
+
forwardedEnv: await collectForwardedEnv(),
|
|
39
41
|
id: requestId(),
|
|
40
42
|
type: "open",
|
|
41
43
|
url,
|
|
@@ -44,6 +46,7 @@ export async function openViaDaemon(session, url, options = {}) {
|
|
|
44
46
|
export async function runDriverCommandViaDaemon(session, command, params) {
|
|
45
47
|
return sendDriverRequest(session, {
|
|
46
48
|
command,
|
|
49
|
+
forwardedEnv: await collectForwardedEnv(),
|
|
47
50
|
id: requestId(),
|
|
48
51
|
params,
|
|
49
52
|
type: "command",
|