@jackwener/opencli 1.0.3 → 1.0.5

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 (190) hide show
  1. package/.github/workflows/build-extension.yml +21 -3
  2. package/.github/workflows/docs.yml +52 -0
  3. package/README.md +28 -28
  4. package/README.zh-CN.md +28 -28
  5. package/dist/browser/cdp.d.ts +16 -1
  6. package/dist/browser/cdp.js +124 -80
  7. package/dist/browser/daemon-client.d.ts +3 -1
  8. package/dist/browser/daemon-client.js +4 -0
  9. package/dist/browser/dom-helpers.d.ts +20 -0
  10. package/dist/browser/dom-helpers.js +109 -0
  11. package/dist/browser/mcp.d.ts +1 -0
  12. package/dist/browser/mcp.js +10 -5
  13. package/dist/browser/page.d.ts +7 -0
  14. package/dist/browser/page.js +37 -100
  15. package/dist/browser.test.js +7 -0
  16. package/dist/build-manifest.js +3 -1
  17. package/dist/build-manifest.test.js +34 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +30 -0
  20. package/dist/capabilityRouting.test.d.ts +1 -0
  21. package/dist/capabilityRouting.test.js +42 -0
  22. package/dist/chaoxing.test.js +11 -4
  23. package/dist/cli-manifest.json +635 -1
  24. package/dist/cli.js +48 -8
  25. package/dist/clis/antigravity/serve.d.ts +14 -0
  26. package/dist/clis/antigravity/serve.js +263 -0
  27. package/dist/clis/bilibili/download.js +4 -14
  28. package/dist/clis/boss/resume.d.ts +1 -0
  29. package/dist/clis/boss/resume.js +249 -0
  30. package/dist/clis/hf/top.d.ts +1 -0
  31. package/dist/clis/hf/top.js +119 -0
  32. package/dist/clis/jike/comment.d.ts +1 -0
  33. package/dist/clis/jike/comment.js +107 -0
  34. package/dist/clis/jike/create.d.ts +1 -0
  35. package/dist/clis/jike/create.js +106 -0
  36. package/dist/clis/jike/feed.d.ts +1 -0
  37. package/dist/clis/jike/feed.js +67 -0
  38. package/dist/clis/jike/like.d.ts +1 -0
  39. package/dist/clis/jike/like.js +61 -0
  40. package/dist/clis/jike/notifications.d.ts +1 -0
  41. package/dist/clis/jike/notifications.js +169 -0
  42. package/dist/clis/jike/post.yaml +58 -0
  43. package/dist/clis/jike/repost.d.ts +1 -0
  44. package/dist/clis/jike/repost.js +103 -0
  45. package/dist/clis/jike/search.d.ts +1 -0
  46. package/dist/clis/jike/search.js +67 -0
  47. package/dist/clis/jike/shared.d.ts +19 -0
  48. package/dist/clis/jike/shared.js +25 -0
  49. package/dist/clis/jike/topic.yaml +52 -0
  50. package/dist/clis/jike/user.yaml +51 -0
  51. package/dist/clis/smzdm/search.js +28 -39
  52. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  53. package/dist/clis/stackoverflow/hot.yaml +28 -0
  54. package/dist/clis/stackoverflow/search.yaml +32 -0
  55. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  56. package/dist/clis/twitter/download.js +6 -16
  57. package/dist/clis/xiaohongshu/download.js +3 -3
  58. package/dist/clis/zhihu/download.js +3 -3
  59. package/dist/doctor.d.ts +7 -0
  60. package/dist/doctor.js +16 -0
  61. package/dist/download/index.d.ts +12 -8
  62. package/dist/download/index.js +11 -3
  63. package/dist/download/index.test.d.ts +1 -0
  64. package/dist/download/index.test.js +14 -0
  65. package/dist/engine.js +5 -5
  66. package/dist/explore.d.ts +1 -0
  67. package/dist/explore.js +3 -3
  68. package/dist/generate.js +1 -0
  69. package/dist/interceptor.js +3 -2
  70. package/dist/output.d.ts +1 -0
  71. package/dist/output.js +3 -1
  72. package/dist/pipeline/executor.test.js +1 -0
  73. package/dist/pipeline/steps/download.js +14 -18
  74. package/dist/registry.d.ts +1 -0
  75. package/dist/registry.js +5 -2
  76. package/dist/runtime.d.ts +4 -1
  77. package/dist/runtime.js +2 -2
  78. package/dist/types.d.ts +12 -0
  79. package/dist/verify.d.ts +6 -1
  80. package/dist/verify.js +54 -2
  81. package/docs/.vitepress/config.mts +193 -0
  82. package/docs/adapters/browser/apple-podcasts.md +28 -0
  83. package/docs/adapters/browser/bbc.md +26 -0
  84. package/docs/adapters/browser/bilibili.md +38 -0
  85. package/docs/adapters/browser/boss.md +28 -0
  86. package/docs/adapters/browser/coupang.md +28 -0
  87. package/docs/adapters/browser/ctrip.md +27 -0
  88. package/docs/adapters/browser/github.md +26 -0
  89. package/docs/adapters/browser/hackernews.md +26 -0
  90. package/docs/adapters/browser/linkedin.md +27 -0
  91. package/docs/adapters/browser/reddit.md +41 -0
  92. package/docs/adapters/browser/reuters.md +27 -0
  93. package/docs/adapters/browser/smzdm.md +27 -0
  94. package/docs/adapters/browser/twitter.md +47 -0
  95. package/docs/adapters/browser/v2ex.md +32 -0
  96. package/docs/adapters/browser/weibo.md +27 -0
  97. package/docs/adapters/browser/xiaohongshu.md +32 -0
  98. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  99. package/docs/adapters/browser/xueqiu.md +32 -0
  100. package/docs/adapters/browser/yahoo-finance.md +26 -0
  101. package/docs/adapters/browser/youtube.md +29 -0
  102. package/docs/adapters/browser/zhihu.md +30 -0
  103. package/docs/adapters/desktop/antigravity.md +46 -0
  104. package/docs/adapters/desktop/chatgpt.md +43 -0
  105. package/docs/adapters/desktop/chatwise.md +38 -0
  106. package/docs/adapters/desktop/codex.md +32 -0
  107. package/docs/adapters/desktop/cursor.md +33 -0
  108. package/docs/adapters/desktop/discord.md +28 -0
  109. package/docs/adapters/desktop/feishu.md +20 -0
  110. package/docs/adapters/desktop/neteasemusic.md +31 -0
  111. package/docs/adapters/desktop/notion.md +29 -0
  112. package/docs/adapters/desktop/wechat.md +28 -0
  113. package/docs/adapters/index.md +49 -0
  114. package/docs/advanced/cdp.md +103 -0
  115. package/docs/advanced/download.md +63 -0
  116. package/docs/advanced/electron.md +125 -0
  117. package/docs/advanced/remote-chrome.md +72 -0
  118. package/docs/developer/ai-workflow.md +66 -0
  119. package/docs/developer/architecture.md +90 -0
  120. package/docs/developer/contributing.md +136 -0
  121. package/docs/developer/testing.md +237 -0
  122. package/docs/developer/ts-adapter.md +87 -0
  123. package/docs/developer/yaml-adapter.md +108 -0
  124. package/docs/guide/browser-bridge.md +38 -0
  125. package/docs/guide/getting-started.md +56 -0
  126. package/docs/guide/installation.md +37 -0
  127. package/docs/guide/troubleshooting.md +56 -0
  128. package/docs/index.md +35 -0
  129. package/docs/zh/adapters/index.md +5 -0
  130. package/docs/zh/advanced/cdp.md +3 -0
  131. package/docs/zh/developer/contributing.md +24 -0
  132. package/docs/zh/guide/browser-bridge.md +25 -0
  133. package/docs/zh/guide/getting-started.md +40 -0
  134. package/docs/zh/guide/installation.md +37 -0
  135. package/docs/zh/index.md +29 -0
  136. package/extension/dist/background.js +92 -52
  137. package/extension/package-lock.json +1156 -0
  138. package/extension/src/background.test.ts +151 -0
  139. package/extension/src/background.ts +122 -51
  140. package/extension/src/protocol.ts +3 -1
  141. package/package.json +7 -3
  142. package/src/browser/cdp.ts +154 -82
  143. package/src/browser/daemon-client.ts +7 -1
  144. package/src/browser/dom-helpers.ts +116 -0
  145. package/src/browser/mcp.ts +14 -6
  146. package/src/browser/page.ts +45 -100
  147. package/src/browser.test.ts +10 -0
  148. package/src/build-manifest.test.ts +36 -0
  149. package/src/build-manifest.ts +2 -1
  150. package/src/capabilityRouting.test.ts +47 -0
  151. package/src/capabilityRouting.ts +28 -0
  152. package/src/chaoxing.test.ts +12 -4
  153. package/src/cli.ts +30 -8
  154. package/src/clis/antigravity/serve.ts +329 -0
  155. package/src/clis/bilibili/download.ts +4 -15
  156. package/src/clis/boss/resume.ts +262 -0
  157. package/src/clis/hf/top.ts +141 -0
  158. package/src/clis/jike/comment.ts +113 -0
  159. package/src/clis/jike/create.ts +113 -0
  160. package/src/clis/jike/feed.ts +74 -0
  161. package/src/clis/jike/like.ts +65 -0
  162. package/src/clis/jike/notifications.ts +185 -0
  163. package/src/clis/jike/post.yaml +58 -0
  164. package/src/clis/jike/repost.ts +114 -0
  165. package/src/clis/jike/search.ts +74 -0
  166. package/src/clis/jike/shared.ts +36 -0
  167. package/src/clis/jike/topic.yaml +52 -0
  168. package/src/clis/jike/user.yaml +51 -0
  169. package/src/clis/smzdm/search.ts +30 -39
  170. package/src/clis/stackoverflow/bounties.yaml +29 -0
  171. package/src/clis/stackoverflow/hot.yaml +28 -0
  172. package/src/clis/stackoverflow/search.yaml +32 -0
  173. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  174. package/src/clis/twitter/download.ts +6 -17
  175. package/src/clis/xiaohongshu/download.ts +3 -3
  176. package/src/clis/zhihu/download.ts +3 -3
  177. package/src/doctor.ts +18 -2
  178. package/src/download/index.test.ts +16 -0
  179. package/src/download/index.ts +22 -4
  180. package/src/engine.ts +4 -4
  181. package/src/explore.ts +4 -4
  182. package/src/generate.ts +1 -0
  183. package/src/interceptor.ts +3 -2
  184. package/src/output.ts +3 -1
  185. package/src/pipeline/executor.test.ts +1 -0
  186. package/src/pipeline/steps/download.ts +14 -17
  187. package/src/registry.ts +6 -2
  188. package/src/runtime.ts +3 -2
  189. package/src/types.ts +9 -0
  190. package/src/verify.ts +64 -3
