@agentrhq/webcmd 0.3.0 → 0.3.1

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 (181) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +217 -13
  3. package/clis/chatgpt/ask.js +1 -1
  4. package/clis/chatgpt/ask.test.js +11 -0
  5. package/clis/chatgpt/commands.test.js +102 -1
  6. package/clis/chatgpt/deep-research-result.js +45 -3
  7. package/clis/chatgpt/image.js +22 -2
  8. package/clis/chatgpt/project-file-add.js +24 -1
  9. package/clis/chatgpt/utils.js +258 -52
  10. package/clis/chatgpt/utils.test.js +259 -1
  11. package/clis/claude/ask.js +21 -1
  12. package/clis/facebook/feed.js +110 -24
  13. package/clis/facebook/feed.test.js +62 -0
  14. package/clis/facebook/search.js +185 -38
  15. package/clis/facebook/search.test.js +95 -50
  16. package/clis/instagram/explore.js +30 -12
  17. package/clis/instagram/explore.test.js +77 -0
  18. package/clis/instagram/post.js +14 -1
  19. package/clis/instagram/reel.js +13 -1
  20. package/clis/linkedin/company.js +153 -0
  21. package/clis/linkedin/company.test.js +111 -0
  22. package/clis/linkedin/connections.js +135 -0
  23. package/clis/linkedin/connections.test.js +141 -0
  24. package/clis/mercury/reimbursement-draft.js +12 -1
  25. package/clis/twitter/article-evaluate.test.js +27 -0
  26. package/clis/twitter/article.js +73 -16
  27. package/clis/twitter/article.test.js +209 -0
  28. package/clis/twitter/download.js +6 -1
  29. package/clis/twitter/post.js +14 -1
  30. package/clis/twitter/profile.js +1 -1
  31. package/clis/twitter/profile.test.js +8 -0
  32. package/clis/twitter/quote.js +11 -1
  33. package/clis/twitter/reply.js +11 -1
  34. package/dist/src/browser/base-page.d.ts +26 -0
  35. package/dist/src/browser/base-page.js +23 -8
  36. package/dist/src/browser/base-page.test.js +45 -12
  37. package/dist/src/browser/command-catalog.d.ts +6 -0
  38. package/dist/src/browser/command-catalog.js +259 -0
  39. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  40. package/dist/src/browser/command-catalog.test.js +79 -0
  41. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  42. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  43. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  44. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  45. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
  46. package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
  47. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  48. package/dist/src/browser/target-resolver.d.ts +1 -0
  49. package/dist/src/browser/target-resolver.js +99 -4
  50. package/dist/src/browser/target-resolver.test.js +141 -1
  51. package/dist/src/build-manifest.d.ts +6 -0
  52. package/dist/src/build-manifest.js +18 -1
  53. package/dist/src/build-manifest.test.js +57 -1
  54. package/dist/src/builtin-command-surface.d.ts +9 -0
  55. package/dist/src/builtin-command-surface.js +16 -0
  56. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  57. package/dist/src/check-hosted-contract.test.js +56 -0
  58. package/dist/src/cli.js +58 -106
  59. package/dist/src/cli.test.js +71 -1
  60. package/dist/src/command-presentation.d.ts +95 -0
  61. package/dist/src/command-presentation.js +486 -0
  62. package/dist/src/command-presentation.test.d.ts +1 -0
  63. package/dist/src/command-presentation.test.js +97 -0
  64. package/dist/src/command-surface.d.ts +43 -0
  65. package/dist/src/command-surface.js +205 -0
  66. package/dist/src/command-surface.test.d.ts +1 -0
  67. package/dist/src/command-surface.test.js +406 -0
  68. package/dist/src/commanderAdapter.d.ts +5 -1
  69. package/dist/src/commanderAdapter.js +12 -55
  70. package/dist/src/commanderAdapter.test.js +15 -3
  71. package/dist/src/community-plugin-sync.d.ts +11 -0
  72. package/dist/src/community-plugin-sync.js +138 -0
  73. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  74. package/dist/src/community-plugin-sync.test.js +123 -0
  75. package/dist/src/completion-fast.d.ts +4 -4
  76. package/dist/src/completion-fast.js +15 -34
  77. package/dist/src/completion-shared.d.ts +4 -0
  78. package/dist/src/completion-shared.js +38 -0
  79. package/dist/src/completion.js +3 -27
  80. package/dist/src/completion.test.js +31 -2
  81. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  82. package/dist/src/docs-sync-review-cli.test.js +322 -0
  83. package/dist/src/docs-sync-review.d.ts +117 -0
  84. package/dist/src/docs-sync-review.js +475 -0
  85. package/dist/src/docs-sync-review.test.d.ts +1 -0
  86. package/dist/src/docs-sync-review.test.js +324 -0
  87. package/dist/src/errors.d.ts +12 -3
  88. package/dist/src/errors.js +11 -7
  89. package/dist/src/errors.test.js +14 -1
  90. package/dist/src/execution.d.ts +1 -2
  91. package/dist/src/execution.js +4 -45
  92. package/dist/src/generate-release-notes-cli.test.js +7 -2
  93. package/dist/src/help.d.ts +4 -0
  94. package/dist/src/help.js +50 -255
  95. package/dist/src/help.test.js +27 -1
  96. package/dist/src/hosted/args.d.ts +2 -7
  97. package/dist/src/hosted/args.js +2 -99
  98. package/dist/src/hosted/args.test.js +15 -1
  99. package/dist/src/hosted/availability.d.ts +13 -0
  100. package/dist/src/hosted/availability.js +16 -0
  101. package/dist/src/hosted/availability.test.d.ts +1 -0
  102. package/dist/src/hosted/availability.test.js +180 -0
  103. package/dist/src/hosted/browser-args.d.ts +18 -0
  104. package/dist/src/hosted/browser-args.js +152 -0
  105. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  106. package/dist/src/hosted/browser-args.test.js +182 -0
  107. package/dist/src/hosted/client.d.ts +31 -2
  108. package/dist/src/hosted/client.js +441 -53
  109. package/dist/src/hosted/client.test.js +651 -32
  110. package/dist/src/hosted/config.d.ts +7 -2
  111. package/dist/src/hosted/config.js +26 -4
  112. package/dist/src/hosted/config.test.js +34 -1
  113. package/dist/src/hosted/contract.d.ts +94 -0
  114. package/dist/src/hosted/contract.js +208 -0
  115. package/dist/src/hosted/contract.test.d.ts +1 -0
  116. package/dist/src/hosted/contract.test.js +361 -0
  117. package/dist/src/hosted/credentials.d.ts +38 -0
  118. package/dist/src/hosted/credentials.js +248 -0
  119. package/dist/src/hosted/credentials.test.d.ts +1 -0
  120. package/dist/src/hosted/credentials.test.js +93 -0
  121. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  122. package/dist/src/hosted/file-contract.test.js +174 -0
  123. package/dist/src/hosted/files.d.ts +29 -0
  124. package/dist/src/hosted/files.js +296 -0
  125. package/dist/src/hosted/files.test.d.ts +1 -0
  126. package/dist/src/hosted/files.test.js +231 -0
  127. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  128. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  129. package/dist/src/hosted/manifest.d.ts +5 -0
  130. package/dist/src/hosted/manifest.js +21 -66
  131. package/dist/src/hosted/manifest.test.js +120 -2
  132. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  133. package/dist/src/hosted/output-parity.test.js +108 -0
  134. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  135. package/dist/src/hosted/root-command-surface.test.js +646 -0
  136. package/dist/src/hosted/runner.d.ts +7 -0
  137. package/dist/src/hosted/runner.js +554 -253
  138. package/dist/src/hosted/runner.test.js +1471 -53
  139. package/dist/src/hosted/setup.d.ts +3 -2
  140. package/dist/src/hosted/setup.js +45 -15
  141. package/dist/src/hosted/setup.test.js +133 -6
  142. package/dist/src/hosted/types.d.ts +101 -23
  143. package/dist/src/main.js +120 -108
  144. package/dist/src/manifest-types.d.ts +2 -0
  145. package/dist/src/output.d.ts +17 -2
  146. package/dist/src/output.js +88 -81
  147. package/dist/src/output.test.js +141 -51
  148. package/dist/src/pipeline/executor.test.js +1 -0
  149. package/dist/src/pipeline/steps/download.test.js +1 -0
  150. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  151. package/dist/src/plugin-create-cli.test.js +37 -0
  152. package/dist/src/plugin-manifest.d.ts +12 -0
  153. package/dist/src/plugin-manifest.js +16 -0
  154. package/dist/src/plugin-manifest.test.js +19 -1
  155. package/dist/src/plugin-scaffold.d.ts +4 -1
  156. package/dist/src/plugin-scaffold.js +4 -1
  157. package/dist/src/plugin-scaffold.test.js +23 -8
  158. package/dist/src/plugin.js +4 -1
  159. package/dist/src/plugin.test.js +13 -0
  160. package/dist/src/registry.d.ts +2 -0
  161. package/dist/src/release-notes.js +12 -4
  162. package/dist/src/release-notes.test.js +27 -15
  163. package/dist/src/root-command-surface.d.ts +31 -0
  164. package/dist/src/root-command-surface.js +106 -0
  165. package/dist/src/serialization.d.ts +1 -16
  166. package/dist/src/serialization.js +5 -55
  167. package/dist/src/stream-write.d.ts +12 -0
  168. package/dist/src/stream-write.js +91 -0
  169. package/dist/src/stream-write.test.d.ts +1 -0
  170. package/dist/src/stream-write.test.js +186 -0
  171. package/dist/src/types.d.ts +5 -0
  172. package/dist/src/utils.d.ts +1 -1
  173. package/dist/src/utils.js +2 -8
  174. package/dist/src/utils.test.js +50 -0
  175. package/hosted-contract.json +36748 -0
  176. package/package.json +6 -1
  177. package/scripts/check-hosted-contract.mjs +108 -0
  178. package/scripts/docs-sync-review.ts +332 -0
  179. package/scripts/sync-community-plugins.ts +14 -0
  180. package/skills/webcmd-autofix/SKILL.md +26 -37
  181. package/skills/webcmd-usage/SKILL.md +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrhq/webcmd",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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"
