@agentrhq/webcmd 0.3.2 → 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 (53) hide show
  1. package/README.md +13 -6
  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/daemon-client.d.ts +1 -0
  8. package/dist/src/browser/daemon-client.js +25 -1
  9. package/dist/src/browser/daemon-client.test.js +86 -1
  10. package/dist/src/browser/daemon-transport.d.ts +2 -0
  11. package/dist/src/browser/protocol.d.ts +12 -1
  12. package/dist/src/browser/runtime/local-cloak/actions.d.ts +1 -0
  13. package/dist/src/browser/runtime/local-cloak/actions.js +9 -9
  14. package/dist/src/browser/runtime/local-cloak/provider.d.ts +1 -0
  15. package/dist/src/browser/runtime/local-cloak/provider.js +6 -3
  16. package/dist/src/browser/runtime/local-cloak/provider.test.js +1 -0
  17. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +5 -0
  18. package/dist/src/browser/runtime/local-cloak/session-manager.js +95 -19
  19. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +235 -0
  20. package/dist/src/browser/runtime/provider.d.ts +1 -0
  21. package/dist/src/cli.js +36 -12
  22. package/dist/src/cli.test.js +5 -0
  23. package/dist/src/daemon/server.js +65 -24
  24. package/dist/src/daemon/server.test.js +211 -2
  25. package/dist/src/docs-sync-review-cli.test.js +1 -1
  26. package/dist/src/errors.d.ts +5 -0
  27. package/dist/src/errors.js +23 -0
  28. package/dist/src/errors.test.js +58 -1
  29. package/dist/src/execution.js +57 -6
  30. package/dist/src/execution.test.js +426 -2
  31. package/dist/src/hosted/client.js +3 -1
  32. package/dist/src/hosted/client.test.js +62 -0
  33. package/dist/src/hosted/main-lifecycle.test.js +66 -5
  34. package/dist/src/hosted/types.d.ts +1 -0
  35. package/dist/src/main.js +4 -0
  36. package/dist/src/package-bin-process.d.ts +13 -0
  37. package/dist/src/package-bin-process.js +24 -0
  38. package/dist/src/package-bin-process.test.d.ts +1 -0
  39. package/dist/src/package-bin-process.test.js +22 -0
  40. package/dist/src/session-lease.d.ts +82 -0
  41. package/dist/src/session-lease.js +163 -0
  42. package/dist/src/session-lease.test.d.ts +1 -0
  43. package/dist/src/session-lease.test.js +217 -0
  44. package/dist/src/skills.d.ts +8 -4
  45. package/dist/src/skills.js +30 -1
  46. package/dist/src/skills.test.js +40 -12
  47. package/hosted-contract.json +1 -1
  48. package/package.json +3 -2
  49. package/scripts/check-package-bin.mjs +7 -6
  50. package/scripts/collect-ci-diagnostics.ps1 +274 -0
  51. package/scripts/docs-sync-review.ts +1 -1
  52. package/skills/webcmd-adapter-author/SKILL.md +1 -1
  53. package/skills/webcmd-adapter-author/references/adapter-template.md +2 -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.3",
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.3",
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"
@@ -77,6 +77,7 @@
77
77
  ],
78
78
  "author": "AgentR",
79
79
  "license": "Apache-2.0",
80
+ "homepage": "https://docs.webcmd.dev",
80
81
  "repository": {
81
82
  "type": "git",
82
83
  "url": "git+https://github.com/agentrhq/webcmd.git"
@@ -112,4 +113,4 @@
112
113
  "overrides": {
113
114
  "postcss": "^8.5.10"
114
115
  }
115
- }
116
+ }
@@ -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