@agentrhq/webcmd 0.2.5 → 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 (200) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +217 -13
  3. package/clis/_shared/site-auth.js +3 -3
  4. package/clis/_shared/site-auth.test.js +4 -4
  5. package/clis/chatgpt/ask.js +1 -1
  6. package/clis/chatgpt/ask.test.js +11 -0
  7. package/clis/chatgpt/commands.test.js +102 -1
  8. package/clis/chatgpt/deep-research-result.js +45 -3
  9. package/clis/chatgpt/image.js +22 -2
  10. package/clis/chatgpt/project-file-add.js +24 -1
  11. package/clis/chatgpt/utils.js +258 -52
  12. package/clis/chatgpt/utils.test.js +259 -1
  13. package/clis/claude/ask.js +21 -1
  14. package/clis/facebook/feed.js +110 -24
  15. package/clis/facebook/feed.test.js +62 -0
  16. package/clis/facebook/search.js +185 -38
  17. package/clis/facebook/search.test.js +95 -50
  18. package/clis/instagram/explore.js +30 -12
  19. package/clis/instagram/explore.test.js +77 -0
  20. package/clis/instagram/post.js +14 -1
  21. package/clis/instagram/reel.js +13 -1
  22. package/clis/linkedin/company.js +153 -0
  23. package/clis/linkedin/company.test.js +111 -0
  24. package/clis/linkedin/connections.js +135 -0
  25. package/clis/linkedin/connections.test.js +141 -0
  26. package/clis/mercury/reimbursement-draft.js +12 -1
  27. package/clis/slock/whoami.test.js +2 -2
  28. package/clis/twitter/article-evaluate.test.js +27 -0
  29. package/clis/twitter/article.js +73 -16
  30. package/clis/twitter/article.test.js +209 -0
  31. package/clis/twitter/download.js +6 -1
  32. package/clis/twitter/post.js +14 -1
  33. package/clis/twitter/profile.js +1 -1
  34. package/clis/twitter/profile.test.js +8 -0
  35. package/clis/twitter/quote.js +11 -1
  36. package/clis/twitter/reply.js +11 -1
  37. package/dist/src/browser/base-page.d.ts +26 -0
  38. package/dist/src/browser/base-page.js +23 -8
  39. package/dist/src/browser/base-page.test.js +45 -12
  40. package/dist/src/browser/command-catalog.d.ts +6 -0
  41. package/dist/src/browser/command-catalog.js +259 -0
  42. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  43. package/dist/src/browser/command-catalog.test.js +79 -0
  44. package/dist/src/browser/daemon-client.d.ts +2 -1
  45. package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
  46. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  47. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  48. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  49. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  50. package/dist/src/browser/runtime/local-cloak/provider.test.js +36 -0
  51. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +10 -2
  52. package/dist/src/browser/runtime/local-cloak/session-manager.js +25 -9
  53. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  54. package/dist/src/browser/target-resolver.d.ts +1 -0
  55. package/dist/src/browser/target-resolver.js +99 -4
  56. package/dist/src/browser/target-resolver.test.js +141 -1
  57. package/dist/src/build-manifest.d.ts +6 -0
  58. package/dist/src/build-manifest.js +18 -1
  59. package/dist/src/build-manifest.test.js +57 -1
  60. package/dist/src/builtin-command-surface.d.ts +9 -0
  61. package/dist/src/builtin-command-surface.js +16 -0
  62. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  63. package/dist/src/check-hosted-contract.test.js +56 -0
  64. package/dist/src/cli.js +63 -110
  65. package/dist/src/cli.test.js +82 -6
  66. package/dist/src/command-presentation.d.ts +95 -0
  67. package/dist/src/command-presentation.js +486 -0
  68. package/dist/src/command-presentation.test.d.ts +1 -0
  69. package/dist/src/command-presentation.test.js +97 -0
  70. package/dist/src/command-surface.d.ts +43 -0
  71. package/dist/src/command-surface.js +205 -0
  72. package/dist/src/command-surface.test.d.ts +1 -0
  73. package/dist/src/command-surface.test.js +406 -0
  74. package/dist/src/commanderAdapter.d.ts +5 -1
  75. package/dist/src/commanderAdapter.js +12 -55
  76. package/dist/src/commanderAdapter.test.js +15 -3
  77. package/dist/src/commands/auth.js +3 -2
  78. package/dist/src/commands/auth.test.js +6 -6
  79. package/dist/src/community-plugin-sync.d.ts +11 -0
  80. package/dist/src/community-plugin-sync.js +138 -0
  81. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  82. package/dist/src/community-plugin-sync.test.js +123 -0
  83. package/dist/src/completion-fast.d.ts +4 -4
  84. package/dist/src/completion-fast.js +15 -34
  85. package/dist/src/completion-shared.d.ts +4 -0
  86. package/dist/src/completion-shared.js +38 -0
  87. package/dist/src/completion.js +3 -27
  88. package/dist/src/completion.test.js +31 -2
  89. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  90. package/dist/src/docs-sync-review-cli.test.js +322 -0
  91. package/dist/src/docs-sync-review.d.ts +117 -0
  92. package/dist/src/docs-sync-review.js +475 -0
  93. package/dist/src/docs-sync-review.test.d.ts +1 -0
  94. package/dist/src/docs-sync-review.test.js +324 -0
  95. package/dist/src/errors.d.ts +12 -3
  96. package/dist/src/errors.js +11 -7
  97. package/dist/src/errors.test.js +14 -1
  98. package/dist/src/execution.d.ts +1 -2
  99. package/dist/src/execution.js +4 -45
  100. package/dist/src/generate-release-notes-cli.test.js +11 -1
  101. package/dist/src/help.d.ts +4 -0
  102. package/dist/src/help.js +50 -255
  103. package/dist/src/help.test.js +27 -1
  104. package/dist/src/hosted/args.d.ts +4 -0
  105. package/dist/src/hosted/args.js +4 -0
  106. package/dist/src/hosted/args.test.d.ts +1 -0
  107. package/dist/src/hosted/args.test.js +49 -0
  108. package/dist/src/hosted/availability.d.ts +13 -0
  109. package/dist/src/hosted/availability.js +16 -0
  110. package/dist/src/hosted/availability.test.d.ts +1 -0
  111. package/dist/src/hosted/availability.test.js +180 -0
  112. package/dist/src/hosted/browser-args.d.ts +18 -0
  113. package/dist/src/hosted/browser-args.js +152 -0
  114. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  115. package/dist/src/hosted/browser-args.test.js +182 -0
  116. package/dist/src/hosted/client.d.ts +59 -0
  117. package/dist/src/hosted/client.js +510 -0
  118. package/dist/src/hosted/client.test.d.ts +1 -0
  119. package/dist/src/hosted/client.test.js +738 -0
  120. package/dist/src/hosted/config.d.ts +55 -0
  121. package/dist/src/hosted/config.js +112 -0
  122. package/dist/src/hosted/config.test.d.ts +1 -0
  123. package/dist/src/hosted/config.test.js +81 -0
  124. package/dist/src/hosted/contract.d.ts +94 -0
  125. package/dist/src/hosted/contract.js +208 -0
  126. package/dist/src/hosted/contract.test.d.ts +1 -0
  127. package/dist/src/hosted/contract.test.js +361 -0
  128. package/dist/src/hosted/credentials.d.ts +38 -0
  129. package/dist/src/hosted/credentials.js +248 -0
  130. package/dist/src/hosted/credentials.test.d.ts +1 -0
  131. package/dist/src/hosted/credentials.test.js +93 -0
  132. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  133. package/dist/src/hosted/file-contract.test.js +174 -0
  134. package/dist/src/hosted/files.d.ts +29 -0
  135. package/dist/src/hosted/files.js +296 -0
  136. package/dist/src/hosted/files.test.d.ts +1 -0
  137. package/dist/src/hosted/files.test.js +231 -0
  138. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  139. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  140. package/dist/src/hosted/manifest.d.ts +14 -0
  141. package/dist/src/hosted/manifest.js +47 -0
  142. package/dist/src/hosted/manifest.test.d.ts +1 -0
  143. package/dist/src/hosted/manifest.test.js +164 -0
  144. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  145. package/dist/src/hosted/output-parity.test.js +108 -0
  146. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  147. package/dist/src/hosted/root-command-surface.test.js +646 -0
  148. package/dist/src/hosted/runner.d.ts +19 -0
  149. package/dist/src/hosted/runner.js +705 -0
  150. package/dist/src/hosted/runner.test.d.ts +1 -0
  151. package/dist/src/hosted/runner.test.js +1607 -0
  152. package/dist/src/hosted/setup.d.ts +10 -0
  153. package/dist/src/hosted/setup.js +79 -0
  154. package/dist/src/hosted/setup.test.d.ts +1 -0
  155. package/dist/src/hosted/setup.test.js +195 -0
  156. package/dist/src/hosted/types.d.ts +175 -0
  157. package/dist/src/hosted/types.js +1 -0
  158. package/dist/src/main.js +123 -97
  159. package/dist/src/manifest-types.d.ts +2 -0
  160. package/dist/src/output.d.ts +17 -2
  161. package/dist/src/output.js +88 -81
  162. package/dist/src/output.test.js +141 -51
  163. package/dist/src/pipeline/executor.test.js +1 -0
  164. package/dist/src/pipeline/steps/download.test.js +1 -0
  165. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  166. package/dist/src/plugin-create-cli.test.js +37 -0
  167. package/dist/src/plugin-manifest.d.ts +12 -0
  168. package/dist/src/plugin-manifest.js +16 -0
  169. package/dist/src/plugin-manifest.test.js +19 -1
  170. package/dist/src/plugin-scaffold.d.ts +4 -1
  171. package/dist/src/plugin-scaffold.js +4 -1
  172. package/dist/src/plugin-scaffold.test.js +23 -8
  173. package/dist/src/plugin.js +4 -1
  174. package/dist/src/plugin.test.js +13 -0
  175. package/dist/src/registry.d.ts +2 -0
  176. package/dist/src/release-notes.d.ts +6 -1
  177. package/dist/src/release-notes.js +192 -4
  178. package/dist/src/release-notes.test.js +143 -4
  179. package/dist/src/root-command-surface.d.ts +31 -0
  180. package/dist/src/root-command-surface.js +106 -0
  181. package/dist/src/serialization.d.ts +1 -16
  182. package/dist/src/serialization.js +5 -55
  183. package/dist/src/stream-write.d.ts +12 -0
  184. package/dist/src/stream-write.js +91 -0
  185. package/dist/src/stream-write.test.d.ts +1 -0
  186. package/dist/src/stream-write.test.js +186 -0
  187. package/dist/src/types.d.ts +5 -0
  188. package/dist/src/utils.d.ts +1 -1
  189. package/dist/src/utils.js +2 -8
  190. package/dist/src/utils.test.js +50 -0
  191. package/hosted-contract.json +36748 -0
  192. package/package.json +6 -1
  193. package/scripts/check-hosted-contract.mjs +108 -0
  194. package/scripts/docs-sync-review.ts +332 -0
  195. package/scripts/generate-release-notes.ts +1 -1
  196. package/scripts/sync-community-plugins.ts +14 -0
  197. package/skills/smart-search/SKILL.md +14 -3
  198. package/skills/webcmd-autofix/SKILL.md +26 -37
  199. package/skills/webcmd-usage/SKILL.md +21 -7
  200. package/clis/antigravity/SKILL.md +0 -38
