@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
@@ -0,0 +1,260 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { redactValue } from './redaction.js';
5
+ import { pruneTraceArtifacts, traceExpiresAt } from './retention.js';
6
+ import { CliError, getErrorMessage } from '../errors.js';
7
+ import { log } from '../logger.js';
8
+ import { PKG_VERSION } from '../version.js';
9
+ function baseOpenCliDir() {
10
+ return process.env.OPENCLI_CONFIG_DIR || path.join(os.homedir(), '.opencli');
11
+ }
12
+ function safeSegment(value) {
13
+ const safe = (value || 'default').replace(/[^a-zA-Z0-9_-]+/g, '_');
14
+ return safe || 'default';
15
+ }
16
+ export function getTraceDirectory(contextId, traceId, baseDir = baseOpenCliDir()) {
17
+ return path.join(baseDir, 'profiles', safeSegment(contextId), 'traces', safeSegment(traceId));
18
+ }
19
+ export function exportObservationSession(session, opts = {}) {
20
+ const dir = getTraceDirectory(session.scope.contextId, session.id, opts.baseDir);
21
+ const status = opts.status ?? (opts.error === undefined ? 'success' : 'failure');
22
+ const createdAt = new Date().toISOString();
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ fs.mkdirSync(path.join(dir, 'screenshots'), { recursive: true });
25
+ fs.mkdirSync(path.join(dir, 'state'), { recursive: true });
26
+ const originalEvents = session.events();
27
+ const sanitizedEvents = originalEvents.map((event) => redactObservationEvent(event));
28
+ const traceLines = [];
29
+ const networkLines = [];
30
+ const consoleLines = [];
31
+ let screenshotIndex = 0;
32
+ let stateIndex = 0;
33
+ for (let i = 0; i < sanitizedEvents.length; i++) {
34
+ const originalEvent = originalEvents[i];
35
+ const event = sanitizedEvents[i];
36
+ const serializable = { ...event };
37
+ if (event.stream === 'screenshot' && originalEvent.stream === 'screenshot' && typeof originalEvent.data === 'string') {
38
+ const ext = event.format === 'jpeg' ? 'jpg' : 'png';
39
+ const file = `screenshots/${String(++screenshotIndex).padStart(4, '0')}.${ext}`;
40
+ fs.writeFileSync(path.join(dir, file), originalEvent.data, 'base64');
41
+ serializable.path = file;
42
+ delete serializable.data;
43
+ }
44
+ if (event.stream === 'state' && serializable.snapshot !== undefined) {
45
+ const file = `state/${String(++stateIndex).padStart(4, '0')}.json`;
46
+ fs.writeFileSync(path.join(dir, file), JSON.stringify(serializable.snapshot, null, 2), 'utf-8');
47
+ serializable.snapshotPath = file;
48
+ delete serializable.snapshot;
49
+ }
50
+ const line = JSON.stringify(serializable);
51
+ traceLines.push(line);
52
+ if (event.stream === 'network')
53
+ networkLines.push(line);
54
+ if (event.stream === 'console')
55
+ consoleLines.push(line);
56
+ }
57
+ fs.writeFileSync(path.join(dir, 'trace.jsonl'), traceLines.join('\n') + (traceLines.length ? '\n' : ''), 'utf-8');
58
+ fs.writeFileSync(path.join(dir, 'network.jsonl'), networkLines.join('\n') + (networkLines.length ? '\n' : ''), 'utf-8');
59
+ fs.writeFileSync(path.join(dir, 'console.jsonl'), consoleLines.join('\n') + (consoleLines.length ? '\n' : ''), 'utf-8');
60
+ const summaryPath = path.join(dir, 'summary.md');
61
+ fs.writeFileSync(summaryPath, renderSummary(session, sanitizedEvents, {
62
+ error: opts.error,
63
+ status,
64
+ dir,
65
+ createdAt,
66
+ retentionPolicy: opts.retentionPolicy,
67
+ }), 'utf-8');
68
+ const receiptPath = path.join(dir, 'receipt.json');
69
+ const resultBase = { traceId: session.id, dir, summaryPath, receiptPath };
70
+ const receipt = buildTraceReceipt(resultBase, status, opts.error, {
71
+ createdAt,
72
+ scope: session.scope,
73
+ retentionPolicy: opts.retentionPolicy,
74
+ });
75
+ fs.writeFileSync(receiptPath, JSON.stringify(receipt, null, 2), 'utf-8');
76
+ pruneTraceArtifactsBestEffort(path.dirname(dir), dir, opts.retentionPolicy);
77
+ return { ...resultBase, receipt };
78
+ }
79
+ function redactObservationEvent(event) {
80
+ return redactValue(event);
81
+ }
82
+ export function buildTraceReceipt(result, status, error, opts = {}) {
83
+ const maybeCliError = error instanceof CliError ? error : undefined;
84
+ const createdAt = opts.createdAt ?? new Date().toISOString();
85
+ return {
86
+ schemaVersion: 1,
87
+ opencliVersion: PKG_VERSION,
88
+ traceId: result.traceId,
89
+ traceDir: result.dir,
90
+ summaryPath: result.summaryPath,
91
+ receiptPath: result.receiptPath,
92
+ status,
93
+ createdAt,
94
+ expiresAt: traceExpiresAt(createdAt, opts.retentionPolicy),
95
+ ...(opts.scope ? { scope: opts.scope } : {}),
96
+ ...(error === undefined ? {} : {
97
+ error: {
98
+ ...(error instanceof Error ? { name: error.name } : {}),
99
+ ...(maybeCliError ? { code: maybeCliError.code, hint: maybeCliError.hint, exitCode: maybeCliError.exitCode } : {}),
100
+ message: String(redactValue(getErrorMessage(error))),
101
+ },
102
+ }),
103
+ };
104
+ }
105
+ function pruneTraceArtifactsBestEffort(tracesDir, protectedTraceDir, retentionPolicy) {
106
+ try {
107
+ pruneTraceArtifacts(tracesDir, {
108
+ policy: retentionPolicy,
109
+ protectedTraceDirs: [protectedTraceDir],
110
+ warn: (message) => log.warn(`[trace] ${message}`),
111
+ });
112
+ }
113
+ catch (err) {
114
+ log.warn(`[trace] Failed to prune trace artifacts: ${err instanceof Error ? err.message : String(err)}`);
115
+ }
116
+ }
117
+ function renderSummary(session, events, opts) {
118
+ const counts = events.reduce((acc, event) => {
119
+ acc[event.stream] = (acc[event.stream] ?? 0) + 1;
120
+ return acc;
121
+ }, {});
122
+ const error = serializeSummaryError(opts.error);
123
+ const errorEvents = events.filter((event) => event.stream === 'error').slice(-20).reverse();
124
+ const failedNetwork = events
125
+ .filter((event) => event.stream === 'network')
126
+ .filter((event) => event.status === undefined || event.status === 0 || event.status >= 400)
127
+ .slice(-20)
128
+ .reverse();
129
+ const suspiciousConsole = events
130
+ .filter((event) => event.stream === 'console')
131
+ .filter((event) => /^(error|warning|warn|assert)$/i.test(event.level))
132
+ .slice(-20)
133
+ .reverse();
134
+ const actions = events
135
+ .filter((event) => event.stream === 'action')
136
+ .slice(-30);
137
+ const lines = [
138
+ '---',
139
+ 'schemaVersion: 1',
140
+ `opencliVersion: ${yamlScalar(PKG_VERSION)}`,
141
+ `traceId: ${yamlScalar(session.id)}`,
142
+ `status: ${opts.status}`,
143
+ `contextId: ${yamlScalar(session.scope.contextId ?? 'default')}`,
144
+ `workspace: ${yamlScalar(session.scope.workspace)}`,
145
+ ...(session.scope.target ? [`target: ${yamlScalar(session.scope.target)}`] : []),
146
+ ...(session.scope.site ? [`site: ${yamlScalar(session.scope.site)}`] : []),
147
+ ...(session.scope.command ? [`command: ${yamlScalar(session.scope.command)}`] : []),
148
+ ...(session.scope.adapterSourcePath ? [`adapterSourcePath: ${yamlScalar(session.scope.adapterSourcePath)}`] : []),
149
+ ...(session.scope.adapterSourcePath ? [`adapterSourcePathExists: ${fs.existsSync(session.scope.adapterSourcePath)}`] : []),
150
+ `traceDir: ${yamlScalar(opts.dir)}`,
151
+ `startedAt: ${yamlScalar(new Date(session.startedAt).toISOString())}`,
152
+ `exportedAt: ${yamlScalar(opts.createdAt)}`,
153
+ `expiresAt: ${yamlScalar(traceExpiresAt(opts.createdAt, opts.retentionPolicy))}`,
154
+ ...(error ? [
155
+ `errorCode: ${yamlScalar(error.code ?? 'UNKNOWN')}`,
156
+ `errorMessage: ${yamlScalar(error.message)}`,
157
+ ] : []),
158
+ '---',
159
+ '',
160
+ '# OpenCLI Trace Summary',
161
+ '',
162
+ '## How To Use',
163
+ '',
164
+ '- Start with this summary, then inspect `trace.jsonl` only when the evidence below is insufficient.',
165
+ '- For adapter repair policy and retry limits, use the `opencli-autofix` skill.',
166
+ '- `adapterSourcePathExists: false` means the path is a best-effort hint, not a confirmed editable file.',
167
+ '',
168
+ '## Error',
169
+ '',
170
+ ...renderErrorSection(error, errorEvents),
171
+ '',
172
+ '## Failed Network',
173
+ '',
174
+ ...renderNetworkSection(failedNetwork),
175
+ '',
176
+ '## Suspicious Console',
177
+ '',
178
+ ...renderConsoleSection(suspiciousConsole),
179
+ '',
180
+ '## Action Timeline',
181
+ '',
182
+ ...renderActionSection(actions),
183
+ '',
184
+ '## Event Counts',
185
+ '',
186
+ ...Object.entries(counts).map(([stream, count]) => `- ${stream}: ${count}`),
187
+ '',
188
+ '## Artifact Files',
189
+ '',
190
+ '- `trace.jsonl`: full redacted event timeline',
191
+ '- `network.jsonl`: redacted network events',
192
+ '- `console.jsonl`: redacted console events',
193
+ '- `state/`: final state snapshots when available',
194
+ '- `screenshots/`: final screenshots when available',
195
+ '',
196
+ ];
197
+ return lines.join('\n');
198
+ }
199
+ function serializeSummaryError(error) {
200
+ if (error === undefined)
201
+ return undefined;
202
+ if (error instanceof CliError) {
203
+ return {
204
+ code: error.code,
205
+ message: String(redactValue(error.message)),
206
+ ...(error.hint ? { hint: String(redactValue(error.hint)) } : {}),
207
+ };
208
+ }
209
+ return { message: String(redactValue(getErrorMessage(error))) };
210
+ }
211
+ function yamlScalar(value) {
212
+ return JSON.stringify(value);
213
+ }
214
+ function renderErrorSection(error, errorEvents) {
215
+ const lines = [];
216
+ if (error) {
217
+ lines.push(`- ${error.code ?? 'UNKNOWN'}: ${error.message}`);
218
+ if (error.hint)
219
+ lines.push(`- hint: ${error.hint}`);
220
+ }
221
+ for (const event of errorEvents) {
222
+ if (event.stream !== 'error')
223
+ continue;
224
+ lines.push(`- ${formatTs(event.ts)} ${event.code ?? 'ERROR'}: ${event.message}`);
225
+ }
226
+ return lines.length ? lines : ['- none'];
227
+ }
228
+ function renderNetworkSection(events) {
229
+ if (!events.length)
230
+ return ['- none'];
231
+ return events.map((event) => {
232
+ const status = event.status ?? 'unknown';
233
+ const method = event.method ?? 'GET';
234
+ const contentType = event.contentType ? ` ${event.contentType}` : '';
235
+ return `- ${formatTs(event.ts)} ${status} ${method} ${event.url}${contentType}`;
236
+ });
237
+ }
238
+ function renderConsoleSection(events) {
239
+ if (!events.length)
240
+ return ['- none'];
241
+ return events.map((event) => `- ${formatTs(event.ts)} ${event.level}: ${trimLine(event.text, 240)}`);
242
+ }
243
+ function renderActionSection(events) {
244
+ if (!events.length)
245
+ return ['- none'];
246
+ return events.map((event) => {
247
+ const phase = event.phase ? ` ${event.phase}` : '';
248
+ const data = event.data && Object.keys(event.data).length
249
+ ? ` ${trimLine(JSON.stringify(redactValue(event.data)), 240)}`
250
+ : '';
251
+ return `- ${formatTs(event.ts)} ${event.name}${phase}${data}`;
252
+ });
253
+ }
254
+ function formatTs(ts) {
255
+ return new Date(ts).toISOString();
256
+ }
257
+ function trimLine(value, max) {
258
+ const compact = value.replace(/\s+/g, ' ').trim();
259
+ return compact.length > max ? `${compact.slice(0, max)}...` : compact;
260
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { buildTraceReceipt, exportObservationSession, getTraceDirectory } from './artifact.js';
6
+ import { ObservationSession } from './session.js';
7
+ describe('observation artifact', () => {
8
+ let baseDir;
9
+ beforeEach(() => {
10
+ baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-trace-'));
11
+ });
12
+ afterEach(() => {
13
+ fs.rmSync(baseDir, { recursive: true, force: true });
14
+ });
15
+ it('writes artifacts under profile-scoped trace directory', () => {
16
+ const session = new ObservationSession({
17
+ id: 'trace-1',
18
+ scope: {
19
+ contextId: 'work',
20
+ workspace: 'site:demo',
21
+ site: 'demo',
22
+ command: 'demo/run',
23
+ adapterSourcePath: '/tmp/clis/demo/run.js',
24
+ },
25
+ now: () => 1_700_000_000_000,
26
+ });
27
+ session.record({ stream: 'action', name: 'command', phase: 'start' });
28
+ session.record({ stream: 'screenshot', format: 'png', data: Buffer.from('png-bytes').toString('base64'), label: 'final' });
29
+ session.record({
30
+ stream: 'network',
31
+ url: 'https://api.test/data?token=secret',
32
+ method: 'GET',
33
+ status: 500,
34
+ requestHeaders: { authorization: 'Bearer secret' },
35
+ responseBody: { ok: false },
36
+ });
37
+ session.record({ stream: 'console', level: 'error', text: 'boom password=supersecret' });
38
+ const result = exportObservationSession(session, { baseDir, error: new Error('failed') });
39
+ expect(result.dir).toBe(getTraceDirectory('work', 'trace-1', baseDir));
40
+ expect(fs.existsSync(path.join(result.dir, 'trace.jsonl'))).toBe(true);
41
+ expect(fs.existsSync(path.join(result.dir, 'network.jsonl'))).toBe(true);
42
+ expect(fs.existsSync(path.join(result.dir, 'console.jsonl'))).toBe(true);
43
+ expect(fs.existsSync(result.receiptPath)).toBe(true);
44
+ expect(fs.readFileSync(path.join(result.dir, 'screenshots', '0001.png'), 'utf-8')).toBe('png-bytes');
45
+ const trace = fs.readFileSync(path.join(result.dir, 'trace.jsonl'), 'utf-8');
46
+ expect(trace).toContain('token=[REDACTED]');
47
+ expect(trace).toContain('"authorization":"[REDACTED]"');
48
+ expect(trace).not.toContain('supersecret');
49
+ const summary = fs.readFileSync(result.summaryPath, 'utf-8');
50
+ expect(summary).toContain('schemaVersion: 1');
51
+ expect(summary).toContain('opencliVersion:');
52
+ expect(summary).toContain('expiresAt:');
53
+ expect(summary).toContain('status: failure');
54
+ expect(summary).toContain('contextId: "work"');
55
+ expect(summary).toContain('adapterSourcePath: "/tmp/clis/demo/run.js"');
56
+ expect(summary).toContain('adapterSourcePathExists: false');
57
+ expect(summary).toContain('## Failed Network');
58
+ expect(summary).toContain('500 GET https://api.test/data?token=[REDACTED]');
59
+ expect(summary).toContain('network: 1');
60
+ const receipt = JSON.parse(fs.readFileSync(result.receiptPath, 'utf-8'));
61
+ expect(receipt).toMatchObject({
62
+ schemaVersion: 1,
63
+ opencliVersion: expect.any(String),
64
+ traceId: 'trace-1',
65
+ traceDir: result.dir,
66
+ summaryPath: result.summaryPath,
67
+ receiptPath: result.receiptPath,
68
+ status: 'failure',
69
+ expiresAt: expect.any(String),
70
+ scope: {
71
+ contextId: 'work',
72
+ workspace: 'site:demo',
73
+ site: 'demo',
74
+ command: 'demo/run',
75
+ adapterSourcePath: '/tmp/clis/demo/run.js',
76
+ },
77
+ error: { message: 'failed' },
78
+ });
79
+ });
80
+ it('builds a compact trace receipt', () => {
81
+ const receipt = buildTraceReceipt({
82
+ traceId: 'trace-1',
83
+ dir: '/tmp/opencli/profiles/work/traces/trace-1',
84
+ summaryPath: '/tmp/opencli/profiles/work/traces/trace-1/summary.md',
85
+ receiptPath: '/tmp/opencli/profiles/work/traces/trace-1/receipt.json',
86
+ }, 'failure', new Error('failed with token=secret'), {
87
+ createdAt: '2026-05-03T00:00:00.000Z',
88
+ retentionPolicy: { maxAgeDays: 7 },
89
+ });
90
+ expect(receipt).toMatchObject({
91
+ schemaVersion: 1,
92
+ opencliVersion: expect.any(String),
93
+ traceId: 'trace-1',
94
+ traceDir: '/tmp/opencli/profiles/work/traces/trace-1',
95
+ receiptPath: '/tmp/opencli/profiles/work/traces/trace-1/receipt.json',
96
+ status: 'failure',
97
+ expiresAt: '2026-05-10T00:00:00.000Z',
98
+ });
99
+ expect(receipt.error?.message).toContain('token=[REDACTED]');
100
+ });
101
+ it('prunes older traces after export while protecting the exported trace', () => {
102
+ const oldTraceDir = getTraceDirectory('work', 'old-trace', baseDir);
103
+ fs.mkdirSync(oldTraceDir, { recursive: true });
104
+ fs.writeFileSync(path.join(oldTraceDir, 'receipt.json'), JSON.stringify({
105
+ createdAt: '2026-04-01T00:00:00.000Z',
106
+ }), 'utf-8');
107
+ fs.writeFileSync(path.join(oldTraceDir, 'trace.jsonl'), '{}\n', 'utf-8');
108
+ const session = new ObservationSession({
109
+ id: 'new-trace',
110
+ scope: { contextId: 'work', workspace: 'site:demo' },
111
+ now: () => Date.parse('2026-05-03T00:00:00.000Z'),
112
+ });
113
+ session.record({ stream: 'action', name: 'command', phase: 'start' });
114
+ const result = exportObservationSession(session, {
115
+ baseDir,
116
+ retentionPolicy: { maxAgeDays: 365, maxCountPerProfile: 1, maxBytesPerProfile: '500MB' },
117
+ });
118
+ expect(fs.existsSync(oldTraceDir)).toBe(false);
119
+ expect(fs.existsSync(result.dir)).toBe(true);
120
+ });
121
+ });
@@ -0,0 +1,89 @@
1
+ export type ObservationStream = 'action' | 'network' | 'console' | 'screenshot' | 'state' | 'error';
2
+ export interface ObservationScope {
3
+ contextId?: string;
4
+ workspace: string;
5
+ target?: string;
6
+ site?: string;
7
+ command?: string;
8
+ adapterSourcePath?: string;
9
+ }
10
+ interface BaseObservationEvent {
11
+ id: string;
12
+ ts: number;
13
+ stream: ObservationStream;
14
+ }
15
+ export interface ActionObservationEvent extends BaseObservationEvent {
16
+ stream: 'action';
17
+ name: string;
18
+ phase?: 'start' | 'end' | 'error';
19
+ data?: Record<string, unknown>;
20
+ }
21
+ export interface NetworkObservationEvent extends BaseObservationEvent {
22
+ stream: 'network';
23
+ url: string;
24
+ method?: string;
25
+ status?: number;
26
+ contentType?: string;
27
+ size?: number;
28
+ requestHeaders?: Record<string, unknown>;
29
+ responseHeaders?: Record<string, unknown>;
30
+ requestBody?: unknown;
31
+ responseBody?: unknown;
32
+ }
33
+ export interface ConsoleObservationEvent extends BaseObservationEvent {
34
+ stream: 'console';
35
+ level: string;
36
+ text: string;
37
+ source?: string;
38
+ }
39
+ export interface ScreenshotObservationEvent extends BaseObservationEvent {
40
+ stream: 'screenshot';
41
+ format: 'png' | 'jpeg';
42
+ data: string;
43
+ label?: string;
44
+ }
45
+ export interface StateObservationEvent extends BaseObservationEvent {
46
+ stream: 'state';
47
+ url?: string | null;
48
+ target?: string;
49
+ snapshot?: unknown;
50
+ label?: string;
51
+ }
52
+ export interface ErrorObservationEvent extends BaseObservationEvent {
53
+ stream: 'error';
54
+ code?: string;
55
+ message: string;
56
+ stack?: string;
57
+ hint?: string;
58
+ }
59
+ export type ObservationEvent = ActionObservationEvent | NetworkObservationEvent | ConsoleObservationEvent | ScreenshotObservationEvent | StateObservationEvent | ErrorObservationEvent;
60
+ export type ObservationEventInput = ObservationEvent extends infer T ? T extends ObservationEvent ? Omit<T, 'id' | 'ts'> & Partial<Pick<T, 'id' | 'ts'>> : never : never;
61
+ export interface ObservationExportResult {
62
+ traceId: string;
63
+ dir: string;
64
+ summaryPath: string;
65
+ receiptPath: string;
66
+ receipt: ObservationTraceReceipt;
67
+ }
68
+ export type ObservationExportStatus = 'success' | 'failure';
69
+ export interface ObservationTraceReceipt {
70
+ schemaVersion: 1;
71
+ opencliVersion: string;
72
+ traceId: string;
73
+ traceDir: string;
74
+ summaryPath: string;
75
+ receiptPath: string;
76
+ status: ObservationExportStatus;
77
+ createdAt: string;
78
+ /** Advisory only; actual deletion is governed by current trace retention budgets. */
79
+ expiresAt?: string;
80
+ scope?: ObservationScope;
81
+ error?: {
82
+ name?: string;
83
+ code?: string;
84
+ message: string;
85
+ hint?: string;
86
+ exitCode?: number;
87
+ };
88
+ }
89
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export * from './events.js';
2
+ export * from './ring-buffer.js';
3
+ export * from './redaction.js';
4
+ export * from './session.js';
5
+ export * from './manager.js';
6
+ export * from './artifact.js';
7
+ export * from './retention.js';
@@ -0,0 +1,7 @@
1
+ export * from './events.js';
2
+ export * from './ring-buffer.js';
3
+ export * from './redaction.js';
4
+ export * from './session.js';
5
+ export * from './manager.js';
6
+ export * from './artifact.js';
7
+ export * from './retention.js';
@@ -0,0 +1,9 @@
1
+ import { ObservationSession, type ObservationSessionOptions } from './session.js';
2
+ import type { ObservationScope } from './events.js';
3
+ export declare class ObservationManager {
4
+ private readonly sessions;
5
+ start(opts: ObservationSessionOptions): ObservationSession;
6
+ get(id: string): ObservationSession | undefined;
7
+ stop(id: string): ObservationSession | undefined;
8
+ findByScope(scope: ObservationScope): ObservationSession[];
9
+ }
@@ -0,0 +1,27 @@
1
+ import { ObservationSession } from './session.js';
2
+ export class ObservationManager {
3
+ sessions = new Map();
4
+ start(opts) {
5
+ const session = new ObservationSession(opts);
6
+ this.sessions.set(session.id, session);
7
+ return session;
8
+ }
9
+ get(id) {
10
+ return this.sessions.get(id);
11
+ }
12
+ stop(id) {
13
+ const session = this.sessions.get(id);
14
+ this.sessions.delete(id);
15
+ return session;
16
+ }
17
+ findByScope(scope) {
18
+ return [...this.sessions.values()].filter((session) => scopeMatches(session.scope, scope));
19
+ }
20
+ }
21
+ function scopeMatches(actual, expected) {
22
+ return actual.workspace === expected.workspace
23
+ && (expected.contextId === undefined || actual.contextId === expected.contextId)
24
+ && (expected.target === undefined || actual.target === expected.target)
25
+ && (expected.site === undefined || actual.site === expected.site)
26
+ && (expected.command === undefined || actual.command === expected.command);
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ObservationManager } from './manager.js';
3
+ describe('ObservationManager', () => {
4
+ it('indexes sessions by id and scope', () => {
5
+ const manager = new ObservationManager();
6
+ const work = manager.start({ id: 'work-1', scope: { contextId: 'work', workspace: 'site:x', target: 'tab-1' } });
7
+ manager.start({ id: 'personal-1', scope: { contextId: 'personal', workspace: 'site:x', target: 'tab-2' } });
8
+ expect(manager.get('work-1')).toBe(work);
9
+ expect(manager.findByScope({ contextId: 'work', workspace: 'site:x' }).map((session) => session.id)).toEqual(['work-1']);
10
+ expect(manager.stop('work-1')).toBe(work);
11
+ expect(manager.get('work-1')).toBeUndefined();
12
+ });
13
+ });
@@ -0,0 +1,11 @@
1
+ export interface RedactionOptions {
2
+ allowlist?: string[];
3
+ maxStringLength?: number;
4
+ maxDepth?: number;
5
+ maxArrayItems?: number;
6
+ maxObjectFields?: number;
7
+ }
8
+ export declare function redactUrl(url: string): string;
9
+ export declare function redactHeaders(headers: Record<string, unknown> | undefined, opts?: RedactionOptions): Record<string, unknown> | undefined;
10
+ export declare function redactText(text: string, opts?: RedactionOptions): string;
11
+ export declare function redactValue(value: unknown, opts?: RedactionOptions, keyHint?: string, depth?: number): unknown;
@@ -0,0 +1,81 @@
1
+ const DEFAULT_REDACTION = '[REDACTED]';
2
+ const SENSITIVE_HEADER_NAMES = new Set([
3
+ 'authorization',
4
+ 'cookie',
5
+ 'set-cookie',
6
+ 'proxy-authorization',
7
+ 'x-api-key',
8
+ 'x-auth-token',
9
+ 'x-csrf-token',
10
+ 'x-xsrf-token',
11
+ ]);
12
+ const SENSITIVE_FIELD_PATTERN = /(password|passwd|pwd|token|secret|authorization|cookie|set-cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?id|csrf|xsrf)/i;
13
+ const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|password|auth|access_token|api_key|session_id|csrf|xsrf)=[^&]*/gi;
14
+ export function redactUrl(url) {
15
+ return url.replace(SENSITIVE_URL_PARAMS, '$1$2=[REDACTED]');
16
+ }
17
+ export function redactHeaders(headers, opts = {}) {
18
+ if (!headers)
19
+ return headers;
20
+ const allow = new Set((opts.allowlist ?? []).map((key) => key.toLowerCase()));
21
+ const out = {};
22
+ for (const [key, value] of Object.entries(headers)) {
23
+ const lower = key.toLowerCase();
24
+ out[key] = SENSITIVE_HEADER_NAMES.has(lower) && !allow.has(lower)
25
+ ? DEFAULT_REDACTION
26
+ : redactValue(value, opts, key);
27
+ }
28
+ return out;
29
+ }
30
+ export function redactText(text, opts = {}) {
31
+ const max = opts.maxStringLength ?? 50_000;
32
+ let out = text
33
+ .replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
34
+ .replace(/(["'])(password|passwd|pwd|token|secret|api_key|apikey|access_token|session_id)\1\s*:\s*(["'])(.*?)\3/gi, '$1$2$1:$3[REDACTED]$3')
35
+ .replace(/(token|secret|password|api_key|apikey|access_token|session_id)[=:]\s*['"]?[^'"\s,;}&]+['"]?/gi, '$1=[REDACTED]')
36
+ .replace(/(cookie[=:]\s*)[^\n;]{3,}/gi, '$1[REDACTED]')
37
+ .replace(/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, '[REDACTED_JWT]');
38
+ if (out.length > max)
39
+ out = out.slice(0, max) + `\n...[truncated, ${out.length - max} chars omitted]`;
40
+ return out;
41
+ }
42
+ export function redactValue(value, opts = {}, keyHint, depth = 0) {
43
+ const allow = new Set((opts.allowlist ?? []).map((key) => key.toLowerCase()));
44
+ if (keyHint && SENSITIVE_FIELD_PATTERN.test(keyHint) && !allow.has(keyHint.toLowerCase())) {
45
+ return DEFAULT_REDACTION;
46
+ }
47
+ if (typeof value === 'string') {
48
+ return keyHint === 'url' ? redactUrl(value) : redactText(value, opts);
49
+ }
50
+ if (value === null || typeof value === 'number' || typeof value === 'boolean')
51
+ return value;
52
+ const maxDepth = opts.maxDepth ?? 5;
53
+ if (depth >= maxDepth)
54
+ return '[truncated: max depth reached]';
55
+ if (Array.isArray(value)) {
56
+ const max = opts.maxArrayItems ?? 100;
57
+ const items = value.slice(0, max).map((item) => redactValue(item, opts, undefined, depth + 1));
58
+ if (value.length > max)
59
+ items.push(`[truncated, ${value.length - max} items omitted]`);
60
+ return items;
61
+ }
62
+ if (typeof value === 'object') {
63
+ const entries = Object.entries(value);
64
+ const max = opts.maxObjectFields ?? 100;
65
+ const out = {};
66
+ for (const [key, child] of entries.slice(0, max)) {
67
+ if (key.toLowerCase() === 'url' && typeof child === 'string')
68
+ out[key] = redactUrl(child);
69
+ else if (key.toLowerCase().includes('headers') && child && typeof child === 'object' && !Array.isArray(child)) {
70
+ out[key] = redactHeaders(child, opts);
71
+ }
72
+ else {
73
+ out[key] = redactValue(child, opts, key, depth + 1);
74
+ }
75
+ }
76
+ if (entries.length > max)
77
+ out.__truncated__ = `[${entries.length - max} fields omitted]`;
78
+ return out;
79
+ }
80
+ return value;
81
+ }
@@ -0,0 +1 @@
1
+ export {};