@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.
Files changed (183) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +228 -24
  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/producthunt/hot.js +1 -2
  26. package/clis/spotify/spotify.js +11 -11
  27. package/clis/twitter/article-evaluate.test.js +27 -0
  28. package/clis/twitter/article.js +73 -16
  29. package/clis/twitter/article.test.js +209 -0
  30. package/clis/twitter/download.js +6 -1
  31. package/clis/twitter/post.js +14 -1
  32. package/clis/twitter/profile.js +1 -1
  33. package/clis/twitter/profile.test.js +8 -0
  34. package/clis/twitter/quote.js +11 -1
  35. package/clis/twitter/reply.js +11 -1
  36. package/dist/src/browser/base-page.d.ts +26 -0
  37. package/dist/src/browser/base-page.js +23 -8
  38. package/dist/src/browser/base-page.test.js +45 -12
  39. package/dist/src/browser/command-catalog.d.ts +6 -0
  40. package/dist/src/browser/command-catalog.js +259 -0
  41. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  42. package/dist/src/browser/command-catalog.test.js +79 -0
  43. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  44. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  45. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  46. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  47. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
  48. package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
  49. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  50. package/dist/src/browser/target-resolver.d.ts +1 -0
  51. package/dist/src/browser/target-resolver.js +99 -4
  52. package/dist/src/browser/target-resolver.test.js +141 -1
  53. package/dist/src/build-manifest.d.ts +6 -0
  54. package/dist/src/build-manifest.js +18 -1
  55. package/dist/src/build-manifest.test.js +57 -1
  56. package/dist/src/builtin-command-surface.d.ts +9 -0
  57. package/dist/src/builtin-command-surface.js +16 -0
  58. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  59. package/dist/src/check-hosted-contract.test.js +56 -0
  60. package/dist/src/cli.js +58 -106
  61. package/dist/src/cli.test.js +71 -1
  62. package/dist/src/command-presentation.d.ts +95 -0
  63. package/dist/src/command-presentation.js +486 -0
  64. package/dist/src/command-presentation.test.d.ts +1 -0
  65. package/dist/src/command-presentation.test.js +97 -0
  66. package/dist/src/command-surface.d.ts +43 -0
  67. package/dist/src/command-surface.js +205 -0
  68. package/dist/src/command-surface.test.d.ts +1 -0
  69. package/dist/src/command-surface.test.js +406 -0
  70. package/dist/src/commanderAdapter.d.ts +5 -1
  71. package/dist/src/commanderAdapter.js +12 -55
  72. package/dist/src/commanderAdapter.test.js +15 -3
  73. package/dist/src/community-plugin-sync.d.ts +11 -0
  74. package/dist/src/community-plugin-sync.js +138 -0
  75. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  76. package/dist/src/community-plugin-sync.test.js +123 -0
  77. package/dist/src/completion-fast.d.ts +4 -4
  78. package/dist/src/completion-fast.js +15 -34
  79. package/dist/src/completion-shared.d.ts +4 -0
  80. package/dist/src/completion-shared.js +38 -0
  81. package/dist/src/completion.js +3 -27
  82. package/dist/src/completion.test.js +31 -2
  83. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  84. package/dist/src/docs-sync-review-cli.test.js +322 -0
  85. package/dist/src/docs-sync-review.d.ts +117 -0
  86. package/dist/src/docs-sync-review.js +475 -0
  87. package/dist/src/docs-sync-review.test.d.ts +1 -0
  88. package/dist/src/docs-sync-review.test.js +324 -0
  89. package/dist/src/errors.d.ts +12 -3
  90. package/dist/src/errors.js +11 -7
  91. package/dist/src/errors.test.js +14 -1
  92. package/dist/src/execution.d.ts +1 -2
  93. package/dist/src/execution.js +4 -45
  94. package/dist/src/generate-release-notes-cli.test.js +7 -2
  95. package/dist/src/help.d.ts +4 -0
  96. package/dist/src/help.js +50 -255
  97. package/dist/src/help.test.js +27 -1
  98. package/dist/src/hosted/args.d.ts +2 -7
  99. package/dist/src/hosted/args.js +2 -99
  100. package/dist/src/hosted/args.test.js +15 -1
  101. package/dist/src/hosted/availability.d.ts +13 -0
  102. package/dist/src/hosted/availability.js +16 -0
  103. package/dist/src/hosted/availability.test.d.ts +1 -0
  104. package/dist/src/hosted/availability.test.js +191 -0
  105. package/dist/src/hosted/browser-args.d.ts +18 -0
  106. package/dist/src/hosted/browser-args.js +152 -0
  107. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  108. package/dist/src/hosted/browser-args.test.js +182 -0
  109. package/dist/src/hosted/client.d.ts +31 -2
  110. package/dist/src/hosted/client.js +441 -53
  111. package/dist/src/hosted/client.test.js +651 -32
  112. package/dist/src/hosted/config.d.ts +7 -2
  113. package/dist/src/hosted/config.js +26 -4
  114. package/dist/src/hosted/config.test.js +34 -1
  115. package/dist/src/hosted/contract.d.ts +94 -0
  116. package/dist/src/hosted/contract.js +208 -0
  117. package/dist/src/hosted/contract.test.d.ts +1 -0
  118. package/dist/src/hosted/contract.test.js +361 -0
  119. package/dist/src/hosted/credentials.d.ts +38 -0
  120. package/dist/src/hosted/credentials.js +248 -0
  121. package/dist/src/hosted/credentials.test.d.ts +1 -0
  122. package/dist/src/hosted/credentials.test.js +93 -0
  123. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  124. package/dist/src/hosted/file-contract.test.js +174 -0
  125. package/dist/src/hosted/files.d.ts +29 -0
  126. package/dist/src/hosted/files.js +296 -0
  127. package/dist/src/hosted/files.test.d.ts +1 -0
  128. package/dist/src/hosted/files.test.js +231 -0
  129. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  130. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  131. package/dist/src/hosted/manifest.d.ts +5 -0
  132. package/dist/src/hosted/manifest.js +21 -66
  133. package/dist/src/hosted/manifest.test.js +120 -2
  134. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  135. package/dist/src/hosted/output-parity.test.js +108 -0
  136. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  137. package/dist/src/hosted/root-command-surface.test.js +646 -0
  138. package/dist/src/hosted/runner.d.ts +7 -0
  139. package/dist/src/hosted/runner.js +554 -253
  140. package/dist/src/hosted/runner.test.js +1471 -53
  141. package/dist/src/hosted/setup.d.ts +3 -2
  142. package/dist/src/hosted/setup.js +45 -15
  143. package/dist/src/hosted/setup.test.js +133 -6
  144. package/dist/src/hosted/types.d.ts +101 -23
  145. package/dist/src/main.js +120 -108
  146. package/dist/src/manifest-types.d.ts +2 -0
  147. package/dist/src/output.d.ts +17 -2
  148. package/dist/src/output.js +88 -81
  149. package/dist/src/output.test.js +141 -51
  150. package/dist/src/pipeline/executor.test.js +1 -0
  151. package/dist/src/pipeline/steps/download.test.js +1 -0
  152. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  153. package/dist/src/plugin-create-cli.test.js +37 -0
  154. package/dist/src/plugin-manifest.d.ts +12 -0
  155. package/dist/src/plugin-manifest.js +16 -0
  156. package/dist/src/plugin-manifest.test.js +19 -1
  157. package/dist/src/plugin-scaffold.d.ts +4 -1
  158. package/dist/src/plugin-scaffold.js +4 -1
  159. package/dist/src/plugin-scaffold.test.js +23 -8
  160. package/dist/src/plugin.js +4 -1
  161. package/dist/src/plugin.test.js +13 -0
  162. package/dist/src/registry.d.ts +2 -0
  163. package/dist/src/release-notes.js +12 -4
  164. package/dist/src/release-notes.test.js +27 -15
  165. package/dist/src/root-command-surface.d.ts +31 -0
  166. package/dist/src/root-command-surface.js +106 -0
  167. package/dist/src/serialization.d.ts +1 -16
  168. package/dist/src/serialization.js +5 -55
  169. package/dist/src/stream-write.d.ts +12 -0
  170. package/dist/src/stream-write.js +91 -0
  171. package/dist/src/stream-write.test.d.ts +1 -0
  172. package/dist/src/stream-write.test.js +186 -0
  173. package/dist/src/types.d.ts +5 -0
  174. package/dist/src/utils.d.ts +1 -1
  175. package/dist/src/utils.js +2 -8
  176. package/dist/src/utils.test.js +50 -0
  177. package/hosted-contract.json +36759 -0
  178. package/package.json +6 -1
  179. package/scripts/check-hosted-contract.mjs +108 -0
  180. package/scripts/docs-sync-review.ts +332 -0
  181. package/scripts/sync-community-plugins.ts +14 -0
  182. package/skills/webcmd-autofix/SKILL.md +26 -37
  183. package/skills/webcmd-usage/SKILL.md +7 -0
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'node:fs';
2
+ import type { HostedCredentialBackend } from './credentials.js';
2
3
  export interface HostedManifestCache {
3
4
  fetchedAt: string;
4
5
  manifest: unknown;
@@ -11,7 +12,9 @@ export type WebcmdConfig = {
11
12
  updatedAt: string;
12
13
  hosted: {
13
14
  apiBaseUrl: string;
14
- apiKey: string;
15
+ apiKey?: string;
16
+ apiKeyRef?: string;
17
+ credentialBackend?: HostedCredentialBackend;
15
18
  manifestCache?: HostedManifestCache;
16
19
  };
17
20
  };
@@ -38,7 +41,9 @@ export type HostedWebcmdConfig = Extract<WebcmdConfig, {
38
41
  export declare function makeLocalConfig(now?: Date): LocalWebcmdConfig;
39
42
  export declare function makeHostedConfig(input: {
40
43
  apiBaseUrl: string;
41
- apiKey: string;
44
+ apiKey?: string;
45
+ apiKeyRef?: string;
46
+ credentialBackend?: HostedCredentialBackend;
42
47
  manifestCache?: HostedManifestCache;
43
48
  now?: Date;
44
49
  }): HostedWebcmdConfig;
@@ -17,13 +17,16 @@ function parseConfig(raw) {
17
17
  if (parsed.mode === 'hosted'
18
18
  && typeof parsed.updatedAt === 'string'
19
19
  && typeof parsed.hosted?.apiBaseUrl === 'string'
20
- && typeof parsed.hosted?.apiKey === 'string') {
20
+ && (typeof parsed.hosted?.apiKey === 'string' || typeof parsed.hosted?.apiKeyRef === 'string')) {
21
+ const credentialBackend = readCredentialBackend(parsed.hosted.credentialBackend);
21
22
  return {
22
23
  mode: 'hosted',
23
24
  updatedAt: parsed.updatedAt,
24
25
  hosted: {
25
26
  apiBaseUrl: parsed.hosted.apiBaseUrl,
26
- apiKey: parsed.hosted.apiKey,
27
+ ...(typeof parsed.hosted.apiKey === 'string' ? { apiKey: parsed.hosted.apiKey } : {}),
28
+ ...(typeof parsed.hosted.apiKeyRef === 'string' ? { apiKeyRef: parsed.hosted.apiKeyRef } : {}),
29
+ ...(credentialBackend ? { credentialBackend } : {}),
27
30
  ...(parsed.hosted.manifestCache ? { manifestCache: parsed.hosted.manifestCache } : {}),
28
31
  },
29
32
  };
@@ -45,7 +48,7 @@ export function saveWebcmdConfig(config, io = {}) {
45
48
  const chmodSync = io.chmodSync ?? fs.chmodSync;
46
49
  const target = getConfigPath(io);
47
50
  mkdirSync(path.dirname(target), { recursive: true });
48
- writeFileSync(target, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
51
+ writeFileSync(target, `${JSON.stringify(persistableConfig(config), null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
49
52
  try {
50
53
  chmodSync(target, 0o600);
51
54
  }
@@ -65,7 +68,9 @@ export function makeHostedConfig(input) {
65
68
  updatedAt: (input.now ?? new Date()).toISOString(),
66
69
  hosted: {
67
70
  apiBaseUrl: normalizeApiBaseUrl(input.apiBaseUrl),
68
- apiKey: input.apiKey.trim(),
71
+ ...(input.apiKey !== undefined ? { apiKey: input.apiKey.trim() } : {}),
72
+ ...(input.apiKeyRef !== undefined ? { apiKeyRef: input.apiKeyRef } : {}),
73
+ ...(input.credentialBackend !== undefined ? { credentialBackend: input.credentialBackend } : {}),
69
74
  ...(input.manifestCache ? { manifestCache: input.manifestCache } : {}),
70
75
  },
71
76
  };
@@ -88,3 +93,20 @@ export function isHostedConfig(config) {
88
93
  export function shouldUseHostedMode(io = {}) {
89
94
  return isHostedConfig(loadWebcmdConfig(io));
90
95
  }
96
+ function readCredentialBackend(value) {
97
+ return value === 'os' || value === 'file-fallback' ? value : undefined;
98
+ }
99
+ function persistableConfig(config) {
100
+ if (config.mode !== 'hosted' || !config.hosted.apiKeyRef)
101
+ return config;
102
+ return {
103
+ mode: 'hosted',
104
+ updatedAt: config.updatedAt,
105
+ hosted: {
106
+ apiBaseUrl: config.hosted.apiBaseUrl,
107
+ apiKeyRef: config.hosted.apiKeyRef,
108
+ ...(config.hosted.credentialBackend ? { credentialBackend: config.hosted.credentialBackend } : {}),
109
+ ...(config.hosted.manifestCache ? { manifestCache: config.hosted.manifestCache } : {}),
110
+ },
111
+ };
112
+ }
@@ -1,8 +1,9 @@
1
- import { mkdtemp, rm } from 'node:fs/promises';
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { afterEach, describe, expect, it } from 'vitest';
5
5
  import { defaultHostedApiBaseUrl, getConfigPath, isHostedConfig, loadWebcmdConfig, makeHostedConfig, makeLocalConfig, saveWebcmdConfig, } from './config.js';
6
+ import { resolveHostedApiKey } from './credentials.js';
6
7
  let tempDir;
7
8
  afterEach(async () => {
8
9
  if (tempDir)
@@ -37,6 +38,38 @@ describe('hosted config', () => {
37
38
  });
38
39
  expect(isHostedConfig(loadWebcmdConfig({ env }))).toBe(true);
39
40
  });
41
+ it('migrates a legacy inline API key without losing manifest cache', async () => {
42
+ tempDir = await mkdtemp(join(tmpdir(), 'webcmd-config-migrate-'));
43
+ const env = {
44
+ WEBCMD_CONFIG_DIR: tempDir,
45
+ WEBCMD_CREDENTIAL_BACKEND: 'file',
46
+ };
47
+ await writeFile(getConfigPath({ env }), JSON.stringify({
48
+ mode: 'hosted',
49
+ updatedAt: '2026-07-08T00:00:00.000Z',
50
+ hosted: {
51
+ apiBaseUrl: 'https://api.example.com',
52
+ apiKey: 'wcmd_legacy_secret',
53
+ manifestCache: {
54
+ fetchedAt: '2026-07-08T00:01:00.000Z',
55
+ manifest: { ok: true },
56
+ },
57
+ },
58
+ }, null, 2));
59
+ const loaded = loadWebcmdConfig({ env });
60
+ if (!isHostedConfig(loaded))
61
+ throw new Error('Expected hosted config');
62
+ const credential = await resolveHostedApiKey(loaded, { env, platform: 'linux' });
63
+ const persisted = await readFile(getConfigPath({ env }), 'utf8');
64
+ expect(credential).toMatchObject({
65
+ apiKey: 'wcmd_legacy_secret',
66
+ backend: 'file-fallback',
67
+ migrated: true,
68
+ });
69
+ expect(persisted).not.toContain('wcmd_legacy_secret');
70
+ expect(persisted).toContain('apiKeyRef');
71
+ expect(persisted).toContain('manifestCache');
72
+ });
40
73
  it('writes local config and resolves default API URL from env', () => {
41
74
  expect(makeLocalConfig(new Date('2026-07-08T00:00:00.000Z'))).toEqual({
42
75
  mode: 'local',
@@ -0,0 +1,94 @@
1
+ import type { Arg, CliCommand } from '../registry.js';
2
+ import { type HostedAvailability } from './availability.js';
3
+ export declare const HOSTED_CONTRACT_SCHEMA_VERSION: 1;
4
+ export type { HostedAvailability } from './availability.js';
5
+ export interface FileArgumentContract {
6
+ direction: 'input' | 'output';
7
+ pathKind: 'file' | 'directory';
8
+ multiple?: boolean;
9
+ separator?: ',';
10
+ contentTypes?: string[];
11
+ maxBytes?: number;
12
+ /** Local destination used when the adapter itself supplies an implicit path. */
13
+ defaultPath?: string;
14
+ }
15
+ export interface HostedFileArgumentContract {
16
+ name: string;
17
+ direction: 'input' | 'output';
18
+ pathKind: 'file' | 'directory';
19
+ multiple: boolean;
20
+ required: boolean;
21
+ separator?: ',';
22
+ contentTypes?: string[];
23
+ maxBytes?: number;
24
+ defaultPath?: string;
25
+ }
26
+ export interface HostedArgumentContract {
27
+ name: string;
28
+ type: 'string' | 'int' | 'number' | 'boolean';
29
+ description: string;
30
+ positional: boolean;
31
+ required: boolean;
32
+ variadic: boolean;
33
+ default?: unknown;
34
+ choices?: string[];
35
+ }
36
+ export interface HostedOptionContract extends HostedArgumentContract {
37
+ name: string;
38
+ flags: string;
39
+ positional: false;
40
+ }
41
+ export type HostedSessionPolicy = 'create-or-reuse' | 'require-existing' | 'close-existing' | 'local-only';
42
+ export interface HostedBrowserCommandContract {
43
+ command: string;
44
+ aliases: string[];
45
+ description: string;
46
+ positionals: HostedArgumentContract[];
47
+ options: HostedArgumentContract[];
48
+ sessionPolicy: HostedSessionPolicy;
49
+ action?: string;
50
+ }
51
+ export interface HostedContractCommand {
52
+ command: string;
53
+ site: string;
54
+ name: string;
55
+ description: string;
56
+ access: 'read' | 'write';
57
+ strategy: 'PUBLIC' | 'COOKIE' | 'INTERCEPT' | 'UI' | 'LOCAL';
58
+ browser: boolean;
59
+ domain?: string;
60
+ positionals: HostedArgumentContract[];
61
+ options: HostedArgumentContract[];
62
+ columns: string[];
63
+ aliases: string[];
64
+ defaultFormat: string;
65
+ example?: string;
66
+ fileArguments: HostedFileArgumentContract[];
67
+ sessionPolicy: HostedSessionPolicy;
68
+ availability: HostedAvailability;
69
+ }
70
+ export interface HostedContract {
71
+ schemaVersion: typeof HOSTED_CONTRACT_SCHEMA_VERSION;
72
+ webcmdVersion: string;
73
+ outputFormats: Array<'table' | 'plain' | 'json' | 'yaml' | 'md' | 'csv'>;
74
+ traceModes: Array<'off' | 'on' | 'retain-on-failure'>;
75
+ commonOptions: HostedOptionContract[];
76
+ commands: HostedContractCommand[];
77
+ browserCommands: HostedBrowserCommandContract[];
78
+ }
79
+ export interface HostedContractCommandInput {
80
+ site: string;
81
+ name: string;
82
+ aliases?: string[];
83
+ description: string;
84
+ access: 'read' | 'write';
85
+ example?: string;
86
+ domain?: string;
87
+ strategy?: string;
88
+ browser?: boolean;
89
+ args: Arg[];
90
+ columns?: string[];
91
+ defaultFormat?: CliCommand['defaultFormat'];
92
+ }
93
+ export declare function buildHostedContract(commands: readonly HostedContractCommandInput[], browserCatalogInput: readonly HostedBrowserCommandContract[], packageVersion: string): HostedContract;
94
+ export declare function serializeHostedContract(contract: HostedContract): string;
@@ -0,0 +1,208 @@
1
+ import { commandHelpData } from '../help.js';
2
+ import { browserCommandCatalog } from '../browser/command-catalog.js';
3
+ import { deriveBrowserAvailability, deriveHostedAvailability, } from './availability.js';
4
+ export const HOSTED_CONTRACT_SCHEMA_VERSION = 1;
5
+ const KNOWN_SESSION_POLICIES = new Set([
6
+ 'create-or-reuse',
7
+ 'require-existing',
8
+ 'close-existing',
9
+ 'local-only',
10
+ ]);
11
+ function sharedContractOptions() {
12
+ const metadata = commandHelpData({
13
+ site: '__contract__',
14
+ name: '__contract__',
15
+ description: '',
16
+ access: 'read',
17
+ strategy: 'public',
18
+ browser: false,
19
+ args: [],
20
+ });
21
+ const common = metadata.common_options;
22
+ const format = common.find(option => option.name === 'format');
23
+ const trace = common.find(option => option.name === 'trace');
24
+ if (!format?.choices || !trace?.choices) {
25
+ throw new Error('Shared format and trace option choices are required');
26
+ }
27
+ return {
28
+ outputFormats: [...format.choices],
29
+ traceModes: [...trace.choices],
30
+ commonOptions: common.map(option => ({
31
+ name: option.name,
32
+ flags: option.flags,
33
+ type: option.flags.includes('<') || option.flags.includes('[') ? 'string' : 'boolean',
34
+ description: option.help ?? '',
35
+ positional: false,
36
+ required: false,
37
+ variadic: false,
38
+ ...(option.default !== undefined ? { default: option.default } : {}),
39
+ ...(option.choices?.length ? { choices: [...option.choices] } : {}),
40
+ })),
41
+ };
42
+ }
43
+ function normalizeArgumentType(type) {
44
+ switch ((type ?? 'string').toLowerCase()) {
45
+ case 'str':
46
+ case 'string':
47
+ return 'string';
48
+ case 'bool':
49
+ case 'boolean':
50
+ return 'boolean';
51
+ case 'int':
52
+ case 'integer':
53
+ return 'int';
54
+ case 'number':
55
+ case 'float':
56
+ return 'number';
57
+ default:
58
+ throw new Error(`Unsupported hosted argument type: ${type}`);
59
+ }
60
+ }
61
+ function normalizeArgument(arg) {
62
+ return {
63
+ name: arg.name,
64
+ type: normalizeArgumentType(arg.type),
65
+ description: arg.help ?? '',
66
+ positional: arg.positional === true,
67
+ required: arg.required === true,
68
+ variadic: false,
69
+ ...(arg.default !== undefined ? { default: arg.default } : {}),
70
+ ...(arg.choices?.length ? { choices: [...arg.choices] } : {}),
71
+ };
72
+ }
73
+ function normalizeStrategy(command) {
74
+ const raw = command.strategy ?? (command.browser === false ? 'public' : 'cookie');
75
+ const strategy = raw.toUpperCase();
76
+ if (strategy === 'PUBLIC' || strategy === 'COOKIE' || strategy === 'INTERCEPT'
77
+ || strategy === 'UI' || strategy === 'LOCAL') {
78
+ return strategy;
79
+ }
80
+ throw new Error(`Unsupported hosted strategy for ${command.site}/${command.name}: ${raw}`);
81
+ }
82
+ function normalizeDefaultFormat(format) {
83
+ if (format === 'yml')
84
+ return 'yaml';
85
+ if (format === 'markdown')
86
+ return 'md';
87
+ return format ?? 'table';
88
+ }
89
+ function normalizeFileArguments(command) {
90
+ return command.args.flatMap((arg) => {
91
+ if (!arg.file)
92
+ return [];
93
+ if (arg.file.direction !== 'input' && arg.file.direction !== 'output') {
94
+ throw new Error(`File argument ${command.site}/${command.name} ${arg.name} must declare direction`);
95
+ }
96
+ if (arg.file.pathKind !== 'file' && arg.file.pathKind !== 'directory') {
97
+ throw new Error(`File argument ${command.site}/${command.name} ${arg.name} must declare pathKind`);
98
+ }
99
+ if (arg.file.separator !== undefined && arg.file.separator !== ',') {
100
+ throw new Error(`File argument ${command.site}/${command.name} ${arg.name} declares unsupported separator`);
101
+ }
102
+ return [{
103
+ name: arg.name,
104
+ direction: arg.file.direction,
105
+ pathKind: arg.file.pathKind,
106
+ multiple: arg.file.multiple === true,
107
+ required: arg.required === true,
108
+ ...(arg.file.separator !== undefined ? { separator: arg.file.separator } : {}),
109
+ ...(arg.file.contentTypes?.length ? { contentTypes: [...arg.file.contentTypes] } : {}),
110
+ ...(arg.file.maxBytes !== undefined ? { maxBytes: arg.file.maxBytes } : {}),
111
+ ...(arg.file.defaultPath !== undefined ? { defaultPath: arg.file.defaultPath } : {}),
112
+ }];
113
+ });
114
+ }
115
+ function assertUniqueAdapterCommands(commands) {
116
+ const canonical = new Set();
117
+ for (const command of commands) {
118
+ const key = `${command.site}/${command.name}`;
119
+ if (canonical.has(key))
120
+ throw new Error(`Duplicate canonical command: ${key}`);
121
+ canonical.add(key);
122
+ }
123
+ const invocations = new Set(canonical);
124
+ for (const command of commands) {
125
+ for (const alias of command.aliases ?? []) {
126
+ const key = `${command.site}/${alias}`;
127
+ if (invocations.has(key))
128
+ throw new Error(`Duplicate command alias: ${key}`);
129
+ invocations.add(key);
130
+ }
131
+ }
132
+ }
133
+ function normalizeBrowserCatalog(browserCatalog) {
134
+ const canonical = new Set();
135
+ for (const browserCommand of browserCatalog) {
136
+ if (canonical.has(browserCommand.command)) {
137
+ throw new Error(`Duplicate canonical browser command: ${browserCommand.command}`);
138
+ }
139
+ canonical.add(browserCommand.command);
140
+ if (!KNOWN_SESSION_POLICIES.has(browserCommand.sessionPolicy)) {
141
+ throw new Error(`Browser command ${browserCommand.command} must declare a known session policy`);
142
+ }
143
+ const availability = deriveBrowserAvailability(browserCommand.command);
144
+ if (availability.mode === 'local-only' && browserCommand.sessionPolicy !== 'local-only') {
145
+ throw new Error(`Browser command ${browserCommand.command} must use local-only session policy`);
146
+ }
147
+ }
148
+ const invocations = new Set(canonical);
149
+ for (const browserCommand of browserCatalog) {
150
+ for (const alias of browserCommand.aliases ?? []) {
151
+ if (invocations.has(alias))
152
+ throw new Error(`Duplicate browser command alias: ${alias}`);
153
+ invocations.add(alias);
154
+ }
155
+ }
156
+ return browserCatalog
157
+ .map(browserCommand => ({
158
+ command: browserCommand.command,
159
+ aliases: [...(browserCommand.aliases ?? [])],
160
+ description: browserCommand.description,
161
+ positionals: browserCommand.positionals.map(arg => ({ ...arg })),
162
+ options: browserCommand.options.map(arg => ({ ...arg })),
163
+ sessionPolicy: browserCommand.sessionPolicy,
164
+ ...(browserCommand.action !== undefined ? { action: browserCommand.action } : {}),
165
+ }))
166
+ .sort((a, b) => a.command.localeCompare(b.command));
167
+ }
168
+ export function buildHostedContract(commands, browserCatalogInput, packageVersion) {
169
+ assertUniqueAdapterCommands(commands);
170
+ const browserCatalog = browserCatalogInput.length > 0
171
+ ? browserCatalogInput
172
+ : browserCommandCatalog;
173
+ const shared = sharedContractOptions();
174
+ const contractCommands = commands.map((command) => {
175
+ const availability = deriveHostedAvailability(command);
176
+ return {
177
+ command: `${command.site}/${command.name}`,
178
+ site: command.site,
179
+ name: command.name,
180
+ description: command.description,
181
+ access: command.access,
182
+ strategy: normalizeStrategy(command),
183
+ browser: command.browser !== false,
184
+ ...(command.domain !== undefined ? { domain: command.domain } : {}),
185
+ positionals: command.args.filter(arg => arg.positional).map(normalizeArgument),
186
+ options: command.args.filter(arg => !arg.positional).map(normalizeArgument),
187
+ columns: [...(command.columns ?? [])],
188
+ aliases: [...(command.aliases ?? [])],
189
+ defaultFormat: normalizeDefaultFormat(command.defaultFormat),
190
+ ...(command.example !== undefined ? { example: command.example } : {}),
191
+ fileArguments: normalizeFileArguments(command),
192
+ sessionPolicy: availability.mode === 'hosted' ? 'create-or-reuse' : 'local-only',
193
+ availability,
194
+ };
195
+ }).sort((a, b) => a.command.localeCompare(b.command));
196
+ return {
197
+ schemaVersion: HOSTED_CONTRACT_SCHEMA_VERSION,
198
+ webcmdVersion: packageVersion,
199
+ outputFormats: shared.outputFormats,
200
+ traceModes: shared.traceModes,
201
+ commonOptions: shared.commonOptions,
202
+ commands: contractCommands,
203
+ browserCommands: normalizeBrowserCatalog(browserCatalog),
204
+ };
205
+ }
206
+ export function serializeHostedContract(contract) {
207
+ return `${JSON.stringify(contract, null, 2)}\n`;
208
+ }
@@ -0,0 +1 @@
1
+ export {};