@@ -8,6 +8,25 @@ export const RELEASE_NOTE_SECTIONS = [
8
8
  const SQUASH_MERGE_PR_NUMBER_PATTERN = /\(#(?<number>\d+)\)\s*$/;
9
9
  const MERGE_COMMIT_PR_NUMBER_PATTERN = /^Merge pull request #(?<number>\d+) /;
10
10
  const RELEASE_PLEASE_TITLE_PATTERN = /^chore(?:\([^)]+\))?: release(?:\s|$)/;
11
+ const CONTRIBUTOR_AVATAR_SIZE = 40;
12
+ const SERVICE_ACCOUNT_HANDLES = new Set([
13
+ 'allcontributors',
14
+ 'copilot-pull-request-reviewer',
15
+ 'dependabot',
16
+ 'github-actions',
17
+ 'release-please',
18
+ 'renovate',
19
+ 'semantic-release-bot',
20
+ 'snyk-bot',
21
+ 'web-flow',
22
+ ]);
23
+ function normalizeHandle(handle) {
24
+ const trimmed = handle.trim();
25
+ return trimmed.startsWith('@') ? trimmed.slice(1) : trimmed;
26
+ }
27
+ function uniqueSortedHandles(handles) {
28
+ return [...new Set(handles.map(normalizeHandle).filter(Boolean))].sort((left, right) => left.localeCompare(right));
29
+ }
11
30
  function isNoChangeContent(content) {
12
31
  const lines = content
13
32
  .split(/\r?\n/)
@@ -34,7 +53,11 @@ function formatReleaseNotesForChangelog(notes) {
34
53
  const trimmed = notes.trim();
35
54
  if (!trimmed)
36
55
  return '';
37
- return trimmed.replace(/^##\s+/gm, '### ');
56
+ const lines = trimmed.split(/\r?\n/);
57
+ if (lines[0]?.startsWith('# ')) {
58
+ lines[0] = `_${lines[0].replace(/^#\s+/, '').trim()}_`;
59
+ }
60
+ return stripContributorAvatarLines(lines.join('\n')).replace(/^##\s+/gm, '### ');
38
61
  }
39
62
  export function releaseVersionFromTag(tag) {
40
63
  const value = tag.trim();
@@ -44,6 +67,150 @@ export function releaseVersionFromTag(tag) {
44
67
  return value.slice(1);
45
68
  return value;
46
69
  }
70
+ function releaseDisplayVersionFromTag(tag) {
71
+ const version = releaseVersionFromTag(tag);
72
+ return version.startsWith('v') ? version : `v${version}`;
73
+ }
74
+ function parseReleaseSemver(tag) {
75
+ const match = releaseVersionFromTag(tag).match(/^v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:[-+].*)?$/);
76
+ if (!match?.groups)
77
+ return null;
78
+ return {
79
+ major: Number(match.groups.major),
80
+ minor: Number(match.groups.minor),
81
+ patch: Number(match.groups.patch),
82
+ };
83
+ }
84
+ export function isMajorRelease(context) {
85
+ const current = parseReleaseSemver(context.tag);
86
+ if (!current)
87
+ return false;
88
+ const previous = parseReleaseSemver(context.previousTag);
89
+ if (current.major === 0) {
90
+ if (current.minor === 0 || current.patch !== 0)
91
+ return false;
92
+ if (!previous)
93
+ return true;
94
+ return previous.major === 0 && current.minor > previous.minor;
95
+ }
96
+ if (!previous)
97
+ return current.minor === 0 && current.patch === 0;
98
+ return current.major > previous.major;
99
+ }
100
+ function isServiceAccount(handle) {
101
+ const normalized = normalizeHandle(handle).toLowerCase();
102
+ return SERVICE_ACCOUNT_HANDLES.has(normalized)
103
+ || normalized.endsWith('[bot]')
104
+ || normalized.endsWith('-bot');
105
+ }
106
+ export function releaseContributorHandles(pullRequests) {
107
+ return uniqueSortedHandles(pullRequests.flatMap((pr) => (pr.author?.login ? [pr.author.login] : []))).filter((handle) => !isServiceAccount(handle));
108
+ }
109
+ function escapeHtml(value) {
110
+ return value.replace(/[&<>"']/g, (character) => {
111
+ switch (character) {
112
+ case '&':
113
+ return '&amp;';
114
+ case '<':
115
+ return '&lt;';
116
+ case '>':
117
+ return '&gt;';
118
+ case '"':
119
+ return '&quot;';
120
+ case "'":
121
+ return '&#39;';
122
+ default:
123
+ return character;
124
+ }
125
+ });
126
+ }
127
+ function githubHandleUrl(handle) {
128
+ return `https://github.com/${encodeURIComponent(handle)}`;
129
+ }
130
+ function formatContributorAvatar(handle) {
131
+ const escapedHandle = escapeHtml(handle);
132
+ const encodedHandle = encodeURIComponent(handle);
133
+ return `<a href="${githubHandleUrl(handle)}" title="@${escapedHandle}"><img src="https://github.com/${encodedHandle}.png?size=${CONTRIBUTOR_AVATAR_SIZE}" width="${CONTRIBUTOR_AVATAR_SIZE}" height="${CONTRIBUTOR_AVATAR_SIZE}" alt="@${escapedHandle}" /></a>`;
134
+ }
135
+ function formatContributorLink(handle) {
136
+ return `[@${handle}](${githubHandleUrl(handle)})`;
137
+ }
138
+ function formatContributorsSection(handles) {
139
+ if (handles.length === 0)
140
+ return null;
141
+ return [
142
+ '## Contributors',
143
+ handles.map(formatContributorAvatar).join(' '),
144
+ '',
145
+ handles.map(formatContributorLink).join(' | '),
146
+ ].join('\n');
147
+ }
148
+ function stripContributorAvatarLines(notes) {
149
+ return notes
150
+ .split(/\r?\n/)
151
+ .filter((line) => !(/<img\b/i.test(line) && /https:\/\/github\.com\/[^"'\s>]+\.png\?size=\d+/.test(line)))
152
+ .join('\n')
153
+ .replace(/(^|\n)(## Contributors)\n\n/g, '$1$2\n')
154
+ .replace(/\n{3,}/g, '\n\n')
155
+ .trim();
156
+ }
157
+ function parseMajorReleaseTitle(raw) {
158
+ for (const line of raw.split(/\r?\n/)) {
159
+ const titleMatch = line.match(/^#\s+(.+?)\s*$/);
160
+ if (titleMatch)
161
+ return titleMatch[1];
162
+ if (/^##\s+/.test(line))
163
+ return null;
164
+ }
165
+ return null;
166
+ }
167
+ function fallbackMajorReleaseTitle(context) {
168
+ const releaseText = context.pullRequests.map((pr) => [
169
+ pr.title,
170
+ ...pr.labels.map((label) => label.name),
171
+ ...pr.files.map((file) => file.path),
172
+ ].join('\n')).join('\n').toLowerCase();
173
+ if (/\bclis\//.test(releaseText) || /\badapter\b/.test(releaseText) || /\bcli\b/.test(releaseText)) {
174
+ return 'The Command Surface Expands';
175
+ }
176
+ if (/\bbrowser\b|\bcloak\b|\bdaemon\b/.test(releaseText)) {
177
+ return 'The Browser Runtime Matures';
178
+ }
179
+ if (/\bskills?\b/.test(releaseText)) {
180
+ return 'The Agent Authoring Edition';
181
+ }
182
+ if (/\bdocs?\b|\breadme\b/.test(releaseText)) {
183
+ return 'The Documentation Edition';
184
+ }
185
+ if (/\bplugins?\b/.test(releaseText)) {
186
+ return 'The Plugin System Opens Up';
187
+ }
188
+ return 'A New Command Surface';
189
+ }
190
+ function normalizeMajorReleaseTitle(rawTitle, context) {
191
+ const title = (rawTitle ?? '')
192
+ .replace(/^webcmd[-\s]+v?\d+\.\d+\.\d+(?:[-+][^:\s]+)?\s*:\s*/i, '')
193
+ .replace(/^webcmd\s*:\s*/i, '')
194
+ .replace(/[`*_]/g, '')
195
+ .replace(/\s+/g, ' ')
196
+ .replace(/^[#:\-\s]+/, '')
197
+ .replace(/[.!]+$/, '')
198
+ .trim();
199
+ const lowerTitle = title.toLowerCase();
200
+ if (!title
201
+ || title.length > 80
202
+ || lowerTitle === 'none'
203
+ || lowerTitle === 'n/a'
204
+ || lowerTitle === 'release notes'
205
+ || lowerTitle === 'webcmd'
206
+ || lowerTitle === 'webcmd release') {
207
+ return fallbackMajorReleaseTitle(context);
208
+ }
209
+ return title;
210
+ }
211
+ function formatMajorReleaseHeading(raw, context) {
212
+ return `# webcmd ${releaseDisplayVersionFromTag(context.tag)}: ${normalizeMajorReleaseTitle(parseMajorReleaseTitle(raw), context)}`;
213
+ }
47
214
  export function replaceChangelogReleaseNotes(changelog, tag, notes) {
48
215
  const version = releaseVersionFromTag(tag);
49
216
  const headingPattern = new RegExp(`^## \\[${escapeRegExp(version)}\\]\\([^\\n]+\\) \\([^\\n]+\\)\\s*$`, 'm');
@@ -104,12 +271,24 @@ function parseReleaseNoteSections(raw) {
104
271
  }
105
272
  return sections;
106
273
  }
107
- export function normalizeReleaseNotes(raw) {
274
+ export function normalizeReleaseNotes(raw, options = {}) {
108
275
  const sections = parseReleaseNoteSections(raw);
109
- return RELEASE_NOTE_SECTIONS.flatMap((section) => {
276
+ const sectionBlocks = RELEASE_NOTE_SECTIONS.flatMap((section) => {
110
277
  const content = normalizeSectionContent(sections[section]?.join('\n'));
111
278
  return content ? [`## ${section}\n${content}`] : [];
112
- }).join('\n\n');
279
+ });
280
+ if (sectionBlocks.length === 0)
281
+ return '';
282
+ const blocks = [];
283
+ if (options.context && isMajorRelease(options.context)) {
284
+ blocks.push(formatMajorReleaseHeading(raw, options.context));
285
+ }
286
+ blocks.push(...sectionBlocks);
287
+ const contributors = options.context ? releaseContributorHandles(options.context.pullRequests) : [];
288
+ const contributorsSection = formatContributorsSection(contributors);
289
+ if (contributorsSection)
290
+ blocks.push(contributorsSection);
291
+ return blocks.join('\n\n');
113
292
  }
114
293
  function formatPullRequest(pr) {
115
294
  const author = pr.author?.login ?? 'unknown';
@@ -128,11 +307,20 @@ function formatPullRequest(pr) {
128
307
  }
129
308
  export function buildReleaseNotesPrompt(context) {
130
309
  const prSummaries = context.pullRequests.map(formatPullRequest).join('\n\n');
310
+ const majorReleaseInstructions = isMajorRelease(context)
311
+ ? [
312
+ `This is a major release. Start with exactly one H1: # webcmd ${releaseDisplayVersionFromTag(context.tag)}: <Elegant Release Title>.`,
313
+ 'Make the title grand enough to feel memorable, but polished rather than loud. Keep it short, specific to the supplied PRs, and avoid hype words.',
314
+ ]
315
+ : [
316
+ 'Do not include a top-level release title.',
317
+ ];
131
318
  return [
132
319
  `Write user-facing release notes for ${context.tag}.`,
133
320
  `Release range: ${context.previousTag}...${context.currentRef}.`,
134
321
  'Use only the supplied pull requests below. Do not invent changes or pull in information from elsewhere.',
135
322
  `Allowed sections: ${RELEASE_NOTE_SECTIONS.map((section) => `## ${section}`).join(', ')}.`,
323
+ ...majorReleaseInstructions,
136
324
  'Include only sections that have user-visible changes. Omit empty sections entirely; do not write "None", "N/A", or similar placeholder text.',
137
325
  'Do not include a Contributors section.',
138
326
  'In this project, CLI commands and adapters are the same thing. Treat any PR that adds, removes, or changes files under clis/** as an adapter change, even if the PR title says "CLI" instead of "adapter".',
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { RELEASE_NOTE_SECTIONS, buildReleaseNotesPrompt, extractPullRequestNumber, filterReleasePullRequests, normalizeReleaseNotes, replaceChangelogReleaseNotes, } from './release-notes.js';
2
+ import { RELEASE_NOTE_SECTIONS, buildReleaseNotesPrompt, extractPullRequestNumber, filterReleasePullRequests, isMajorRelease, normalizeReleaseNotes, releaseContributorHandles, replaceChangelogReleaseNotes, } from './release-notes.js';
3
3
  describe('release notes helpers', () => {
4
4
  it('extracts PR numbers from squash and merge commit messages', () => {
5
5
  expect(extractPullRequestNumber('feat: add release notes (#123)')).toBe(123);
@@ -17,6 +17,25 @@ describe('release notes helpers', () => {
17
17
  ];
18
18
  expect(filterReleasePullRequests(prs).map((pr) => pr.number)).toEqual([1]);
19
19
  });
20
+ it('detects major releases from semver tag ranges', () => {
21
+ expect(isMajorRelease({ tag: 'webcmd-v2.0.0', previousTag: 'webcmd-v1.9.9' })).toBe(true);
22
+ expect(isMajorRelease({ tag: 'v3.1.0', previousTag: 'v2.9.9' })).toBe(true);
23
+ expect(isMajorRelease({ tag: 'webcmd-v0.3.0', previousTag: 'webcmd-v0.2.5' })).toBe(true);
24
+ expect(isMajorRelease({ tag: 'webcmd-v0.1.0', previousTag: 'webcmd-v0.0.9' })).toBe(true);
25
+ expect(isMajorRelease({ tag: 'webcmd-v2.1.0', previousTag: 'webcmd-v2.0.0' })).toBe(false);
26
+ expect(isMajorRelease({ tag: 'webcmd-v0.3.1', previousTag: 'webcmd-v0.3.0' })).toBe(false);
27
+ expect(isMajorRelease({ tag: 'webcmd-v0.0.1', previousTag: 'webcmd-v0.0.0' })).toBe(false);
28
+ });
29
+ it('deduplicates PR author contributors and excludes service accounts', () => {
30
+ const prs = [
31
+ { number: 1, title: 'feat: browser polish', author: { login: 'alice' }, labels: [], files: [], url: 'https://example.com/1' },
32
+ { number: 2, title: 'feat: docs polish', author: { login: '@alice' }, labels: [], files: [], url: 'https://example.com/2' },
33
+ { number: 3, title: 'chore: release', author: { login: 'github-actions[bot]' }, labels: [], files: [], url: 'https://example.com/3' },
34
+ { number: 4, title: 'feat: adapter polish', author: { login: 'bob' }, labels: [], files: [], url: 'https://example.com/4' },
35
+ { number: 5, title: 'chore: release notes', author: { login: 'release-please' }, labels: [], files: [], url: 'https://example.com/5' },
36
+ ];
37
+ expect(releaseContributorHandles(prs)).toEqual(['alice', 'bob']);
38
+ });
20
39
  it('normalizes only sections with real release-note content', () => {
21
40
  const raw = [
22
41
  '## Highlights',
@@ -51,6 +70,92 @@ describe('release notes helpers', () => {
51
70
  expect(normalized).not.toContain('## Reverts');
52
71
  expect(normalized).not.toContain('None.');
53
72
  });
73
+ it('adds an elegant major release title and visual contributor credits', () => {
74
+ const context = {
75
+ tag: 'webcmd-v2.0.0',
76
+ previousTag: 'webcmd-v1.9.9',
77
+ currentRef: 'webcmd-v2.0.0',
78
+ pullRequests: [
79
+ {
80
+ number: 42,
81
+ title: 'feat: expand browser adapter coverage',
82
+ author: { login: 'alice' },
83
+ labels: [{ name: 'feature' }],
84
+ files: [{ path: 'clis/github/auth.js' }],
85
+ url: 'https://github.com/agentrhq/webcmd/pull/42',
86
+ },
87
+ {
88
+ number: 43,
89
+ title: 'feat: improve release presentation',
90
+ author: { login: 'bob' },
91
+ labels: [{ name: 'feature' }],
92
+ files: [{ path: 'src/release-notes.ts' }],
93
+ url: 'https://github.com/agentrhq/webcmd/pull/43',
94
+ },
95
+ {
96
+ number: 44,
97
+ title: 'chore: release automation',
98
+ author: { login: 'github-actions[bot]' },
99
+ labels: [],
100
+ files: [{ path: '.github/workflows/release.yml' }],
101
+ url: 'https://github.com/agentrhq/webcmd/pull/44',
102
+ },
103
+ ],
104
+ };
105
+ const raw = [
106
+ '# webcmd v2.0.0: The Command Surface Opens',
107
+ '',
108
+ '## Highlights',
109
+ '- Broader adapter authoring workflows.',
110
+ ].join('\n');
111
+ const normalized = normalizeReleaseNotes(raw, { context });
112
+ expect(normalized).toContain('# webcmd v2.0.0: The Command Surface Opens');
113
+ expect(normalized).toContain('## Contributors');
114
+ expect(normalized).toContain('<img src="https://github.com/alice.png?size=40" width="40" height="40" alt="@alice" /></a> <a href="https://github.com/bob"');
115
+ expect(normalized).toContain('[@alice](https://github.com/alice)');
116
+ expect(normalized).toContain('[@alice](https://github.com/alice) | [@bob](https://github.com/bob)');
117
+ expect(normalized).not.toContain('github-actions');
118
+ });
119
+ it('uses a deterministic major release title fallback when the model omits one', () => {
120
+ const context = {
121
+ tag: 'webcmd-v0.3.0',
122
+ previousTag: 'webcmd-v0.2.5',
123
+ currentRef: 'webcmd-v0.3.0',
124
+ pullRequests: [
125
+ {
126
+ number: 42,
127
+ title: 'feat: add github adapter',
128
+ author: { login: 'alice' },
129
+ labels: [{ name: 'feature' }],
130
+ files: [{ path: 'clis/github/auth.js' }],
131
+ url: 'https://github.com/agentrhq/webcmd/pull/42',
132
+ },
133
+ ],
134
+ };
135
+ const normalized = normalizeReleaseNotes('## Highlights\n- Added a GitHub adapter.', { context });
136
+ expect(normalized).toContain('# webcmd v0.3.0: The Command Surface Expands');
137
+ });
138
+ it('does not add major release title treatment to minor releases', () => {
139
+ const context = {
140
+ tag: 'webcmd-v2.1.0',
141
+ previousTag: 'webcmd-v2.0.0',
142
+ currentRef: 'webcmd-v2.1.0',
143
+ pullRequests: [
144
+ {
145
+ number: 42,
146
+ title: 'feat: improve release notes',
147
+ author: { login: 'alice' },
148
+ labels: [{ name: 'feature' }],
149
+ files: [{ path: 'src/release-notes.ts' }],
150
+ url: 'https://github.com/agentrhq/webcmd/pull/42',
151
+ },
152
+ ],
153
+ };
154
+ const normalized = normalizeReleaseNotes('# The Polished Edition\n\n## Highlights\n- Better notes.', { context });
155
+ expect(normalized).not.toContain('# webcmd v2.1.0');
156
+ expect(normalized).toContain('## Highlights\n- Better notes.');
157
+ expect(normalized).toContain('## Contributors');
158
+ });
54
159
  it('replaces a matching changelog release entry with generated notes', () => {
55
160
  const changelog = [
56
161
  '# Changelog',
@@ -70,24 +175,34 @@ describe('release notes helpers', () => {
70
175
  '',
71
176
  ].join('\n');
72
177
  const notes = [
178
+ '# webcmd v2.0.0: The Command Surface Expands',
179
+ '',
73
180
  '## Highlights',
74
181
  '- Better release notes.',
75
182
  '',
76
183
  '## Adapters',
77
184
  '- Improved district checkout.',
185
+ '',
186
+ '## Contributors',
187
+ '<a href="https://github.com/alice" title="@alice"><img src="https://github.com/alice.png?size=40" width="40" height="40" alt="@alice" /></a>',
188
+ '',
189
+ '[@alice](https://github.com/alice)',
78
190
  ].join('\n');
79
191
  const updated = replaceChangelogReleaseNotes(changelog, 'webcmd-v0.2.3', notes);
80
192
  expect(updated).toContain('## [0.2.3]');
193
+ expect(updated).toContain('_webcmd v2.0.0: The Command Surface Expands_');
81
194
  expect(updated).toContain('### Highlights\n- Better release notes.');
82
195
  expect(updated).toContain('### Adapters\n- Improved district checkout.');
196
+ expect(updated).toContain('### Contributors\n[@alice](https://github.com/alice)');
197
+ expect(updated).not.toContain('<img');
83
198
  expect(updated).not.toContain('release-please generated note');
84
199
  expect(updated).toContain('## [0.2.2]');
85
200
  expect(updated).toContain('* older note');
86
201
  });
87
202
  it('builds a prompt grounded in the exact release range and PR list', () => {
88
203
  const context = {
89
- tag: 'v0.2.0',
90
- previousTag: 'v0.1.1',
204
+ tag: 'v0.2.1',
205
+ previousTag: 'v0.2.0',
91
206
  currentRef: 'abc123',
92
207
  pullRequests: [
93
208
  {
@@ -103,12 +218,13 @@ describe('release notes helpers', () => {
103
218
  ],
104
219
  };
105
220
  const prompt = buildReleaseNotesPrompt(context);
106
- expect(prompt).toContain('v0.1.1...abc123');
221
+ expect(prompt).toContain('v0.2.0...abc123');
107
222
  expect(prompt).toContain('PR #42: feat: add docs scaffold');
108
223
  expect(prompt).toContain('docs/docs.json');
109
224
  expect(prompt).toContain('## Highlights');
110
225
  expect(prompt).toContain('## Adapters');
111
226
  expect(prompt).not.toContain('## Contributors');
227
+ expect(prompt).toContain('Do not include a top-level release title');
112
228
  expect(prompt).toContain('Omit empty sections entirely');
113
229
  expect(prompt).toContain('Do not include a Contributors section');
114
230
  expect(prompt).toContain('CLI commands and adapters are the same thing');
@@ -116,4 +232,27 @@ describe('release notes helpers', () => {
116
232
  expect(prompt).toContain('Put new site adapters/CLIs, adapter promotions, adapter hardening');
117
233
  expect(prompt).toContain('## Reverts');
118
234
  });
235
+ it('asks for a tasteful H1 title only when building major release notes', () => {
236
+ const context = {
237
+ tag: 'webcmd-v0.3.0',
238
+ previousTag: 'webcmd-v0.2.5',
239
+ currentRef: 'webcmd-v0.3.0',
240
+ pullRequests: [
241
+ {
242
+ number: 42,
243
+ title: 'feat: expand adapter authoring',
244
+ body: 'Adds richer authoring workflows.',
245
+ author: { login: 'alice' },
246
+ labels: [{ name: 'feature' }],
247
+ files: [{ path: 'skills/webcmd-adapter-author/SKILL.md' }],
248
+ diff: 'diff --git a/skills/webcmd-adapter-author/SKILL.md b/skills/webcmd-adapter-author/SKILL.md',
249
+ url: 'https://github.com/agentrhq/webcmd/pull/42',
250
+ },
251
+ ],
252
+ };
253
+ const prompt = buildReleaseNotesPrompt(context);
254
+ expect(prompt).toContain('This is a major release');
255
+ expect(prompt).toContain('# webcmd v0.3.0: <Elegant Release Title>');
256
+ expect(prompt).toContain('grand enough to feel memorable, but polished rather than loud');
257
+ });
119
258
  });
@@ -0,0 +1,31 @@
1
+ import { Command } from 'commander';
2
+ export declare const ROOT_PROFILE_FLAGS = "--profile <name>";
3
+ export declare const ROOT_PROFILE_DESCRIPTION = "Chrome profile/context alias for browser runtime commands";
4
+ export declare const COMPLETION_SENTINEL = "--get-completions";
5
+ /**
6
+ * Configure the structural root options shared by the local and hosted CLI.
7
+ * Keeping this in one place makes Commander the source of truth for attached
8
+ * values, short-option clusters, missing values, and help/version precedence.
9
+ */
10
+ export declare function configureRootCommandSurface(program: Command): Command;
11
+ export type HostedRootCommandSurface = {
12
+ kind: 'help';
13
+ exitCode: number;
14
+ } | {
15
+ kind: 'version';
16
+ output: string;
17
+ } | {
18
+ kind: 'completion';
19
+ argv: string[];
20
+ } | {
21
+ kind: 'dispatch';
22
+ argv: string[];
23
+ profile?: string;
24
+ literal: boolean;
25
+ };
26
+ /**
27
+ * Parse only the root command surface without registering or discovering local
28
+ * commands. The completion/version checks reproduce the entry-point fast paths
29
+ * in main.ts; all remaining root grammar is delegated to Commander itself.
30
+ */
31
+ export declare function parseHostedRootCommandSurface(argv: readonly string[]): HostedRootCommandSurface;
@@ -0,0 +1,106 @@
1
+ import { Command, CommanderError } from 'commander';
2
+ import { CommanderStructuralError } from './command-surface.js';
3
+ import { PKG_VERSION } from './version.js';
4
+ export const ROOT_PROFILE_FLAGS = '--profile <name>';
5
+ export const ROOT_PROFILE_DESCRIPTION = 'Chrome profile/context alias for browser runtime commands';
6
+ export const COMPLETION_SENTINEL = '--get-completions';
7
+ /**
8
+ * Configure the structural root options shared by the local and hosted CLI.
9
+ * Keeping this in one place makes Commander the source of truth for attached
10
+ * values, short-option clusters, missing values, and help/version precedence.
11
+ */
12
+ export function configureRootCommandSurface(program) {
13
+ return program
14
+ .version(PKG_VERSION)
15
+ .option(ROOT_PROFILE_FLAGS, ROOT_PROFILE_DESCRIPTION)
16
+ .enablePositionalOptions();
17
+ }
18
+ /**
19
+ * Parse only the root command surface without registering or discovering local
20
+ * commands. The completion/version checks reproduce the entry-point fast paths
21
+ * in main.ts; all remaining root grammar is delegated to Commander itself.
22
+ */
23
+ export function parseHostedRootCommandSurface(argv) {
24
+ const input = [...argv];
25
+ // main.ts checks an exact first-token version before its completion scan.
26
+ if (input[0] === '--version' || input[0] === '-V') {
27
+ return { kind: 'version', output: `${PKG_VERSION}\n` };
28
+ }
29
+ // The local completion path scans the complete raw argv before discovery or
30
+ // Commander parsing, including after `--` and malformed root options.
31
+ if (input.includes(COMPLETION_SENTINEL)) {
32
+ return { kind: 'completion', argv: input };
33
+ }
34
+ let stdout = '';
35
+ let stderr = '';
36
+ const boundary = findRootCommandBoundary(input);
37
+ const root = configureRootCommandSurface(new Command('webcmd'))
38
+ .exitOverride()
39
+ .configureOutput({
40
+ writeOut: value => { stdout += value; },
41
+ writeErr: value => { stderr += value; },
42
+ });
43
+ if (boundary.commandIndex !== undefined) {
44
+ // Register only the token Commander needs in order to stop root option
45
+ // parsing. This is structural scaffolding, not adapter discovery.
46
+ root.command(input[boundary.commandIndex])
47
+ .helpOption(false)
48
+ .allowUnknownOption(true)
49
+ .allowExcessArguments(true)
50
+ .argument('[commandArgs...]')
51
+ .exitOverride()
52
+ .configureOutput({ writeOut: () => undefined, writeErr: () => undefined });
53
+ }
54
+ const structuralArgv = boundary.commandIndex !== undefined
55
+ ? input
56
+ : boundary.separatorIndex !== undefined
57
+ ? input.slice(0, boundary.separatorIndex + 1)
58
+ : input;
59
+ try {
60
+ root.parse(structuralArgv, { from: 'user' });
61
+ }
62
+ catch (error) {
63
+ if (!(error instanceof CommanderError))
64
+ throw error;
65
+ if (error.code === 'commander.helpDisplayed')
66
+ return { kind: 'help', exitCode: error.exitCode };
67
+ if (error.code === 'commander.version') {
68
+ return { kind: 'version', output: stdout || `${PKG_VERSION}\n` };
69
+ }
70
+ throw new CommanderStructuralError(stderr || `${error.message}\n`, error.exitCode);
71
+ }
72
+ const profile = root.opts().profile;
73
+ if (boundary.commandIndex === undefined && boundary.separatorIndex === undefined)
74
+ return { kind: 'help', exitCode: 1 };
75
+ const literal = boundary.separatorIndex !== undefined;
76
+ const parsedArgv = boundary.commandIndex !== undefined
77
+ ? input.slice(boundary.commandIndex)
78
+ : input.slice(boundary.separatorIndex + 1);
79
+ if (parsedArgv.length === 0)
80
+ return { kind: 'help', exitCode: 1 };
81
+ return {
82
+ kind: 'dispatch',
83
+ argv: parsedArgv,
84
+ ...(profile !== undefined ? { profile } : {}),
85
+ literal,
86
+ };
87
+ }
88
+ /** Locates the undiscovered command token while respecting root value options. */
89
+ function findRootCommandBoundary(argv) {
90
+ for (let index = 0; index < argv.length; index += 1) {
91
+ const token = argv[index];
92
+ if (token === '--profile') {
93
+ // Commander requires and consumes the next token even when it is `--` or
94
+ // starts with a dash. Structural failures have already been reported.
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (token.startsWith('--profile='))
99
+ continue;
100
+ if (token === '--')
101
+ return { separatorIndex: index };
102
+ if (!token.startsWith('-') || token === '-')
103
+ return { commandIndex: index };
104
+ }
105
+ return {};
106
+ }
@@ -18,22 +18,7 @@ export type SerializedArg = {
18
18
  /** Stable arg schema — every field is always present (no sparse objects). */
19
19
  export declare function serializeArg(a: Arg): SerializedArg;
20
20
  /** Full command metadata for structured output (json/yaml). */
21
- export declare function serializeCommand(cmd: CliCommand): {
22
- command: string;
23
- site: string;
24
- name: string;
25
- aliases: string[];
26
- description: string;
27
- access: import("./registry.js").CommandAccess;
28
- strategy: string;
29
- browser: boolean;
30
- args: SerializedArg[];
31
- columns: string[];
32
- domain: string | null;
33
- example: string;
34
- defaultFormat: "table" | "plain" | "json" | "yaml" | "yml" | "md" | "markdown" | "csv" | null;
35
- siteSession: import("./registry.js").SiteSessionMode | null;
36
- };
21
+ export declare function serializeCommand(cmd: CliCommand): Record<string, unknown>;
37
22
  /** Human-readable arg summary: `<required> [optional]` style. */
38
23
  export declare function formatArgSummary(args: Arg[]): string;
39
24
  /** Agent-facing canonical invocation. Adapter authors may override with `example`. */