@jackwener/opencli 1.7.8 → 1.7.10

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 (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -6,7 +6,7 @@
6
6
  import type { BrowserSessionInfo } from '../types.js';
7
7
  export interface DaemonCommand {
8
8
  id: string;
9
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind-current' | 'network-capture-start' | 'network-capture-read' | 'cdp' | 'frames';
9
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'cdp' | 'frames';
10
10
  /** Target page identity (targetId). Cross-layer contract with the extension. */
11
11
  page?: string;
12
12
  code?: string;
@@ -30,21 +30,32 @@ export interface DaemonCommand {
30
30
  pattern?: string;
31
31
  cdpMethod?: string;
32
32
  cdpParams?: Record<string, unknown>;
33
- /** When true, automation windows are created in the foreground */
33
+ /** When true, the owned automation container is created in the foreground */
34
34
  windowFocused?: boolean;
35
35
  /** Custom idle timeout in seconds for this workspace session. Overrides the default. */
36
36
  idleTimeout?: number;
37
+ /** Explicitly allow navigation inside a borrowed bound tab. */
38
+ allowBoundNavigation?: boolean;
37
39
  /** Frame index for cross-frame operations (0-based, from 'frames' action) */
38
40
  frameIndex?: number;
41
+ /** Browser profile/context to route the command to. */
42
+ contextId?: string;
39
43
  }
40
44
  export interface DaemonResult {
41
45
  id: string;
42
46
  ok: boolean;
43
47
  data?: unknown;
44
48
  error?: string;
49
+ errorCode?: string;
50
+ errorHint?: string;
45
51
  /** Page identity (targetId) — present on page-scoped command responses */
46
52
  page?: string;
47
53
  }
54
+ export declare class BrowserCommandError extends Error {
55
+ readonly code?: string | undefined;
56
+ readonly hint?: string | undefined;
57
+ constructor(message: string, code?: string | undefined, hint?: string | undefined);
58
+ }
48
59
  export interface DaemonStatus {
49
60
  ok: boolean;
50
61
  pid: number;
@@ -53,12 +64,25 @@ export interface DaemonStatus {
53
64
  extensionConnected: boolean;
54
65
  extensionVersion?: string;
55
66
  extensionCompatRange?: string;
67
+ contextId?: string;
68
+ profileRequired?: boolean;
69
+ profileDisconnected?: boolean;
70
+ profiles?: BrowserProfileStatus[];
56
71
  pending: number;
57
72
  memoryMB: number;
58
73
  port: number;
59
74
  }
75
+ export interface BrowserProfileStatus {
76
+ contextId: string;
77
+ extensionConnected: boolean;
78
+ extensionVersion?: string;
79
+ extensionCompatRange?: string;
80
+ pending: number;
81
+ lastSeenAt?: number;
82
+ }
60
83
  export declare function fetchDaemonStatus(opts?: {
61
84
  timeout?: number;
85
+ contextId?: string;
62
86
  }): Promise<DaemonStatus | null>;
63
87
  export type DaemonHealth = {
64
88
  state: 'stopped';
@@ -66,6 +90,12 @@ export type DaemonHealth = {
66
90
  } | {
67
91
  state: 'no-extension';
68
92
  status: DaemonStatus;
93
+ } | {
94
+ state: 'profile-required';
95
+ status: DaemonStatus;
96
+ } | {
97
+ state: 'profile-disconnected';
98
+ status: DaemonStatus;
69
99
  } | {
70
100
  state: 'ready';
71
101
  status: DaemonStatus;
@@ -76,6 +106,7 @@ export type DaemonHealth = {
76
106
  */
77
107
  export declare function getDaemonHealth(opts?: {
78
108
  timeout?: number;
109
+ contextId?: string;
79
110
  }): Promise<DaemonHealth>;
80
111
  export declare function requestDaemonShutdown(opts?: {
81
112
  timeout?: number;
@@ -92,8 +123,11 @@ export declare function sendCommandFull(action: DaemonCommand['action'], params?
92
123
  data: unknown;
93
124
  page?: string;
94
125
  }>;
95
- export declare function listSessions(): Promise<BrowserSessionInfo[]>;
96
- export declare function bindCurrentTab(workspace: string, opts?: {
126
+ export declare function listSessions(opts?: {
127
+ contextId?: string;
128
+ }): Promise<BrowserSessionInfo[]>;
129
+ export declare function bindTab(workspace: string, opts?: {
97
130
  matchDomain?: string;
98
131
  matchPathPrefix?: string;
132
+ contextId?: string;
99
133
  }): Promise<unknown>;
@@ -6,6 +6,7 @@
6
6
  import { DEFAULT_DAEMON_PORT } from '../constants.js';
7
7
  import { sleep } from '../utils.js';
8
8
  import { classifyBrowserError } from './errors.js';
9
+ import { resolveProfileContextId } from './profile.js';
9
10
  const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
10
11
  const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
11
12
  const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
@@ -13,6 +14,16 @@ let _idCounter = 0;
13
14
  function generateId() {
14
15
  return `cmd_${process.pid}_${Date.now()}_${++_idCounter}`;
15
16
  }
17
+ export class BrowserCommandError extends Error {
18
+ code;
19
+ hint;
20
+ constructor(message, code, hint) {
21
+ super(message);
22
+ this.code = code;
23
+ this.hint = hint;
24
+ this.name = 'BrowserCommandError';
25
+ }
26
+ }
16
27
  async function requestDaemon(pathname, init) {
17
28
  const { timeout = 2000, headers, ...rest } = init ?? {};
18
29
  const controller = new AbortController();
@@ -30,7 +41,8 @@ async function requestDaemon(pathname, init) {
30
41
  }
31
42
  export async function fetchDaemonStatus(opts) {
32
43
  try {
33
- const res = await requestDaemon('/status', { timeout: opts?.timeout ?? 2000 });
44
+ const params = opts?.contextId ? `?contextId=${encodeURIComponent(opts.contextId)}` : '';
45
+ const res = await requestDaemon(`/status${params}`, { timeout: opts?.timeout ?? 2000 });
34
46
  if (!res.ok)
35
47
  return null;
36
48
  return await res.json();
@@ -47,6 +59,10 @@ export async function getDaemonHealth(opts) {
47
59
  const status = await fetchDaemonStatus(opts);
48
60
  if (!status)
49
61
  return { state: 'stopped', status: null };
62
+ if (status.profileRequired)
63
+ return { state: 'profile-required', status };
64
+ if (status.profileDisconnected)
65
+ return { state: 'profile-disconnected', status };
50
66
  if (!status.extensionConnected)
51
67
  return { state: 'no-extension', status };
52
68
  return { state: 'ready', status };
@@ -75,7 +91,8 @@ async function sendCommandRaw(action, params) {
75
91
  const id = generateId();
76
92
  const wf = process.env.OPENCLI_WINDOW_FOCUSED;
77
93
  const windowFocused = (wf === '1' || wf === 'true') ? true : undefined;
78
- const command = { id, action, ...params, ...(windowFocused && { windowFocused }) };
94
+ const contextId = params.contextId ?? resolveProfileContextId();
95
+ const command = { id, action, ...params, ...(contextId && { contextId }), ...(windowFocused && { windowFocused }) };
79
96
  try {
80
97
  const res = await requestDaemon('/command', {
81
98
  method: 'POST',
@@ -95,7 +112,7 @@ async function sendCommandRaw(action, params) {
95
112
  await sleep(advice.delayMs);
96
113
  continue;
97
114
  }
98
- throw new Error(result.error ?? 'Daemon command failed');
115
+ throw new BrowserCommandError(result.error ?? 'Daemon command failed', result.errorCode, result.errorHint);
99
116
  }
100
117
  return result;
101
118
  }
@@ -126,10 +143,10 @@ export async function sendCommandFull(action, params = {}) {
126
143
  const result = await sendCommandRaw(action, params);
127
144
  return { data: result.data, page: result.page };
128
145
  }
129
- export async function listSessions() {
130
- const result = await sendCommand('sessions');
146
+ export async function listSessions(opts) {
147
+ const result = await sendCommand('sessions', { ...(opts?.contextId && { contextId: opts.contextId }) });
131
148
  return Array.isArray(result) ? result : [];
132
149
  }
133
- export async function bindCurrentTab(workspace, opts = {}) {
134
- return sendCommand('bind-current', { workspace, ...opts });
150
+ export async function bindTab(workspace, opts = {}) {
151
+ return sendCommand('bind', { workspace, ...opts });
135
152
  }
@@ -6,6 +6,7 @@ describe('daemon-client', () => {
6
6
  });
7
7
  afterEach(() => {
8
8
  vi.restoreAllMocks();
9
+ vi.unstubAllEnvs();
9
10
  });
10
11
  it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
11
12
  const status = {
@@ -78,6 +79,43 @@ describe('daemon-client', () => {
78
79
  });
79
80
  await expect(getDaemonHealth()).resolves.toEqual({ state: 'ready', status });
80
81
  });
82
+ it('getDaemonHealth returns profile-required when multiple profiles are connected without a selection', async () => {
83
+ const status = {
84
+ ok: true,
85
+ pid: 123,
86
+ uptime: 10,
87
+ extensionConnected: false,
88
+ profileRequired: true,
89
+ profiles: [
90
+ { contextId: 'work', extensionConnected: true, pending: 0 },
91
+ { contextId: 'personal', extensionConnected: true, pending: 0 },
92
+ ],
93
+ pending: 0,
94
+ memoryMB: 32,
95
+ port: 19825,
96
+ };
97
+ vi.mocked(fetch).mockResolvedValue({
98
+ ok: true,
99
+ json: () => Promise.resolve(status),
100
+ });
101
+ await expect(getDaemonHealth()).resolves.toEqual({ state: 'profile-required', status });
102
+ });
103
+ it('fetchDaemonStatus includes contextId in the status query', async () => {
104
+ vi.mocked(fetch).mockResolvedValue({
105
+ ok: true,
106
+ json: () => Promise.resolve({
107
+ ok: true,
108
+ pid: 1,
109
+ uptime: 0,
110
+ extensionConnected: true,
111
+ pending: 0,
112
+ memoryMB: 1,
113
+ port: 19825,
114
+ }),
115
+ });
116
+ await fetchDaemonStatus({ contextId: 'work' });
117
+ expect(vi.mocked(fetch).mock.calls[0][0]).toMatch(/\/status\?contextId=work$/);
118
+ });
81
119
  it('sendCommand includes the current pid in generated command ids', async () => {
82
120
  vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_000);
83
121
  vi.mocked(fetch).mockResolvedValue({
@@ -95,6 +133,17 @@ describe('daemon-client', () => {
95
133
  expect(ids[1]).toMatch(new RegExp(`^cmd_${process.pid}_1763000000000_\\d+$`));
96
134
  expect(ids[0]).not.toBe(ids[1]);
97
135
  });
136
+ it('sendCommand forwards OPENCLI_PROFILE as command contextId', async () => {
137
+ vi.stubEnv('OPENCLI_PROFILE', 'work');
138
+ vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_000);
139
+ vi.mocked(fetch).mockResolvedValue({
140
+ status: 200,
141
+ json: () => Promise.resolve({ id: 'server', ok: true, data: 'ok' }),
142
+ });
143
+ await sendCommand('exec', { code: '1 + 1' });
144
+ const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body));
145
+ expect(body.contextId).toBe('work');
146
+ });
98
147
  it('sendCommand retries with a new id when daemon reports a duplicate pending id', async () => {
99
148
  vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_123);
100
149
  const fetchMock = vi.mocked(fetch);
@@ -0,0 +1,23 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import { DEFAULT_DAEMON_PORT } from '../constants.js';
3
+ import { type DaemonStatus } from './daemon-client.js';
4
+ export interface DaemonLaunchSpec {
5
+ binary: string;
6
+ args: string[];
7
+ scriptPath: string;
8
+ }
9
+ export interface DaemonRestartResult {
10
+ previousStatus: DaemonStatus | null;
11
+ status: DaemonStatus | null;
12
+ stopped: boolean;
13
+ spawned: boolean;
14
+ }
15
+ export declare function resolveDaemonLaunchSpec(): DaemonLaunchSpec;
16
+ export declare function spawnDaemonProcess(): ChildProcess;
17
+ export declare function waitForDaemonStop(timeoutMs: number): Promise<boolean>;
18
+ export declare function waitForDaemonStatus(timeoutMs: number): Promise<DaemonStatus | null>;
19
+ export declare function restartDaemon(opts?: {
20
+ stopTimeoutMs?: number;
21
+ startTimeoutMs?: number;
22
+ }): Promise<DaemonRestartResult>;
23
+ export { DEFAULT_DAEMON_PORT };
@@ -0,0 +1,67 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { DEFAULT_DAEMON_PORT } from '../constants.js';
6
+ import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown } from './daemon-client.js';
7
+ export function resolveDaemonLaunchSpec() {
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const parentDir = path.resolve(__dirname, '..');
10
+ const daemonTs = path.join(parentDir, 'daemon.ts');
11
+ const daemonJs = path.join(parentDir, 'daemon.js');
12
+ const isTs = fs.existsSync(daemonTs);
13
+ const scriptPath = isTs ? daemonTs : daemonJs;
14
+ return {
15
+ binary: process.execPath,
16
+ args: isTs ? ['--import', 'tsx/esm', scriptPath] : [scriptPath],
17
+ scriptPath,
18
+ };
19
+ }
20
+ export function spawnDaemonProcess() {
21
+ const launch = resolveDaemonLaunchSpec();
22
+ const proc = spawn(launch.binary, launch.args, {
23
+ detached: true,
24
+ stdio: 'ignore',
25
+ env: { ...process.env },
26
+ });
27
+ proc.unref();
28
+ return proc;
29
+ }
30
+ export async function waitForDaemonStop(timeoutMs) {
31
+ const deadline = Date.now() + timeoutMs;
32
+ while (Date.now() < deadline) {
33
+ await sleep(200);
34
+ const h = await getDaemonHealth();
35
+ if (h.state === 'stopped')
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ export async function waitForDaemonStatus(timeoutMs) {
41
+ const deadline = Date.now() + timeoutMs;
42
+ while (Date.now() < deadline) {
43
+ const status = await fetchDaemonStatus({ timeout: Math.min(1000, Math.max(100, deadline - Date.now())) });
44
+ if (status)
45
+ return status;
46
+ await sleep(200);
47
+ }
48
+ return null;
49
+ }
50
+ export async function restartDaemon(opts = {}) {
51
+ const previousStatus = await fetchDaemonStatus();
52
+ let stopped = previousStatus === null;
53
+ if (previousStatus) {
54
+ const shutdownAccepted = await requestDaemonShutdown();
55
+ stopped = shutdownAccepted && await waitForDaemonStop(opts.stopTimeoutMs ?? 3000);
56
+ if (!stopped) {
57
+ return { previousStatus, status: previousStatus, stopped: false, spawned: false };
58
+ }
59
+ }
60
+ spawnDaemonProcess();
61
+ const status = await waitForDaemonStatus(opts.startTimeoutMs ?? 5000);
62
+ return { previousStatus, status, stopped, spawned: true };
63
+ }
64
+ function sleep(ms) {
65
+ return new Promise((resolve) => setTimeout(resolve, ms));
66
+ }
67
+ export { DEFAULT_DAEMON_PORT };
@@ -0,0 +1,4 @@
1
+ import type { DaemonStatus } from './daemon-client.js';
2
+ export declare function isDaemonStale(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined, cliVersion?: string): boolean;
3
+ export declare function formatDaemonVersion(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined): string;
4
+ export declare function staleDaemonIssue(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined, cliVersion: string): string;
@@ -0,0 +1,12 @@
1
+ export function isDaemonStale(status, cliVersion) {
2
+ if (!status || !cliVersion)
3
+ return false;
4
+ return !status.daemonVersion || status.daemonVersion !== cliVersion;
5
+ }
6
+ export function formatDaemonVersion(status) {
7
+ return status?.daemonVersion ? `v${status.daemonVersion}` : 'version unknown';
8
+ }
9
+ export function staleDaemonIssue(status, cliVersion) {
10
+ return `Stale daemon detected: daemon ${formatDaemonVersion(status)} != CLI v${cliVersion}.\n` +
11
+ ' Run: opencli daemon restart';
12
+ }
@@ -15,7 +15,10 @@ const EXTENSION_TRANSIENT_PATTERNS = [
15
15
  'Extension disconnected',
16
16
  'Extension not connected',
17
17
  'attach failed',
18
+ 'Detached while handling command',
19
+ 'Debugger is not attached to the tab',
18
20
  'no longer exists',
21
+ 'No tab with id',
19
22
  'CDP connection',
20
23
  'Daemon command failed',
21
24
  'No window with id',
@@ -6,7 +6,10 @@ describe('classifyBrowserError', () => {
6
6
  'Extension disconnected',
7
7
  'Extension not connected',
8
8
  'attach failed',
9
+ 'Detached while handling command',
10
+ 'Debugger is not attached to the tab: 123',
9
11
  'no longer exists',
12
+ 'No tab with id: 456',
10
13
  'CDP connection reset',
11
14
  'Daemon command failed',
12
15
  'No window with id: 123',
@@ -25,6 +25,7 @@ export interface CachedNetworkEntry {
25
25
  */
26
26
  body_truncated?: boolean;
27
27
  body_full_size?: number;
28
+ timestamp?: number;
28
29
  }
29
30
  export interface NetworkCacheFile {
30
31
  version: 1;
@@ -15,8 +15,9 @@ import { BasePage } from './base-page.js';
15
15
  */
16
16
  export declare class Page extends BasePage {
17
17
  private readonly workspace;
18
+ readonly contextId?: string | undefined;
18
19
  private readonly _idleTimeout;
19
- constructor(workspace?: string, idleTimeout?: number);
20
+ constructor(workspace?: string, idleTimeout?: number, contextId?: string | undefined);
20
21
  /** Active page identity (targetId), set after navigate and used in all subsequent commands */
21
22
  private _page;
22
23
  private _networkCaptureUnsupported;
@@ -28,6 +29,7 @@ export declare class Page extends BasePage {
28
29
  goto(url: string, options?: {
29
30
  waitUntil?: 'load' | 'none';
30
31
  settleMs?: number;
32
+ allowBoundNavigation?: boolean;
31
33
  }): Promise<void>;
32
34
  /** Get the active page identity (targetId) */
33
35
  getActivePage(): string | undefined;
@@ -27,10 +27,12 @@ function isUnsupportedNetworkCaptureError(err) {
27
27
  */
28
28
  export class Page extends BasePage {
29
29
  workspace;
30
+ contextId;
30
31
  _idleTimeout;
31
- constructor(workspace = 'default', idleTimeout) {
32
+ constructor(workspace = 'default', idleTimeout, contextId) {
32
33
  super();
33
34
  this.workspace = workspace;
35
+ this.contextId = contextId;
34
36
  this._idleTimeout = idleTimeout;
35
37
  }
36
38
  /** Active page identity (targetId), set after navigate and used in all subsequent commands */
@@ -39,12 +41,17 @@ export class Page extends BasePage {
39
41
  _networkCaptureWarned = false;
40
42
  /** Helper: spread workspace into command params */
41
43
  _wsOpt() {
42
- return { workspace: this.workspace, ...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }) };
44
+ return {
45
+ workspace: this.workspace,
46
+ ...(this.contextId && { contextId: this.contextId }),
47
+ ...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
48
+ };
43
49
  }
44
50
  /** Helper: spread workspace + page identity into command params */
45
51
  _cmdOpts() {
46
52
  return {
47
53
  workspace: this.workspace,
54
+ ...(this.contextId && { contextId: this.contextId }),
48
55
  ...(this._page !== undefined && { page: this._page }),
49
56
  ...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
50
57
  };
@@ -53,6 +60,7 @@ export class Page extends BasePage {
53
60
  const result = await sendCommandFull('navigate', {
54
61
  url,
55
62
  ...this._cmdOpts(),
63
+ ...(options?.allowBoundNavigation === true && { allowBoundNavigation: true }),
56
64
  });
57
65
  // Remember the page identity (targetId) for subsequent calls
58
66
  if (result.page) {
@@ -0,0 +1,14 @@
1
+ export declare const DEFAULT_CONTEXT_ID = "default";
2
+ export type ProfileConfig = {
3
+ version: 1;
4
+ defaultContextId?: string;
5
+ aliases: Record<string, string>;
6
+ };
7
+ export declare function normalizeContextId(value: string | undefined | null): string | undefined;
8
+ export declare function emptyProfileConfig(): ProfileConfig;
9
+ export declare function loadProfileConfig(): ProfileConfig;
10
+ export declare function saveProfileConfig(config: ProfileConfig): void;
11
+ export declare function resolveProfileContextId(profile?: string): string | undefined;
12
+ export declare function aliasForContextId(config: ProfileConfig, contextId: string): string | undefined;
13
+ export declare function renameProfile(contextId: string, alias: string): ProfileConfig;
14
+ export declare function setDefaultProfile(profile: string): ProfileConfig;
@@ -0,0 +1,85 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ export const DEFAULT_CONTEXT_ID = 'default';
5
+ function profileConfigPath() {
6
+ const baseDir = process.env.OPENCLI_CONFIG_DIR || path.join(os.homedir(), '.opencli');
7
+ return path.join(baseDir, 'browser-profiles.json');
8
+ }
9
+ export function normalizeContextId(value) {
10
+ const trimmed = value?.trim();
11
+ return trimmed || undefined;
12
+ }
13
+ export function emptyProfileConfig() {
14
+ return { version: 1, aliases: {} };
15
+ }
16
+ export function loadProfileConfig() {
17
+ try {
18
+ const raw = fs.readFileSync(profileConfigPath(), 'utf-8');
19
+ const parsed = JSON.parse(raw);
20
+ const aliases = parsed.aliases && typeof parsed.aliases === 'object'
21
+ ? Object.fromEntries(Object.entries(parsed.aliases).filter((entry) => {
22
+ const [key, value] = entry;
23
+ return typeof key === 'string' && key.trim().length > 0
24
+ && typeof value === 'string' && value.trim().length > 0;
25
+ }))
26
+ : {};
27
+ return {
28
+ version: 1,
29
+ aliases,
30
+ ...(typeof parsed.defaultContextId === 'string' && parsed.defaultContextId.trim()
31
+ ? { defaultContextId: parsed.defaultContextId.trim() }
32
+ : {}),
33
+ };
34
+ }
35
+ catch {
36
+ return emptyProfileConfig();
37
+ }
38
+ }
39
+ export function saveProfileConfig(config) {
40
+ const target = profileConfigPath();
41
+ fs.mkdirSync(path.dirname(target), { recursive: true });
42
+ fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n', 'utf-8');
43
+ }
44
+ export function resolveProfileContextId(profile) {
45
+ const config = loadProfileConfig();
46
+ const requested = normalizeContextId(profile)
47
+ ?? normalizeContextId(process.env.OPENCLI_PROFILE)
48
+ ?? normalizeContextId(config.defaultContextId);
49
+ if (!requested)
50
+ return undefined;
51
+ return config.aliases[requested] ?? requested;
52
+ }
53
+ export function aliasForContextId(config, contextId) {
54
+ for (const [alias, id] of Object.entries(config.aliases)) {
55
+ if (id === contextId)
56
+ return alias;
57
+ }
58
+ return undefined;
59
+ }
60
+ export function renameProfile(contextId, alias) {
61
+ const normalizedContextId = normalizeContextId(contextId);
62
+ const normalizedAlias = normalizeContextId(alias);
63
+ if (!normalizedContextId)
64
+ throw new Error('profile contextId is required');
65
+ if (!normalizedAlias)
66
+ throw new Error('profile alias is required');
67
+ const config = loadProfileConfig();
68
+ for (const [existingAlias, existingContextId] of Object.entries(config.aliases)) {
69
+ if (existingAlias !== normalizedAlias && existingContextId === normalizedContextId) {
70
+ delete config.aliases[existingAlias];
71
+ }
72
+ }
73
+ config.aliases[normalizedAlias] = normalizedContextId;
74
+ saveProfileConfig(config);
75
+ return config;
76
+ }
77
+ export function setDefaultProfile(profile) {
78
+ const contextId = resolveProfileContextId(profile) ?? normalizeContextId(profile);
79
+ if (!contextId)
80
+ throw new Error('profile is required');
81
+ const config = loadProfileConfig();
82
+ config.defaultContextId = contextId;
83
+ saveProfileConfig(config);
84
+ return config;
85
+ }
@@ -39,5 +39,7 @@ export interface ManifestEntry {
39
39
  /** Pre-navigation control — see CliCommand.navigateBefore */
40
40
  navigateBefore?: boolean | string;
41
41
  }
42
+ export declare function normalizeManifestPath(relativePath: string): string;
42
43
  export declare function loadManifestEntries(filePath: string, site: string, importer?: (moduleHref: string) => Promise<unknown>): Promise<ManifestEntry[]>;
43
44
  export declare function buildManifest(): Promise<ManifestEntry[]>;
45
+ export declare function serializeManifest(manifest: ManifestEntry[]): string;
@@ -36,6 +36,12 @@ function toModulePath(filePath, site) {
36
36
  const baseName = path.basename(filePath, path.extname(filePath));
37
37
  return `${site}/${baseName}.js`;
38
38
  }
39
+ export function normalizeManifestPath(relativePath) {
40
+ return relativePath.replace(/\\/g, '/');
41
+ }
42
+ function toManifestRelativePath(filePath) {
43
+ return normalizeManifestPath(path.relative(CLIS_DIR, filePath));
44
+ }
39
45
  function isCliCommandValue(value, site) {
40
46
  return isRecord(value)
41
47
  && typeof value.site === 'string'
@@ -85,8 +91,9 @@ export async function loadManifestEntries(filePath, site, importer = moduleHref
85
91
  return !previous || previous !== cmd;
86
92
  })
87
93
  .map(([, cmd]) => cmd);
88
- // Resolve sourceFile relative to clis/.
89
- const sourceRelative = path.relative(CLIS_DIR, filePath);
94
+ // Manifest paths are cross-platform artifacts; keep them POSIX-style even
95
+ // when build-manifest runs on Windows.
96
+ const sourceRelative = toManifestRelativePath(filePath);
90
97
  const seen = new Set();
91
98
  return runtimeCommands
92
99
  .filter((cmd) => {
@@ -128,10 +135,13 @@ export async function buildManifest() {
128
135
  }
129
136
  return [...manifest.values()].sort((a, b) => a.site.localeCompare(b.site) || a.name.localeCompare(b.name));
130
137
  }
138
+ export function serializeManifest(manifest) {
139
+ return `${JSON.stringify(manifest, null, 2)}\n`;
140
+ }
131
141
  async function main() {
132
142
  const manifest = await buildManifest();
133
143
  fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
134
- fs.writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2));
144
+ fs.writeFileSync(OUTPUT, serializeManifest(manifest));
135
145
  console.log(`✅ Manifest compiled: ${manifest.length} entries → ${OUTPUT}`);
136
146
  // Restore executable permissions on bin entries.
137
147
  // tsc does not preserve the +x bit, so after a clean rebuild the CLI
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { cli, getRegistry, Strategy } from './registry.js';
6
- import { loadManifestEntries } from './build-manifest.js';
6
+ import { loadManifestEntries, normalizeManifestPath, serializeManifest } from './build-manifest.js';
7
7
  describe('manifest helper rules', () => {
8
8
  const tempDirs = [];
9
9
  afterEach(() => {
@@ -76,8 +76,9 @@ describe('manifest helper rules', () => {
76
76
  replacedBy: 'opencli demo new',
77
77
  },
78
78
  ]);
79
- // Verify sourceFile is included
79
+ // Verify sourceFile is included and stable for manifest consumers.
80
80
  expect(entries[0].sourceFile).toBeDefined();
81
+ expect(entries[0].sourceFile).not.toContain('\\');
81
82
  getRegistry().delete(key);
82
83
  });
83
84
  it('falls back to registry delta for side-effect-only cli modules', async () => {
@@ -139,4 +140,21 @@ describe('manifest helper rules', () => {
139
140
  getRegistry().delete(screenKey);
140
141
  getRegistry().delete(statusKey);
141
142
  });
143
+ it('normalizes manifest paths to POSIX separators', () => {
144
+ expect(normalizeManifestPath('demo\\status.js')).toBe('demo/status.js');
145
+ expect(normalizeManifestPath('demo/status.js')).toBe('demo/status.js');
146
+ });
147
+ it('serializes manifest json with a trailing newline', () => {
148
+ const serialized = serializeManifest([{
149
+ site: 'demo',
150
+ name: 'status',
151
+ description: '',
152
+ strategy: 'public',
153
+ browser: false,
154
+ args: [],
155
+ type: 'js',
156
+ }]);
157
+ expect(serialized.endsWith('\n')).toBe(true);
158
+ expect(serialized).toContain('\n]');
159
+ });
142
160
  });
package/dist/src/cli.d.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  */
7
7
  import { Command } from 'commander';
8
8
  import { findPackageRoot } from './package-paths.js';
9
+ export declare function selectFreshByTimestamp<T extends {
10
+ timestamp?: unknown;
11
+ }>(items: T[], lastSeenTs: number): {
12
+ fresh: T[];
13
+ lastSeenTs: number;
14
+ };
9
15
  /**
10
16
  * Check whether the site-memory scaffolding exists under
11
17
  * ~/.opencli/sites/<site>/. Agents have a strong tendency to forget to write