Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/authentication-and-profiles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ WEBCMD_WORKSPACE=user_64256 webcmd --profile work github whoami

`--profile <name>` still selects a persona within the ambient workspace; it never crosses workspace boundaries.

Hosted `profile list` returns profile rows; `delete` returns `{ "ok": true, "deleted": true }`. Public profile fields are `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` while the hosted profile is being provisioned and `available` once it is ready. Kernel identifiers are never exposed as Webcmd API fields.
Hosted `profile list` returns profile rows; `delete` returns `{ "ok": true, "deleted": true }`. Public profile fields are `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` while the hosted profile is being provisioned and `available` once it is ready. Cloud provider identifiers are never exposed as Webcmd API fields.

Delete hosted profiles only by immutable ID:

Expand Down
6 changes: 4 additions & 2 deletions docs/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ webcmd profile delete profile_abc123

In local mode, arbitrary `--profile <name>` values lazily create separate local state; local `list`, `rename`, and `use` are unchanged. In hosted mode, `--profile <name>` selects a persona within the ambient workspace; an omitted selector or `--profile default` lazily creates that workspace's `default` profile.

Hosted `list` returns profile rows scoped to the ambient workspace; `delete` takes an immutable profile ID and returns `{ "ok": true, "deleted": true }`, permanently removing that hosted browser state. Each profile row has `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` or `available`. Kernel identifiers are never exposed.
Hosted `list` returns profile rows scoped to the ambient workspace; `delete` takes an immutable profile ID and returns `{ "ok": true, "deleted": true }`, permanently removing that hosted browser state. Each profile row has `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` or `available`. Cloud provider identifiers are never exposed.

Prompt example:

Expand All @@ -168,9 +168,11 @@ the returned `installSource`:

```bash
webcmd plugin search ycombinator -f json
webcmd plugin install github:agentrhq/webcmd/ycombinator
webcmd plugin install <installSource-from-search>
```

Use direct `plugin install github:...` only when you already know the source. For missing or unknown sites, `plugin search` is the freshness boundary; it reads the current catalog instead of relying on package contents.

Hosted mode supports the same `plugin search` and `plugin install` grammar for Webcmd-verified marketplace adapters. Other plugin management commands remain local-only in hosted mode.

## Skills
Expand Down
2 changes: 1 addition & 1 deletion docs/local-or-cloud.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Local mode runs Webcmd against this machine's tools, adapters, and browser. Choo

## Webcmd Cloud Alpha

Webcmd Cloud alpha is for supported commands and browser sessions on hosted infrastructure. Kernel provides the hosted browser infrastructure behind Webcmd.
Webcmd Cloud alpha is for supported commands and browser sessions on hosted infrastructure. Hosted browser sessions run through Webcmd Cloud's Browser Use-backed infrastructure for browser lifecycle, proxying, and live viewport access.

## Set Up Hosted Mode

Expand Down
2 changes: 1 addition & 1 deletion docs/publish-community-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ The agent runs `npm run check-community-plugins`, improves the plugin README and

## What Happens After Merge

Repository tooling adds the approved plugin to the generated root catalog and README community table, making it available through the repository's community plugin listing.
Repository tooling adds the approved plugin to the generated root catalog and README community table, making it discoverable through `webcmd plugin search`. Users install the returned `installSource`.
2 changes: 1 addition & 1 deletion scripts/migrate-cli-sites.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function readme(site, description, commands) {
.slice()
.sort((a, b) => String(a.name).localeCompare(String(b.name)))
.map(command => `| \`webcmd ${site} ${command.name}\` | ${String(command.description ?? '').replaceAll('|', '\\|')} |`);
return `# webcmd-plugin-${site}\n\n${description}.\n\n## Install\n\n\`\`\`bash\nwebcmd plugin install github:agentrhq/webcmd/${site}\n\`\`\`\n\n## Commands\n\n| Command | Description |\n| --- | --- |\n${rows.join('\n')}\n`;
return `# webcmd-plugin-${site}\n\n${description}.\n\n## Install\n\n\`\`\`bash\nwebcmd plugin search ${site} -f json\nwebcmd plugin install <installSource-from-search>\n\`\`\`\n\n## Commands\n\n| Command | Description |\n| --- | --- |\n${rows.join('\n')}\n`;
}

