@jackwener/opencli 1.5.9 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +18 -0
- package/SKILL.md +59 -0
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- package/dist/browser/daemon-client.d.ts +20 -1
- package/dist/browser/daemon-client.js +37 -30
- package/dist/browser/daemon-client.test.d.ts +1 -0
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.js +8 -19
- package/dist/browser/page.d.ts +4 -0
- package/dist/browser/page.js +48 -1
- package/dist/cli.js +392 -0
- package/dist/clis/twitter/article.js +28 -1
- package/dist/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +1 -1
- package/dist/commanderAdapter.test.js +43 -0
- package/dist/commands/daemon.js +7 -46
- package/dist/commands/daemon.test.js +44 -69
- package/dist/discovery.js +27 -0
- package/dist/types.d.ts +8 -0
- package/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.ts +51 -4
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -1
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +6 -0
- package/skills/opencli-oneshot/SKILL.md +6 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +113 -32
- package/src/browser/daemon-client.test.ts +103 -0
- package/src/browser/daemon-client.ts +53 -30
- package/src/browser/discover.ts +8 -17
- package/src/browser/page.ts +48 -1
- package/src/cli.ts +392 -0
- package/src/clis/twitter/article.ts +31 -1
- package/src/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +62 -0
- package/src/commanderAdapter.ts +1 -1
- package/src/commands/daemon.test.ts +49 -83
- package/src/commands/daemon.ts +7 -55
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +1 -1
- package/src/types.ts +8 -0
- package/extension/dist/background.js +0 -681
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
fetchDaemonStatusMock,
|
|
5
|
+
requestDaemonShutdownMock,
|
|
6
|
+
} = vi.hoisted(() => ({
|
|
7
|
+
fetchDaemonStatusMock: vi.fn(),
|
|
8
|
+
requestDaemonShutdownMock: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
3
11
|
vi.mock('chalk', () => ({
|
|
4
12
|
default: {
|
|
5
13
|
green: (s: string) => s,
|
|
@@ -16,6 +24,11 @@ vi.mock('../browser/bridge.js', () => ({
|
|
|
16
24
|
},
|
|
17
25
|
}));
|
|
18
26
|
|
|
27
|
+
vi.mock('../browser/daemon-client.js', () => ({
|
|
28
|
+
fetchDaemonStatus: fetchDaemonStatusMock,
|
|
29
|
+
requestDaemonShutdown: requestDaemonShutdownMock,
|
|
30
|
+
}));
|
|
31
|
+
|
|
19
32
|
import { daemonStatus, daemonStop, daemonRestart } from './daemon.js';
|
|
20
33
|
|
|
21
34
|
describe('daemon commands', () => {
|
|
@@ -25,6 +38,8 @@ describe('daemon commands', () => {
|
|
|
25
38
|
beforeEach(() => {
|
|
26
39
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
27
40
|
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
41
|
+
fetchDaemonStatusMock.mockReset();
|
|
42
|
+
requestDaemonShutdownMock.mockReset();
|
|
28
43
|
});
|
|
29
44
|
|
|
30
45
|
afterEach(() => {
|
|
@@ -34,7 +49,7 @@ describe('daemon commands', () => {
|
|
|
34
49
|
|
|
35
50
|
describe('daemonStatus', () => {
|
|
36
51
|
it('shows "not running" when daemon is unreachable', async () => {
|
|
37
|
-
|
|
52
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
38
53
|
|
|
39
54
|
await daemonStatus();
|
|
40
55
|
|
|
@@ -42,7 +57,7 @@ describe('daemon commands', () => {
|
|
|
42
57
|
});
|
|
43
58
|
|
|
44
59
|
it('shows "not running" when daemon returns non-ok response', async () => {
|
|
45
|
-
|
|
60
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
46
61
|
|
|
47
62
|
await daemonStatus();
|
|
48
63
|
|
|
@@ -61,13 +76,7 @@ describe('daemon commands', () => {
|
|
|
61
76
|
port: 19825,
|
|
62
77
|
};
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
'fetch',
|
|
66
|
-
vi.fn().mockResolvedValue({
|
|
67
|
-
ok: true,
|
|
68
|
-
json: () => Promise.resolve(status),
|
|
69
|
-
}),
|
|
70
|
-
);
|
|
79
|
+
fetchDaemonStatusMock.mockResolvedValue(status);
|
|
71
80
|
|
|
72
81
|
await daemonStatus();
|
|
73
82
|
|
|
@@ -91,13 +100,7 @@ describe('daemon commands', () => {
|
|
|
91
100
|
port: 19825,
|
|
92
101
|
};
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
'fetch',
|
|
96
|
-
vi.fn().mockResolvedValue({
|
|
97
|
-
ok: true,
|
|
98
|
-
json: () => Promise.resolve(status),
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
103
|
+
fetchDaemonStatusMock.mockResolvedValue(status);
|
|
101
104
|
|
|
102
105
|
await daemonStatus();
|
|
103
106
|
|
|
@@ -107,7 +110,7 @@ describe('daemon commands', () => {
|
|
|
107
110
|
|
|
108
111
|
describe('daemonStop', () => {
|
|
109
112
|
it('reports "not running" when daemon is unreachable', async () => {
|
|
110
|
-
|
|
113
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
111
114
|
|
|
112
115
|
await daemonStop();
|
|
113
116
|
|
|
@@ -115,59 +118,36 @@ describe('daemon commands', () => {
|
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
it('sends shutdown and reports success', async () => {
|
|
118
|
-
|
|
121
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
119
122
|
ok: true,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
port: 19825,
|
|
130
|
-
}),
|
|
131
|
-
};
|
|
132
|
-
const shutdownResponse = { ok: true };
|
|
133
|
-
|
|
134
|
-
const mockFetch = vi.fn()
|
|
135
|
-
.mockResolvedValueOnce(statusResponse)
|
|
136
|
-
.mockResolvedValueOnce(shutdownResponse);
|
|
137
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
123
|
+
pid: 12345,
|
|
124
|
+
uptime: 100,
|
|
125
|
+
extensionConnected: true,
|
|
126
|
+
pending: 0,
|
|
127
|
+
lastCliRequestTime: Date.now(),
|
|
128
|
+
memoryMB: 50,
|
|
129
|
+
port: 19825,
|
|
130
|
+
});
|
|
131
|
+
requestDaemonShutdownMock.mockResolvedValue(true);
|
|
138
132
|
|
|
139
133
|
await daemonStop();
|
|
140
134
|
|
|
141
|
-
|
|
142
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
143
|
-
const shutdownCall = mockFetch.mock.calls[1];
|
|
144
|
-
expect(shutdownCall[0]).toContain('/shutdown');
|
|
145
|
-
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
146
|
-
|
|
135
|
+
expect(requestDaemonShutdownMock).toHaveBeenCalledTimes(1);
|
|
147
136
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon stopped'));
|
|
148
137
|
});
|
|
149
138
|
|
|
150
139
|
it('reports failure when shutdown request fails', async () => {
|
|
151
|
-
|
|
140
|
+
fetchDaemonStatusMock.mockResolvedValue({
|
|
152
141
|
ok: true,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
port: 19825,
|
|
163
|
-
}),
|
|
164
|
-
};
|
|
165
|
-
const shutdownResponse = { ok: false };
|
|
166
|
-
|
|
167
|
-
const mockFetch = vi.fn()
|
|
168
|
-
.mockResolvedValueOnce(statusResponse)
|
|
169
|
-
.mockResolvedValueOnce(shutdownResponse);
|
|
170
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
142
|
+
pid: 12345,
|
|
143
|
+
uptime: 100,
|
|
144
|
+
extensionConnected: true,
|
|
145
|
+
pending: 0,
|
|
146
|
+
lastCliRequestTime: Date.now(),
|
|
147
|
+
memoryMB: 50,
|
|
148
|
+
port: 19825,
|
|
149
|
+
});
|
|
150
|
+
requestDaemonShutdownMock.mockResolvedValue(false);
|
|
171
151
|
|
|
172
152
|
await daemonStop();
|
|
173
153
|
|
|
@@ -188,7 +168,7 @@ describe('daemon commands', () => {
|
|
|
188
168
|
};
|
|
189
169
|
|
|
190
170
|
it('starts daemon directly when not running', async () => {
|
|
191
|
-
|
|
171
|
+
fetchDaemonStatusMock.mockResolvedValue(null);
|
|
192
172
|
mockConnect.mockResolvedValue(undefined);
|
|
193
173
|
|
|
194
174
|
await daemonRestart();
|
|
@@ -198,36 +178,22 @@ describe('daemon commands', () => {
|
|
|
198
178
|
});
|
|
199
179
|
|
|
200
180
|
it('stops then starts when daemon is running', async () => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.mockResolvedValueOnce(
|
|
204
|
-
|
|
205
|
-
.mockResolvedValueOnce({ ok: true })
|
|
206
|
-
// Subsequent calls: polling fetchStatus until unreachable
|
|
207
|
-
.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
208
|
-
|
|
209
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
181
|
+
fetchDaemonStatusMock
|
|
182
|
+
.mockResolvedValueOnce(statusData)
|
|
183
|
+
.mockResolvedValueOnce(null);
|
|
184
|
+
requestDaemonShutdownMock.mockResolvedValue(true);
|
|
210
185
|
mockConnect.mockResolvedValue(undefined);
|
|
211
186
|
|
|
212
187
|
await daemonRestart();
|
|
213
188
|
|
|
214
|
-
|
|
215
|
-
const shutdownCall = mockFetch.mock.calls[1];
|
|
216
|
-
expect(shutdownCall[0]).toContain('/shutdown');
|
|
217
|
-
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
218
|
-
|
|
189
|
+
expect(requestDaemonShutdownMock).toHaveBeenCalledTimes(1);
|
|
219
190
|
expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
|
|
220
191
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
|
|
221
192
|
});
|
|
222
193
|
|
|
223
194
|
it('aborts when shutdown fails', async () => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
|
|
227
|
-
// requestShutdown — failure
|
|
228
|
-
.mockResolvedValueOnce({ ok: false });
|
|
229
|
-
|
|
230
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
195
|
+
fetchDaemonStatusMock.mockResolvedValue(statusData);
|
|
196
|
+
requestDaemonShutdownMock.mockResolvedValue(false);
|
|
231
197
|
|
|
232
198
|
await daemonRestart();
|
|
233
199
|
|
package/src/commands/daemon.ts
CHANGED
|
@@ -6,55 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
12
|
-
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
13
|
-
|
|
14
|
-
interface DaemonStatus {
|
|
15
|
-
ok: boolean;
|
|
16
|
-
pid: number;
|
|
17
|
-
uptime: number;
|
|
18
|
-
extensionConnected: boolean;
|
|
19
|
-
pending: number;
|
|
20
|
-
lastCliRequestTime: number;
|
|
21
|
-
memoryMB: number;
|
|
22
|
-
port: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function fetchStatus(): Promise<DaemonStatus | null> {
|
|
26
|
-
const controller = new AbortController();
|
|
27
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
30
|
-
headers: { 'X-OpenCLI': '1' },
|
|
31
|
-
signal: controller.signal,
|
|
32
|
-
});
|
|
33
|
-
if (!res.ok) return null;
|
|
34
|
-
return await res.json() as DaemonStatus;
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
} finally {
|
|
38
|
-
clearTimeout(timer);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function requestShutdown(): Promise<boolean> {
|
|
43
|
-
const controller = new AbortController();
|
|
44
|
-
const timer = setTimeout(() => controller.abort(), 5000);
|
|
45
|
-
try {
|
|
46
|
-
const res = await fetch(`${DAEMON_URL}/shutdown`, {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
headers: { 'X-OpenCLI': '1' },
|
|
49
|
-
signal: controller.signal,
|
|
50
|
-
});
|
|
51
|
-
return res.ok;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
} finally {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
9
|
+
import { fetchDaemonStatus, requestDaemonShutdown } from '../browser/daemon-client.js';
|
|
58
10
|
|
|
59
11
|
function formatUptime(seconds: number): string {
|
|
60
12
|
const h = Math.floor(seconds / 3600);
|
|
@@ -74,7 +26,7 @@ function formatTimeSince(timestampMs: number): string {
|
|
|
74
26
|
}
|
|
75
27
|
|
|
76
28
|
export async function daemonStatus(): Promise<void> {
|
|
77
|
-
const status = await
|
|
29
|
+
const status = await fetchDaemonStatus();
|
|
78
30
|
if (!status) {
|
|
79
31
|
console.log(`Daemon: ${chalk.dim('not running')}`);
|
|
80
32
|
return;
|
|
@@ -89,13 +41,13 @@ export async function daemonStatus(): Promise<void> {
|
|
|
89
41
|
}
|
|
90
42
|
|
|
91
43
|
export async function daemonStop(): Promise<void> {
|
|
92
|
-
const status = await
|
|
44
|
+
const status = await fetchDaemonStatus();
|
|
93
45
|
if (!status) {
|
|
94
46
|
console.log(chalk.dim('Daemon is not running.'));
|
|
95
47
|
return;
|
|
96
48
|
}
|
|
97
49
|
|
|
98
|
-
const ok = await
|
|
50
|
+
const ok = await requestDaemonShutdown();
|
|
99
51
|
if (ok) {
|
|
100
52
|
console.log(chalk.green('Daemon stopped.'));
|
|
101
53
|
} else {
|
|
@@ -105,9 +57,9 @@ export async function daemonStop(): Promise<void> {
|
|
|
105
57
|
}
|
|
106
58
|
|
|
107
59
|
export async function daemonRestart(): Promise<void> {
|
|
108
|
-
const status = await
|
|
60
|
+
const status = await fetchDaemonStatus();
|
|
109
61
|
if (status) {
|
|
110
|
-
const ok = await
|
|
62
|
+
const ok = await requestDaemonShutdown();
|
|
111
63
|
if (!ok) {
|
|
112
64
|
console.error(chalk.red('Failed to stop daemon.'));
|
|
113
65
|
process.exitCode = 1;
|
|
@@ -117,7 +69,7 @@ export async function daemonRestart(): Promise<void> {
|
|
|
117
69
|
const deadline = Date.now() + 5000;
|
|
118
70
|
while (Date.now() < deadline) {
|
|
119
71
|
await new Promise(r => setTimeout(r, 200));
|
|
120
|
-
if (!(await
|
|
72
|
+
if (!(await fetchDaemonStatus())) break;
|
|
121
73
|
}
|
|
122
74
|
}
|
|
123
75
|
|
package/src/discovery.ts
CHANGED
|
@@ -76,6 +76,28 @@ export async function ensureUserCliCompatShims(baseDir: string = USER_OPENCLI_DI
|
|
|
76
76
|
`${JSON.stringify({ name: 'opencli-user-runtime', private: true, type: 'module' }, null, 2)}\n`,
|
|
77
77
|
),
|
|
78
78
|
]);
|
|
79
|
+
|
|
80
|
+
// Create node_modules/@jackwener/opencli symlink so user TS CLIs can import
|
|
81
|
+
// from '@jackwener/opencli/registry' (the package export).
|
|
82
|
+
// This is needed because ~/.opencli/clis/ is outside opencli's node_modules tree.
|
|
83
|
+
const opencliRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
84
|
+
const symlinkDir = path.join(baseDir, 'node_modules', '@jackwener');
|
|
85
|
+
const symlinkPath = path.join(symlinkDir, 'opencli');
|
|
86
|
+
try {
|
|
87
|
+
// Only recreate if symlink is missing or points to wrong target
|
|
88
|
+
let needsUpdate = true;
|
|
89
|
+
try {
|
|
90
|
+
const existing = await fs.promises.readlink(symlinkPath);
|
|
91
|
+
if (existing === opencliRoot) needsUpdate = false;
|
|
92
|
+
} catch { /* doesn't exist */ }
|
|
93
|
+
if (needsUpdate) {
|
|
94
|
+
await fs.promises.mkdir(symlinkDir, { recursive: true });
|
|
95
|
+
try { await fs.promises.unlink(symlinkPath); } catch { /* doesn't exist */ }
|
|
96
|
+
await fs.promises.symlink(opencliRoot, symlinkPath, 'dir');
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Non-fatal: npm-linked installs or permission issues may prevent this
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
/**
|
package/src/doctor.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type ConnectivityResult = {
|
|
|
25
25
|
durationMs: number;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
|
|
28
29
|
export type DoctorReport = {
|
|
29
30
|
cliVersion?: string;
|
|
30
31
|
daemonRunning: boolean;
|
|
@@ -93,7 +94,6 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
93
94
|
if (connectivity && !connectivity.ok) {
|
|
94
95
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
95
96
|
}
|
|
96
|
-
|
|
97
97
|
if (status.extensionVersion && opts.cliVersion) {
|
|
98
98
|
const extMajor = status.extensionVersion.split('.')[0];
|
|
99
99
|
const cliMajor = opts.cliVersion.split('.')[0];
|
package/src/types.ts
CHANGED
|
@@ -77,4 +77,12 @@ export interface IPage {
|
|
|
77
77
|
getCurrentUrl?(): Promise<string | null>;
|
|
78
78
|
/** Returns the active tab ID, or undefined if not yet resolved. */
|
|
79
79
|
getActiveTabId?(): number | undefined;
|
|
80
|
+
/** Send a raw CDP command via chrome.debugger passthrough. */
|
|
81
|
+
cdp?(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
82
|
+
/** Click at native coordinates via CDP Input.dispatchMouseEvent. */
|
|
83
|
+
nativeClick?(x: number, y: number): Promise<void>;
|
|
84
|
+
/** Type text via CDP Input.insertText. */
|
|
85
|
+
nativeType?(text: string): Promise<void>;
|
|
86
|
+
/** Press a key via CDP Input.dispatchKeyEvent. */
|
|
87
|
+
nativeKeyPress?(key: string, modifiers?: string[]): Promise<void>;
|
|
80
88
|
}
|