@jackwener/opencli 1.0.1 → 1.0.3

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 (91) hide show
  1. package/.github/workflows/build-extension.yml +62 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/.github/workflows/e2e-headed.yml +2 -2
  4. package/.github/workflows/pkg-pr-new.yml +2 -2
  5. package/.github/workflows/release.yml +2 -5
  6. package/.github/workflows/security.yml +2 -2
  7. package/CDP.md +1 -1
  8. package/CDP.zh-CN.md +1 -1
  9. package/README.md +15 -7
  10. package/README.zh-CN.md +15 -7
  11. package/SKILL.md +3 -5
  12. package/dist/browser/cdp.d.ts +27 -0
  13. package/dist/browser/cdp.js +295 -0
  14. package/dist/browser/index.d.ts +3 -0
  15. package/dist/browser/index.js +4 -0
  16. package/dist/browser/page.js +2 -23
  17. package/dist/browser/utils.d.ts +10 -0
  18. package/dist/browser/utils.js +27 -0
  19. package/dist/browser.test.js +42 -1
  20. package/dist/chaoxing.d.ts +58 -0
  21. package/dist/chaoxing.js +225 -0
  22. package/dist/chaoxing.test.d.ts +1 -0
  23. package/dist/chaoxing.test.js +38 -0
  24. package/dist/cli-manifest.json +203 -0
  25. package/dist/cli.d.ts +1 -0
  26. package/dist/cli.js +197 -0
  27. package/dist/clis/boss/chatlist.d.ts +1 -0
  28. package/dist/clis/boss/chatlist.js +50 -0
  29. package/dist/clis/boss/chatmsg.d.ts +1 -0
  30. package/dist/clis/boss/chatmsg.js +73 -0
  31. package/dist/clis/boss/send.d.ts +1 -0
  32. package/dist/clis/boss/send.js +176 -0
  33. package/dist/clis/chaoxing/assignments.d.ts +1 -0
  34. package/dist/clis/chaoxing/assignments.js +74 -0
  35. package/dist/clis/chaoxing/exams.d.ts +1 -0
  36. package/dist/clis/chaoxing/exams.js +74 -0
  37. package/dist/clis/chatgpt/ask.js +15 -14
  38. package/dist/clis/chatgpt/ax.d.ts +1 -0
  39. package/dist/clis/chatgpt/ax.js +78 -0
  40. package/dist/clis/chatgpt/read.js +5 -6
  41. package/dist/clis/twitter/post.js +9 -2
  42. package/dist/clis/twitter/search.js +14 -33
  43. package/dist/clis/xiaohongshu/download.d.ts +1 -1
  44. package/dist/clis/xiaohongshu/download.js +1 -1
  45. package/dist/engine.js +24 -13
  46. package/dist/explore.js +46 -101
  47. package/dist/main.js +4 -193
  48. package/dist/output.d.ts +1 -1
  49. package/dist/registry.d.ts +3 -3
  50. package/dist/scripts/framework.d.ts +4 -0
  51. package/dist/scripts/framework.js +21 -0
  52. package/dist/scripts/interact.d.ts +4 -0
  53. package/dist/scripts/interact.js +20 -0
  54. package/dist/scripts/store.d.ts +9 -0
  55. package/dist/scripts/store.js +44 -0
  56. package/dist/synthesize.js +1 -1
  57. package/extension/dist/background.js +338 -430
  58. package/extension/manifest.json +2 -2
  59. package/extension/src/background.ts +2 -2
  60. package/package.json +1 -1
  61. package/src/browser/cdp.ts +295 -0
  62. package/src/browser/index.ts +4 -0
  63. package/src/browser/page.ts +2 -24
  64. package/src/browser/utils.ts +27 -0
  65. package/src/browser.test.ts +46 -0
  66. package/src/chaoxing.test.ts +45 -0
  67. package/src/chaoxing.ts +268 -0
  68. package/src/cli.ts +185 -0
  69. package/src/clis/antigravity/SKILL.md +5 -0
  70. package/src/clis/boss/chatlist.ts +50 -0
  71. package/src/clis/boss/chatmsg.ts +70 -0
  72. package/src/clis/boss/send.ts +193 -0
  73. package/src/clis/chaoxing/README.md +36 -0
  74. package/src/clis/chaoxing/README.zh-CN.md +35 -0
  75. package/src/clis/chaoxing/assignments.ts +88 -0
  76. package/src/clis/chaoxing/exams.ts +88 -0
  77. package/src/clis/chatgpt/ask.ts +14 -15
  78. package/src/clis/chatgpt/ax.ts +81 -0
  79. package/src/clis/chatgpt/read.ts +5 -7
  80. package/src/clis/twitter/post.ts +9 -2
  81. package/src/clis/twitter/search.ts +15 -33
  82. package/src/clis/xiaohongshu/download.ts +1 -1
  83. package/src/engine.ts +20 -13
  84. package/src/explore.ts +51 -100
  85. package/src/main.ts +4 -180
  86. package/src/output.ts +12 -12
  87. package/src/registry.ts +3 -3
  88. package/src/scripts/framework.ts +20 -0
  89. package/src/scripts/interact.ts +22 -0
  90. package/src/scripts/store.ts +40 -0
  91. package/src/synthesize.ts +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest_version": 3,