function readJson(file, fallback) {
Expand Down
4 changes: 2 additions & 2 deletions skills/smart-search/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ webcmd plugin search <site-or-capability> -f json
Install promising plugins sequentially, at most three plugins per user request:

```bash
webcmd plugin install <source>
webcmd plugin install <installSource>
webcmd list --tag search -f json
```

Inspect the newly visible command help. Stop once a suitable command appears. If hosted marketplace installation is unavailable, state that gap and continue with fetched sources.
Inspect the newly visible command help. Stop once a suitable command appears. If installation fails, report the error and continue with fetched sources.

Do not add custom marketplaces in this workflow. In hosted mode, only verified hosted marketplace adapters are installable.

Expand Down
2 changes: 1 addition & 1 deletion skills/webcmd-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The first reader of this CLI is an agent, not a human. Use browser output as str

Before starting a raw browser session, filter `webcmd list -f json` at the source using request-derived terms across `site`, `name`, `description`, and `columns`; follow `webcmd-usage` for the exact command shape. Any truncation warning means adapter discovery is incomplete: narrow the filter and inspect again. Absence from truncated output never proves that no adapter exists.

Use raw `webcmd browser` only after a complete, non-truncated registry check shows no suitable adapter. If plugin search is relevant and returns a match, offer installation; if it errors, report the error instead of opening the browser.
Use raw `webcmd browser` only after a complete, non-truncated registry check shows no suitable adapter and a plugin search for the missing site or capability returns no match. If plugin search returns a match, offer installation of the returned `installSource`; if it errors, report the error instead of opening the browser.

---

Expand Down
6 changes: 3 additions & 3 deletions skills/webcmd-usage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,18 @@ Adapters import only `@agentrhq/webcmd/registry` and `@agentrhq/webcmd/errors`.
## Plugins

```bash
webcmd plugin install github:user/repo
webcmd plugin search [query] -f json
webcmd plugin install <installSource>
webcmd plugin list [-f json]
webcmd plugin update [name] | --all
webcmd plugin uninstall <name>
webcmd plugin create <name>
webcmd plugin search [query] -f json
webcmd plugin catalog list -f json
webcmd plugin catalog add <source>
webcmd plugin catalog remove <id>
```

Plugins are installable extensions pulled from git or local paths. Use `plugin search` for marketplace discovery and `plugin list` for already-installed plugins. Main-repo sites (official and community alike) are exposed through the root plugin catalog manifest; none of them are bundled into the npm package.
Plugins are installable extensions pulled from git or local paths. Use `plugin search` for marketplace discovery and `plugin list` for already-installed plugins. Direct `plugin install github:...` is only for a source you already know; for a missing or unknown site, search first and install the returned `installSource`. Main-repo sites (official and community alike) are exposed through the root plugin catalog manifest; none of them are bundled into the npm package.

> **Note:** The repository's `plugins/` directory is not shipped in the npm package. Find the required plugin with `webcmd plugin search`, then install its `installSource` with `webcmd plugin install <installSource>`.

Expand Down
6 changes: 3 additions & 3 deletions src/hosted/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { HostedClient, HostedClientError, resolveWorkspace } from './client.js';

const invalidTraceUrlCases = [
{
name: 'raw absolute Kernel URL with token',
name: 'raw absolute provider URL with token',
field: 'liveViewUrl',
value: 'https://kernel.example/session/secret?token=kernel-secret-token',
value: 'https://provider.example/session/secret?token=provider-secret-token',
executionId: 'exec_trace',
},
{
Expand Down Expand Up @@ -427,7 +427,7 @@ describe('HostedClient', () => {
});

it.each([
{ name: 'private provider field', change: { kernelProfileId: 'private' } },
{ name: 'private provider field', change: { providerProfileId: 'private' } },
{ name: 'missing updatedAt', change: { updatedAt: undefined } },
{ name: 'non-nullable name shape', change: { name: 7 } },
{ name: 'non-nullable workspace shape', change: { workspace: false } },
Expand Down
4 changes: 2 additions & 2 deletions src/hosted/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1526,7 +1526,7 @@ describe('runHostedCli', () => {
});

it.each(['success', 'failure'])('rejects a raw provider trace URL before $phase output or attachment', async (phase) => {
const rawUrl = 'https://kernel.example/session/private?token=kernel-secret-token';
const rawUrl = 'https://provider.example/session/private?token=provider-secret-token';
const stdout = sink();
const stderr = sink();
const success = phase === 'success';
Expand Down Expand Up @@ -1561,7 +1561,7 @@ describe('runHostedCli', () => {
expect(stdout.text()).toBe('');
expect(stderr.text()).toContain('HOSTED_PROTOCOL');
expect(`${stdout.text()}\n${stderr.text()}`).not.toContain(rawUrl);
expect(`${stdout.text()}\n${stderr.text()}`).not.toContain('kernel-secret-token');
expect(`${stdout.text()}\n${stderr.text()}`).not.toContain('provider-secret-token');
});

it('accepts a manifest patch bump on the same hosted compatibility line', async () => {
Expand Down
6 changes: 2 additions & 4 deletions src/migrate-cli-sites.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { _parseSource } from './plugin.js';

const script = path.resolve('scripts/migrate-cli-sites.mjs');
const roots: string[] = [];
Expand Down Expand Up @@ -73,9 +72,8 @@ describe('migrate-cli-sites', () => {
});
const readme = fs.readFileSync(path.join(plugin, 'README.md'), 'utf8');
expect(readme).toContain('| `webcmd example search` | Search examples |');
const installSource = readme.match(/webcmd plugin install (\S+)/)?.[1];
expect(installSource).toBe('github:agentrhq/webcmd/example');
expect(_parseSource(installSource!)).not.toBeNull();
expect(readme).toContain('webcmd plugin search example -f json');
expect(readme).toContain('webcmd plugin install <installSource-from-search>');
expect(fs.readFileSync(path.join(root, 'plugins', 'sibling', 'keep.txt'), 'utf8')).toBe('unchanged\n');
expect(fs.readFileSync(path.join(root, 'scripts', 'silent-column-drop-baseline.json'), 'utf8')).toContain('plugins/example/search.js');
expect(fs.readFileSync(path.join(root, 'scripts', 'typed-error-lint-baseline.json'), 'utf8')).toContain('plugins/example/search.js');
Expand Down
2 changes: 2 additions & 0 deletions src/plugin-scaffold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ describe('createPluginScaffold', () => {
const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8');

expect(readme).toContain(`webcmd plugin install file://${dir}`);
expect(readme).toContain('webcmd plugin search test-readme -f json');
expect(readme).toContain('webcmd plugin install <installSource-from-search>');
});

it('rejects invalid names', () => {
Expand Down
5 changes: 3 additions & 2 deletions src/plugin-scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ ${opts.description ?? `A webcmd plugin: ${name}`}
# From local development directory
webcmd plugin install file://${targetDir}

# From GitHub (after publishing)
webcmd plugin install github:<user>/webcmd-plugin-${name}
# From the catalog (after publishing)
webcmd plugin search ${name} -f json
webcmd plugin install <installSource-from-search>
\`\`\`

## Commands
Expand Down
41 changes: 41 additions & 0 deletions src/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,47 @@ describe('installPlugin with existing monorepo', () => {
expect(npmCalls.some(([, , opts]) => opts?.cwd === repoDir)).toBe(true);
expect(fs.realpathSync(pluginLink)).toBe(fs.realpathSync(subDir));
});

it('refreshes an existing monorepo cache when the requested sub-plugin is missing locally', () => {
fs.mkdirSync(path.join(repoDir, 'packages', 'alpha'), { recursive: true });
fs.writeFileSync(path.join(repoDir, 'webcmd-plugin.json'), JSON.stringify({
plugins: {
alpha: { path: 'packages/alpha' },
},
}));

mockExecFileSync.mockImplementation((cmd, args) => {
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
const cloneDir = String(args[4]);
const subDir = path.join(cloneDir, 'packages', pluginName);
fs.mkdirSync(subDir, { recursive: true });
fs.writeFileSync(path.join(cloneDir, 'package.json'), JSON.stringify({
name: repoName,
private: true,
workspaces: ['packages/*'],
}));
fs.writeFileSync(path.join(cloneDir, 'webcmd-plugin.json'), JSON.stringify({
plugins: {
alpha: { path: 'packages/alpha' },
[pluginName]: { path: `packages/${pluginName}` },
},
}));
fs.writeFileSync(path.join(subDir, 'hello.js'), 'cli({ site: "test", name: "hello", access: "read" })');
return '';
}
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
return '1234567890abcdef1234567890abcdef12345678\n';
}
return '';
});

installPlugin(`github:user/${repoName}/${pluginName}`);

const refreshedSubDir = path.join(repoDir, 'packages', pluginName);
const refreshedManifest = JSON.parse(fs.readFileSync(path.join(repoDir, 'webcmd-plugin.json'), 'utf-8'));
expect(refreshedManifest.plugins[pluginName]).toEqual({ path: `packages/${pluginName}` });
expect(fs.realpathSync(pluginLink)).toBe(fs.realpathSync(refreshedSubDir));
});
});

describe('updatePlugin transactional staging', () => {
Expand Down
34 changes: 20 additions & 14 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,8 +939,21 @@ function installMonorepo(
const monoreposDir = getMonoreposDir();
const repoDir = path.join(monoreposDir, repoName);
const repoAlreadyInstalled = fs.existsSync(repoDir);
const repoRoot = repoAlreadyInstalled ? repoDir : cloneDir;
const effectiveManifest = repoAlreadyInstalled ? readPluginManifest(repoDir) : manifest;
let repoRoot = repoAlreadyInstalled ? repoDir : cloneDir;
let effectiveManifest = repoAlreadyInstalled ? readPluginManifest(repoDir) : manifest;
let publishRepo = repoAlreadyInstalled ? undefined : { stagingDir: cloneDir, parentDir: monoreposDir };

if (
repoAlreadyInstalled
&& subPlugin
&& (!effectiveManifest?.plugins?.[subPlugin] || effectiveManifest.plugins[subPlugin].disabled)
&& manifest.plugins?.[subPlugin]
&& !manifest.plugins[subPlugin].disabled
) {
repoRoot = cloneDir;
effectiveManifest = manifest;
publishRepo = { stagingDir: cloneDir, parentDir: monoreposDir };
}

if (!effectiveManifest || !isMonorepo(effectiveManifest)) {
throw new PluginError(`Monorepo manifest missing or invalid at ${repoRoot}`);
Expand Down Expand Up @@ -1009,23 +1022,16 @@ function installMonorepo(

const publishPlugins = eligiblePlugins.map(({ name, entry }) => ({ name, subPath: entry.path }));

if (repoAlreadyInstalled) {
postInstallMonorepoLifecycle(
repoDir,
eligiblePlugins.map((p) => resolveRepoContainedPath(repoDir, p.entry.path)),
);
} else {
postInstallMonorepoLifecycle(
cloneDir,
eligiblePlugins.map((p) => resolveRepoContainedPath(cloneDir, p.entry.path)),
);
}
postInstallMonorepoLifecycle(
repoRoot,
eligiblePlugins.map((p) => resolveRepoContainedPath(repoRoot, p.entry.path)),
);

publishMonorepoPlugins(
repoDir,
PLUGINS_DIR,
publishPlugins,
repoAlreadyInstalled ? undefined : { stagingDir: cloneDir, parentDir: monoreposDir },
publishRepo,
(commitHash) => {
for (const { name, entry } of eligiblePlugins) {
if (commitHash) {
Expand Down
2 changes: 1 addition & 1 deletion src/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe('webcmd skills content', () => {
expect(skill).toContain('handoff.viewUrl');
expect(skill).toContain('handoff.verifyCommand');
expect(skill).toContain('Webcmd browser:');
expect(skill).not.toMatch(/\bhosted\b|\bKernel\b|\blocal mode\b|\blocally\b/i);
expect(skill).not.toMatch(/\bhosted\b|\blocal mode\b|\blocally\b/i);
}
for (const skill of [usage]) {
expect(skill).toContain('already_logged_in');
Expand Down
Loading