@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.
- package/README.md +13 -6
- package/clis/producthunt/browse.js +9 -2
- package/clis/producthunt/browser-commands.test.js +66 -0
- package/clis/producthunt/hot.js +2 -1
- package/clis/producthunt/utils.js +27 -0
- package/clis/producthunt/utils.test.js +8 -0
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +25 -1
- package/dist/src/browser/daemon-client.test.js +86 -1
- package/dist/src/browser/daemon-transport.d.ts +2 -0
- package/dist/src/browser/protocol.d.ts +12 -1
- package/dist/src/browser/runtime/local-cloak/actions.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +9 -9
- package/dist/src/browser/runtime/local-cloak/provider.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/provider.js +6 -3
- package/dist/src/browser/runtime/local-cloak/provider.test.js +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +5 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +95 -19
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +235 -0
- package/dist/src/browser/runtime/provider.d.ts +1 -0
- package/dist/src/cli.js +36 -12
- package/dist/src/cli.test.js +5 -0
- package/dist/src/daemon/server.js +65 -24
- package/dist/src/daemon/server.test.js +211 -2
- package/dist/src/docs-sync-review-cli.test.js +1 -1
- package/dist/src/errors.d.ts +5 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/errors.test.js +58 -1
- package/dist/src/execution.js +57 -6
- package/dist/src/execution.test.js +426 -2
- package/dist/src/hosted/client.js +3 -1
- package/dist/src/hosted/client.test.js +62 -0
- package/dist/src/hosted/main-lifecycle.test.js +66 -5
- package/dist/src/hosted/types.d.ts +1 -0
- package/dist/src/main.js +4 -0
- package/dist/src/package-bin-process.d.ts +13 -0
- package/dist/src/package-bin-process.js +24 -0
- package/dist/src/package-bin-process.test.d.ts +1 -0
- package/dist/src/package-bin-process.test.js +22 -0
- package/dist/src/session-lease.d.ts +82 -0
- package/dist/src/session-lease.js +163 -0
- package/dist/src/session-lease.test.d.ts +1 -0
- package/dist/src/session-lease.test.js +217 -0
- package/dist/src/skills.d.ts +8 -4
- package/dist/src/skills.js +30 -1
- package/dist/src/skills.test.js +40 -12
- package/hosted-contract.json +1 -1
- package/package.json +3 -2
- package/scripts/check-package-bin.mjs +7 -6
- package/scripts/collect-ci-diagnostics.ps1 +274 -0
- package/scripts/docs-sync-review.ts +1 -1
- package/skills/webcmd-adapter-author/SKILL.md +1 -1
- package/skills/webcmd-adapter-author/references/adapter-template.md +2 -6
package/dist/src/skills.test.js
CHANGED
|
@@ -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 {
|
|
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('
|
|
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
|
|
89
|
-
expect(
|
|
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(
|
|
94
|
-
for (const skill of
|
|
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
|
|
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('
|
|
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
|
|
111
|
-
expect(
|
|
112
|
-
expect(
|
|
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
|
|
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
|
});
|
package/hosted-contract.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentrhq/webcmd",
|
|
3
|
-
"version": "0.3.
|
|
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(
|
|
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
|
|
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>
|
|
10
|
+
webcmd browser init <site>/<name>
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
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
|
|