@agentrhq/webcmd 0.3.1 → 0.3.3

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 (56) hide show
  1. package/README.md +13 -6
  2. package/cli-manifest.json +11 -11
  3. package/clis/producthunt/browse.js +9 -2
  4. package/clis/producthunt/browser-commands.test.js +66 -0
  5. package/clis/producthunt/hot.js +3 -3
  6. package/clis/producthunt/utils.js +27 -0
  7. package/clis/producthunt/utils.test.js +8 -0
  8. package/clis/spotify/spotify.js +11 -11
  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/availability.test.js +12 -1
  34. package/dist/src/hosted/client.js +3 -1
  35. package/dist/src/hosted/client.test.js +62 -0
  36. package/dist/src/hosted/main-lifecycle.test.js +66 -5
  37. package/dist/src/hosted/types.d.ts +1 -0
  38. package/dist/src/main.js +4 -0
  39. package/dist/src/package-bin-process.d.ts +13 -0
  40. package/dist/src/package-bin-process.js +24 -0
  41. package/dist/src/package-bin-process.test.d.ts +1 -0
  42. package/dist/src/package-bin-process.test.js +22 -0
  43. package/dist/src/session-lease.d.ts +82 -0
  44. package/dist/src/session-lease.js +163 -0
  45. package/dist/src/session-lease.test.d.ts +1 -0
  46. package/dist/src/session-lease.test.js +217 -0
  47. package/dist/src/skills.d.ts +8 -4
  48. package/dist/src/skills.js +30 -1
  49. package/dist/src/skills.test.js +40 -12
  50. package/hosted-contract.json +45 -34
  51. package/package.json +3 -2
  52. package/scripts/check-package-bin.mjs +7 -6
  53. package/scripts/collect-ci-diagnostics.ps1 +274 -0
  54. package/scripts/docs-sync-review.ts +1 -1
  55. package/skills/webcmd-adapter-author/SKILL.md +1 -1
  56. package/skills/webcmd-adapter-author/references/adapter-template.md +2 -6
@@ -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();
@@ -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