@@ -32,6 +32,7 @@
32
32
  "clis/",
33
33
  "skills/**",
34
34
  "cli-manifest.json",
35
+ "hosted-contract.json",
35
36
  "plugin-catalog.json",
36
37
  "scripts/",
37
38
  "README.md",
@@ -44,9 +45,12 @@
44
45
  "build": "npm run clean-dist && npm run copy-yaml && npm run compile && npm run build-manifest",
45
46
  "compile": "tsc --build && node -e \"require('fs').chmodSync('dist/src/main.js', 0o755)\"",
46
47
  "build-manifest": "tsx src/build-manifest.ts",
48
+ "check:hosted-contract": "node scripts/check-hosted-contract.mjs",
47
49
  "clean-dist": "node scripts/clean-dist.cjs",
48
50
  "copy-yaml": "node scripts/copy-yaml.cjs",
51
+ "sync-community-plugins": "tsx scripts/sync-community-plugins.ts",
49
52
  "generate-release-notes": "tsx scripts/generate-release-notes.ts",
53
+ "docs-sync-review": "tsx scripts/docs-sync-review.ts",
50
54
  "start": "node dist/src/main.js",
51
55
  "start:bun": "bun dist/src/main.js",
52
56
  "preuninstall": "node -e \"fetch('http://127.0.0.1:9777/shutdown',{method:'POST',headers:{'X-Webcmd':'1'},signal:AbortSignal.timeout(3000)}).catch(()=>{})\" || true",
