@agentrhq/webcmd 0.3.2 → 0.3.4

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.
Files changed (58) hide show
  1. package/README.md +12 -7
  2. package/clis/producthunt/browse.js +9 -2
  3. package/clis/producthunt/browser-commands.test.js +66 -0
  4. package/clis/producthunt/hot.js +2 -1
  5. package/clis/producthunt/utils.js +27 -0
  6. package/clis/producthunt/utils.test.js +8 -0
  7. package/dist/src/browser/ax-snapshot.js +15 -5
  8. package/dist/src/browser/ax-snapshot.test.js +49 -0
  9. package/dist/src/browser/daemon-client.d.ts +1 -0
  10. package/dist/src/browser/daemon-client.js +25 -1
  11. package/dist/src/browser/daemon-client.test.js +86 -1
  12. package/dist/src/browser/daemon-transport.d.ts +2 -0
  13. package/dist/src/browser/protocol.d.ts +12 -1
  14. package/dist/src/browser/runtime/local-cloak/actions.d.ts +1 -0
  15. package/dist/src/browser/runtime/local-cloak/actions.js +9 -9
  16. package/dist/src/browser/runtime/local-cloak/provider.d.ts +1 -0
  17. package/dist/src/browser/runtime/local-cloak/provider.js +6 -3
  18. package/dist/src/browser/runtime/local-cloak/provider.test.js +1 -0
  19. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +5 -0
  20. package/dist/src/browser/runtime/local-cloak/session-manager.js +95 -19
  21. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +235 -0
  22. package/dist/src/browser/runtime/provider.d.ts +1 -0
  23. package/dist/src/cli.js +36 -12
  24. package/dist/src/cli.test.js +5 -0
  25. package/dist/src/daemon/server.js +65 -24
  26. package/dist/src/daemon/server.test.js +211 -2
  27. package/dist/src/docs-sync-review-cli.test.js +1 -1
  28. package/dist/src/errors.d.ts +5 -0
  29. package/dist/src/errors.js +23 -0
  30. package/dist/src/errors.test.js +58 -1
  31. package/dist/src/execution.js +57 -6
  32. package/dist/src/execution.test.js +426 -2
  33. package/dist/src/hosted/client.js +3 -1
  34. package/dist/src/hosted/client.test.js +62 -0
  35. package/dist/src/hosted/main-lifecycle.test.js +66 -5
  36. package/dist/src/hosted/types.d.ts +1 -0
  37. package/dist/src/main.js +4 -0
  38. package/dist/src/package-bin-process.d.ts +13 -0
  39. package/dist/src/package-bin-process.js +24 -0
  40. package/dist/src/package-bin-process.test.d.ts +1 -0
  41. package/dist/src/package-bin-process.test.js +22 -0
  42. package/dist/src/session-lease.d.ts +82 -0
  43. package/dist/src/session-lease.js +163 -0
  44. package/dist/src/session-lease.test.d.ts +1 -0
  45. package/dist/src/session-lease.test.js +217 -0
  46. package/dist/src/skills.d.ts +8 -4
  47. package/dist/src/skills.js +30 -1
  48. package/dist/src/skills.test.js +40 -12
  49. package/hosted-contract.json +1 -1
  50. package/package.json +3 -1
  51. package/scripts/check-package-bin.mjs +7 -6
  52. package/scripts/collect-ci-diagnostics.ps1 +274 -0
  53. package/scripts/docs-sync-review.ts +1 -1
  54. package/skills/smart-search/SKILL.md +20 -6
  55. package/skills/webcmd-adapter-author/SKILL.md +1 -1
  56. package/skills/webcmd-adapter-author/references/adapter-template.md +2 -6
  57. package/skills/webcmd-browser/SKILL.md +16 -2
  58. package/skills/webcmd-usage/SKILL.md +21 -6
@@ -4,7 +4,7 @@ import * as path from 'node:path';
4
4
  import yaml from 'js-yaml';
5
5
  import { describe, expect, it } from 'vitest';
6
6
  import { ArgumentError } from './errors.js';
7
- import { installWebcmdSkill, listWebcmdSkills, updateWebcmdSkill } from './skills.js';
7
+ import { addWebcmdSkills, listWebcmdSkills, removeWebcmdSkills, updateWebcmdSkill } from './skills.js';
8
8
  function makePackageRoot(label = 'current') {
9
9
  const root = fs.mkdtempSync(path.join(os.tmpdir(), `webcmd-skills-${label}-`));
10
10
  fs.mkdirSync(path.join(root, 'skills', 'webcmd-browser'), { recursive: true });
@@ -80,18 +80,18 @@ describe('webcmd skills content', () => {
80
80
  'webcmd-usage',
81
81
  ]);
82
82
  });