3
- "name": "opencli Browser Bridge",
3
+ "name": "OpenCLI",
4
4
  "version": "0.2.0",
5
5
  "description": "Bridge between opencli CLI and your browser — execute commands, read cookies, manage tabs.",
6
6
  "permissions": [
@@ -21,7 +21,7 @@
21
21
  "128": "icons/icon-128.png"
22
22
  },
23
23
  "action": {
24
- "default_title": "opencli Browser Bridge",
24
+ "default_title": "OpenCLI",
25
25
  "default_icon": {
26
26
  "16": "icons/icon-16.png",
27
27
  "32": "icons/icon-32.png"
@@ -1,5 +1,5 @@
1
1
  /**
2
- * opencli Browser Bridge — Service Worker (background script).
2
+ * OpenCLI — Service Worker (background script).
3
3
  *
4
4
  * Connects to the opencli daemon via WebSocket, receives commands,
5
5
  * dispatches them to Chrome APIs (debugger/tabs/cookies), returns results.
@@ -155,7 +155,7 @@ function initialize(): void {
155
155
  chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds
156
156
  executor.registerListeners();
157
157
  connect();
158
- console.log('[opencli] Browser Bridge extension initialized');
158
+ console.log('[opencli] OpenCLI extension initialized');
159
159
  }
160
160
 
161
161
  chrome.runtime.onInstalled.addListener(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,295 @@
1
+ /**
2
+ * CDP client — implements IPage by connecting directly to a Chrome/Electron CDP WebSocket.
3
+ */
4
+
5
+ import { WebSocket } from 'ws';
6
+ import type { IPage } from '../types.js';
7
+ import { wrapForEval } from './utils.js';
8
+
9
+ export interface CDPTarget {
10
+ type?: string;
11
+ url?: string;
12
+ title?: string;
13
+ webSocketDebuggerUrl?: string;
14
+ }
15
+
16
+ export class CDPBridge {
17
+ private _ws: WebSocket | null = null;
18
+ private _idCounter = 0;
19
+ private _pending = new Map<number, { resolve: (val: any) => void; reject: (err: Error) => void }>();
20
+
21
+ async connect(opts?: { timeout?: number }): Promise<IPage> {
22
+ const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
23
+ if (!endpoint) throw new Error('OPENCLI_CDP_ENDPOINT is not set');
24
+
25
+ // If it's a direct ws:// URL, use it. Otherwise, fetch the /json endpoint to find a page.
26
+ let wsUrl = endpoint;
27
+ if (endpoint.startsWith('http')) {
28
+ const res = await fetch(`${endpoint.replace(/\/$/, '')}/json`);
29
+ if (!res.ok) throw new Error(`Failed to fetch CDP targets: ${res.statusText}`);
30
+ const targets = await res.json() as CDPTarget[];
31
+ const target = selectCDPTarget(targets);
32
+ if (!target || !target.webSocketDebuggerUrl) {
33
+ throw new Error('No inspectable targets found at CDP endpoint');
34
+ }
35
+ wsUrl = target.webSocketDebuggerUrl;
36
+ }
37
+
38
+ return new Promise((resolve, reject) => {
39
+ const ws = new WebSocket(wsUrl);
40
+ const timeout = setTimeout(() => reject(new Error('CDP connect timeout')), opts?.timeout ?? 10000);
41
+
42
+ ws.on('open', () => {
43
+ clearTimeout(timeout);
44
+ this._ws = ws;
45
+ resolve(new CDPPage(this));
46
+ });
47
+
48
+ ws.on('error', (err) => {
49
+ clearTimeout(timeout);
50
+ reject(err);
51
+ });
52
+
53
+ ws.on('message', (data) => {
54
+ try {
55
+ const msg = JSON.parse(data.toString());
56
+ if (msg.id && this._pending.has(msg.id)) {
57
+ const { resolve, reject } = this._pending.get(msg.id)!;
58
+ this._pending.delete(msg.id);
59
+ if (msg.error) {
60
+ reject(new Error(msg.error.message));
61
+ } else {
62
+ resolve(msg.result);
63
+ }
64
+ }
65
+ } catch (e) {
66
+ // ignore parsing errors
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ async close(): Promise<void> {
73
+ if (this._ws) {
74
+ this._ws.close();
75
+ this._ws = null;
76
+ }
77
+ for (const p of this._pending.values()) {
78
+ p.reject(new Error('CDP connection closed'));
79
+ }
80
+ this._pending.clear();
81
+ }
82
+
83
+ async send(method: string, params: any = {}): Promise<any> {
84
+ if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
85
+ throw new Error('CDP connection is not open');
86
+ }
87
+ const id = ++this._idCounter;
88
+ return new Promise((resolve, reject) => {
89
+ this._pending.set(id, { resolve, reject });
90
+ this._ws!.send(JSON.stringify({ id, method, params }));
91
+ });
92
+ }
93
+ }
94
+
95
+ class CDPPage implements IPage {
96
+ constructor(private bridge: CDPBridge) {}
97
+
98
+ async goto(url: string): Promise<void> {
99
+ await this.bridge.send('Page.navigate', { url });
100
+ await new Promise(r => setTimeout(r, 1000));
101
+ }
102
+
103
+ async evaluate(js: string): Promise<any> {
104
+ const expression = wrapForEval(js);
105
+ const result = await this.bridge.send('Runtime.evaluate', {
106
+ expression,
107
+ returnByValue: true,
108
+ awaitPromise: true
109
+ });
110
+ if (result.exceptionDetails) {
111
+ throw new Error('Evaluate error: ' + (result.exceptionDetails.exception?.description || 'Unknown exception'));
112
+ }
113
+ return result.result?.value;
114
+ }
115
+
116
+ async snapshot(opts?: any): Promise<any> {
117
+ throw new Error('Method not implemented.');
118
+ }
119
+ async click(ref: string): Promise<void> {
120
+ const safeRef = JSON.stringify(ref);
121
+ const code = `
122
+ (() => {
123
+ const ref = ${safeRef};
124
+ const el = document.querySelector('[data-ref="' + ref + '"]')
125
+ || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
126
+ if (!el) throw new Error('Element not found: ' + ref);
127
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
128
+ el.click();
129
+ return 'clicked';
130
+ })()
131
+ `;
132
+ await this.evaluate(code);
133
+ }
134
+ async typeText(ref: string, text: string): Promise<void> {
135
+ const safeRef = JSON.stringify(ref);
136
+ const safeText = JSON.stringify(text);
137
+ const code = `
138
+ (() => {
139
+ const ref = ${safeRef};
140
+ const el = document.querySelector('[data-ref="' + ref + '"]')
141
+ || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
142
+ if (!el) throw new Error('Element not found: ' + ref);
143
+ el.focus();
144
+ el.value = ${safeText};
145
+ el.dispatchEvent(new Event('input', { bubbles: true }));
146
+ el.dispatchEvent(new Event('change', { bubbles: true }));
147
+ return 'typed';
148
+ })()
149
+ `;
150
+ await this.evaluate(code);
151
+ }
152
+ async pressKey(key: string): Promise<void> {
153
+ const code = `
154
+ (() => {
155
+ const el = document.activeElement || document.body;
156
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
157
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
158
+ return 'pressed';
159
+ })()
160
+ `;
161
+ await this.evaluate(code);
162
+ }
163
+ async wait(options: any): Promise<void> {
164
+ if (typeof options === 'number') {
165
+ await new Promise(resolve => setTimeout(resolve, options * 1000));
166
+ return;
167
+ }
168
+ if (options.time) {
169
+ await new Promise(resolve => setTimeout(resolve, options.time * 1000));
170
+ return;
171
+ }
172
+ if (options.text) {
173
+ const timeout = (options.timeout ?? 30) * 1000;
174
+ const code = `
175
+ new Promise((resolve, reject) => {
176
+ const deadline = Date.now() + ${timeout};
177
+ const check = () => {
178
+ if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
179
+ if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
180
+ setTimeout(check, 200);
181
+ };
182
+ check();
183
+ })
184
+ `;
185
+ await this.evaluate(code);
186
+ }
187
+ }
188
+ async tabs(): Promise<any> {
189
+ throw new Error('Method not implemented.');
190
+ }
191
+ async closeTab(index?: number): Promise<void> {
192
+ throw new Error('Method not implemented.');
193
+ }
194
+ async newTab(): Promise<void> {
195
+ throw new Error('Method not implemented.');
196
+ }
197
+ async selectTab(index: number): Promise<void> {
198
+ throw new Error('Method not implemented.');
199
+ }
200
+ async networkRequests(includeStatic?: boolean): Promise<any> {
201
+ throw new Error('Method not implemented.');
202
+ }
203
+ async consoleMessages(level?: string): Promise<any> {
204
+ throw new Error('Method not implemented.');
205
+ }
206
+ async scroll(direction?: string, amount?: number): Promise<void> {
207
+ throw new Error('Method not implemented.');
208
+ }
209
+ async autoScroll(options?: any): Promise<void> {
210
+ throw new Error('Method not implemented.');
211
+ }
212
+ async installInterceptor(pattern: string): Promise<void> {
213
+ throw new Error('Method not implemented.');
214
+ }
215
+ async getInterceptedRequests(): Promise<any[]> {
216
+ throw new Error('Method not implemented.');
217
+ }
218
+ async screenshot(options?: any): Promise<string> {
219
+ throw new Error('Method not implemented.');
220
+ }
221
+ }
222
+ function selectCDPTarget(targets: CDPTarget[]): CDPTarget | undefined {
223
+ const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET);
224
+
225
+ const ranked = targets
226
+ .map((target, index) => ({ target, index, score: scoreCDPTarget(target, preferredPattern) }))
227
+ .filter(({ score }) => Number.isFinite(score))
228
+ .sort((a, b) => {
229
+ if (b.score !== a.score) return b.score - a.score;
230
+ return a.index - b.index;
231
+ });
232
+
233
+ return ranked[0]?.target;
234
+ }
235
+
236
+ function scoreCDPTarget(target: CDPTarget, preferredPattern?: RegExp): number {
237
+ if (!target.webSocketDebuggerUrl) return Number.NEGATIVE_INFINITY;
238
+
239
+ const type = (target.type ?? '').toLowerCase();
240
+ const url = (target.url ?? '').toLowerCase();
241
+ const title = (target.title ?? '').toLowerCase();
242
+ const haystack = `${title} ${url}`;
243
+
244
+ if (!haystack.trim() && !type) return Number.NEGATIVE_INFINITY;
245
+ if (haystack.includes('devtools')) return Number.NEGATIVE_INFINITY;
246
+
247
+ let score = 0;
248
+
249
+ if (preferredPattern && preferredPattern.test(haystack)) score += 1000;
250
+
251
+ if (type === 'app') score += 120;
252
+ else if (type === 'webview') score += 100;
253
+ else if (type === 'page') score += 80;
254
+ else if (type === 'iframe') score += 20;
255
+
256
+ if (url.startsWith('http://localhost') || url.startsWith('https://localhost')) score += 90;
257
+ if (url.startsWith('file://')) score += 60;
258
+ if (url.startsWith('http://127.0.0.1') || url.startsWith('https://127.0.0.1')) score += 50;
259
+ if (url.startsWith('about:blank')) score -= 120;
260
+ if (url === '' || url === 'about:blank') score -= 40;
261
+
262
+ if (title && title !== 'devtools') score += 25;
263
+ if (title.includes('antigravity')) score += 120;
264
+ if (title.includes('codex')) score += 120;
265
+ if (title.includes('cursor')) score += 120;
266
+ if (title.includes('chatwise')) score += 120;
267
+ if (title.includes('notion')) score += 120;
268
+ if (title.includes('discord')) score += 120;
269
+ if (title.includes('netease')) score += 120;
270
+
271
+ if (url.includes('antigravity')) score += 100;
272
+ if (url.includes('codex')) score += 100;
273
+ if (url.includes('cursor')) score += 100;
274
+ if (url.includes('chatwise')) score += 100;
275
+ if (url.includes('notion')) score += 100;
276
+ if (url.includes('discord')) score += 100;
277
+ if (url.includes('netease')) score += 100;
278
+
279
+ return score;
280
+ }
281
+
282
+ function compilePreferredPattern(raw: string | undefined): RegExp | undefined {
283
+ const value = raw?.trim();
284
+ if (!value) return undefined;
285
+ return new RegExp(escapeRegExp(value.toLowerCase()));
286
+ }
287
+
288
+ function escapeRegExp(value: string): string {
289
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
290
+ }
291
+
292
+ export const __test__ = {
293
+ selectCDPTarget,
294
+ scoreCDPTarget,
295
+ };
@@ -7,9 +7,11 @@
7
7
 
8
8
  export { Page } from './page.js';
9
9
  export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
10
+ export { CDPBridge } from './cdp.js';
10
11
  export { isDaemonRunning } from './daemon-client.js';
11
12
 
12
13
  import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
14
+ import { __test__ as cdpTest } from './cdp.js';
13
15
  import { withTimeoutMs } from '../runtime.js';
14
16
 
15
17
  export const __test__ = {
@@ -17,4 +19,6 @@ export const __test__ = {
17
19
  diffTabIndexes,
18
20
  appendLimited,
19
21
  withTimeoutMs,
22
+ selectCDPTarget: cdpTest.selectCDPTarget,
23
+ scoreCDPTarget: cdpTest.scoreCDPTarget,
20
24
  };
@@ -13,6 +13,7 @@
13
13
  import { formatSnapshot } from '../snapshotFormatter.js';
14
14
  import type { IPage } from '../types.js';
15
15
  import { sendCommand } from './daemon-client.js';
16
+ import { wrapForEval } from './utils.js';
16
17
 
17
18
  /**
18
19
  * Page — implements IPage by talking to the daemon via HTTP.
@@ -285,27 +286,4 @@ export class Page implements IPage {
285
286
  }
286
287
  }
287
288
 
288
- // ─── Helpers ─────────────────────────────────────────────────────────
289
-
290
- /**
291
- * Wrap JS code for CDP Runtime.evaluate:
292
- * - Already an IIFE `(...)()` → send as-is
293
- * - Arrow/function literal → wrap as IIFE `(code)()`
294
- * - `new Promise(...)` or raw expression → send as-is (expression)
295
- */
296
- function wrapForEval(js: string): string {
297
- const code = js.trim();
298
- if (!code) return 'undefined';
299
-
300
- // Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
301
- if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code)) return code;
302
-
303
- // Arrow function: `() => ...` or `async () => ...`
304
- if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code)) return `(${code})()`;
305
-
306
- // Function declaration: `function ...` or `async function ...`
307
- if (/^(async\s+)?function[\s(]/.test(code)) return `(${code})()`;
308
-
309
- // Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
310
- return code;
311
- }
289
+ // (End of file)
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Utility functions for browser operations
3
+ */
4
+
5
+ /**
6
+ * Wrap JS code for CDP Runtime.evaluate:
7
+ * - Already an IIFE `(...)()` → send as-is
8
+ * - Arrow/function literal → wrap as IIFE `(code)()`
9
+ * - `new Promise(...)` or raw expression → send as-is (expression)
10
+ */
11
+ export function wrapForEval(js: string): string {
12
+ if (typeof js !== 'string') return 'undefined';
13
+ const code = js.trim();
14
+ if (!code) return 'undefined';
15
+
16
+ // Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
17
+ if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code)) return code;
18
+
19
+ // Arrow function: `() => ...` or `async () => ...`
20
+ if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code)) return `(${code})()`;
21
+
22
+ // Function declaration: `function ...` or `async function ...`
23
+ if (/^(async\s+)?function[\s(]/.test(code)) return `(${code})()`;
24
+
25
+ // Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
26
+ return code;
27
+ }
@@ -43,6 +43,52 @@ describe('browser helpers', () => {
43
43
  it('times out slow promises', async () => {
44
44
  await expect(__test__.withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout');
45
45
  });
46
+
47
+ it('prefers the real Electron app target over DevTools and blank pages', () => {
48
+ const target = __test__.selectCDPTarget([
49
+ {
50
+ type: 'page',
51
+ title: 'DevTools - localhost:9224',
52
+ url: 'devtools://devtools/bundled/inspector.html',
53
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/devtools',
54
+ },
55
+ {
56
+ type: 'page',
57
+ title: '',
58
+ url: 'about:blank',
59
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/blank',
60
+ },
61
+ {
62
+ type: 'app',
63
+ title: 'Antigravity',
64
+ url: 'http://localhost:3000/',
65
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/app',
66
+ },
67
+ ]);
68
+
69
+ expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9224/app');
70
+ });
71
+
72
+ it('honors OPENCLI_CDP_TARGET when multiple inspectable targets exist', () => {
73
+ vi.stubEnv('OPENCLI_CDP_TARGET', 'codex');
74
+
75
+ const target = __test__.selectCDPTarget([
76
+ {
77
+ type: 'app',
78
+ title: 'Cursor',
79
+ url: 'http://localhost:3000/cursor',
80
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9226/cursor',
81
+ },
82
+ {
83
+ type: 'app',
84
+ title: 'OpenAI Codex',
85
+ url: 'http://localhost:3000/codex',
86
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9226/codex',
87
+ },
88
+ ]);
89
+
90
+ expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9226/codex');
91
+ });
46
92
  });
47
93
 
48
94
  describe('BrowserBridge state', () => {
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatTimestamp, workStatusLabel } from './chaoxing.js';
3
+
4
+ describe('formatTimestamp', () => {
5
+ it('formats millisecond timestamp', () => {
6
+ // 2026-01-15 08:30 UTC+8
7
+ const ts = new Date('2026-01-15T00:30:00Z').getTime();
8
+ const result = formatTimestamp(ts);
9
+ expect(result).toMatch(/2026-01-15/);
10
+ });
11
+
12
+ it('formats second timestamp', () => {
13
+ const ts = Math.floor(new Date('2026-06-01T12:00:00Z').getTime() / 1000);
14
+ const result = formatTimestamp(ts);
15
+ expect(result).toMatch(/2026-06-01/);
16
+ });
17
+
18
+ it('returns empty for null/undefined/0', () => {
19
+ expect(formatTimestamp(null)).toBe('');
20
+ expect(formatTimestamp(undefined)).toBe('');
21
+ expect(formatTimestamp(0)).toBe('');
22
+ expect(formatTimestamp('')).toBe('');
23
+ });
24
+
25
+ it('passes through readable date strings', () => {
26
+ expect(formatTimestamp('2026-03-20 23:59')).toBe('2026-03-20 23:59');
27
+ });
28
+ });
29
+
30
+ describe('workStatusLabel', () => {
31
+ it('maps numeric status codes', () => {
32
+ expect(workStatusLabel(0)).toBe('未交');
33
+ expect(workStatusLabel(1)).toBe('已交');
34
+ expect(workStatusLabel(2)).toBe('已批阅');
35
+ });
36
+
37
+ it('passes through string status', () => {
38
+ expect(workStatusLabel('已交')).toBe('已交');
39
+ });
40
+
41
+ it('returns 未知 for empty/null', () => {
42
+ expect(workStatusLabel(null)).toBe('未知');
43
+ expect(workStatusLabel('')).toBe('未知');
44
+ });
45
+ });