@@ -59,6 +63,7 @@
59
63
  "test:adapter": "vitest run --project adapter",
60
64
  "test:all": "vitest run",
61
65
  "test:e2e": "vitest run --project e2e-fixed-port --project e2e",
66
+ "check-community-plugins": "tsx scripts/sync-community-plugins.ts --check",
62
67
  "advise:listing-id-pairing": "node scripts/check-listing-id-pairing.mjs",
63
68
  "check:package-bin": "node scripts/check-package-bin.mjs",
64
69
  "check:silent-column-drop": "node scripts/check-silent-column-drop.mjs",
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const committedRoot = process.env.WEBCMD_CONTRACT_COMMITTED_ROOT
11
+ ? path.resolve(process.env.WEBCMD_CONTRACT_COMMITTED_ROOT)
12
+ : packageRoot;
13
+ const generatedRoot = mkdtempSync(path.join(tmpdir(), 'webcmd-hosted-contract-'));
14
+ const committedArtifactNames = ['cli-manifest.json'];
15
+
16
+ const generator = String.raw`
17
+ import { readFile, writeFile } from 'node:fs/promises';
18
+ import path from 'node:path';
19
+ import { pathToFileURL } from 'node:url';
20
+
21
+ const packageRoot = process.env.WEBCMD_PACKAGE_ROOT;
22
+ const outputRoot = process.env.WEBCMD_CONTRACT_OUTPUT_ROOT;
23
+ if (!packageRoot || !outputRoot) throw new Error('Missing contract generation paths');
24
+
25
+ const moduleUrl = pathToFileURL(path.join(packageRoot, 'src/build-manifest.ts')).href;
26
+ const {
27
+ buildManifest,
28
+ buildManifestArtifacts,
29
+ findManifestMetadataIssues,
30
+ } = await import(moduleUrl);
31
+ const { entries, failures } = await buildManifest();
32
+ if (failures.length > 0) {
33
+ throw new Error(failures.map((failure) => failure.message).join('\n'));
34
+ }
35
+ const metadataIssues = findManifestMetadataIssues(entries);
36
+ if (metadataIssues.length > 0) {
37
+ throw new Error(
38
+ metadataIssues
39
+ .map((issue) => issue.site + '/' + issue.command + ': ' + issue.reason)
40
+ .join('\n'),
41
+ );
42
+ }
43
+
44
+ const packageMetadata = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
45
+ if (typeof packageMetadata.version !== 'string') {
46
+ throw new Error('Package version is required');
47
+ }
48
+ const artifacts = buildManifestArtifacts(entries, packageMetadata.version, []);
49
+ await Promise.all([
50
+ writeFile(path.join(outputRoot, 'cli-manifest.json'), artifacts.manifestJson),
51
+ writeFile(path.join(outputRoot, 'hosted-contract.json'), artifacts.hostedContractJson),
52
+ ]);
53
+ `;
54
+
55
+ try {
56
+ const generation = spawnSync(
57
+ process.execPath,
58
+ ['--import', 'tsx', '--input-type=module', '--eval', generator],
59
+ {
60
+ cwd: packageRoot,
61
+ encoding: 'utf8',
62
+ env: {
63
+ ...process.env,
64
+ WEBCMD_PACKAGE_ROOT: packageRoot,
65
+ WEBCMD_CONTRACT_OUTPUT_ROOT: generatedRoot,
66
+ },
67
+ },
68
+ );
69
+ if (generation.error) throw generation.error;
70
+ if (generation.status !== 0) {
71
+ process.stderr.write(generation.stdout);
72
+ process.stderr.write(generation.stderr);
73
+ process.exitCode = generation.status ?? 1;
74
+ } else {
75
+ const dirtyArtifacts = committedArtifactNames.filter((artifactName) => {
76
+ const committed = readFileSync(path.join(committedRoot, artifactName));
77
+ const generated = readFileSync(path.join(generatedRoot, artifactName));
78
+ return !committed.equals(generated);
79
+ });
80
+
81
+ if (dirtyArtifacts.length > 0) {
82
+ process.stderr.write(
83
+ `Generated contract artifacts are out of date: ${dirtyArtifacts.join(', ')}\n`
84
+ + 'Run `npm run build`, then commit the updated manifest.\n',
85
+ );
86
+ process.exitCode = 1;
87
+ } else {
88
+ const generatedContract = JSON.parse(
89
+ readFileSync(path.join(generatedRoot, 'hosted-contract.json'), 'utf8'),
90
+ );
91
+ if (
92
+ generatedContract?.schemaVersion !== 1
93
+ || typeof generatedContract?.webcmdVersion !== 'string'
94
+ || !Array.isArray(generatedContract?.commands)
95
+ || !Array.isArray(generatedContract?.browserCommands)
96
+ ) {
97
+ process.stderr.write('Generated hosted-contract.json is structurally invalid.\n');
98
+ process.exitCode = 1;
99
+ } else {
100
+ process.stdout.write(
101
+ 'Generated cli-manifest.json matches committed bytes; hosted-contract.json generated successfully.\n',
102
+ );
103
+ }
104
+ }
105
+ }
106
+ } finally {
107
+ rmSync(generatedRoot, { recursive: true, force: true });
108
+ }
@@ -0,0 +1,332 @@
1
+ import { appendFileSync, lstatSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, resolve, sep } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { GoogleGenAI } from '@google/genai';
5
+ import {
6
+ REVIEW_COMMENT_MARKER,
7
+ REVIEW_JSON_SCHEMA,
8
+ buildReviewPrompts,
9
+ classifyPullRequest,
10
+ createDeferredResult,
11
+ createOverrideResult,
12
+ createResolvedResult,
13
+ createUnavailableResult,
14
+ mergeReviewResults,
15
+ renderReviewComment,
16
+ selectDocumentationPaths,
17
+ validateGeminiReview,
18
+ type DocumentationExcerpt,
19
+ type PullRequestReviewContext,
20
+ type ReviewResult,
21
+ } from '../src/docs-sync-review.js';
22
+
23
+ interface Io {
24
+ writeStdout: (chunk: string) => void;
25
+ writeStderr: (chunk: string) => void;
26
+ }
27
+
28
+ export interface RunDependencies {
29
+ loadContext?: (repository: string, number: number, token: string) => Promise<PullRequestReviewContext>;
30
+ loadDocumentation?: (paths: string[]) => DocumentationExcerpt[];
31
+ generateReview?: (prompt: string, model: string, apiKey: string) => Promise<unknown>;
32
+ upsertComment?: (repository: string, number: number, token: string, body: string) => Promise<void>;
33
+ writeSummary?: (body: string, summaryPath: string | undefined) => void;
34
+ }
35
+
36
+ const DEFAULT_IO: Io = {
37
+ writeStdout: (chunk) => process.stdout.write(chunk),
38
+ writeStderr: (chunk) => process.stderr.write(chunk),
39
+ };
40
+
41
+ type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
42
+
43
+ interface PullRequestResponse {
44
+ number: number;
45
+ title: string;
46
+ body?: string | null;
47
+ draft?: boolean;
48
+ head?: { sha?: string };
49
+ labels?: Array<{ name?: string }>;
50
+ }
51
+
52
+ interface PullRequestFileResponse {
53
+ filename: string;
54
+ status: string;
55
+ patch?: string;
56
+ }
57
+
58
+ interface IssueCommentResponse {
59
+ id: number;
60
+ body?: string | null;
61
+ user?: { login?: string } | null;
62
+ }
63
+
64
+ interface GeminiClientLike {
65
+ models: {
66
+ generateContent: (request: {
67
+ model: string;
68
+ contents: string;
69
+ config: {
70
+ responseMimeType: string;
71
+ responseJsonSchema: unknown;
72
+ temperature: number;
73
+ abortSignal: AbortSignal;
74
+ };
75
+ }) => Promise<{ text?: string }>;
76
+ };
77
+ }
78
+
79
+ export type GeminiClientFactory = (apiKey: string) => GeminiClientLike;
80
+
81
+ const GITHUB_API_ROOT = 'https://api.github.com';
82
+ const GITHUB_API_VERSION = '2022-11-28';
83
+
84
+ async function githubRequest<T>(
85
+ path: string,
86
+ token: string,
87
+ init: RequestInit = {},
88
+ fetchImpl: FetchLike = fetch,
89
+ ): Promise<T> {
90
+ const response = await fetchImpl(`${GITHUB_API_ROOT}${path}`, {
91
+ ...init,
92
+ headers: {
93
+ Accept: 'application/vnd.github+json',
94
+ Authorization: `Bearer ${token}`,
95
+ 'X-GitHub-Api-Version': GITHUB_API_VERSION,
96
+ 'Content-Type': 'application/json',
97
+ ...init.headers,
98
+ },
99
+ });
100
+ if (!response.ok) {
101
+ const detail = (await response.text()).slice(0, 600);
102
+ throw new Error(`GitHub API ${response.status}: ${detail}`);
103
+ }
104
+ if (response.status === 204) return undefined as T;
105
+ return await response.json() as T;
106
+ }
107
+
108
+ export async function loadPullRequestContext(
109
+ repository: string,
110
+ number: number,
111
+ token: string,
112
+ fetchImpl: FetchLike = fetch,
113
+ ): Promise<PullRequestReviewContext> {
114
+ const details = await githubRequest<PullRequestResponse>(
115
+ `/repos/${repository}/pulls/${number}`,
116
+ token,
117
+ {},
118
+ fetchImpl,
119
+ );
120
+ const files: PullRequestFileResponse[] = [];
121
+ for (let page = 1; ; page += 1) {
122
+ const batch = await githubRequest<PullRequestFileResponse[]>(
123
+ `/repos/${repository}/pulls/${number}/files?per_page=100&page=${page}`,
124
+ token,
125
+ {},
126
+ fetchImpl,
127
+ );
128
+ files.push(...batch);
129
+ if (batch.length < 100) break;
130
+ }
131
+
132
+ return {
133
+ number: details.number,
134
+ title: details.title,
135
+ body: details.body ?? null,
136
+ draft: details.draft ?? false,
137
+ headSha: details.head?.sha ?? '',
138
+ labels: (details.labels ?? []).flatMap((label) => label.name ? [label.name] : []),
139
+ files: files.map((file) => ({
140
+ path: file.filename,
141
+ status: file.status,
142
+ patch: file.patch,
143
+ })),
144
+ };
145
+ }
146
+
147
+ export async function upsertReviewComment(
148
+ repository: string,
149
+ number: number,
150
+ token: string,
151
+ body: string,
152
+ fetchImpl: FetchLike = fetch,
153
+ ): Promise<void> {
154
+ const comments: IssueCommentResponse[] = [];
155
+ for (let page = 1; ; page += 1) {
156
+ const batch = await githubRequest<IssueCommentResponse[]>(
157
+ `/repos/${repository}/issues/${number}/comments?per_page=100&page=${page}`,
158
+ token,
159
+ {},
160
+ fetchImpl,
161
+ );
162
+ comments.push(...batch);
163
+ if (batch.length < 100) break;
164
+ }
165
+ const existing = comments.find((comment) => comment.user?.login === 'github-actions[bot]'
166
+ && comment.body?.includes(REVIEW_COMMENT_MARKER));
167
+ const path = existing
168
+ ? `/repos/${repository}/issues/comments/${existing.id}`
169
+ : `/repos/${repository}/issues/${number}/comments`;
170
+ await githubRequest(
171
+ path,
172
+ token,
173
+ {
174
+ method: existing ? 'PATCH' : 'POST',
175
+ body: JSON.stringify({ body }),
176
+ },
177
+ fetchImpl,
178
+ );
179
+ }
180
+
181
+ export function loadDocumentation(
182
+ paths: string[],
183
+ root = process.cwd(),
184
+ ): DocumentationExcerpt[] {
185
+ const rootPath = resolve(root);
186
+ const rootPrefix = `${rootPath}${sep}`;
187
+ const excerpts: DocumentationExcerpt[] = [];
188
+ for (const path of paths) {
189
+ if (isAbsolute(path) || path.includes('\\') || path.split('/').includes('..')) continue;
190
+ const absolutePath = resolve(rootPath, path);
191
+ if (!absolutePath.startsWith(rootPrefix)) continue;
192
+ try {
193
+ const stat = lstatSync(absolutePath);
194
+ if (!stat.isFile() || stat.isSymbolicLink()) continue;
195
+ excerpts.push({ path, content: readFileSync(absolutePath, 'utf8') });
196
+ } catch {
197
+ // Missing optional context is represented by its absence.
198
+ }
199
+ }
200
+ return excerpts;
201
+ }
202
+
203
+ const defaultGeminiClient: GeminiClientFactory = (apiKey) => {
204
+ const client = new GoogleGenAI({ apiKey });
205
+ return {
206
+ models: {
207
+ generateContent: (request) => client.models.generateContent(request),
208
+ },
209
+ };
210
+ };
211
+
212
+ export async function generateGeminiReview(
213
+ prompt: string,
214
+ model: string,
215
+ apiKey: string,
216
+ createClient: GeminiClientFactory = defaultGeminiClient,
217
+ ): Promise<unknown> {
218
+ const client = createClient(apiKey);
219
+ const response = await client.models.generateContent({
220
+ model,
221
+ contents: prompt,
222
+ config: {
223
+ responseMimeType: 'application/json',
224
+ responseJsonSchema: REVIEW_JSON_SCHEMA,
225
+ temperature: 0.1,
226
+ abortSignal: AbortSignal.timeout(60_000),
227
+ },
228
+ });
229
+ const text = response.text?.trim();
230
+ if (!text) throw new Error('Gemini returned empty content.');
231
+ try {
232
+ return JSON.parse(text) as unknown;
233
+ } catch {
234
+ throw new Error('Gemini returned invalid JSON.');
235
+ }
236
+ }
237
+
238
+ function defaultWriteSummary(body: string, summaryPath: string | undefined): void {
239
+ if (!summaryPath) return;
240
+ appendFileSync(summaryPath, body);
241
+ }
242
+
243
+ function errorMessage(error: unknown): string {
244
+ return error instanceof Error ? error.message : String(error);
245
+ }
246
+
247
+ export async function runDocsSyncReview(
248
+ argv: string[] = process.argv,
249
+ env: NodeJS.ProcessEnv = process.env,
250
+ deps: RunDependencies = {},
251
+ io: Io = DEFAULT_IO,
252
+ ): Promise<number> {
253
+ const pullRequestNumber = Number.parseInt(argv[2] ?? '', 10);
254
+ if (!Number.isInteger(pullRequestNumber) || pullRequestNumber <= 0) {
255
+ io.writeStderr('Usage: docs-sync-review <pull-request-number>\n');
256
+ return 1;
257
+ }
258
+
259
+ const repository = env.GITHUB_REPOSITORY?.trim();
260
+ const githubToken = env.GH_TOKEN?.trim();
261
+ if (!repository || !githubToken) {
262
+ io.writeStderr('GITHUB_REPOSITORY and GH_TOKEN are required.\n');
263
+ return 0;
264
+ }
265
+
266
+ const loadContext = deps.loadContext ?? loadPullRequestContext;
267
+ const readDocumentation = deps.loadDocumentation ?? loadDocumentation;
268
+ const generateReview = deps.generateReview ?? generateGeminiReview;
269
+ const upsertComment = deps.upsertComment ?? upsertReviewComment;
270
+ const writeSummary = deps.writeSummary ?? defaultWriteSummary;
271
+
272
+ let context: PullRequestReviewContext | undefined;
273
+ let result: ReviewResult | undefined;
274
+ try {
275
+ context = await loadContext(repository, pullRequestNumber, githubToken);
276
+ } catch (error) {
277
+ io.writeStderr(`Unable to load pull request context: ${errorMessage(error)}\n`);
278
+ result = createUnavailableResult();
279
+ }
280
+
281
+ if (context?.draft) {
282
+ result = createDeferredResult();
283
+ } else if (context?.labels.includes('docs-not-needed')) {
284
+ result = createOverrideResult();
285
+ } else if (context) {
286
+ const routing = classifyPullRequest(context.files);
287
+ if (routing.route === 'resolved') {
288
+ result = createResolvedResult(routing);
289
+ } else {
290
+ const apiKey = env.GEMINI_API_KEY?.trim();
291
+ if (!apiKey) {
292
+ io.writeStderr('Semantic review API key is not configured.\n');
293
+ result = createUnavailableResult();
294
+ } else {
295
+ const documentation = readDocumentation(selectDocumentationPaths(context.files));
296
+ const prompts = buildReviewPrompts(context, documentation);
297
+ const reviews: ReviewResult[] = [];
298
+ const model = env.GEMINI_DOCS_REVIEW_MODEL?.trim() || 'gemini-3.5-flash';
299
+ for (const [index, prompt] of prompts.entries()) {
300
+ try {
301
+ const raw = await generateReview(prompt.prompt, model, apiKey);
302
+ reviews.push(validateGeminiReview(raw, context, routing, prompt));
303
+ } catch (error) {
304
+ io.writeStderr(`Semantic review chunk ${index + 1}/${prompts.length} failed: ${errorMessage(error)}\n`);
305
+ reviews.push(createUnavailableResult());
306
+ }
307
+ }
308
+ result = mergeReviewResults(reviews);
309
+ }
310
+ }
311
+ }
312
+
313
+ const body = renderReviewComment(result ?? createUnavailableResult());
314
+ try {
315
+ await upsertComment(repository, pullRequestNumber, githubToken, body);
316
+ io.writeStdout(`Documentation sync review updated for #${pullRequestNumber}.\n`);
317
+ } catch (error) {
318
+ io.writeStderr(`Unable to update the pull request comment: ${errorMessage(error)}\n`);
319
+ try {
320
+ writeSummary(body, env.GITHUB_STEP_SUMMARY);
321
+ } catch (summaryError) {
322
+ io.writeStderr(`Unable to write the workflow summary: ${errorMessage(summaryError)}\n`);
323
+ }
324
+ }
325
+
326
+ return 0;
327
+ }
328
+
329
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
330
+ const exitCode = await runDocsSyncReview();
331
+ process.exit(exitCode);
332
+ }
@@ -0,0 +1,14 @@
1
+ import * as path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import {
4
+ buildCommunityPluginOutputs,
5
+ writeCommunityPluginOutputs,
6
+ } from '../src/community-plugin-sync.js';
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const checkOnly = process.argv.includes('--check');
10
+ const result = checkOnly
11
+ ? buildCommunityPluginOutputs(repoRoot)
12
+ : writeCommunityPluginOutputs(repoRoot);
13
+
14
+ console.log(`${checkOnly ? 'Checked' : 'Synced'} ${Object.keys(result.rootManifest.plugins ?? {}).length} community plugin(s).`);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: webcmd-autofix
3
- description: Automatically fix broken Webcmd adapters when commands fail. Load this skill when a webcmd command fails; it guides you through collecting a trace artifact, patching the adapter, retrying, and filing an upstream GitHub issue after a verified fix. Works with any AI agent.
3
+ description: Automatically fix broken Webcmd adapters when commands fail. Load this skill when a webcmd command fails; it guides you through collecting a trace artifact, patching the adapter, retrying, and safely reporting reproducible upstream defects. Works with any AI agent.
4
4
  allowed-tools: Bash(webcmd:*), Bash(gh:*), Read, Edit, Write