83
- it('installs bundled skills once and refreshes them after package updates', () => {
83
+ it('adds bundled skills once and refreshes them after package updates', () => {
84
84
  const firstRoot = makePackageRoot('first');
85
85
  const secondRoot = makePackageRoot('second');
86
86
  const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-home-'));
87
87
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-project-'));
88
- const installed = installWebcmdSkill({ packageRoot: firstRoot, homeDir, cwd, provider: 'codex', scope: 'project' });
89
- expect(installed).toMatchObject({
88
+ const added = addWebcmdSkills({ packageRoot: firstRoot, homeDir, cwd, provider: 'codex', scope: 'project' });
89
+ expect(added).toMatchObject({
90
90
  provider: 'codex',
91
91
  scope: 'project',
92
92
  });
93
- expect(installed.skills.map((skill) => skill.name)).toEqual(['smart-search', 'webcmd-autofix', 'webcmd-browser']);
94
- for (const skill of installed.skills) {
93
+ expect(added.skills.map((skill) => skill.name)).toEqual(['smart-search', 'webcmd-autofix', 'webcmd-browser']);
94
+ for (const skill of added.skills) {
95
95
  expect(skill.source).toBe(path.join(firstRoot, 'skills', skill.name));
96
96
  expect(skill.stableLink).toBe(path.join(homeDir, '.webcmd', 'skills', skill.name));
97
97
  expect(skill.destination).toBe(path.join(cwd, '.codex', 'skills', skill.name));
@@ -99,22 +99,22 @@ describe('webcmd skills content', () => {
99
99
  }
100
100
  const updated = updateWebcmdSkill({ packageRoot: secondRoot, homeDir });
101
101
  expect(updated.skills.every((skill) => skill.destination === undefined)).toBe(true);
102
- for (const skill of installed.skills) {
102
+ for (const skill of added.skills) {
103
103
  expect(real(skill.destination)).toBe(real(path.join(secondRoot, 'skills', skill.name)));
104
104
  }
105
105
  });
106
- it('installs bundled skills into a custom skills directory', () => {
106
+ it('adds bundled skills into a custom skills directory', () => {
107
107
  const packageRoot = makePackageRoot();
108
108
  const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-home-'));
109
109
  const customPath = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-custom-skills-'));
110
- const installed = installWebcmdSkill({ packageRoot, homeDir, customPath });
111
- expect(installed.provider).toBeUndefined();
112
- expect(installed.skills.map((skill) => skill.destination)).toEqual([
110
+ const added = addWebcmdSkills({ packageRoot, homeDir, customPath });
111
+ expect(added.provider).toBeUndefined();
112
+ expect(added.skills.map((skill) => skill.destination)).toEqual([
113
113
  path.join(customPath, 'smart-search'),
114
114
  path.join(customPath, 'webcmd-autofix'),
115
115
  path.join(customPath, 'webcmd-browser'),
116
116
  ]);
117
- for (const skill of installed.skills) {
117
+ for (const skill of added.skills) {
118
118
  expect(real(skill.destination)).toBe(real(skill.source));
119
119
  }
120
120
  });
@@ -125,4 +125,32 @@ describe('webcmd skills content', () => {
125
125
  fs.mkdirSync(stablePath, { recursive: true });
126
126
  expect(() => updateWebcmdSkill({ packageRoot, homeDir })).toThrow(ArgumentError);
127
127
  });
128
+ it('removes bundled skill links from every supported location', () => {
129
+ const packageRoot = makePackageRoot();
130
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-home-'));
131
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-project-'));
132
+ const customPath = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-custom-skills-'));
133
+ for (const provider of ['agents', 'codex', 'claude']) {
134
+ addWebcmdSkills({ packageRoot, homeDir, cwd, provider, scope: 'user' });
135
+ addWebcmdSkills({ packageRoot, homeDir, cwd, provider, scope: 'project' });
136
+ }
137
+ addWebcmdSkills({ packageRoot, homeDir, cwd, customPath });
138
+ const result = removeWebcmdSkills({ packageRoot, homeDir, cwd, customPath });
139
+ expect(result.removed).toHaveLength(24);
140
+ for (const linkPath of result.removed) {
141
+ expect(() => fs.lstatSync(linkPath)).toThrow();
142
+ }
143
+ expect(removeWebcmdSkills({ packageRoot, homeDir, cwd, customPath })).toEqual({ removed: [] });
144
+ });
145
+ it('refuses removal before deleting any links when a destination is not a symlink', () => {
146
+ const packageRoot = makePackageRoot();
147
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-home-'));
148
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-project-'));
149
+ const added = addWebcmdSkills({ packageRoot, homeDir, cwd, provider: 'agents', scope: 'user' });
150
+ const blocker = path.join(cwd, '.codex', 'skills', 'smart-search');
151
+ fs.mkdirSync(blocker, { recursive: true });
152
+ expect(() => removeWebcmdSkills({ packageRoot, homeDir, cwd })).toThrow(ArgumentError);
153
+ expect(fs.lstatSync(added.skills[0].destination).isSymbolicLink()).toBe(true);
154
+ expect(fs.lstatSync(blocker).isDirectory()).toBe(true);
155
+ });
128
156
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "webcmdVersion": "0.3.2",
3
+ "webcmdVersion": "0.3.4",
4
4
  "outputFormats": [
5
5
  "table",
6
6
  "plain",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrhq/webcmd",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Turn websites, browser sessions, desktop apps, and local tools into deterministic CLI surfaces for humans and AI agents.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -18,6 +18,7 @@
18
18
  "./utils": "./dist/src/utils.js",
19
19
  "./logger": "./dist/src/logger.js",
20
20
  "./launcher": "./dist/src/launcher.js",
21
+ "./browser/ax-snapshot": "./dist/src/browser/ax-snapshot.js",
21
22
  "./browser/cdp": "./dist/src/browser/cdp.js",
22
23
  "./browser/page": "./dist/src/browser/page.js",
23
24
  "./browser/utils": "./dist/src/browser/utils.js",
@@ -77,6 +78,7 @@
77
78
  ],
78
79
  "author": "AgentR",
79
80
  "license": "Apache-2.0",
81
+ "homepage": "https://docs.webcmd.dev",
80
82
  "repository": {
81
83
  "type": "git",
82
84
  "url": "git+https://github.com/agentrhq/webcmd.git"
@@ -5,6 +5,10 @@ import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import {
9
+ formatPackageBinSpawnFailure,
10
+ packageBinSpawnOptions,
11
+ } from '../dist/src/package-bin-process.js';
8
12
 
9
13
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
14
  const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
@@ -20,14 +24,11 @@ function run(command, args, opts = {}) {
20
24
  cwd: ROOT,
21
25
  encoding: 'utf8',
22
26
  stdio: ['ignore', 'pipe', 'pipe'],
27
+ ...packageBinSpawnOptions(process.platform, command),
23
28
  ...opts,
24
29
  });
25
- if (result.status !== 0) {
26
- fail([
27
- `${command} ${args.join(' ')} exited ${result.status}`,
28
- result.stdout.trim(),
29
- result.stderr.trim(),
30
- ].filter(Boolean).join('\n'));
30
+ if (result.error || result.status !== 0) {
31
+ fail(formatPackageBinSpawnFailure(command, args, result));
31
32
  }
32
33
  return result;
33
34
  }
@@ -0,0 +1,274 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [switch]$SelfTest
4
+ )
5
+
6
+ $ErrorActionPreference = 'Continue'
7
+
8
+ function Protect-SensitiveDiagnosticText {
9
+ param(
10
+ [AllowEmptyString()][string]$Text
11
+ )
12
+
13
+ $sensitiveLinePattern = '(?i)(?:^|[^a-z0-9])(?:auth(?:[-_]?token)?|authorization|proxy[-_]?authorization|bearer|token|api[-_]?key|secret|password|credentials?|set[-_]?cookie|cookie|cdp(?:[-_\s]?(?:endpoint|url))?|devtools(?:[-_\s]?url)?|live[-_\s]?url|browser[-_\s]?url|websocket[-_\s]?url|ws[-_\s]?url|page[-_\s]?(?:content|text|html)|innerhtml|outerhtml|snapshot)(?:$|[^a-z0-9])|document\.|<html|<body|\b(?:html|body|content)\b\s*"?\s*:'
14
+ $schemeUrlPattern = '(?i)\b[a-z][a-z0-9+.-]*://[^\s"''<>]+'
15
+ $sanitized = foreach ($line in ($Text -split "`r?`n")) {
16
+ if ($line -match $sensitiveLinePattern) {
17
+ '[REDACTED sensitive diagnostic line]'
18
+ continue
19
+ }
20
+ $line -replace $schemeUrlPattern, '[REDACTED_URL]'
21
+ }
22
+
23
+ return ($sanitized -join [Environment]::NewLine)
24
+ }
25
+
26
+ function Write-SanitizedText {
27
+ param(
28
+ [Parameter(Mandatory = $true)][string]$Path,
29
+ [AllowEmptyString()][string]$Text
30
+ )
31
+
32
+ Protect-SensitiveDiagnosticText -Text $Text | Set-Content -LiteralPath $Path -Encoding utf8
33
+ }
34
+
35
+ function Write-SanitizedJson {
36
+ param(
37
+ [Parameter(Mandatory = $true)][string]$Path,
38
+ [Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()]$Value
39
+ )
40
+
41
+ Write-SanitizedText -Path $Path -Text (ConvertTo-Json -InputObject $Value -Depth 20)
42
+ }
43
+
44
+ function Get-DaemonLogMetadata {
45
+ param(
46
+ [Parameter(Mandatory = $true)][AllowNull()]$Response
47
+ )
48
+
49
+ $knownLevels = @('debug', 'info', 'warn', 'error')
50
+ $logs = if ($null -ne $Response -and $null -ne $Response.logs) { @($Response.logs) } else { @() }
51
+ $countsByKnownLevel = [ordered]@{}
52
+ foreach ($knownLevel in $knownLevels) {
53
+ $countsByKnownLevel[$knownLevel] = 0
54
+ }
55
+
56
+ $unrecognizedLevelCount = 0
57
+ $timestamps = @()
58
+ $maximumTimestamp = [DateTimeOffset]::UtcNow.AddDays(1).ToUnixTimeMilliseconds()
59
+ foreach ($entry in $logs) {
60
+ $level = if ($null -ne $entry.level) { ([string]$entry.level).ToLowerInvariant() } else { '' }
61
+ if ($knownLevels -contains $level) {
62
+ $countsByKnownLevel[$level] += 1
63
+ } else {
64
+ $unrecognizedLevelCount += 1
65
+ }
66
+
67
+ $timestamp = 0L
68
+ if ($null -ne $entry.ts -and [Int64]::TryParse(([string]$entry.ts), [ref]$timestamp) -and $timestamp -ge 0 -and $timestamp -le $maximumTimestamp) {
69
+ $timestamps += $timestamp
70
+ }
71
+ }
72
+
73
+ $earliestTimestampUtc = $null
74
+ $latestTimestampUtc = $null
75
+ if ($timestamps.Count -gt 0) {
76
+ $earliestTimestamp = [Int64](($timestamps | Measure-Object -Minimum).Minimum)
77
+ $latestTimestamp = [Int64](($timestamps | Measure-Object -Maximum).Maximum)
78
+ $earliestTimestampUtc = [DateTimeOffset]::FromUnixTimeMilliseconds($earliestTimestamp).UtcDateTime.ToString('o')
79
+ $latestTimestampUtc = [DateTimeOffset]::FromUnixTimeMilliseconds($latestTimestamp).UtcDateTime.ToString('o')
80
+ }
81
+
82
+ return [pscustomobject][ordered]@{
83
+ totalCount = $logs.Count
84
+ countsByKnownLevel = [pscustomobject]$countsByKnownLevel
85
+ unrecognizedLevelCount = $unrecognizedLevelCount
86
+ earliestTimestampUtc = $earliestTimestampUtc
87
+ latestTimestampUtc = $latestTimestampUtc
88
+ }
89
+ }
90
+
91
+ function Test-CloakDiagnosticLogName {
92
+ param(
93
+ [Parameter(Mandatory = $true)][string]$Name
94
+ )
95
+
96
+ return $Name.EndsWith('.log', [StringComparison]::OrdinalIgnoreCase) -or
97
+ $Name.Equals('LOG', [StringComparison]::OrdinalIgnoreCase) -or
98
+ $Name.Equals('LOG.old', [StringComparison]::OrdinalIgnoreCase)
99
+ }
100
+
101
+ function Get-CloakLogFileMetadata {
102
+ param(
103
+ [Parameter(Mandatory = $true)][string]$Root,
104
+ [Parameter(Mandatory = $true)]$File
105
+ )
106
+
107
+ return [pscustomobject][ordered]@{
108
+ relativePath = [System.IO.Path]::GetRelativePath($Root, $File.FullName)
109
+ bytes = $File.Length
110
+ lastWriteUtc = $File.LastWriteTimeUtc.ToString('o')
111
+ }
112
+ }
113
+
114
+ function Assert-DiagnosticsSelfTest {
115
+ param(
116
+ [Parameter(Mandatory = $true)][bool]$Condition,
117
+ [Parameter(Mandatory = $true)][string]$Message
118
+ )
119
+
120
+ if (-not $Condition) {
121
+ throw "Diagnostics self-test failed: $Message"
122
+ }
123
+ }
124
+
125
+ function Invoke-DiagnosticsSelfTest {
126
+ $sensitiveSamples = @(
127
+ 'auth: sensitive-value',
128
+ 'authToken: sensitive-value',
129
+ 'auth_token: sensitive-value',
130
+ 'authorization: Bearer sensitive-value',
131
+ 'bearer sensitive-value',
132
+ 'token: sensitive-value',
133
+ 'api-key: sensitive-value',
134
+ 'api_key: sensitive-value',
135
+ 'apiKey: sensitive-value',
136
+ 'secret: sensitive-value',
137
+ 'password: sensitive-value',
138
+ 'credentials: sensitive-value',
139
+ 'cookie: sensitive-value',
140
+ 'cdpUrl: ws://127.0.0.1/devtools/sensitive-value',
141
+ 'cdp_url: ws://127.0.0.1/devtools/sensitive-value',
142
+ 'CDP endpoint: ws://127.0.0.1/devtools/sensitive-value',
143
+ 'liveUrl: https://example.test/sensitive-value',
144
+ 'live_url: https://example.test/sensitive-value',
145
+ 'live URL: https://example.test/sensitive-value',
146
+ 'scheme only https://example.test/sensitive-value'
147
+ )
148
+ foreach ($sample in $sensitiveSamples) {
149
+ $protected = Protect-SensitiveDiagnosticText -Text $sample
150
+ Assert-DiagnosticsSelfTest -Condition (-not $protected.Contains('sensitive-value')) -Message "sensitive sample survived: $sample"
151
+ }
152
+
153
+ $unlabeledPageText = 'Customer account balance is 1234 and the recovery phrase is violet quartz'
154
+ $syntheticLogs = [pscustomobject]@{
155
+ ok = $true
156
+ logs = @(
157
+ [pscustomobject]@{ level = 'warn'; msg = $unlabeledPageText; text = 'private text'; value = 'private value'; data = @{ private = $true }; ts = 1704067200000 },
158
+ [pscustomobject]@{ level = 'info'; msg = 'https://example.test/private'; ts = 1704153600000 },
159
+ [pscustomobject]@{ level = 'custom'; msg = 'secret but unlabeled'; ts = -1 }
160
+ )
161
+ }
162
+ $metadata = Get-DaemonLogMetadata -Response $syntheticLogs
163
+ $metadataJson = ConvertTo-Json -InputObject $metadata -Depth 5
164
+ Assert-DiagnosticsSelfTest -Condition ($metadata.totalCount -eq 3) -Message 'daemon log total count is incorrect'
165
+ Assert-DiagnosticsSelfTest -Condition ($metadata.countsByKnownLevel.warn -eq 1 -and $metadata.countsByKnownLevel.info -eq 1) -Message 'known daemon log level counts are incorrect'
166
+ Assert-DiagnosticsSelfTest -Condition ($metadata.unrecognizedLevelCount -eq 1) -Message 'unrecognized daemon log level count is incorrect'
167
+ Assert-DiagnosticsSelfTest -Condition ($metadata.earliestTimestampUtc -eq '2024-01-01T00:00:00.0000000Z') -Message 'earliest bounded timestamp is incorrect'
168
+ Assert-DiagnosticsSelfTest -Condition ($metadata.latestTimestampUtc -eq '2024-01-02T00:00:00.0000000Z') -Message 'latest bounded timestamp is incorrect'
169
+ Assert-DiagnosticsSelfTest -Condition (-not $metadataJson.Contains($unlabeledPageText)) -Message 'unlabeled page text survived daemon metadata projection'
170
+ Assert-DiagnosticsSelfTest -Condition ($metadataJson -notmatch '(?i)"(?:msg|text|value|data)"\s*:') -Message 'free-form daemon fields survived metadata projection'
171
+ Assert-DiagnosticsSelfTest -Condition (($metadata.PSObject.Properties.Name -join ',') -eq 'totalCount,countsByKnownLevel,unrecognizedLevelCount,earliestTimestampUtc,latestTimestampUtc') -Message 'daemon metadata contains a non-allowlisted field'
172
+
173
+ $diagnosticLogNames = @('browser.log', 'debug.log', 'LOG', 'LOG.old')
174
+ foreach ($name in $diagnosticLogNames) {
175
+ Assert-DiagnosticsSelfTest -Condition (Test-CloakDiagnosticLogName -Name $name) -Message "known diagnostic log name was rejected: $name"
176
+ }
177
+ foreach ($name in @('Login Data', 'catalog.json', 'debug.txt', 'LOG.bak')) {
178
+ Assert-DiagnosticsSelfTest -Condition (-not (Test-CloakDiagnosticLogName -Name $name)) -Message "non-log profile file was accepted: $name"
179
+ }
180
+
181
+ $syntheticFile = [pscustomobject]@{
182
+ FullName = Join-Path ([System.IO.Path]::GetTempPath()) 'debug.log'
183
+ Length = 42
184
+ LastWriteTimeUtc = [DateTime]::Parse('2024-01-03T00:00:00Z').ToUniversalTime()
185
+ SensitiveContent = 'cookie=sensitive-value'
186
+ }
187
+ $fileMetadata = Get-CloakLogFileMetadata -Root ([System.IO.Path]::GetTempPath()) -File $syntheticFile
188
+ $fileMetadataJson = ConvertTo-Json -InputObject $fileMetadata -Depth 3
189
+ Assert-DiagnosticsSelfTest -Condition (($fileMetadata.PSObject.Properties.Name -join ',') -eq 'relativePath,bytes,lastWriteUtc') -Message 'Cloak file metadata contains a non-allowlisted field'
190
+ Assert-DiagnosticsSelfTest -Condition (-not $fileMetadataJson.Contains('sensitive-value')) -Message 'Cloak file contents survived metadata projection'
191
+
192
+ Write-Host 'Diagnostics self-test passed.'
193
+ }
194
+
195
+ if ($SelfTest) {
196
+ $ErrorActionPreference = 'Stop'
197
+ Invoke-DiagnosticsSelfTest
198
+ return
199
+ }
200
+
201
+ $artifactRoot = Join-Path $PWD 'artifacts/windows-cloak'
202
+ New-Item -ItemType Directory -Path $artifactRoot -Force | Out-Null
203
+
204
+ $runnerMetadata = [ordered]@{
205
+ collectedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
206
+ nodeVersion = (& node --version 2>&1 | Out-String).Trim()
207
+ npmVersion = (& npm --version 2>&1 | Out-String).Trim()
208
+ runner = [ordered]@{
209
+ os = $env:RUNNER_OS
210
+ architecture = $env:RUNNER_ARCH
211
+ imageOs = $env:ImageOS
212
+ imageVersion = $env:ImageVersion
213
+ machineName = $env:COMPUTERNAME
214
+ }
215
+ github = [ordered]@{
216
+ runId = $env:GITHUB_RUN_ID
217
+ runAttempt = $env:GITHUB_RUN_ATTEMPT
218
+ sha = $env:GITHUB_SHA
219
+ }
220
+ }
221
+
222
+ try {
223
+ $windows = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
224
+ $runnerMetadata['windows'] = [ordered]@{
225
+ caption = $windows.Caption
226
+ version = $windows.Version
227
+ buildNumber = $windows.BuildNumber
228
+ }
229
+ } catch {
230
+ $runnerMetadata['windows'] = @{ error = 'Windows metadata unavailable' }
231
+ }
232
+ Write-SanitizedJson -Path (Join-Path $artifactRoot 'runner-metadata.json') -Value $runnerMetadata
233
+
234
+ $headers = @{ 'X-Webcmd' = '1' }
235
+ try {
236
+ $daemonStatus = Invoke-RestMethod -Uri 'http://127.0.0.1:9777/status' -Headers $headers -TimeoutSec 5
237
+ Write-SanitizedJson -Path (Join-Path $artifactRoot 'daemon-status.json') -Value $daemonStatus
238
+ } catch {
239
+ Write-SanitizedText -Path (Join-Path $artifactRoot 'daemon-status.txt') -Text 'Daemon status unavailable.'
240
+ }
241
+
242
+ try {
243
+ $daemonLogs = Invoke-RestMethod -Uri 'http://127.0.0.1:9777/logs' -Headers $headers -TimeoutSec 5
244
+ $daemonLogMetadata = Get-DaemonLogMetadata -Response $daemonLogs
245
+ Write-SanitizedJson -Path (Join-Path $artifactRoot 'daemon-log-metadata.json') -Value $daemonLogMetadata
246
+ } catch {
247
+ Write-SanitizedText -Path (Join-Path $artifactRoot 'daemon-log-metadata.txt') -Text 'Daemon log metadata unavailable.'
248
+ }
249
+
250
+ $processMetadata = Get-Process -ErrorAction SilentlyContinue |
251
+ Where-Object { $_.ProcessName -match '(?i)^(node|chrome|chromium|cloak)' } |
252
+ ForEach-Object {
253
+ [ordered]@{
254
+ name = $_.ProcessName
255
+ id = $_.Id
256
+ startedAtUtc = if ($_.StartTime) { $_.StartTime.ToUniversalTime().ToString('o') } else { $null }
257
+ workingSetBytes = $_.WorkingSet64
258
+ cpuSeconds = $_.CPU
259
+ }
260
+ }
261
+ Write-SanitizedJson -Path (Join-Path $artifactRoot 'process-metadata.json') -Value @($processMetadata)
262
+
263
+ $cloakRoot = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.webcmd/cloak' } else { $null }
264
+ $cloakLogMetadata = @()
265
+ if ($cloakRoot -and (Test-Path -LiteralPath $cloakRoot)) {
266
+ $cloakLogMetadata = Get-ChildItem -LiteralPath $cloakRoot -Recurse -File -ErrorAction SilentlyContinue |
267
+ Where-Object { Test-CloakDiagnosticLogName -Name $_.Name } |
268
+ ForEach-Object {
269
+ Get-CloakLogFileMetadata -Root $cloakRoot -File $_
270
+ }
271
+ }
272
+ Write-SanitizedJson -Path (Join-Path $artifactRoot 'cloak-log-metadata.json') -Value @($cloakLogMetadata)
273
+
274
+ Write-Host "Sanitized diagnostics written to $artifactRoot"
@@ -223,7 +223,7 @@ export async function generateGeminiReview(
223
223
  responseMimeType: 'application/json',
224
224
  responseJsonSchema: REVIEW_JSON_SCHEMA,
225
225
  temperature: 0.1,
226
- abortSignal: AbortSignal.timeout(60_000),
226
+ abortSignal: AbortSignal.timeout(180_000),
227
227
  },
228
228
  });
229
229
  const text = response.text?.trim();
@@ -11,10 +11,24 @@ Route a query to the best webcmd search source based on the topic and context. T
11
11
 
12
12
  Before every use, do both of these steps:
13
13
 
14
- - Run `webcmd list -f json`
15
- - Use the live registry to confirm that candidate sites exist, and inspect `strategy`, `browser`, and `domain`
14
+ - Derive literal workflow terms from the requested action, entity, output fields, and any explicitly named site, then filter the live registry before it reaches bounded agent output:
16
15
 
17
- If the user explicitly names a site/source and it is missing from `webcmd list` or when none of the sites cover the query, check installable plugins before marking it unavailable:
16
+ ```bash
17
+ WORKFLOW_TERMS='["requested action", "output field", "named site"]'
18
+ webcmd list -f json | jq --argjson terms "$WORKFLOW_TERMS" '
19
+ [.[] | select(
20
+ ([.site, .name, .description, ((.columns // []) | join(" "))]
21
+ | map(. // "") | join(" ") | ascii_downcase) as $text
22
+ | any($terms[]; . as $term | $text | contains($term | ascii_downcase))
23
+ )]
24
+ '
25
+ ```
26
+
27
+ - Use the complete, non-truncated filtered registry to confirm that candidate sites exist, and inspect `strategy`, `browser`, and `domain`
28
+
29
+ Any truncation warning from registry or plugin output means the inspection is incomplete. Narrow the filter or query and inspect again; absence from truncated output never proves a site, command, or plugin is unavailable. Do not search plugins until a complete filtered registry result for the missing capability is exactly `[]`, and do not use raw browser fallback until plugin output is also complete and non-truncated.
30
+
31
+ If the user explicitly names a site/source, or no installed command covers the query, refine the filter for that missing site or capability. Only when the complete filtered result is `[]`, check installable plugins before marking it unavailable:
18
32
 
19
33
  ```bash
20
34
  webcmd plugin search <site-or-source-or-capability> -f json
@@ -22,7 +36,7 @@ webcmd plugin search <site-or-source-or-capability> -f json
22
36
 
23
37
  Derive a short plugin query from the missing site or capability. Preserve the user's term when practical: `find flights` becomes `flight`.
24
38
 
25
- If plugin search finds a match, tell the user it is available as a plugin and offer `webcmd plugin install <source>`. If plugin search fails because catalog sources cannot be fetched, report that catalog/search error separately from no plugin match.
39
+ If a complete, non-truncated plugin search finds a match, tell the user it is available as a plugin and offer `webcmd plugin install <source>`. If plugin search is truncated, refine it before fallback. If it fails because catalog sources cannot be fetched, report that catalog/search error separately from no plugin match.
26
40
 
27
41
  After choosing a site, do both of these steps:
28
42
 
@@ -144,12 +158,12 @@ Keep a typical query to 1 AI source plus 1-2 specialized sources to avoid result
144
158
  When a site is unavailable:
145
159
 
146
160
  - Do not stop the whole search because one source failed
147
- - For a missing site or capability, run `webcmd plugin search <query> -f json` before recording unavailable
161
+ - For a missing site or capability, first require a complete filtered registry result of `[]`, then run `webcmd plugin search <query> -f json` before recording unavailable
148
162
  - Record: `Skipped: <site> unavailable`
149
163
  - Fall back to another site of the same type, or to one AI source
150
164
  - Always trust the actual output of `webcmd list -f json`, `webcmd plugin search -f json`, and `webcmd <site> -h`
151
165
 
152
- Do not assume any site is always available. Even for public sites, trust live help and execution results in the current environment.
166
+ Do not assume any site is always available. Even for public sites, trust complete, non-truncated live registry and plugin output, live help, and execution results in the current environment. Raw browser fallback requires both the complete `[]` registry result and a complete, non-truncated plugin search that returned no match and no error.
153
167
 
154
168
  ## Reference Files
155
169
 
@@ -175,7 +175,7 @@ Check these off step by step:
175
175
  [ ] Order: identifier columns -> business numbers -> metadata.
176
176
 
177
177
  [ ] 9. Write the adapter (`adapter-template.md`):
178
- [ ] `webcmd browser init <site>/<name> --strategy <strategy>`
178
+ [ ] `webcmd browser init <site>/<name>`, then set `strategy: Strategy.<strategy>` in the generated file
179
179
  [ ] Find the closest same-site or same-type adapter and copy it.
180
180
  [ ] Edit name, URL, and field mapping.
181
181
 
@@ -7,14 +7,10 @@ Use this after recon, endpoint verification, field decoding, output design, and
7
7
  For private iteration:
8
8
 
9
9
  ```bash
10
- webcmd browser init <site>/<name> --strategy <strategy>
10
+ webcmd browser init <site>/<name>
11
11
  ```
12
12
 
13
- Write the working file at:
14
-
15
- ```text
16
- ~/.webcmd/clis/<site>/<name>.js
17
- ```
13
+ This scaffolds `~/.webcmd/clis/<site>/<name>.js` with a `Strategy.PUBLIC` placeholder — `init` takes no flags, so set the real `strategy:` value (and the other `TODO` fields) by hand once the file exists.
18
14
 
19
15
  Promote a community CLI to the main repo as a plugin:
20
16
 
@@ -12,13 +12,26 @@ This skill is for **driving a live browser** to accomplish an agent task. If you
12
12
 
13
13
  ---
14
14
 
15
+ ## Adapter fallback gate
16
+
17
+ 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 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.
18
+
19
+ Raw `webcmd browser` use is allowed only after both conditions hold:
20
+
21
+ 1. The complete, non-truncated filtered registry result for the missing capability is exactly `[]`.
22
+ 2. A complete, non-truncated `webcmd plugin search <capability> -f json` result returns no match and no error.
23
+
24
+ If plugin search returns a match, offer installation. If its output is truncated, refine the query or output and inspect again. If it errors, report the error and stop instead of opening the browser.
25
+
26
+ ---
27
+
15
28
  ## Prerequisites
16
29
 
17
30
  ```bash
18
31
  webcmd doctor
19
32
  ```
20
33
 
21
- Until `doctor` is green, nothing else will work. Typical failures: Chrome not running, extension not installed, debug port blocked by 1Password / other extensions. The doctor output tells you which.
34
+ Until `doctor` is green, browser commands will not work. Registry and plugin discovery do not require `doctor`. Typical failures: Chrome not running, extension not installed, debug port blocked by 1Password / other extensions. The doctor output tells you which.
22
35
 
23
36
  ---
24
37
 
@@ -61,7 +74,7 @@ Bound sessions use the normal Webcmd session lifecycle; `unbind` releases the Cl
61
74
  ## Critical rules
62
75
 
63
76
  1. **Always inspect before you act.** Run `state` or `find` first. Never hard-code a ref or selector from memory across sessions — indices are per-snapshot.
64
- 2. **Prefer site adapters before raw browser driving.** If `webcmd <site> <command>` already covers the task, use that adapter command first (`webcmd facebook notifications`, `webcmd reddit read`, `webcmd chatgpt model <level>`, etc.). Use `webcmd browser ...` only for gaps, debugging, or one-off UI flows the adapter does not expose.
77
+ 2. **Prefer site adapters before raw browser driving.** Complete the adapter fallback gate above. If `webcmd <site> <command>` already covers the task, use that adapter command first (`webcmd facebook notifications`, `webcmd reddit read`, `webcmd chatgpt model <level>`, etc.). Use `webcmd browser ...` only for gaps, debugging, or one-off UI flows the adapter does not expose.
65
78
  3. **Prefer numeric ref over CSS once you have it.** Numeric refs survive mild DOM shifts because the CLI fingerprints each tagged element. A CSS selector written by hand will break the first time the site re-renders.
66
79
  4. **Read `match_level` after every write.** `exact` = all good. `stable` = the element is the same but some soft attrs drifted — your action still applied. `reidentified` = the original ref was gone and the CLI found a unique replacement; double-check you hit the right element.
67
80
  5. **Use the `compound` field for form controls.** Do not regex-guess a date format, do not `state` twice to get the full `<select>` options list. The compound envelope has the format string, full option list up to 50, `options_total` for overflow, and `accept`/`multiple` for `<input type=file>`.
@@ -433,6 +446,7 @@ normal DOM `state`, or navigate/bind directly to the iframe URL when possible.
433
446
  | `stale_ref` across every command | You are reusing refs from a prior page. Re-`state`. |
434
447
  | `click` succeeds but nothing happens | The element is probably a decorative wrapper stealing clicks from the real target. `find --css "..."` with a narrower selector and retry on the inner element. |
435
448
  | `type` appears to finish but value is wrong | Autocomplete, masked input, or React controlled re-render. Verify with `get value`. Add `keys Enter` or re-type. |
449
+ | `webcmd list -f json` output is truncated | Adapter discovery is incomplete. Filter at the source with request-derived terms and narrow until the complete result is `[]`; do not start browser fallback yet. |
436
450
  | Giant `get html` output | Pass `--selector` + `--as json --depth 3 --children-max 20 --text-max 200`. |
437
451
  | Network cache seems stale | Bump `--ttl` down, or let it expire. The cache lives at `~/.webcmd/cache/browser-network/`. |
438
452
 
@@ -50,19 +50,33 @@ Run commands instead of reading static docs:
50
50
 
51
51
  ```bash
52
52
  webcmd
53
- webcmd list -f json
54
53
  webcmd <site> --help
55
54
  webcmd <site> <command> --help
56
55
  ```
57
56
 
58
57
  Run `webcmd` with no arguments to see all available functions and installed site adapters. Do not hard-code adapter lists: `webcmd list -f json` is the source of truth for installed commands and emits one entry per command with fields such as `{site, name, aliases, description, strategy, browser, args, columns}`.
59
58
 
59
+ Large registries can exceed an agent or tool output budget. Filter the JSON stream before it is emitted, using broad literal terms derived from the whole requested workflow:
60
+
61
+ ```bash
62
+ WORKFLOW_TERMS='["requested action", "output field", "named site"]'
63
+ webcmd list -f json | jq --argjson terms "$WORKFLOW_TERMS" '
64
+ [.[] | select(
65
+ ([.site, .name, .description, ((.columns // []) | join(" "))]
66
+ | map(. // "") | join(" ") | ascii_downcase) as $text
67
+ | any($terms[]; . as $term | $text | contains($term | ascii_downcase))
68
+ )]
69
+ '
70
+ ```
71
+
72
+ Replace `WORKFLOW_TERMS` with terms from the current request: the requested action, entity, output fields, and any explicitly named site. Literal matching avoids regex errors from terms such as `C++` or `[foo]`. Do not maintain a site or category allowlist. Match across `site`, `name`, `description`, and `columns`. If any layer reports truncated output, the inspection is incomplete. Narrow the filter and inspect again. Never treat absence from truncated output as proof that an adapter or plugin is missing, and do not proceed to the next fallback stage from that evidence.
73
+
60
74
  Use this fallback order:
61
75
 
62
- 1. Run `webcmd list -f json` once.
63
- 2. Check that result against the whole requested workflow, not only a named site. If one installed command covers it, use that command and stop discovery.
64
- 3. If none covers it, derive a short plugin query from the missing site or capability and run `webcmd plugin search <query> -f json`. Preserve the user's term when practical: `find flights` becomes `flight`.
65
- 4. If plugin search returns a match, offer `webcmd plugin install <installSource>`. If it returns no match and no error, raw `webcmd browser` is allowed. If it errors, report plugin discovery as unavailable and stop. If `fetch failed` appears in `errors[].message`, report plugin discovery as unavailable due to network/reachability and ask the user whether to rerun with network/escalated permissions. Do not retry unless they approve.
76
+ 1. Run `webcmd list -f json` through a workflow-derived filter before returning its output to the agent.
77
+ 2. Check the complete, non-truncated filtered result against the whole requested workflow. If one installed command covers it, use that command and stop discovery. If candidates do not cover the missing capability, refine the capability filter until its complete result is exactly `[]`.
78
+ 3. Only after that complete filtered result is `[]`, derive a short plugin query from the missing site or capability and run `webcmd plugin search <query> -f json`. Preserve the user's term when practical: `find flights` becomes `flight`.
79
+ 4. If the complete, non-truncated plugin search returns a match, offer `webcmd plugin install <installSource>`. Only if that complete result returns no match and no error is raw `webcmd browser` allowed. Both plugin search and raw browser fallback require the prior complete filtered registry result to be `[]`. A truncated plugin result is incomplete evidence: refine the query or output before fallback. If plugin search errors, report plugin discovery as unavailable and stop. If `fetch failed` appears in `errors[].message`, report plugin discovery as unavailable due to network/reachability and ask the user whether to rerun with network/escalated permissions. Do not retry unless they approve.
66
80
 
67
81
  ## Universal Flags
68
82
 
@@ -194,6 +208,7 @@ Do not invoke these removed commands:
194
208
 
195
209
  ## Do Not
196
210
 
197
- - Do not paste static command lists into plans; call `webcmd list -f json`.
211
+ - Do not paste static command lists into plans; query `webcmd list -f json` through a workflow-derived filter.
212
+ - Do not emit a large unfiltered registry into a bounded output or infer absence from a truncation warning; filter at the source and narrow until the result is complete.
198
213
  - Do not assume every adapter needs a browser; check `strategy`.
199
214
  - Do not silently fall back from a failing adapter to hand-rolled `fetch`; use `--trace retain-on-failure` first.