package/dist/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { executeCommand } from './engine.js';
4
- import { fullName, getRegistry, strategyLabel } from './registry.js';
4
+ import { Strategy, fullName, getRegistry, strategyLabel } from './registry.js';
5
5
  import { render as renderOutput } from './output.js';
6
6
  import { BrowserBridge, CDPBridge } from './browser/index.js';
7
7
  import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
8
8
  import { PKG_VERSION } from './version.js';
9
9
  import { printCompletionScript } from './completion.js';
10
10
  import { CliError } from './errors.js';
11
+ import { shouldUseBrowserSession } from './capabilityRouting.js';
11
12
  export function runCli(BUILTIN_CLIS, USER_CLIS) {
12
13
  const program = new Command();
13
14
  program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
@@ -68,11 +69,21 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
68
69
  process.exitCode = r.ok ? 0 : 1;
69
70
  });
70
71
  program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
71
- .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
72
+ .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `explore:${opts.site ?? (() => { try {
73
+ return new URL(url).host;
74
+ }
75
+ catch {
76
+ return 'default';
77
+ } })()}`; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels, workspace }))); });
72
78
  program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
73
79
  .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
74
80
  program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
75
- .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
81
+ .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `generate:${opts.site ?? (() => { try {
82
+ return new URL(url).host;
83
+ }
84
+ catch {
85
+ return 'default';
86
+ } })()}`; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site, workspace }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
76
87
  program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