5
5
  ---
6
6
 
@@ -204,54 +204,41 @@ webcmd <site> <command> [args...]
204
204
 
205
205
  If it still fails, collect a fresh trace and start another round. Stop after 3 rounds.
206
206
 
207
- ## Step 6: File An Upstream Issue
207
+ ## Step 6: Report A Reproducible Upstream Defect
208
208
 
209
- If the retry passes, prepare an upstream issue so the local fix can flow back to `agentrhq/webcmd`.
209
+ Offer to file an upstream issue after either outcome:
210
210
 
211
- Do not file for:
211
+ - A local adapter repair was verified and should be contributed upstream.
212
+ - A Webcmd defect remains reproducible after the three-round retry budget.
212
213
 
213
- - `AUTH_REQUIRED`, `BROWSER_CONNECT`, `ARGUMENT`, or `CONFIG`
214
- - CAPTCHA or rate limiting
215
- - failures you could not fix
214
+ Do not file for `AUTH_REQUIRED`, `BROWSER_CONNECT`, `ARGUMENT`, or `CONFIG`;
215
+ CAPTCHA, rate limiting, IP blocking, site policy restrictions, successful empty
216
+ results, invalid input, transient network failures, or unreproduced failures.
216
217
 
217
- Only file after a verified local fix.
218
+ Include the sanitized command, reproduction steps, expected and actual behavior,
219
+ error code/excerpt, Webcmd/Node/OS versions, trace ID, and any verified local fix.
220
+ Remove credentials, cookies, authorization headers, browser session data,
221
+ private content, personal data, and raw trace artifacts.
218
222
 
