@agentrhq/webcmd 0.3.0 → 0.3.2
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 -2
- package/cli-manifest.json +228 -24
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/ask.test.js +11 -0
- package/clis/chatgpt/commands.test.js +102 -1
- package/clis/chatgpt/deep-research-result.js +45 -3
- package/clis/chatgpt/image.js +22 -2
- package/clis/chatgpt/project-file-add.js +24 -1
- package/clis/chatgpt/utils.js +258 -52
- package/clis/chatgpt/utils.test.js +259 -1
- package/clis/claude/ask.js +21 -1
- package/clis/facebook/feed.js +110 -24
- package/clis/facebook/feed.test.js +62 -0
- package/clis/facebook/search.js +185 -38
- package/clis/facebook/search.test.js +95 -50
- package/clis/instagram/explore.js +30 -12
- package/clis/instagram/explore.test.js +77 -0
- package/clis/instagram/post.js +14 -1
- package/clis/instagram/reel.js +13 -1
- package/clis/linkedin/company.js +153 -0
- package/clis/linkedin/company.test.js +111 -0
- package/clis/linkedin/connections.js +135 -0
- package/clis/linkedin/connections.test.js +141 -0
- package/clis/mercury/reimbursement-draft.js +12 -1
- package/clis/producthunt/hot.js +1 -2
- package/clis/spotify/spotify.js +11 -11
- package/clis/twitter/article-evaluate.test.js +27 -0
- package/clis/twitter/article.js +73 -16
- package/clis/twitter/article.test.js +209 -0
- package/clis/twitter/download.js +6 -1
- package/clis/twitter/post.js +14 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/profile.test.js +8 -0
- package/clis/twitter/quote.js +11 -1
- package/clis/twitter/reply.js +11 -1
- package/dist/src/browser/base-page.d.ts +26 -0
- package/dist/src/browser/base-page.js +23 -8
- package/dist/src/browser/base-page.test.js +45 -12
- package/dist/src/browser/command-catalog.d.ts +6 -0
- package/dist/src/browser/command-catalog.js +259 -0
- package/dist/src/browser/command-catalog.test.d.ts +1 -0
- package/dist/src/browser/command-catalog.test.js +79 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +99 -4
- package/dist/src/browser/target-resolver.test.js +141 -1
- package/dist/src/build-manifest.d.ts +6 -0
- package/dist/src/build-manifest.js +18 -1
- package/dist/src/build-manifest.test.js +57 -1
- package/dist/src/builtin-command-surface.d.ts +9 -0
- package/dist/src/builtin-command-surface.js +16 -0
- package/dist/src/check-hosted-contract.test.d.ts +1 -0
- package/dist/src/check-hosted-contract.test.js +56 -0
- package/dist/src/cli.js +58 -106
- package/dist/src/cli.test.js +71 -1
- package/dist/src/command-presentation.d.ts +95 -0
- package/dist/src/command-presentation.js +486 -0
- package/dist/src/command-presentation.test.d.ts +1 -0
- package/dist/src/command-presentation.test.js +97 -0
- package/dist/src/command-surface.d.ts +43 -0
- package/dist/src/command-surface.js +205 -0
- package/dist/src/command-surface.test.d.ts +1 -0
- package/dist/src/command-surface.test.js +406 -0
- package/dist/src/commanderAdapter.d.ts +5 -1
- package/dist/src/commanderAdapter.js +12 -55
- package/dist/src/commanderAdapter.test.js +15 -3
- package/dist/src/community-plugin-sync.d.ts +11 -0
- package/dist/src/community-plugin-sync.js +138 -0
- package/dist/src/community-plugin-sync.test.d.ts +1 -0
- package/dist/src/community-plugin-sync.test.js +123 -0
- package/dist/src/completion-fast.d.ts +4 -4
- package/dist/src/completion-fast.js +15 -34
- package/dist/src/completion-shared.d.ts +4 -0
- package/dist/src/completion-shared.js +38 -0
- package/dist/src/completion.js +3 -27
- package/dist/src/completion.test.js +31 -2
- package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
- package/dist/src/docs-sync-review-cli.test.js +322 -0
- package/dist/src/docs-sync-review.d.ts +117 -0
- package/dist/src/docs-sync-review.js +475 -0
- package/dist/src/docs-sync-review.test.d.ts +1 -0
- package/dist/src/docs-sync-review.test.js +324 -0
- package/dist/src/errors.d.ts +12 -3
- package/dist/src/errors.js +11 -7
- package/dist/src/errors.test.js +14 -1
- package/dist/src/execution.d.ts +1 -2
- package/dist/src/execution.js +4 -45
- package/dist/src/generate-release-notes-cli.test.js +7 -2
- package/dist/src/help.d.ts +4 -0
- package/dist/src/help.js +50 -255
- package/dist/src/help.test.js +27 -1
- package/dist/src/hosted/args.d.ts +2 -7
- package/dist/src/hosted/args.js +2 -99
- package/dist/src/hosted/args.test.js +15 -1
- package/dist/src/hosted/availability.d.ts +13 -0
- package/dist/src/hosted/availability.js +16 -0
- package/dist/src/hosted/availability.test.d.ts +1 -0
- package/dist/src/hosted/availability.test.js +191 -0
- package/dist/src/hosted/browser-args.d.ts +18 -0
- package/dist/src/hosted/browser-args.js +152 -0
- package/dist/src/hosted/browser-args.test.d.ts +1 -0
- package/dist/src/hosted/browser-args.test.js +182 -0
- package/dist/src/hosted/client.d.ts +31 -2
- package/dist/src/hosted/client.js +441 -53
- package/dist/src/hosted/client.test.js +651 -32
- package/dist/src/hosted/config.d.ts +7 -2
- package/dist/src/hosted/config.js +26 -4
- package/dist/src/hosted/config.test.js +34 -1
- package/dist/src/hosted/contract.d.ts +94 -0
- package/dist/src/hosted/contract.js +208 -0
- package/dist/src/hosted/contract.test.d.ts +1 -0
- package/dist/src/hosted/contract.test.js +361 -0
- package/dist/src/hosted/credentials.d.ts +38 -0
- package/dist/src/hosted/credentials.js +248 -0
- package/dist/src/hosted/credentials.test.d.ts +1 -0
- package/dist/src/hosted/credentials.test.js +93 -0
- package/dist/src/hosted/file-contract.test.d.ts +1 -0
- package/dist/src/hosted/file-contract.test.js +174 -0
- package/dist/src/hosted/files.d.ts +29 -0
- package/dist/src/hosted/files.js +296 -0
- package/dist/src/hosted/files.test.d.ts +1 -0
- package/dist/src/hosted/files.test.js +231 -0
- package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
- package/dist/src/hosted/main-lifecycle.test.js +213 -0
- package/dist/src/hosted/manifest.d.ts +5 -0
- package/dist/src/hosted/manifest.js +21 -66
- package/dist/src/hosted/manifest.test.js +120 -2
- package/dist/src/hosted/output-parity.test.d.ts +1 -0
- package/dist/src/hosted/output-parity.test.js +108 -0
- package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
- package/dist/src/hosted/root-command-surface.test.js +646 -0
- package/dist/src/hosted/runner.d.ts +7 -0
- package/dist/src/hosted/runner.js +554 -253
- package/dist/src/hosted/runner.test.js +1471 -53
- package/dist/src/hosted/setup.d.ts +3 -2
- package/dist/src/hosted/setup.js +45 -15
- package/dist/src/hosted/setup.test.js +133 -6
- package/dist/src/hosted/types.d.ts +101 -23
- package/dist/src/main.js +120 -108
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/output.d.ts +17 -2
- package/dist/src/output.js +88 -81
- package/dist/src/output.test.js +141 -51
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/plugin-create-cli.test.d.ts +1 -0
- package/dist/src/plugin-create-cli.test.js +37 -0
- package/dist/src/plugin-manifest.d.ts +12 -0
- package/dist/src/plugin-manifest.js +16 -0
- package/dist/src/plugin-manifest.test.js +19 -1
- package/dist/src/plugin-scaffold.d.ts +4 -1
- package/dist/src/plugin-scaffold.js +4 -1
- package/dist/src/plugin-scaffold.test.js +23 -8
- package/dist/src/plugin.js +4 -1
- package/dist/src/plugin.test.js +13 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/release-notes.js +12 -4
- package/dist/src/release-notes.test.js +27 -15
- package/dist/src/root-command-surface.d.ts +31 -0
- package/dist/src/root-command-surface.js +106 -0
- package/dist/src/serialization.d.ts +1 -16
- package/dist/src/serialization.js +5 -55
- package/dist/src/stream-write.d.ts +12 -0
- package/dist/src/stream-write.js +91 -0
- package/dist/src/stream-write.test.d.ts +1 -0
- package/dist/src/stream-write.test.js +186 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.js +2 -8
- package/dist/src/utils.test.js +50 -0
- package/hosted-contract.json +36759 -0
- package/package.json +6 -1
- package/scripts/check-hosted-contract.mjs +108 -0
- package/scripts/docs-sync-review.ts +332 -0
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/webcmd-autofix/SKILL.md +26 -37
- 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.
|
|
3
|
+
"version": "0.3.2",
|
|
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
|
|
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:
|
|
207
|
+
## Step 6: Report A Reproducible Upstream Defect
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
Offer to file an upstream issue after either outcome:
|
|
210
210
|
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
250
|
-
--
|
|
251
|
-
--
|
|
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
|
-
|
|
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:
|