77
88
  .action(async (url, opts) => {
78
89
  const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
@@ -86,15 +97,21 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
86
97
  }
87
98
  catch { }
88
99
  return cascadeProbe(page, url);
89
- });
100
+ }, { workspace: `cascade:${opts.site ?? (() => { try {
101
+ return new URL(url).host;
102
+ }
103
+ catch {
104
+ return 'default';
105
+ } })()}` });
90
106
  console.log(renderCascadeResult(result));
91
107
  });
92
108
  program.command('doctor')
93
109
  .description('Diagnose opencli browser bridge connectivity')
94
110
  .option('--live', 'Test browser connectivity (requires Chrome running)', false)
111
+ .option('--sessions', 'Show active automation sessions', false)
95
112
  .action(async (opts) => {
96
113
  const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
97
- const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
114
+ const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
98
115
  console.log(renderBrowserDoctorReport(report));
99
116
  });
100
117
  program.command('setup')
@@ -109,15 +126,29 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
109
126
  .action((shell) => {
110
127
  printCompletionScript(shell);
111
128
  });
129
+ // ── Antigravity serve (built-in, long-running) ──────────────────────────────
130
+ const antigravityCmd = program.command('antigravity').description('antigravity commands');
131
+ antigravityCmd.command('serve')
132
+ .description('Start Anthropic-compatible API proxy for Antigravity')
133
+ .option('--port <port>', 'Server port (default: 8082)', '8082')
134
+ .action(async (opts) => {
135
+ const { startServe } = await import('./clis/antigravity/serve.js');
136
+ await startServe({ port: parseInt(opts.port) });
137
+ });
112
138
  // ── Dynamic site commands ──────────────────────────────────────────────────
113
139
  const registry = getRegistry();
114
140
  const siteGroups = new Map();
141
+ // Pre-seed with the antigravity command registered above to avoid duplicates
142
+ siteGroups.set('antigravity', antigravityCmd);
115
143
  for (const [, cmd] of registry) {
116
144
  let siteCmd = siteGroups.get(cmd.site);
117
145
  if (!siteCmd) {
118
146
  siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
119
147
  siteGroups.set(cmd.site, siteCmd);
120
148
  }
149
+ // Skip if this subcommand was already hardcoded (e.g. antigravity serve)
150
+ if (siteCmd.commands.some((c) => c.name() === cmd.name))
151
+ continue;
121
152
  const subCmd = siteCmd.command(cmd.name).description(cmd.description);
122
153
  // Register positional args first, then named options
123
154
  const positionalArgs = [];
@@ -163,11 +194,19 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
163
194
  if (actionOpts.verbose)
164
195
  process.env.OPENCLI_VERBOSE = '1';
165
196
  let result;
166
- if (cmd.browser) {
197
+ if (shouldUseBrowserSession(cmd)) {
167
198
  const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
168
199
  result = await browserSession(BrowserFactory, async (page) => {
200
+ // Cookie/header strategies require same-origin context for credentialed fetch.
201
+ if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
202
+ try {
203
+ await page.goto(`https://${cmd.domain}`);
204
+ await page.wait(2);
205
+ }
206
+ catch { }
207
+ }
169
208
  return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
170
- });
209
+ }, { workspace: `site:${cmd.site}` });
171
210
  }
172
211
  else {
173
212
  result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
@@ -175,7 +214,8 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
175
214
  if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
176
215
  console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
177
216
  }
178
- renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
217
+ const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
218
+ renderOutput(result, { fmt: actionOpts.format, columns: resolved.columns, title: `${resolved.site}/${resolved.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(resolved), footerExtra: resolved.footerExtra?.(kwargs) });
179
219
  }
180
220
  catch (err) {
181
221
  if (err instanceof CliError) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * antigravity serve — Anthropic-compatible `/v1/messages` proxy server.
3
+ *
4
+ * Starts an HTTP server that accepts Anthropic Messages API requests,
5
+ * forwards them to a running Antigravity app via CDP, polls for the response,
6
+ * and returns it in Anthropic format.
7
+ *
8
+ * Usage:
9
+ * OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve --port 8082
10
+ * ANTHROPIC_BASE_URL=http://localhost:8082 claude
11
+ */
12
+ export declare function startServe(opts?: {
13
+ port?: number;
14
+ }): Promise<void>;
@@ -0,0 +1,263 @@
1
+ /**
2
+ * antigravity serve — Anthropic-compatible `/v1/messages` proxy server.
3
+ *
4
+ * Starts an HTTP server that accepts Anthropic Messages API requests,
5
+ * forwards them to a running Antigravity app via CDP, polls for the response,
6
+ * and returns it in Anthropic format.
7
+ *
8
+ * Usage:
9
+ * OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve --port 8082
10
+ * ANTHROPIC_BASE_URL=http://localhost:8082 claude
11
+ */
12
+ import { createServer } from 'node:http';
13
+ import { CDPBridge } from '../../browser/cdp.js';
14
+ // ─── Helpers ─────────────────────────────────────────────────────────
15
+ function generateMsgId() {
16
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
17
+ let id = 'msg_';
18
+ for (let i = 0; i < 24; i++)
19
+ id += chars[Math.floor(Math.random() * chars.length)];
20
+ return id;
21
+ }
22
+ function estimateTokens(text) {
23
+ // Rough approximation: ~4 chars per token for English, ~2 for CJK
24
+ return Math.max(1, Math.ceil(text.length / 3));
25
+ }
26
+ function extractTextContent(content) {
27
+ if (typeof content === 'string')
28
+ return content;
29
+ return content
30
+ .filter(b => b.type === 'text' && b.text)
31
+ .map(b => b.text)
32
+ .join('\n');
33
+ }
34
+ function readBody(req) {
35
+ return new Promise((resolve, reject) => {
36
+ const chunks = [];
37
+ req.on('data', (c) => chunks.push(c));
38
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
39
+ req.on('error', reject);
40
+ });
41
+ }
42
+ function jsonResponse(res, status, data) {
43
+ const body = JSON.stringify(data);
44
+ res.writeHead(status, {
45
+ 'Content-Type': 'application/json',
46
+ 'Access-Control-Allow-Origin': '*',
47
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
48
+ 'Access-Control-Allow-Headers': 'Content-Type, x-api-key, anthropic-version, Authorization',
49
+ });
50
+ res.end(body);
51
+ }
52
+ function sleep(ms) {
53
+ return new Promise(resolve => setTimeout(resolve, ms));
54
+ }
55
+ // ─── Antigravity CDP Operations ──────────────────────────────────────
56
+ async function getConversationText(page) {
57
+ const text = await page.evaluate(`
58
+ (() => {
59
+ const container = document.getElementById('conversation');
60
+ return container ? container.innerText : '';
61
+ })()
62
+ `);
63
+ return String(text ?? '');
64
+ }
65
+ async function sendMessage(page, message) {
66
+ await page.evaluate(`
67
+ (async () => {
68
+ const container = document.getElementById('antigravity.agentSidePanelInputBox');
69
+ if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
70
+ const editor = container.querySelector('[data-lexical-editor="true"]');
71
+ if (!editor) throw new Error('Could not find Antigravity input box');
72
+
73
+ editor.focus();
74
+ document.execCommand('insertText', false, ${JSON.stringify(message)});
75
+ })()
76
+ `);
77
+ await sleep(500);
78
+ await page.pressKey('Enter');
79
+ }
80
+ async function waitForReply(page, beforeText, opts = {}) {
81
+ const timeout = opts.timeout ?? 120_000; // 2 minutes max
82
+ const pollInterval = opts.pollInterval ?? 500; // 500ms polling
83
+ const stableThreshold = opts.stableThreshold ?? 6; // 6 × 500ms = 3s stable
84
+ const deadline = Date.now() + timeout;
85
+ let lastText = beforeText;
86
+ let stableCount = 0;
87
+ // Wait a bit for the model to start generating
88
+ await sleep(1000);
89
+ while (Date.now() < deadline) {
90
+ const current = await getConversationText(page);
91
+ if (current.length > beforeText.length) {
92
+ // New content appeared
93
+ if (current === lastText) {
94
+ stableCount++;
95
+ if (stableCount >= stableThreshold) {
96
+ // Text has been stable — reply is complete
97
+ return current.slice(beforeText.length).trim();
98
+ }
99
+ }
100
+ else {
101
+ // Still generating
102
+ stableCount = 0;
103
+ lastText = current;
104
+ }
105
+ }
106
+ await sleep(pollInterval);
107
+ }
108
+ // Timeout — return whatever we have
109
+ const finalText = await getConversationText(page);
110
+ if (finalText.length > beforeText.length) {
111
+ return finalText.slice(beforeText.length).trim();
112
+ }
113
+ throw new Error('Timeout waiting for Antigravity reply');
114
+ }
115
+ // ─── Request Handlers ────────────────────────────────────────────────
116
+ async function handleMessages(body, page) {
117
+ // Extract the last user message
118
+ const userMessages = body.messages.filter(m => m.role === 'user');
119
+ if (userMessages.length === 0) {
120
+ throw new Error('No user message found in request');
121
+ }
122
+ const lastUserMsg = userMessages[userMessages.length - 1];
123
+ const userText = extractTextContent(lastUserMsg.content);
124
+ if (!userText.trim()) {
125
+ throw new Error('Empty user message');
126
+ }
127
+ // Get conversation state before sending
128
+ const beforeText = await getConversationText(page);
129
+ // Send the message
130
+ console.error(`[serve] Sending: "${userText.slice(0, 80)}${userText.length > 80 ? '...' : ''}"`);
131
+ await sendMessage(page, userText);
132
+ // Poll for reply
133
+ console.error('[serve] Waiting for reply...');
134
+ const replyText = await waitForReply(page, beforeText);
135
+ console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
136
+ return {
137
+ id: generateMsgId(),
138
+ type: 'message',
139
+ role: 'assistant',
140
+ content: [{ type: 'text', text: replyText }],
141
+ model: body.model ?? 'antigravity',
142
+ stop_reason: 'end_turn',
143
+ stop_sequence: null,
144
+ usage: {
145
+ input_tokens: estimateTokens(userText),
146
+ output_tokens: estimateTokens(replyText),
147
+ },
148
+ };
149
+ }
150
+ // ─── Server ──────────────────────────────────────────────────────────
151
+ export async function startServe(opts = {}) {
152
+ const port = opts.port ?? 8082;
153
+ // Establish persistent CDP connection
154
+ console.error('[serve] Connecting to Antigravity via CDP...');
155
+ const cdp = new CDPBridge();
156
+ const page = await cdp.connect({ timeout: 15_000 });
157
+ console.error('[serve] CDP connected successfully.');
158
+ // Verify we can read conversation
159
+ const testText = await getConversationText(page);
160
+ console.error(`[serve] Conversation element found (${testText.length} chars).`);
161
+ let requestInFlight = false;
162
+ const server = createServer(async (req, res) => {
163
+ // CORS preflight
164
+ if (req.method === 'OPTIONS') {
165
+ res.writeHead(204, {
166
+ 'Access-Control-Allow-Origin': '*',
167
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
168
+ 'Access-Control-Allow-Headers': 'Content-Type, x-api-key, anthropic-version, Authorization',
169
+ });
170
+ res.end();
171
+ return;
172
+ }
173
+ const url = req.url ?? '/';
174
+ const pathname = url.split('?')[0];
175
+ try {
176
+ // GET /v1/models — return available models
177
+ if (req.method === 'GET' && pathname === '/v1/models') {
178
+ jsonResponse(res, 200, {
179
+ data: [
180
+ {
181
+ id: 'antigravity',
182
+ object: 'model',
183
+ created: Math.floor(Date.now() / 1000),
184
+ owned_by: 'antigravity',
185
+ },
186
+ ],
187
+ });
188
+ return;
189
+ }
190
+ // POST /v1/messages — main endpoint
191
+ if (req.method === 'POST' && pathname === '/v1/messages') {
192
+ if (requestInFlight) {
193
+ jsonResponse(res, 429, {
194
+ type: 'error',
195
+ error: {
196
+ type: 'rate_limit_error',
197
+ message: 'Another request is currently being processed. Antigravity can only handle one request at a time.',
198
+ },
199
+ });
200
+ return;
201
+ }
202
+ requestInFlight = true;
203
+ try {
204
+ const rawBody = await readBody(req);
205
+ const body = JSON.parse(rawBody);
206
+ if (body.stream) {
207
+ // We don't support streaming — return error
208
+ jsonResponse(res, 400, {
209
+ type: 'error',
210
+ error: {
211
+ type: 'invalid_request_error',
212
+ message: 'Streaming is not supported. Set "stream": false.',
213
+ },
214
+ });
215
+ return;
216
+ }
217
+ const response = await handleMessages(body, page);
218
+ jsonResponse(res, 200, response);
219
+ }
220
+ finally {
221
+ requestInFlight = false;
222
+ }
223
+ return;
224
+ }
225
+ // Health check
226
+ if (req.method === 'GET' && (pathname === '/' || pathname === '/health')) {
227
+ jsonResponse(res, 200, { ok: true, status: 'connected' });
228
+ return;
229
+ }
230
+ jsonResponse(res, 404, {
231
+ type: 'error',
232
+ error: { type: 'not_found_error', message: `Not found: ${pathname}` },
233
+ });
234
+ }
235
+ catch (err) {
236
+ console.error('[serve] Error:', err);
237
+ jsonResponse(res, 500, {
238
+ type: 'error',
239
+ error: {
240
+ type: 'api_error',
241
+ message: err instanceof Error ? err.message : 'Internal server error',
242
+ },
243
+ });
244
+ }
245
+ });
246
+ server.listen(port, '127.0.0.1', () => {
247
+ console.error(`\n[serve] ✅ Antigravity API proxy running at http://127.0.0.1:${port}`);
248
+ console.error(`[serve] Compatible with Anthropic /v1/messages API`);
249
+ console.error(`\n[serve] Usage with Claude Code:`);
250
+ console.error(` ANTHROPIC_BASE_URL=http://localhost:${port} claude\n`);
251
+ });
252
+ // Graceful shutdown
253
+ const shutdown = () => {
254
+ console.error('\n[serve] Shutting down...');
255
+ cdp.close().catch(() => { });
256
+ server.close();
257
+ process.exit(0);
258
+ };
259
+ process.on('SIGTERM', shutdown);
260
+ process.on('SIGINT', shutdown);
261
+ // Keep alive
262
+ await new Promise(() => { });
263
+ }
@@ -10,7 +10,7 @@
10
10
  import * as fs from 'node:fs';
11
11
  import * as path from 'node:path';
12
12
  import { cli, Strategy } from '../../registry.js';
13
- import { ytdlpDownload, checkYtdlp, sanitizeFilename, getTempDir, exportCookiesToNetscape, } from '../../download/index.js';
13
+ import { ytdlpDownload, checkYtdlp, sanitizeFilename, getTempDir, exportCookiesToNetscape, formatCookieHeader, } from '../../download/index.js';
14
14
  import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
15
15
  cli({
16
16
  site: 'bilibili',
@@ -50,26 +50,16 @@ cli({
50
50
  `);
51
51
  const title = sanitizeFilename(data?.title || 'video');
52
52
  // Extract cookies for authenticated downloads
53
- const cookieString = await page.evaluate(`(() => document.cookie)()`);
53
+ const cookies = await page.getCookies({ domain: 'bilibili.com' });
54
+ const cookieString = formatCookieHeader(cookies);
54
55
  // Create output directory
55
56
  fs.mkdirSync(output, { recursive: true });
56
57
  // Export cookies to Netscape format for yt-dlp
57
58
  let cookiesFile;
58
- if (typeof cookieString === 'string' && cookieString) {
59
+ if (cookies.length > 0) {
59
60
  const tempDir = getTempDir();
60
61
  fs.mkdirSync(tempDir, { recursive: true });
61
62
  cookiesFile = path.join(tempDir, `bilibili_cookies_${Date.now()}.txt`);
62
- const cookies = cookieString.split(';').map((c) => {
63
- const [name, ...rest] = c.trim().split('=');
64
- return {
65
- name: name || '',
66
- value: rest.join('=') || '',
67
- domain: '.bilibili.com',
68
- path: '/',
69
- secure: true,
70
- httpOnly: false,
71
- };
72
- }).filter((c) => c.name);
73
63
  exportCookiesToNetscape(cookies, cookiesFile);
74
64
  }
75
65
  // Build yt-dlp format string based on quality
@@ -0,0 +1 @@
1
+ export {};