219
- Draft:
223
+ Show the title and body to the user and get approval unless automatic filing was
224
+ already authorized. Then check their existing GitHub login:
220
225
 
221
- ```markdown
222
- ## Summary
223
- Webcmd autofix repaired this adapter locally, and the retry passed.
224
-
225
- ## Adapter
226
- - Site: `<site>`
227
- - Command: `<command>`
228
- - Webcmd version: `<version from webcmd --version>`
229
-
230
- ## Original failure
231
- - Error code: `<error_code>`
232
-
233
- ~~~
234
- <error_message>
235
- ~~~
236
-
237
- ## Local fix summary
238
-
239
- ~~~
240
- <1-2 sentence description of what changed and why>
241
- ~~~
242
-
243
- _Issue filed by Webcmd autofix after a verified local repair._
226
+ ```bash
227
+ gh auth status
244
228
  ```
245
229
 
246
- Ask the user before filing. Show the draft title and body. If they approve and `gh auth status` succeeds:
230
+ If `gh` is unavailable or unauthenticated, explain that sign-in is required and
231
+ stop without requesting a token. Otherwise, submit the approved, redacted draft:
247
232
 
248
233
  ```bash
249
- gh issue create --repo agentrhq/webcmd \
250
- --title "[autofix] <site>/<command>: <error_code>" \
251
- --body "<the body above>"
234
+ gh issue create \
235
+ --repo "${WEBCMD_FEEDBACK_REPO:-agentrhq/webcmd}" \
236
+ --title "[Bug]: <site>/<command>: <short failure summary>" \
237
+ --body "<approved, sanitized draft>"
252
238
  ```
253
239
 
254
- If `gh` is unavailable or unauthenticated, tell the user and skip issue creation.
240
+ Report the issue URL after GitHub confirms creation. If submission fails, show
241
+ the error without retrying.
255
242
 
256
243
  ## When To Stop
257
244
 
@@ -267,6 +254,8 @@ Soft stops:
267
254
  - Feature completely removed.
268
255
  - Major redesign requiring `webcmd-adapter-author`.
269
256
 
257
+ For a reproducible Webcmd defect at a soft stop, offer the Step 6 reporting flow.
258
+
270
259
  In all stop cases, clearly report the situation instead of making speculative patches.
271
260
 
272
261
  ## Example Repair Session
@@ -104,6 +104,13 @@ webcmd <site> <command> [args...] --trace retain-on-failure
104
104
 
105
105
  The error envelope includes a `trace` block pointing at `summary.md`. Patch only `adapterSourcePath` from that summary and retry. Maximum 3 repair rounds. See `webcmd-autofix`.
106
106
 
107
+ ## Report A Webcmd Defect
108
+
109
+ After a reproducible Webcmd failure has been diagnosed or the `webcmd-autofix`
110
+ retry budget is exhausted, load `webcmd-autofix` and follow its Step 6 reporting
111
+ flow. Do not report expected authentication, configuration, CAPTCHA, rate-limit,
112
+ argument, transient, or unreproduced failures.
113
+
107
114
  ## Writing An Adapter
108
115
 
109
116
  Storage paths: