Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/browse-common-list-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"browse": minor
---

Add opt-in `--list-version 2` to resource and catalog lists, with a shared total `--limit`, `--all`, table/JSON formatting, and `{ data, hasMore, nextCursor }` JSON output. Secrets lists can follow cursors automatically without skipping records. Existing commands retain their default output and pagination behavior.
41 changes: 41 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,47 @@ browse templates clone google-trends-keywords
browse templates clone amazon-product-scraping --language python ./my-scraper
```

## Common list behavior (opt-in)

Existing commands keep their current output shapes and limits. Use `--list-version 2`
to opt into shared collection behavior on `cloud projects list`, `cloud sessions list`,
`cloud contexts list`, `cloud secrets list`, `functions secrets list`, and
`skills` / `templates` `list` and `find`.

```bash
browse cloud sessions list --list-version 2 --limit 10 --json
browse cloud secrets list --list-version 2 --all --format table
browse functions secrets list <functionId> --list-version 2 --limit 50 --json
browse templates find scraping --list-version 2 --limit 5 --wide
```

- `--limit N` is a total record limit in every output format; the default is 20.
- `--all` removes that limit and follows available API cursors. It cannot be combined
with `--limit`.
- `--format table|json`, `--json`, and `--wide` work across these commands. The default
is table output in a terminal and JSON when piped. Exact `find` matches also use
collection output in version 2.
- JSON always has `{ "data": [...], "hasMore": true|false|null, "nextCursor": string|null }`.
Table output uses common empty and continuation messages with resource-specific columns.
- `hasMore: true` means more records are known to exist; `false` means the source was
exhausted; `null` means the API does not expose completeness. Sessions use `null`
after all returned records are shown: `--all` cannot promise the entire session history.
- Only the two secrets lists support `--cursor`. Resume from `nextCursor` with the same
filters. The token always follows the last emitted record; a total limit can span
multiple API pages. Array sources have no resume token even when output is truncated.
- A pagination failure exits unsuccessfully without printing a partial collection.
Context lists cover locally saved aliases only.

Version 1 remains the default, including existing JSON envelopes, JSON output that
ignores table-only limits, and single-page secrets results. New collection-only flags
require version 2 where the command did not already support them. No existing command
or flag is removed. This opt-in is intended for a minor release; changing the default
requires a separate compatibility decision.

This initial collection adapter does not change ranked `cloud search` results (the
API exposes a capped result set without pagination), browser-driver `tab list`, or
non-collection commands such as logs and downloads.

## Configuration

Set your Browserbase API key to enable remote sessions and cloud commands:
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/commands/cloud/contexts/list.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import {
collectionVersionFlag,
collectionLimitFlags,
usesCollectionContract,
outputCollection,
validateCollectionFlags,
requireCollectionVersion,
} from "../../../lib/collections.js";
import { outputJson } from "../../../lib/cloud/api.js";
import {
type ContextAliasEntry,
Expand All @@ -22,12 +30,25 @@ export default class ContextsList extends BrowseCommand {

static override flags = {
...outputFormatFlags,
...collectionVersionFlag,
...collectionLimitFlags,
};

async run(): Promise<void> {
const { flags } = await this.parse(ContextsList);
if (usesCollectionContract(flags)) validateCollectionFlags(flags);
else requireCollectionVersion(flags, ["limit", "all"]);
const contexts = await listContextAliases();

if (usesCollectionContract(flags)) {
await outputCollection({
flags,
source: { kind: "array", complete: true, load: async () => contexts },
table: (items) => outputContextsTable(items, { wide: flags.wide }),
});
return;
}

if (resolveOutputFormat(flags) === "json") {
// Wrap in a named key to match `templates list` / `skills list` so the
// JSON shape is consistent and machine-readable across list commands.
Expand Down
26 changes: 25 additions & 1 deletion packages/cli/src/commands/cloud/projects/list.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import {
collectionVersionFlag,
collectionLimitFlags,
usesCollectionContract,
outputCollection,
validateCollectionFlags,
requireCollectionVersion,
} from "../../../lib/collections.js";
import {
createBrowserbaseClient,
outputJson,
Expand Down Expand Up @@ -27,13 +35,29 @@ export default class ProjectsList extends BrowseCommand {
"browse cloud projects list",
"browse cloud projects list --json",
];
static override flags = { ...apiCommonFlags, ...outputFormatFlags };
static override flags = {
...apiCommonFlags,
...outputFormatFlags,
...collectionVersionFlag,
...collectionLimitFlags,
};

async run(): Promise<void> {
const { flags } = await this.parse(ProjectsList);
if (usesCollectionContract(flags)) validateCollectionFlags(flags);
else requireCollectionVersion(flags, ["limit", "all"]);
await withBrowserbaseApi("projects", async () => {
const client = createBrowserbaseClient(toApiOptions(flags));
const projects = (await client.projects.list()) as BrowserbaseProject[];
if (usesCollectionContract(flags)) {
await outputCollection({
flags,
source: { kind: "array", complete: true, load: async () => projects },
table: (items) => outputProjectsTable(items, { wide: flags.wide }),
});
return;
}

if (resolveOutputFormat(flags) === "json") {
outputJson(projects);
return;
Expand Down
26 changes: 24 additions & 2 deletions packages/cli/src/commands/cloud/secrets/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import { BrowseCommand } from "../../../base.js";
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
import { listSecrets } from "../../../lib/secrets/api.js";
import {
listSecretsFlags,
collectionSecretsFlags,
toListSecretsOptions,
} from "../../../lib/secrets/flags.js";
import { outputJson } from "../../../lib/output.js";
import {
usesCollectionContract,
outputCollection,
} from "../../../lib/collections.js";
import {
outputSecretsTable,
validateSecretsCollectionFlags,
} from "../../../lib/secrets/output.js";

export default class SecretsList extends BrowseCommand {
static override description =
Expand All @@ -16,10 +24,24 @@ export default class SecretsList extends BrowseCommand {
"browse cloud secrets list --start-at 2026-01-01T00:00:00Z --end-at 2026-02-01T00:00:00Z",
"browse cloud secrets list --limit 10",
];
static override flags = { ...apiCommonFlags, ...listSecretsFlags };
static override flags = { ...apiCommonFlags, ...collectionSecretsFlags };
async run(): Promise<void> {
const { flags } = await this.parse(SecretsList);
validateSecretsCollectionFlags(flags);
const options = toApiOptions(flags);
if (usesCollectionContract(flags)) {
const query = toListSecretsOptions(flags);
await outputCollection({
flags,
source: {
kind: "cursor",
pageSize: 1000,
loadPage: (page) => listSecrets(options, { ...query, ...page }),
},
table: (items) => outputSecretsTable(items, flags),
});
return;
}
outputJson(await listSecrets(options, toListSecretsOptions(flags)));
}
}
34 changes: 30 additions & 4 deletions packages/cli/src/commands/cloud/sessions/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Flags } from "@oclif/core";

import {
collectionVersionFlag,
usesCollectionContract,
outputCollection,
validateCollectionFlags,
} from "../../../lib/collections.js";
import {
createBrowserbaseClient,
outputJson,
Expand Down Expand Up @@ -42,12 +48,14 @@ export default class SessionsList extends BrowseCommand {
static override flags = {
...apiCommonFlags,
...outputFormatFlags,
...collectionVersionFlag,
all: Flags.boolean({
description: "Show all returned sessions in table output.",
description:
"Show all returned sessions (all output formats in version 2).",
}),
limit: Flags.integer({
default: 20,
description: "Maximum sessions to show in table output.",
description:
"Maximum sessions: table rows in version 1 (default 20), records in version 2 (default 20).",
helpValue: "<count>",
min: 1,
}),
Expand All @@ -64,6 +72,7 @@ export default class SessionsList extends BrowseCommand {

async run(): Promise<void> {
const { flags } = await this.parse(SessionsList);
if (usesCollectionContract(flags)) validateCollectionFlags(flags);
await withBrowserbaseApi("sessions", async () => {
const client = createBrowserbaseClient(toApiOptions(flags));
const query: { q?: string; status?: SessionStatus } = {};
Expand All @@ -77,13 +86,30 @@ export default class SessionsList extends BrowseCommand {
const sessions = (await client.sessions.list(
query,
)) as BrowserbaseSession[];
if (usesCollectionContract(flags)) {
await outputCollection({
flags,
source: {
kind: "array",
complete: false,
load: async () => sessions,
},
table: (items) =>
outputSessionsTable(items, {
limit: items.length,
wide: flags.wide,
}),
});
return;
}

if (resolveOutputFormat(flags) === "json") {
outputJson(sessions);
return;
}

outputSessionsTable(sessions, {
limit: flags.all ? sessions.length : flags.limit,
limit: flags.all ? sessions.length : (flags.limit ?? 20),
wide: flags.wide,
});
});
Expand Down
30 changes: 28 additions & 2 deletions packages/cli/src/commands/functions/secrets/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import { BrowseCommand } from "../../../base.js";
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
import { listFunctionSecrets } from "../../../lib/secrets/api.js";
import {
listSecretsFlags,
collectionSecretsFlags,
toListSecretsOptions,
} from "../../../lib/secrets/flags.js";
import { outputJson } from "../../../lib/output.js";
import {
usesCollectionContract,
outputCollection,
} from "../../../lib/collections.js";
import {
outputSecretsTable,
validateSecretsCollectionFlags,
} from "../../../lib/secrets/output.js";

export default class FunctionSecretsList extends BrowseCommand {
static override description =
Expand All @@ -24,10 +32,28 @@ export default class FunctionSecretsList extends BrowseCommand {
required: true,
}),
};
static override flags = { ...apiCommonFlags, ...listSecretsFlags };
static override flags = { ...apiCommonFlags, ...collectionSecretsFlags };
async run(): Promise<void> {
const { args, flags } = await this.parse(FunctionSecretsList);
validateSecretsCollectionFlags(flags);
const options = toApiOptions(flags);
if (usesCollectionContract(flags)) {
const query = toListSecretsOptions(flags);
await outputCollection({
flags,
source: {
kind: "cursor",
pageSize: 1000,
loadPage: (page) =>
listFunctionSecrets(options, args.functionId, {
...query,
...page,
}),
},
table: (items) => outputSecretsTable(items, flags),
});
return;
}
outputJson(
await listFunctionSecrets(
options,
Expand Down
31 changes: 27 additions & 4 deletions packages/cli/src/commands/skills/find.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Args, Flags } from "@oclif/core";

import {
collectionVersionFlag,
usesCollectionContract,
outputCollection,
validateCollectionFlags,
} from "../../lib/collections.js";
import { BrowseCommand } from "../../base.js";
import {
outputFormatFlags,
Expand Down Expand Up @@ -35,24 +41,41 @@ export default class SkillsFind extends BrowseCommand {

static override flags = {
...outputFormatFlags,
...collectionVersionFlag,
all: Flags.boolean({
description: "Show all matching skills in table output.",
description:
"Show all matching skills (all output formats in version 2).",
}),
limit: Flags.integer({
default: 25,
description: "Maximum matching skills to show in table output.",
description:
"Maximum matches: table rows in version 1 (default 25), records in version 2 (default 20).",
helpValue: "<count>",
min: 1,
}),
};

async run(): Promise<void> {
const { args, flags } = await this.parse(SkillsFind);
if (usesCollectionContract(flags)) validateCollectionFlags(flags);
const skills = prioritizeExactSkillMatch(
await listCatalogSkills({ query: args.query }),
args.query,
);

if (usesCollectionContract(flags)) {
await outputCollection({
flags,
source: { kind: "array", complete: true, load: async () => skills },
table: (items) =>
outputSkillTable(items, {
limit: items.length,
wide: flags.wide,
footer: false,
}),
});
return;
}

const outputFormat = resolveOutputFormat(flags);
if (outputFormat === "json") {
outputJson({ query: args.query, skills });
Expand All @@ -67,7 +90,7 @@ export default class SkillsFind extends BrowseCommand {

outputSkillTable(skills, {
heading: `Skills matching "${args.query}"`,
limit: flags.all ? skills.length : flags.limit,
limit: flags.all ? skills.length : (flags.limit ?? 25),
wide: flags.wide,
});
}
Expand Down
Loading
Loading