autokap 1.0.6 → 1.0.8

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 (347) hide show
  1. package/assets/chrome/ios-statusbar-comparison-reference.jpg +0 -0
  2. package/assets/chrome/ios-statusbar-dark-reference.jpg +0 -0
  3. package/assets/chrome/ios-statusbar-light-reference.jpg +0 -0
  4. package/assets/cursors/macos.svg +4 -0
  5. package/assets/cursors/windows.svg +15 -0
  6. package/assets/devices/ipad-pro-11-m4.json +52 -0
  7. package/assets/devices/iphone-16-pro.json +53 -0
  8. package/assets/devices/macbook-air-13.json +45 -0
  9. package/assets/frames/MacBook Air 13.svg +242 -0
  10. package/assets/frames/Status bar - iPhone.png +0 -0
  11. Menu bar- iPad.png +0 -0
  12. package/assets/frames/iPad Pro M4 11_.png +0 -0
  13. package/assets/frames/iPhone 16 Pro.png +0 -0
  14. package/assets/icons/Cellular Connection.svg +3 -0
  15. package/assets/icons/Union.svg +6 -0
  16. package/assets/icons/Wifi.svg +3 -0
  17. package/assets/icons/battery.svg +5 -0
  18. package/assets/icons/battery_charging.svg +8 -0
  19. package/assets/skill/OPCODE-REFERENCE.md +607 -0
  20. package/assets/skill/README.md +39 -0
  21. package/assets/skill/SKILL.md +453 -468
  22. package/assets/skill/STUDIO-SKILL.md +476 -0
  23. package/assets/skill/references/examples.md +104 -0
  24. package/assets/skill/references/interactive-demo.md +225 -0
  25. package/assets/skill/references/mock-data.md +178 -0
  26. package/dist/abort.d.ts +5 -0
  27. package/dist/abort.js +44 -0
  28. package/dist/action-verifier.d.ts +29 -0
  29. package/dist/action-verifier.js +133 -0
  30. package/dist/agent-action-recovery.d.ts +45 -0
  31. package/dist/agent-action-recovery.js +370 -0
  32. package/dist/agent-message-utils.d.ts +21 -0
  33. package/dist/agent-message-utils.js +77 -0
  34. package/dist/agent-url-utils.d.ts +30 -0
  35. package/dist/agent-url-utils.js +138 -0
  36. package/dist/agent.d.ts +226 -0
  37. package/dist/agent.js +6666 -0
  38. package/dist/ak-tree.d.ts +39 -0
  39. package/dist/ak-tree.js +368 -0
  40. package/dist/alt-text.d.ts +26 -0
  41. package/dist/alt-text.js +55 -0
  42. package/dist/auth-capture.d.ts +17 -0
  43. package/dist/auth-capture.js +164 -0
  44. package/dist/benchmark.d.ts +59 -0
  45. package/dist/benchmark.js +135 -0
  46. package/dist/billing-operation-logging.d.ts +38 -0
  47. package/dist/billing-operation-logging.js +248 -0
  48. package/dist/browser-bar.d.ts +48 -0
  49. package/dist/browser-bar.js +284 -0
  50. package/dist/browser-pool.d.ts +7 -0
  51. package/dist/browser-pool.js +15 -5
  52. package/dist/browser-utils.d.ts +31 -0
  53. package/dist/browser-utils.js +97 -0
  54. package/dist/browser.d.ts +76 -1
  55. package/dist/browser.js +1657 -39
  56. package/dist/capture-alt-text.d.ts +12 -0
  57. package/dist/capture-alt-text.js +52 -0
  58. package/dist/capture-encryption.d.ts +10 -0
  59. package/dist/capture-encryption.js +41 -0
  60. package/dist/capture-language-preflight.d.ts +41 -0
  61. package/dist/capture-language-preflight.js +300 -0
  62. package/dist/capture-llm-page-identity.d.ts +15 -0
  63. package/dist/capture-llm-page-identity.js +128 -0
  64. package/dist/capture-model-resolution.d.ts +9 -0
  65. package/dist/capture-model-resolution.js +21 -0
  66. package/dist/capture-page-identity.d.ts +7 -0
  67. package/dist/capture-page-identity.js +352 -0
  68. package/dist/capture-preset-credentials.d.ts +62 -0
  69. package/dist/capture-preset-credentials.js +184 -0
  70. package/dist/capture-request-plan.d.ts +58 -0
  71. package/dist/capture-request-plan.js +264 -0
  72. package/dist/capture-run-optimizer.d.ts +139 -0
  73. package/dist/capture-run-optimizer.js +863 -0
  74. package/dist/capture-selector-memory.d.ts +31 -0
  75. package/dist/capture-selector-memory.js +345 -0
  76. package/dist/capture-session-profile-encryption.d.ts +2 -0
  77. package/dist/capture-session-profile-encryption.js +22 -0
  78. package/dist/capture-step-timeout.d.ts +10 -0
  79. package/dist/capture-step-timeout.js +30 -0
  80. package/dist/capture-strategy.d.ts +36 -0
  81. package/dist/capture-strategy.js +95 -0
  82. package/dist/capture-studio-sync.d.ts +23 -0
  83. package/dist/capture-studio-sync.js +172 -0
  84. package/dist/capture-surface-contract.d.ts +36 -0
  85. package/dist/capture-surface-contract.js +299 -0
  86. package/dist/capture-transition-engine.d.ts +28 -0
  87. package/dist/capture-transition-engine.js +292 -0
  88. package/dist/capture-variant-state.d.ts +56 -0
  89. package/dist/capture-variant-state.js +182 -0
  90. package/dist/capture-verification.d.ts +35 -0
  91. package/dist/capture-verification.js +95 -0
  92. package/dist/capture-viewport-lock.d.ts +48 -0
  93. package/dist/capture-viewport-lock.js +74 -0
  94. package/dist/circuit-breaker.d.ts +42 -0
  95. package/dist/circuit-breaker.js +119 -0
  96. package/dist/cli-config.d.ts +8 -1
  97. package/dist/cli-config.js +62 -6
  98. package/dist/cli-contract.d.ts +15 -0
  99. package/dist/cli-contract.js +167 -0
  100. package/dist/cli-runner-local.d.ts +12 -0
  101. package/dist/cli-runner-local.js +102 -0
  102. package/dist/cli-runner.d.ts +34 -0
  103. package/dist/cli-runner.js +433 -0
  104. package/dist/cli-utils.d.ts +0 -1
  105. package/dist/cli-utils.js +2 -5
  106. package/dist/cli.js +1005 -252
  107. package/dist/clip-orchestrator.d.ts +148 -0
  108. package/dist/clip-orchestrator.js +957 -0
  109. package/dist/clip-postprocess.d.ts +42 -0
  110. package/dist/clip-postprocess.js +201 -0
  111. package/dist/cookie-dismiss.d.ts +2 -0
  112. package/dist/cookie-dismiss.js +48 -13
  113. package/dist/cost-logging.d.ts +35 -0
  114. package/dist/cost-logging.js +242 -0
  115. package/dist/cost-resolution-monitor.d.ts +16 -0
  116. package/dist/cost-resolution-monitor.js +34 -0
  117. package/dist/credential-templates.d.ts +5 -0
  118. package/dist/credential-templates.js +60 -0
  119. package/dist/cursor-overlay-script.d.ts +6 -0
  120. package/dist/cursor-overlay-script.js +169 -0
  121. package/dist/dom-css-purger.d.ts +65 -0
  122. package/dist/dom-css-purger.js +333 -0
  123. package/dist/dom-font-inliner.d.ts +45 -0
  124. package/dist/dom-font-inliner.js +148 -0
  125. package/dist/dom-patch-resolver.d.ts +52 -0
  126. package/dist/dom-patch-resolver.js +242 -0
  127. package/dist/dom-serializer.d.ts +82 -0
  128. package/dist/dom-serializer.js +378 -0
  129. package/dist/element-capture.d.ts +13 -0
  130. package/dist/element-capture.js +522 -0
  131. package/dist/env-validation.d.ts +5 -0
  132. package/dist/env-validation.js +29 -0
  133. package/dist/execution-schema.d.ts +4423 -0
  134. package/dist/execution-schema.js +507 -0
  135. package/dist/execution-types.d.ts +886 -0
  136. package/dist/execution-types.js +65 -0
  137. package/dist/fonts-loader.d.ts +14 -0
  138. package/dist/fonts-loader.js +55 -0
  139. package/dist/hybrid-navigator.d.ts +138 -0
  140. package/dist/hybrid-navigator.js +468 -0
  141. package/dist/index.d.ts +18 -0
  142. package/dist/index.js +17 -0
  143. package/dist/legacy/agent-action-recovery.d.ts +45 -0
  144. package/dist/legacy/agent-action-recovery.js +370 -0
  145. package/dist/legacy/agent-message-utils.d.ts +21 -0
  146. package/dist/legacy/agent-message-utils.js +77 -0
  147. package/dist/legacy/agent-url-utils.d.ts +30 -0
  148. package/dist/legacy/agent-url-utils.js +138 -0
  149. package/dist/legacy/agent.d.ts +226 -0
  150. package/dist/legacy/agent.js +6666 -0
  151. package/dist/legacy/clip-orchestrator.d.ts +148 -0
  152. package/dist/legacy/clip-orchestrator.js +957 -0
  153. package/dist/legacy/credential-templates.d.ts +5 -0
  154. package/dist/legacy/credential-templates.js +60 -0
  155. package/dist/legacy/hybrid-navigator.d.ts +138 -0
  156. package/dist/legacy/hybrid-navigator.js +468 -0
  157. package/dist/legacy/llm-usage.d.ts +17 -0
  158. package/dist/legacy/llm-usage.js +45 -0
  159. package/dist/legacy/prompt-cache.d.ts +10 -0
  160. package/dist/legacy/prompt-cache.js +24 -0
  161. package/dist/legacy/prompts.d.ts +175 -0
  162. package/dist/legacy/prompts.js +1038 -0
  163. package/dist/legacy/tools.d.ts +4 -0
  164. package/dist/legacy/tools.js +216 -0
  165. package/dist/legacy/video-agent.d.ts +143 -0
  166. package/dist/legacy/video-agent.js +4788 -0
  167. package/dist/legacy/video-observation.d.ts +36 -0
  168. package/dist/legacy/video-observation.js +192 -0
  169. package/dist/legacy/video-planner.d.ts +12 -0
  170. package/dist/legacy/video-planner.js +501 -0
  171. package/dist/legacy/video-prompts.d.ts +37 -0
  172. package/dist/legacy/video-prompts.js +569 -0
  173. package/dist/legacy/video-tools.d.ts +3 -0
  174. package/dist/legacy/video-tools.js +59 -0
  175. package/dist/legacy/video-variant-state.d.ts +29 -0
  176. package/dist/legacy/video-variant-state.js +80 -0
  177. package/dist/legacy/vision-model.d.ts +17 -0
  178. package/dist/legacy/vision-model.js +74 -0
  179. package/dist/llm-healer.d.ts +63 -0
  180. package/dist/llm-healer.js +166 -0
  181. package/dist/llm-provider.d.ts +29 -0
  182. package/dist/llm-provider.js +80 -0
  183. package/dist/llm-usage.d.ts +17 -0
  184. package/dist/llm-usage.js +45 -0
  185. package/dist/logger.d.ts +6 -2
  186. package/dist/logger.js +15 -1
  187. package/dist/mockup-html.d.ts +119 -0
  188. package/dist/mockup-html.js +263 -0
  189. package/dist/mockup.d.ts +187 -0
  190. package/dist/mockup.js +869 -0
  191. package/dist/mouse-animation.d.ts +46 -0
  192. package/dist/mouse-animation.js +114 -0
  193. package/dist/opcode-actions.d.ts +42 -0
  194. package/dist/opcode-actions.js +511 -0
  195. package/dist/opcode-runner.d.ts +51 -0
  196. package/dist/opcode-runner.js +770 -0
  197. package/dist/openrouter-client.d.ts +40 -0
  198. package/dist/openrouter-client.js +16 -0
  199. package/dist/overlay-engine.d.ts +24 -0
  200. package/dist/overlay-engine.js +176 -0
  201. package/dist/overlay-utils.d.ts +14 -0
  202. package/dist/overlay-utils.js +13 -0
  203. package/dist/postcondition.d.ts +16 -0
  204. package/dist/postcondition.js +269 -0
  205. package/dist/posthog.d.ts +4 -0
  206. package/dist/posthog.js +26 -0
  207. package/dist/program-patcher.d.ts +25 -0
  208. package/dist/program-patcher.js +44 -0
  209. package/dist/prompt-cache.d.ts +10 -0
  210. package/dist/prompt-cache.js +24 -0
  211. package/dist/prompts.d.ts +175 -0
  212. package/dist/prompts.js +1038 -0
  213. package/dist/provider-config.d.ts +12 -0
  214. package/dist/provider-config.js +15 -0
  215. package/dist/recovery-chain.d.ts +37 -0
  216. package/dist/recovery-chain.js +350 -0
  217. package/dist/remote-browser.d.ts +215 -0
  218. package/dist/remote-browser.js +360 -0
  219. package/dist/safari-browser-bar.d.ts +15 -0
  220. package/dist/safari-browser-bar.js +95 -0
  221. package/dist/safari-toolbar-asset.d.ts +15 -0
  222. package/dist/safari-toolbar-asset.js +12 -0
  223. package/dist/security.d.ts +21 -0
  224. package/dist/security.js +608 -0
  225. package/dist/selector-resolver.d.ts +34 -0
  226. package/dist/selector-resolver.js +181 -0
  227. package/dist/semantic-resolver.d.ts +35 -0
  228. package/dist/semantic-resolver.js +161 -0
  229. package/dist/server-capture-runtime.d.ts +125 -0
  230. package/dist/server-capture-runtime.js +585 -0
  231. package/dist/server-credit-usage.d.ts +12 -0
  232. package/dist/server-credit-usage.js +41 -0
  233. package/dist/server-posthog.d.ts +2 -0
  234. package/dist/server-posthog.js +16 -0
  235. package/dist/server-project-webhooks.d.ts +59 -0
  236. package/dist/server-project-webhooks.js +123 -0
  237. package/dist/server-screenshot-watermark.d.ts +7 -0
  238. package/dist/server-screenshot-watermark.js +60 -0
  239. package/dist/session-profile.d.ts +86 -0
  240. package/dist/session-profile.js +1536 -0
  241. package/dist/sf-pro-fonts.d.ts +4 -0
  242. package/dist/sf-pro-fonts.js +7 -0
  243. package/dist/sf-pro-symbols.d.ts +1 -0
  244. package/dist/sf-pro-symbols.js +55 -0
  245. package/dist/skill-packaging.d.ts +28 -0
  246. package/dist/skill-packaging.js +169 -0
  247. package/dist/smart-wait.d.ts +27 -0
  248. package/dist/smart-wait.js +81 -0
  249. package/dist/status-bar-l10n.d.ts +14 -0
  250. package/dist/status-bar-l10n.js +177 -0
  251. package/dist/status-bar-render.d.ts +20 -0
  252. package/dist/status-bar-render.js +410 -0
  253. package/dist/status-bar.d.ts +53 -0
  254. package/dist/status-bar.js +620 -0
  255. package/dist/svg-browser-bar.d.ts +33 -0
  256. package/dist/svg-browser-bar.js +206 -0
  257. package/dist/svg-status-bar.d.ts +36 -0
  258. package/dist/svg-status-bar.js +597 -0
  259. package/dist/svg-text.d.ts +61 -0
  260. package/dist/svg-text.js +118 -0
  261. package/dist/tools.d.ts +4 -0
  262. package/dist/tools.js +216 -0
  263. package/dist/types.d.ts +240 -5
  264. package/dist/types.js +23 -1
  265. package/dist/v2/action-verifier.d.ts +29 -0
  266. package/dist/v2/action-verifier.js +133 -0
  267. package/dist/v2/alt-text.d.ts +26 -0
  268. package/dist/v2/alt-text.js +55 -0
  269. package/dist/v2/benchmark.d.ts +59 -0
  270. package/dist/v2/benchmark.js +135 -0
  271. package/dist/v2/capture-strategy.d.ts +30 -0
  272. package/dist/v2/capture-strategy.js +67 -0
  273. package/dist/v2/capture-verification.d.ts +35 -0
  274. package/dist/v2/capture-verification.js +95 -0
  275. package/dist/v2/circuit-breaker.d.ts +42 -0
  276. package/dist/v2/circuit-breaker.js +119 -0
  277. package/dist/v2/cli-runner-local.d.ts +11 -0
  278. package/dist/v2/cli-runner-local.js +91 -0
  279. package/dist/v2/cli-runner.d.ts +34 -0
  280. package/dist/v2/cli-runner.js +300 -0
  281. package/dist/v2/compiler-prompts.d.ts +27 -0
  282. package/dist/v2/compiler-prompts.js +123 -0
  283. package/dist/v2/compiler.d.ts +37 -0
  284. package/dist/v2/compiler.js +147 -0
  285. package/dist/v2/explorer.d.ts +41 -0
  286. package/dist/v2/explorer.js +56 -0
  287. package/dist/v2/index.d.ts +37 -0
  288. package/dist/v2/index.js +31 -0
  289. package/dist/v2/llm-healer.d.ts +62 -0
  290. package/dist/v2/llm-healer.js +166 -0
  291. package/dist/v2/llm-provider.d.ts +29 -0
  292. package/dist/v2/llm-provider.js +80 -0
  293. package/dist/v2/opcode-runner.d.ts +47 -0
  294. package/dist/v2/opcode-runner.js +634 -0
  295. package/dist/v2/overlay-engine.d.ts +24 -0
  296. package/dist/v2/overlay-engine.js +150 -0
  297. package/dist/v2/postcondition.d.ts +16 -0
  298. package/dist/v2/postcondition.js +249 -0
  299. package/dist/v2/program-patcher.d.ts +25 -0
  300. package/dist/v2/program-patcher.js +44 -0
  301. package/dist/v2/recovery-chain.d.ts +30 -0
  302. package/dist/v2/recovery-chain.js +368 -0
  303. package/dist/v2/schema.d.ts +2580 -0
  304. package/dist/v2/schema.js +295 -0
  305. package/dist/v2/selector-resolver.d.ts +34 -0
  306. package/dist/v2/selector-resolver.js +181 -0
  307. package/dist/v2/semantic-resolver.d.ts +35 -0
  308. package/dist/v2/semantic-resolver.js +161 -0
  309. package/dist/v2/smart-wait.d.ts +27 -0
  310. package/dist/v2/smart-wait.js +81 -0
  311. package/dist/v2/types.d.ts +444 -0
  312. package/dist/v2/types.js +19 -0
  313. package/dist/v2/web-playwright-local.d.ts +69 -0
  314. package/dist/v2/web-playwright-local.js +392 -0
  315. package/dist/version.d.ts +1 -0
  316. package/dist/version.js +5 -0
  317. package/dist/video-agent.d.ts +143 -0
  318. package/dist/video-agent.js +4788 -0
  319. package/dist/video-observation.d.ts +36 -0
  320. package/dist/video-observation.js +192 -0
  321. package/dist/video-planner.d.ts +12 -0
  322. package/dist/video-planner.js +501 -0
  323. package/dist/video-prompts.d.ts +37 -0
  324. package/dist/video-prompts.js +554 -0
  325. package/dist/video-tools.d.ts +3 -0
  326. package/dist/video-tools.js +59 -0
  327. package/dist/video-variant-state.d.ts +29 -0
  328. package/dist/video-variant-state.js +80 -0
  329. package/dist/vision-model.d.ts +17 -0
  330. package/dist/vision-model.js +74 -0
  331. package/dist/web-playwright-local.d.ts +126 -0
  332. package/dist/web-playwright-local.js +819 -0
  333. package/dist/ws-auth.d.ts +20 -0
  334. package/dist/ws-auth.js +70 -0
  335. package/dist/ws-broadcast.d.ts +34 -0
  336. package/dist/ws-broadcast.js +85 -0
  337. package/dist/ws-connection-limits.d.ts +12 -0
  338. package/dist/ws-connection-limits.js +44 -0
  339. package/dist/ws-handler-utils.d.ts +32 -0
  340. package/dist/ws-handler-utils.js +139 -0
  341. package/dist/ws-handler.d.ts +10 -0
  342. package/dist/ws-handler.js +1793 -0
  343. package/dist/ws-metrics-server.d.ts +9 -0
  344. package/dist/ws-metrics-server.js +31 -0
  345. package/dist/ws-server.d.ts +9 -0
  346. package/dist/ws-server.js +92 -0
  347. package/package.json +142 -71
package/dist/cli.js CHANGED
@@ -3,32 +3,129 @@ import { Command } from 'commander';
3
3
  import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
5
  import fs from 'node:fs/promises';
6
- import WebSocket from 'ws';
7
6
  const require = createRequire(import.meta.url);
8
7
  const { version } = require('../package.json');
9
- import { Browser } from './browser.js';
10
8
  import { logger } from './logger.js';
11
- import { writeConfig, requireConfig, DEFAULT_API_BASE_URL, DEFAULT_WS_URL, } from './cli-config.js';
12
- import { replaceSkillPlaceholders } from './cli-utils.js';
9
+ import { writeConfig, deleteConfig, requireConfig, getConfigPath, DEFAULT_API_BASE_URL, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
10
+ import { renderSkillSingleFile, writeSkillExport } from './skill-packaging.js';
13
11
  // ── Program definition ──────────────────────────────────────────────
14
12
  export const program = new Command();
15
13
  program
16
14
  .name('autokap')
17
15
  .version(version)
18
16
  .description('AI-powered screenshot capture — local Playwright proxy');
17
+ function fatal(message) {
18
+ logger.error(message);
19
+ process.exit(1);
20
+ }
21
+ function authHeaders(config, extra = {}) {
22
+ return {
23
+ Authorization: `Bearer ${config.apiKey}`,
24
+ ...extra,
25
+ };
26
+ }
27
+ function buildApiUrl(config, pathname, searchParams) {
28
+ const suffix = searchParams && Array.from(searchParams.keys()).length > 0
29
+ ? `?${searchParams.toString()}`
30
+ : '';
31
+ return `${config.apiBaseUrl}${pathname}${suffix}`;
32
+ }
33
+ async function readApiError(response) {
34
+ const body = await response.json().catch(() => ({ error: response.statusText }));
35
+ return body.error || response.statusText;
36
+ }
37
+ async function requestJson(config, pathname, init, errorPrefix, searchParams) {
38
+ try {
39
+ const response = await fetch(buildApiUrl(config, pathname, searchParams), init);
40
+ if (!response.ok) {
41
+ fatal(`${errorPrefix}: ${await readApiError(response)}`);
42
+ }
43
+ return await response.json();
44
+ }
45
+ catch (error) {
46
+ fatal(`${errorPrefix}: ${error.message}`);
47
+ }
48
+ }
49
+ async function requestText(config, pathname, init, errorPrefix, searchParams) {
50
+ try {
51
+ const response = await fetch(buildApiUrl(config, pathname, searchParams), init);
52
+ if (!response.ok) {
53
+ fatal(`${errorPrefix}: ${await readApiError(response)}`);
54
+ }
55
+ return await response.text();
56
+ }
57
+ catch (error) {
58
+ fatal(`${errorPrefix}: ${error.message}`);
59
+ }
60
+ }
61
+ function ensureListFormat(format) {
62
+ if (format !== 'json' && format !== 'table') {
63
+ fatal('Invalid --format. Use "json" or "table".');
64
+ }
65
+ return format;
66
+ }
67
+ function ensureExportFormat(format) {
68
+ if (format !== 'json' && format !== 'csv') {
69
+ fatal('Invalid --format. Use "json" or "csv".');
70
+ }
71
+ return format;
72
+ }
73
+ function printJson(value) {
74
+ console.log(JSON.stringify(value, null, 2));
75
+ }
76
+ function buildEndpointAssetUrl(config, endpointId) {
77
+ return `${config.apiBaseUrl}/api/v1/assets/${endpointId}`;
78
+ }
79
+ function toEndpointOutputRow(config, endpoint) {
80
+ return {
81
+ ...endpoint,
82
+ url: buildEndpointAssetUrl(config, endpoint.id),
83
+ };
84
+ }
85
+ function toCsv(rows, columns) {
86
+ const escape = (value) => {
87
+ const text = value === null || value === undefined ? '' : String(value);
88
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
89
+ };
90
+ const header = columns.join(',');
91
+ const body = rows.map((row) => columns.map((column) => escape(row[column])).join(','));
92
+ return [header, ...body].join('\n');
93
+ }
94
+ async function loadProject(config, projectId) {
95
+ const data = await requestJson(config, `/api/v1/projects/${projectId}`, { headers: authHeaders(config) }, 'Failed to get project');
96
+ return data.project;
97
+ }
98
+ async function listProjectAccounts(config, projectId) {
99
+ const data = await requestJson(config, `/api/cli/projects/${projectId}/credentials`, { headers: authHeaders(config) }, 'Failed to list accounts');
100
+ return data.accounts;
101
+ }
102
+ async function resolveProjectAccount(config, projectId, accountNameOrId) {
103
+ const accounts = await listProjectAccounts(config, projectId);
104
+ const needle = accountNameOrId.toLowerCase();
105
+ const account = accounts.find((candidate) => candidate.id === accountNameOrId ||
106
+ candidate.name.toLowerCase() === needle);
107
+ if (!account) {
108
+ const available = accounts.map((candidate) => candidate.name).join(', ');
109
+ fatal(available
110
+ ? `Account "${accountNameOrId}" not found. Available accounts: ${available}`
111
+ : `Account "${accountNameOrId}" not found. This project has no accounts.`);
112
+ }
113
+ return account;
114
+ }
19
115
  // ── login command ───────────────────────────────────────────────────
20
116
  program
21
117
  .command('login <key>')
22
- .description('Authenticate with the AutoKap API')
23
- .option('--api-base-url <url>', 'API base URL', DEFAULT_API_BASE_URL)
24
- .option('--ws-url <url>', 'WebSocket server URL', DEFAULT_WS_URL)
118
+ .description('Authenticate the AutoKap CLI with your CLI key')
119
+ .option('--api-base-url <url>', `API base URL (env: ${API_BASE_URL_ENV_VAR})`, getDefaultApiBaseUrl())
120
+ .option('--ws-url <url>', `WebSocket server URL (env: ${WS_URL_ENV_VAR})`)
25
121
  .action(async (key, opts) => {
122
+ const wsUrl = opts.wsUrl?.trim() || getDefaultWsUrl(opts.apiBaseUrl);
26
123
  try {
27
124
  const res = await fetch(`${opts.apiBaseUrl}/api/cli/validate`, {
28
125
  headers: { Authorization: `Bearer ${key}` },
29
126
  });
30
127
  if (!res.ok) {
31
- logger.error('Invalid API key. Generate one in the AutoKap dashboard.');
128
+ logger.error('Invalid CLI key. Generate one in the AutoKap dashboard.');
32
129
  process.exit(1);
33
130
  }
34
131
  }
@@ -39,291 +136,947 @@ program
39
136
  await writeConfig({
40
137
  apiKey: key,
41
138
  apiBaseUrl: opts.apiBaseUrl,
42
- wsUrl: opts.wsUrl,
139
+ wsUrl,
43
140
  });
44
- logger.success('Authenticated. Key stored in ~/.autokap/config.json');
141
+ logger.success(`Authenticated. Key stored in ${getConfigPath()}`);
45
142
  process.exit(0);
46
143
  });
47
- // ── run command ─────────────────────────────────────────────────────
144
+ // ── ping command ───────────────────────────────────────────────────
145
+ program
146
+ .command('ping')
147
+ .description('Verify CLI connection with the AutoKap server')
148
+ .action(async () => {
149
+ const config = await requireConfig();
150
+ try {
151
+ const res = await fetch(`${config.apiBaseUrl}/api/cli/validate`, {
152
+ headers: { Authorization: `Bearer ${config.apiKey}` },
153
+ });
154
+ if (!res.ok) {
155
+ logger.error('CLI key is no longer valid. Run `npx autokap@latest init --cli-key <key>` to re-authenticate.');
156
+ process.exit(1);
157
+ }
158
+ }
159
+ catch (err) {
160
+ logger.error(`Cannot reach API: ${err.message}`);
161
+ process.exit(1);
162
+ }
163
+ logger.success('Connection verified. Stored CLI key is active.');
164
+ process.exit(0);
165
+ });
166
+ // ── logout command ─────────────────────────────────────────────────
167
+ program
168
+ .command('logout')
169
+ .description('Remove stored credentials')
170
+ .action(async () => {
171
+ await deleteConfig();
172
+ logger.success(`Logged out. Credentials removed from ${getConfigPath()}`);
173
+ process.exit(0);
174
+ });
175
+ // ── whoami command ─────────────────────────────────────────────────
176
+ program
177
+ .command('whoami')
178
+ .description('Show the account linked to the current CLI key')
179
+ .action(async () => {
180
+ const config = await requireConfig();
181
+ try {
182
+ const res = await fetch(`${config.apiBaseUrl}/api/cli/validate`, {
183
+ headers: { Authorization: `Bearer ${config.apiKey}` },
184
+ });
185
+ if (!res.ok) {
186
+ logger.error('CLI key is no longer valid. Run `npx autokap@latest init --cli-key <key>` to re-authenticate.');
187
+ process.exit(1);
188
+ }
189
+ const data = await res.json();
190
+ const display = data.name
191
+ ? `${data.name} (${data.email})`
192
+ : data.email ?? 'Unknown';
193
+ logger.info(`Logged in as: ${display}`);
194
+ logger.info(`Server: ${config.apiBaseUrl}`);
195
+ }
196
+ catch (err) {
197
+ logger.error(`Cannot reach API: ${err.message}`);
198
+ process.exit(1);
199
+ }
200
+ process.exit(0);
201
+ });
202
+ // ── run command (deterministic capture engine) ───────────────────────
48
203
  program
49
204
  .command('run <preset-id>')
50
- .description('Run a preset capture using local Playwright')
205
+ .description('Run a capture using the deterministic opcode engine (local Playwright)')
51
206
  .option('--headed', 'Show browser window for debugging', false)
207
+ .option('--local', `Use the local AutoKap dev server (${LOCAL_API_BASE_URL})`, false)
208
+ .option('--output <dir>', 'Optional output directory for local artifact copies')
209
+ .option('--program <file>', 'Path to a program JSON file')
210
+ .option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
52
211
  .action(async (presetId, opts) => {
212
+ if (opts.debug) {
213
+ const { setDebugEnabled } = await import('./logger.js');
214
+ setDebugEnabled(true);
215
+ logger.info('[capture] Debug mode enabled — verbose logging on');
216
+ }
217
+ if (opts.local) {
218
+ process.env[API_BASE_URL_ENV_VAR] = LOCAL_API_BASE_URL;
219
+ process.env[WS_URL_ENV_VAR] = LOCAL_WS_URL;
220
+ logger.info(`Using local AutoKap dev server: ${LOCAL_API_BASE_URL}`);
221
+ }
222
+ const { runLocal } = await import('./cli-runner-local.js');
223
+ await runLocal(presetId, opts);
224
+ });
225
+ // ── project commands ───────────────────────────────────────────────
226
+ const projectCmd = program
227
+ .command('project')
228
+ .description('Manage projects');
229
+ projectCmd
230
+ .command('list')
231
+ .description('List accessible projects')
232
+ .action(async () => {
53
233
  const config = await requireConfig();
54
- logger.info(`Connecting to ${config.wsUrl}...`);
55
- const browser = new Browser({
56
- headed: opts.headed,
57
- viewport: { width: 1440, height: 900 },
234
+ const data = await requestJson(config, '/api/v1/projects', { headers: authHeaders(config) }, 'Failed to list projects');
235
+ printJson(data.projects);
236
+ process.exit(0);
237
+ });
238
+ projectCmd
239
+ .command('get <project-id>')
240
+ .description('Get project details')
241
+ .action(async (projectId) => {
242
+ const config = await requireConfig();
243
+ const project = await loadProject(config, projectId);
244
+ printJson(project);
245
+ process.exit(0);
246
+ });
247
+ projectCmd
248
+ .command('create')
249
+ .description('Create a project')
250
+ .requiredOption('--name <name>', 'Project name')
251
+ .requiredOption('--url <url>', 'Project URL')
252
+ .option('--description <text>', 'Project description')
253
+ .action(async (opts) => {
254
+ const config = await requireConfig();
255
+ const data = await requestJson(config, '/api/v1/projects', {
256
+ method: 'POST',
257
+ headers: authHeaders(config, { 'Content-Type': 'application/json' }),
258
+ body: JSON.stringify({
259
+ name: opts.name,
260
+ url: opts.url,
261
+ description: opts.description,
262
+ }),
263
+ }, 'Failed to create project');
264
+ console.log(data.project.id);
265
+ process.exit(0);
266
+ });
267
+ // ── capture commands ───────────────────────────────────────────────
268
+ const captureCmd = program
269
+ .command('capture')
270
+ .description('Query captured outputs');
271
+ captureCmd
272
+ .command('list')
273
+ .description('List captures')
274
+ .option('--project <id>', 'Filter by project ID')
275
+ .option('--preset <id>', 'Filter by preset ID')
276
+ .option('--limit <n>', 'Limit results', '50')
277
+ .option('--offset <n>', 'Offset results', '0')
278
+ .action(async (opts) => {
279
+ const config = await requireConfig();
280
+ const searchParams = new URLSearchParams();
281
+ if (opts.project)
282
+ searchParams.set('project_id', opts.project);
283
+ if (opts.preset)
284
+ searchParams.set('preset_id', opts.preset);
285
+ searchParams.set('limit', String(Math.max(1, Number.parseInt(opts.limit, 10) || 50)));
286
+ searchParams.set('offset', String(Math.max(0, Number.parseInt(opts.offset, 10) || 0)));
287
+ const data = await requestJson(config, '/api/v1/captures', { headers: authHeaders(config) }, 'Failed to list captures', searchParams);
288
+ printJson(data);
289
+ process.exit(0);
290
+ });
291
+ program
292
+ .command('usage')
293
+ .description('Show usage for the current billing period')
294
+ .action(async () => {
295
+ const config = await requireConfig();
296
+ const data = await requestJson(config, '/api/v1/usage', { headers: authHeaders(config) }, 'Failed to get usage');
297
+ printJson(data);
298
+ process.exit(0);
299
+ });
300
+ // ── preset commands ────────────────────────────────────────────────
301
+ async function readJsonInput(filePath) {
302
+ if (filePath === '-') {
303
+ const chunks = [];
304
+ for await (const chunk of process.stdin)
305
+ chunks.push(chunk);
306
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
307
+ }
308
+ const raw = await fs.readFile(filePath, 'utf8');
309
+ return JSON.parse(raw);
310
+ }
311
+ const presetCmd = program
312
+ .command('preset')
313
+ .description('Manage capture presets');
314
+ presetCmd
315
+ .command('create')
316
+ .description('Create a new preset from a JSON config file')
317
+ .requiredOption('--project <id>', 'Project ID')
318
+ .requiredOption('--name <name>', 'Preset name')
319
+ .option('--description <text>', 'Preset description', '')
320
+ .requiredOption('--config <file>', 'Path to config JSON file (use "-" for stdin)')
321
+ .action(async (opts) => {
322
+ const cfg = await requireConfig();
323
+ let configJson;
324
+ try {
325
+ configJson = await readJsonInput(opts.config);
326
+ }
327
+ catch (err) {
328
+ logger.error(`Failed to read config: ${err.message}`);
329
+ process.exit(1);
330
+ }
331
+ const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets`, {
332
+ method: 'POST',
333
+ headers: {
334
+ Authorization: `Bearer ${cfg.apiKey}`,
335
+ 'Content-Type': 'application/json',
336
+ },
337
+ body: JSON.stringify({
338
+ project_id: opts.project,
339
+ name: opts.name,
340
+ description: opts.description,
341
+ config: configJson,
342
+ }),
58
343
  });
59
- await browser.launch();
60
- logger.success('Browser launched');
61
- const wsUrl = `${config.wsUrl}?key=${encodeURIComponent(config.apiKey)}&preset_id=${encodeURIComponent(presetId)}`;
62
- const ws = new WebSocket(wsUrl);
63
- const cleanup = async () => {
64
- try {
65
- await browser.close();
66
- }
67
- catch { /* ignore */ }
68
- try {
69
- ws.close();
70
- }
71
- catch { /* ignore */ }
72
- };
73
- ws.on('error', async (err) => {
74
- logger.error(`WebSocket error: ${err.message}`);
75
- await cleanup();
344
+ if (!res.ok) {
345
+ const body = await res.json().catch(() => ({ error: res.statusText }));
346
+ logger.error(`Failed to create preset: ${body.error || res.statusText}`);
76
347
  process.exit(1);
348
+ }
349
+ const data = await res.json();
350
+ // Output only the preset ID so callers can chain: autokap run $(autokap preset create ...)
351
+ console.log(data.preset.id);
352
+ process.exit(0);
353
+ });
354
+ presetCmd
355
+ .command('update <preset-id>')
356
+ .description('Update an existing preset config')
357
+ .option('--name <name>', 'New preset name')
358
+ .option('--description <text>', 'New description')
359
+ .requiredOption('--config <file>', 'Path to config JSON file (use "-" for stdin)')
360
+ .action(async (presetId, opts) => {
361
+ const cfg = await requireConfig();
362
+ let configJson;
363
+ try {
364
+ configJson = await readJsonInput(opts.config);
365
+ }
366
+ catch (err) {
367
+ logger.error(`Failed to read config: ${err.message}`);
368
+ process.exit(1);
369
+ }
370
+ const payload = { config: configJson };
371
+ if (opts.name !== undefined)
372
+ payload.name = opts.name;
373
+ if (opts.description !== undefined)
374
+ payload.description = opts.description;
375
+ const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
376
+ method: 'PATCH',
377
+ headers: {
378
+ Authorization: `Bearer ${cfg.apiKey}`,
379
+ 'Content-Type': 'application/json',
380
+ },
381
+ body: JSON.stringify(payload),
77
382
  });
78
- ws.on('close', async () => {
79
- await cleanup();
383
+ if (!res.ok) {
384
+ const body = await res.json().catch(() => ({ error: res.statusText }));
385
+ logger.error(`Failed to update preset: ${body.error || res.statusText}`);
386
+ process.exit(1);
387
+ }
388
+ const data = await res.json();
389
+ console.log(data.preset.id);
390
+ process.exit(0);
391
+ });
392
+ presetCmd
393
+ .command('delete <preset-id>')
394
+ .description('Delete a preset (soft-delete)')
395
+ .action(async (presetId) => {
396
+ const cfg = await requireConfig();
397
+ const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
398
+ method: 'DELETE',
399
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
80
400
  });
81
- ws.on('open', () => {
82
- logger.success('Connected to AutoKap server');
401
+ if (!res.ok) {
402
+ const body = await res.json().catch(() => ({ error: res.statusText }));
403
+ logger.error(`Failed to delete preset: ${body.error || res.statusText}`);
404
+ process.exit(1);
405
+ }
406
+ logger.success(`Preset ${presetId} deleted`);
407
+ process.exit(0);
408
+ });
409
+ presetCmd
410
+ .command('get <preset-id>')
411
+ .description('Get preset details')
412
+ .action(async (presetId) => {
413
+ const cfg = await requireConfig();
414
+ const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
415
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
83
416
  });
84
- ws.on('message', async (data) => {
85
- const raw = typeof data === 'string' ? data : data.toString('utf-8');
86
- let msg;
87
- try {
88
- msg = JSON.parse(raw);
417
+ if (!res.ok) {
418
+ const body = await res.json().catch(() => ({ error: res.statusText }));
419
+ logger.error(`Failed to get preset: ${body.error || res.statusText}`);
420
+ process.exit(1);
421
+ }
422
+ const data = await res.json();
423
+ console.log(JSON.stringify(data.preset, null, 2));
424
+ process.exit(0);
425
+ });
426
+ presetCmd
427
+ .command('list')
428
+ .description('List presets for a project')
429
+ .requiredOption('--project <id>', 'Project ID')
430
+ .action(async (opts) => {
431
+ const cfg = await requireConfig();
432
+ const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets?project_id=${encodeURIComponent(opts.project)}`, { headers: { Authorization: `Bearer ${cfg.apiKey}` } });
433
+ if (!res.ok) {
434
+ const body = await res.json().catch(() => ({ error: res.statusText }));
435
+ logger.error(`Failed to list presets: ${body.error || res.statusText}`);
436
+ process.exit(1);
437
+ }
438
+ const data = await res.json();
439
+ const rows = data.presets.map((p) => ({
440
+ id: p.id,
441
+ name: p.name,
442
+ description: p.description ?? '',
443
+ }));
444
+ console.log(JSON.stringify(rows, null, 2));
445
+ process.exit(0);
446
+ });
447
+ presetCmd
448
+ .command('info <preset-id>')
449
+ .description('Get structured integration info for a preset (endpoints, variants, URL params)')
450
+ .action(async (presetId) => {
451
+ const cfg = await requireConfig();
452
+ // Fetch preset + endpoints in parallel
453
+ const [presetRes, endpointsRes] = await Promise.all([
454
+ fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
455
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
456
+ }),
457
+ fetch(`${cfg.apiBaseUrl}/api/v1/endpoints?preset_id=${encodeURIComponent(presetId)}`, {
458
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
459
+ }),
460
+ ]);
461
+ if (!presetRes.ok) {
462
+ const body = await presetRes.json().catch(() => ({ error: presetRes.statusText }));
463
+ logger.error(`Failed to get preset: ${body.error || presetRes.statusText}`);
464
+ process.exit(1);
465
+ }
466
+ if (!endpointsRes.ok) {
467
+ const body = await endpointsRes.json().catch(() => ({ error: endpointsRes.statusText }));
468
+ logger.error(`Failed to list endpoints: ${body.error || endpointsRes.statusText}`);
469
+ process.exit(1);
470
+ }
471
+ const { preset } = await presetRes.json();
472
+ const { endpoints } = await endpointsRes.json();
473
+ // Extract variants from config
474
+ const config = preset.config ?? {};
475
+ const langs = Array.isArray(config.langs) ? config.langs : [];
476
+ const themes = Array.isArray(config.themes) ? config.themes : [];
477
+ const targets = Array.isArray(config.targets)
478
+ ? config.targets
479
+ .map((t) => ({ id: t.id, label: t.label, viewport: t.viewport, deviceFrame: t.deviceFrame ?? null }))
480
+ : [];
481
+ const captureMode = config.captureMode ?? 'screenshot';
482
+ // Build per-endpoint info with type-specific URL params
483
+ const endpointEntries = endpoints.map((ep) => {
484
+ const assetType = ep.asset_type ?? 'screenshot';
485
+ const url = assetType === 'interactive_demo'
486
+ ? `${cfg.apiBaseUrl}/demo/${presetId}`
487
+ : `${cfg.apiBaseUrl}/api/v1/assets/${ep.id}`;
488
+ let urlParams;
489
+ if (assetType === 'interactive_demo') {
490
+ urlParams = {
491
+ embed: 'Set to "1" for iframe embedding',
492
+ lang: 'Language variant',
493
+ theme: 'Color theme ("light" or "dark")',
494
+ target: 'Device target ID',
495
+ };
89
496
  }
90
- catch {
91
- return;
497
+ else if (assetType === 'screenshot') {
498
+ urlParams = {
499
+ lang: 'Language variant',
500
+ theme: 'Color theme ("light" or "dark")',
501
+ target: 'Device target ID',
502
+ w: 'Max width in px (1-2560)',
503
+ quality: 'Image quality 1-100',
504
+ format: 'Output format: "webp", "png", "jpg"',
505
+ scale: 'Resolution multiplier (0.5-4)',
506
+ render: 'Set to "studio" for Studio render',
507
+ slot: 'Studio composition slot',
508
+ };
92
509
  }
93
- // Handle server events (progress, done, error)
94
- if ('type' in msg) {
95
- const event = msg;
96
- switch (event.type) {
97
- case 'progress':
98
- logger.info(event.message ?? '');
99
- break;
100
- case 'done':
101
- if (event.summary) {
102
- logger.success(`Done: ${event.summary.successes}/${event.summary.total} captures succeeded`);
103
- }
104
- await cleanup();
105
- process.exit(0);
106
- break;
107
- case 'error':
108
- logger.error(event.message ?? 'Unknown error');
109
- await cleanup();
110
- process.exit(1);
111
- break;
112
- }
113
- return;
510
+ else if (assetType === 'clip') {
511
+ urlParams = {
512
+ lang: 'Language variant',
513
+ theme: 'Color theme ("light" or "dark")',
514
+ target: 'Device target ID',
515
+ format: 'Output format: "gif" (default), "mp4"',
516
+ render: 'Set to "studio" for Studio render',
517
+ slot: 'Studio composition slot',
518
+ };
114
519
  }
115
- // Handle browser commands from server
116
- const cmd = msg;
117
- try {
118
- const result = await executeBrowserCommand(browser, cmd.method, cmd.params);
119
- ws.send(JSON.stringify({ id: cmd.id, result }));
520
+ else if (assetType === 'composition') {
521
+ urlParams = {
522
+ scale: 'Resolution multiplier (0.5-4)',
523
+ format: 'Output format: "png", "webp", "jpg"',
524
+ };
120
525
  }
121
- catch (err) {
122
- ws.send(JSON.stringify({ id: cmd.id, error: err.message }));
526
+ else {
527
+ urlParams = {};
123
528
  }
529
+ return {
530
+ id: ep.id,
531
+ label: ep.label ?? null,
532
+ capture_name: ep.capture_name ?? null,
533
+ asset_type: assetType,
534
+ url,
535
+ url_params: urlParams,
536
+ };
124
537
  });
125
- // Handle SIGINT gracefully
126
- process.on('SIGINT', async () => {
127
- logger.info('Interrupted. Cleaning up...');
128
- await cleanup();
129
- process.exit(130);
538
+ const info = {
539
+ preset: {
540
+ id: preset.id,
541
+ name: preset.name,
542
+ description: preset.description ?? null,
543
+ capture_mode: captureMode,
544
+ },
545
+ variants: { langs, themes, targets },
546
+ endpoints: endpointEntries,
547
+ };
548
+ if (captureMode === 'interactive_demo') {
549
+ info.interactive_demo = {
550
+ url: `${cfg.apiBaseUrl}/demo/${presetId}`,
551
+ embed_url: `${cfg.apiBaseUrl}/demo/${presetId}?embed=1`,
552
+ };
553
+ }
554
+ console.log(JSON.stringify(info, null, 2));
555
+ process.exit(0);
556
+ });
557
+ // ── auth commands ──────────────────────────────────────────────────
558
+ const authCmd = program
559
+ .command('auth')
560
+ .description('Manage authentication sessions for project credentials accounts');
561
+ authCmd
562
+ .command('capture <project-id>')
563
+ .description('Open a browser, let you log in, and save the resulting session to a project credentials account')
564
+ .option('--account <name-or-id>', 'Account name or ID to attach the session to. If omitted, lists accounts to choose from.')
565
+ .option('--url <url>', 'URL to open in the browser. Defaults to the project URL.')
566
+ .action(async (projectId, opts) => {
567
+ const cfg = await requireConfig();
568
+ const allAccounts = await listProjectAccounts(cfg, projectId);
569
+ const accounts = allAccounts.filter((a) => a.type === 'session');
570
+ if (!accounts.length) {
571
+ if (allAccounts.length > 0) {
572
+ logger.error(`No session-type accounts in project ${projectId}. This project has ${allAccounts.length} credentials account(s), but none are session accounts. Create a new account with type "session" in Settings → Login accounts.`);
573
+ }
574
+ else {
575
+ logger.error(`No accounts found for project ${projectId}. Create a session account in Settings → Login accounts first.`);
576
+ }
577
+ process.exit(1);
578
+ }
579
+ let account;
580
+ if (opts.account) {
581
+ const needle = opts.account.toLowerCase();
582
+ account = accounts.find((a) => a.id === opts.account || a.name.toLowerCase() === needle);
583
+ if (!account) {
584
+ // If the account exists but is a credentials account, give a precise error.
585
+ const otherTypeMatch = allAccounts.find((a) => a.id === opts.account || a.name.toLowerCase() === needle);
586
+ if (otherTypeMatch) {
587
+ logger.error(`Account "${opts.account}" exists but is a credentials account (email/password). auth capture only works with session accounts.`);
588
+ }
589
+ else {
590
+ logger.error(`Account "${opts.account}" not found in project ${projectId}.`);
591
+ logger.info(`Available session accounts: ${accounts.map((a) => a.name).join(', ')}`);
592
+ }
593
+ process.exit(1);
594
+ }
595
+ }
596
+ else if (accounts.length === 1) {
597
+ account = accounts[0];
598
+ logger.info(`Using the only session account in this project: "${account.name}"`);
599
+ }
600
+ else {
601
+ logger.info('Multiple session accounts found. Re-run with --account <name>:');
602
+ for (const a of accounts) {
603
+ const status = a.storage_state_updated_at
604
+ ? `session updated ${a.storage_state_updated_at}`
605
+ : 'no session yet';
606
+ logger.info(` • ${a.name} (${status})`);
607
+ }
608
+ process.exit(0);
609
+ }
610
+ const startUrl = opts.url || (await loadProject(cfg, projectId)).url;
611
+ logger.info(`Opening ${startUrl}`);
612
+ const { captureAuthSession } = await import('./auth-capture.js');
613
+ await captureAuthSession({
614
+ apiBaseUrl: cfg.apiBaseUrl,
615
+ apiKey: cfg.apiKey,
616
+ projectId,
617
+ accountId: account.id,
618
+ startUrl,
130
619
  });
131
620
  });
132
- // ── skill command ───────────────────────────────────────────────────
133
- program
134
- .command('skill')
135
- .description('Output or install the AutoKap preset creation skill for AI coding agents')
136
- .option('--output <path>', 'Write skill file to this path instead of stdout')
137
- .option('--project-url <url>', 'Replace the project URL placeholder in the skill')
138
- .option('--project-id <id>', 'Replace the project ID placeholder in the skill')
139
- .option('--api-key <key>', 'Replace the API key placeholder in the skill')
140
- .option('--api-base-url <url>', 'Replace the API base URL placeholder (default: https://app.autokap.com)')
621
+ const authAccountCmd = authCmd
622
+ .command('account')
623
+ .description('Manage project credential accounts');
624
+ authAccountCmd
625
+ .command('list <project-id>')
626
+ .description('List accounts for a project')
627
+ .action(async (projectId) => {
628
+ const config = await requireConfig();
629
+ const accounts = await listProjectAccounts(config, projectId);
630
+ printJson(accounts);
631
+ process.exit(0);
632
+ });
633
+ authAccountCmd
634
+ .command('create <project-id>')
635
+ .description('Create a project credential account')
636
+ .requiredOption('--name <name>', 'Account name')
637
+ .option('--type <type>', 'Account type: credentials or session', 'credentials')
638
+ .option('--email <email>', 'Email for credentials accounts')
639
+ .option('--password <password>', 'Password for credentials accounts')
640
+ .option('--login-url <url>', 'Login URL for credentials accounts')
641
+ .action(async (projectId, opts) => {
642
+ const type = opts.type === 'session' ? 'session' : opts.type === 'credentials' ? 'credentials' : null;
643
+ if (!type) {
644
+ fatal('Invalid --type. Use "credentials" or "session".');
645
+ }
646
+ if (type === 'session' && (opts.email || opts.password || opts.loginUrl)) {
647
+ fatal('Session accounts do not accept --email, --password, or --login-url.');
648
+ }
649
+ const config = await requireConfig();
650
+ const data = await requestJson(config, `/api/cli/projects/${projectId}/credentials`, {
651
+ method: 'POST',
652
+ headers: authHeaders(config, { 'Content-Type': 'application/json' }),
653
+ body: JSON.stringify({
654
+ name: opts.name,
655
+ type,
656
+ email: opts.email,
657
+ password: opts.password,
658
+ loginUrl: opts.loginUrl,
659
+ }),
660
+ }, 'Failed to create account');
661
+ console.log(data.account.id);
662
+ process.exit(0);
663
+ });
664
+ authAccountCmd
665
+ .command('update <project-id>')
666
+ .description('Update a project credential account')
667
+ .requiredOption('--account <name-or-id>', 'Account name or ID')
668
+ .option('--name <name>', 'New account name')
669
+ .option('--email <email>', 'Replace the email on a credentials account')
670
+ .option('--password <password>', 'Replace the password on a credentials account')
671
+ .option('--login-url <url>', 'Replace the login URL on a credentials account')
672
+ .option('--clear-email', 'Remove the stored email from a credentials account', false)
673
+ .option('--clear-password', 'Remove the stored password from a credentials account', false)
674
+ .option('--clear-login-url', 'Remove the stored login URL from a credentials account', false)
675
+ .action(async (projectId, opts) => {
676
+ if (opts.email && opts.clearEmail)
677
+ fatal('Cannot use --email and --clear-email together.');
678
+ if (opts.password && opts.clearPassword)
679
+ fatal('Cannot use --password and --clear-password together.');
680
+ if (opts.loginUrl && opts.clearLoginUrl)
681
+ fatal('Cannot use --login-url and --clear-login-url together.');
682
+ const config = await requireConfig();
683
+ const account = await resolveProjectAccount(config, projectId, opts.account);
684
+ const payload = {};
685
+ if (opts.name !== undefined)
686
+ payload.name = opts.name;
687
+ if (opts.email !== undefined)
688
+ payload.email = opts.email;
689
+ if (opts.password !== undefined)
690
+ payload.password = opts.password;
691
+ if (opts.loginUrl !== undefined)
692
+ payload.loginUrl = opts.loginUrl;
693
+ if (opts.clearEmail)
694
+ payload.email = null;
695
+ if (opts.clearPassword)
696
+ payload.password = null;
697
+ if (opts.clearLoginUrl)
698
+ payload.loginUrl = null;
699
+ if (Object.keys(payload).length === 0) {
700
+ fatal('No changes provided. Pass at least one update option.');
701
+ }
702
+ const data = await requestJson(config, `/api/cli/projects/${projectId}/credentials/${account.id}`, {
703
+ method: 'PATCH',
704
+ headers: authHeaders(config, { 'Content-Type': 'application/json' }),
705
+ body: JSON.stringify(payload),
706
+ }, 'Failed to update account');
707
+ console.log(data.account.id);
708
+ process.exit(0);
709
+ });
710
+ authAccountCmd
711
+ .command('delete <project-id>')
712
+ .description('Delete a project credential account')
713
+ .requiredOption('--account <name-or-id>', 'Account name or ID')
714
+ .action(async (projectId, opts) => {
715
+ const config = await requireConfig();
716
+ const account = await resolveProjectAccount(config, projectId, opts.account);
717
+ await requestJson(config, `/api/cli/projects/${projectId}/credentials/${account.id}`, {
718
+ method: 'DELETE',
719
+ headers: authHeaders(config),
720
+ }, 'Failed to delete account');
721
+ logger.success(`Account ${account.name} deleted`);
722
+ process.exit(0);
723
+ });
724
+ const authSessionCmd = authCmd
725
+ .command('session')
726
+ .description('Manage captured browser sessions');
727
+ authSessionCmd
728
+ .command('clear <project-id>')
729
+ .description('Clear the stored browser session for a session account')
730
+ .requiredOption('--account <name-or-id>', 'Account name or ID')
731
+ .action(async (projectId, opts) => {
732
+ const config = await requireConfig();
733
+ const account = await resolveProjectAccount(config, projectId, opts.account);
734
+ if (account.type !== 'session') {
735
+ fatal(`Account "${account.name}" is a credentials account. Only session accounts can clear a stored browser session.`);
736
+ }
737
+ await requestJson(config, `/api/cli/projects/${projectId}/credentials/${account.id}/session`, {
738
+ method: 'DELETE',
739
+ headers: authHeaders(config),
740
+ }, 'Failed to clear session');
741
+ logger.success(`Cleared session for ${account.name}`);
742
+ process.exit(0);
743
+ });
744
+ // ── endpoints commands ─────────────────────────────────────────────
745
+ const endpointsCmd = program
746
+ .command('endpoints')
747
+ .description('Manage screenshot endpoints (dev links)');
748
+ endpointsCmd
749
+ .command('list')
750
+ .description('List endpoints for a preset or project')
751
+ .option('--preset <id>', 'Preset ID')
752
+ .option('--project <id>', 'Project ID')
753
+ .option('--format <fmt>', 'Output format: json or table', 'json')
141
754
  .action(async (opts) => {
142
- const skillPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'assets', 'skill', 'SKILL.md');
143
- let rawContent;
144
- try {
145
- rawContent = await fs.readFile(skillPath, 'utf-8');
755
+ if ((opts.preset ? 1 : 0) + (opts.project ? 1 : 0) !== 1) {
756
+ fatal('Pass exactly one of --preset or --project.');
146
757
  }
147
- catch {
148
- console.error('Error: Could not find SKILL.md. Make sure the autokap package is installed correctly.');
149
- process.exit(1);
758
+ const format = ensureListFormat(opts.format);
759
+ const cfg = await requireConfig();
760
+ const searchParams = new URLSearchParams();
761
+ if (opts.preset)
762
+ searchParams.set('preset_id', opts.preset);
763
+ if (opts.project)
764
+ searchParams.set('project_id', opts.project);
765
+ const data = await requestJson(cfg, '/api/v1/endpoints', { headers: authHeaders(cfg) }, 'Failed to list endpoints', searchParams);
766
+ const rows = data.endpoints.map((endpoint) => toEndpointOutputRow(cfg, endpoint));
767
+ if (format === 'table') {
768
+ console.table(rows.map((row) => ({
769
+ id: row.id,
770
+ capture_name: row.capture_name ?? '(default)',
771
+ label: row.label,
772
+ asset_type: row.asset_type ?? 'screenshot',
773
+ url: row.url,
774
+ })));
150
775
  }
151
- const content = replaceSkillPlaceholders(rawContent, opts);
152
- if (opts.output) {
153
- const outDir = path.dirname(opts.output);
154
- await fs.mkdir(outDir, { recursive: true });
155
- await fs.writeFile(opts.output, content, 'utf-8');
156
- console.log(`Skill file written to: ${opts.output}`);
157
- console.log('');
158
- console.log('Next steps:');
159
- console.log(' Claude Code: Place the file in .claude/commands/autokap-preset.md');
160
- console.log(' Cursor: Place the file in .cursor/rules/autokap-preset.md');
161
- console.log(' Other agents: Point your agent to the file when asking it to create presets');
776
+ else {
777
+ printJson(rows);
778
+ }
779
+ process.exit(0);
780
+ });
781
+ endpointsCmd
782
+ .command('get <endpoint-id>')
783
+ .description('Get endpoint details')
784
+ .action(async (endpointId) => {
785
+ const cfg = await requireConfig();
786
+ const data = await requestJson(cfg, `/api/v1/endpoints/${endpointId}`, { headers: authHeaders(cfg) }, 'Failed to get endpoint');
787
+ printJson(toEndpointOutputRow(cfg, data.endpoint));
788
+ process.exit(0);
789
+ });
790
+ endpointsCmd
791
+ .command('export')
792
+ .description('Export endpoints as JSON or CSV')
793
+ .option('--preset <id>', 'Preset ID')
794
+ .option('--project <id>', 'Project ID')
795
+ .option('--format <fmt>', 'Output format: json or csv', 'json')
796
+ .action(async (opts) => {
797
+ if ((opts.preset ? 1 : 0) + (opts.project ? 1 : 0) !== 1) {
798
+ fatal('Pass exactly one of --preset or --project.');
799
+ }
800
+ const format = ensureExportFormat(opts.format);
801
+ const cfg = await requireConfig();
802
+ if (opts.preset) {
803
+ const searchParams = new URLSearchParams({
804
+ preset_id: opts.preset,
805
+ format,
806
+ });
807
+ if (format === 'csv') {
808
+ const csv = await requestText(cfg, '/api/v1/endpoints/export', { headers: authHeaders(cfg) }, 'Failed to export endpoints', searchParams);
809
+ process.stdout.write(csv);
810
+ }
811
+ else {
812
+ const data = await requestJson(cfg, '/api/v1/endpoints/export', { headers: authHeaders(cfg) }, 'Failed to export endpoints', searchParams);
813
+ printJson(data);
814
+ }
815
+ process.exit(0);
816
+ }
817
+ const searchParams = new URLSearchParams({ project_id: opts.project });
818
+ const data = await requestJson(cfg, '/api/v1/endpoints', { headers: authHeaders(cfg) }, 'Failed to list endpoints', searchParams);
819
+ const rows = data.endpoints.map((endpoint) => toEndpointOutputRow(cfg, endpoint));
820
+ if (format === 'csv') {
821
+ process.stdout.write(`${toCsv(rows, ['id', 'label', 'url', 'capture_type', 'asset_type', 'capture_name'])}\n`);
162
822
  }
163
823
  else {
164
- process.stdout.write(content);
824
+ printJson({ endpoints: rows });
165
825
  }
166
826
  process.exit(0);
167
827
  });
168
- // ── Browser command executor ────────────────────────────────────────
169
- async function executeBrowserCommand(browser, method, params) {
170
- switch (method) {
171
- // Lifecycle
172
- case 'launch':
173
- await browser.launch();
174
- return null;
175
- case 'close':
176
- await browser.close();
177
- return null;
178
- case 'closeContext':
179
- await browser.closeContext();
180
- return null;
181
- // Navigation
182
- case 'navigateTo':
183
- await browser.navigateTo(params.url);
184
- return null;
185
- case 'addCookies':
186
- await browser.addCookies(params.cookies);
187
- return null;
188
- // Page info (used by RemoteBrowser.currentPage proxy)
189
- case 'getPageInfo': return {
190
- url: browser.currentPage.url(),
191
- viewport: browser.currentPage.viewportSize(),
192
- };
193
- case 'getPageTitle': return await browser.currentPage.title();
194
- // Screenshots serialize buffers to base64
195
- case 'takeScreenshot': {
196
- const buf = await browser.takeScreenshot();
197
- return { screenshot: buf.toString('base64') };
828
+ // ── skill command ───────────────────────────────────────────────────
829
+ // Agent output path mappings
830
+ const AGENT_PATHS = {
831
+ claude: '.claude/commands/autokap-preset.md',
832
+ codex: '.agents/skills/autokap-preset/SKILL.md',
833
+ cursor: '.cursor/rules/autokap-preset.md',
834
+ windsurf: '.windsurf/rules/autokap-preset.md',
835
+ copilot: '.github/instructions/autokap-preset.instructions.md',
836
+ };
837
+ const STUDIO_AGENT_PATHS = {
838
+ claude: '.claude/commands/autokap-studio.md',
839
+ codex: '.agents/skills/autokap-studio/SKILL.md',
840
+ cursor: '.cursor/rules/autokap-studio.md',
841
+ windsurf: '.windsurf/rules/autokap-studio.md',
842
+ copilot: '.github/instructions/autokap-studio.instructions.md',
843
+ };
844
+ program
845
+ .command('skill')
846
+ .description('Output or install an AutoKap skill for AI coding agents')
847
+ .option('--output <path>', 'Write the generated skill output to this path instead of stdout')
848
+ .option('--agent <name>', 'Target AI coding agent: claude, codex, cursor, windsurf, copilot (auto-resolves output path and packaging mode)')
849
+ .option('--type <type>', 'Skill type: preset (default) or studio (composition designer)')
850
+ .option('--project-url <url>', 'Replace the project URL placeholder in the skill')
851
+ .option('--project-id <id>', 'Replace the project ID placeholder in the skill')
852
+ .option('--api-base-url <url>', 'Replace the API base URL placeholder (default: https://autokap.app)')
853
+ .action(async (opts) => {
854
+ const skillType = opts.type === 'studio' ? 'studio' : undefined;
855
+ const pathMap = skillType === 'studio' ? STUDIO_AGENT_PATHS : AGENT_PATHS;
856
+ // Resolve --agent to an output path
857
+ if (opts.agent) {
858
+ const agentKey = opts.agent.toLowerCase();
859
+ if (!pathMap[agentKey]) {
860
+ logger.error(`Unknown agent "${opts.agent}". Supported: ${Object.keys(pathMap).join(', ')}`);
861
+ process.exit(1);
198
862
  }
199
- case 'takeScreenshotForAI': {
200
- const buf = await browser.takeScreenshotForAI(params);
201
- return { screenshot: buf.toString('base64') };
863
+ if (opts.output) {
864
+ logger.error('Cannot use both --agent and --output. Pick one.');
865
+ process.exit(1);
202
866
  }
203
- case 'screenshotElement': {
204
- const buf = await browser.screenshotElement(params.index, params.padding);
205
- return { screenshot: buf.toString('base64') };
867
+ opts.output = pathMap[agentKey];
868
+ }
869
+ try {
870
+ if (opts.output) {
871
+ const result = await writeSkillExport({
872
+ type: skillType === 'studio' ? 'studio' : 'preset',
873
+ agent: opts.agent?.toLowerCase(),
874
+ outputPath: opts.output,
875
+ placeholders: opts,
876
+ });
877
+ if (result.mode === 'bundle') {
878
+ logger.success(`Skill bundle written to: ${path.dirname(opts.output)}`);
879
+ }
880
+ else {
881
+ logger.success(`Skill file written to: ${opts.output}`);
882
+ }
206
883
  }
207
- case 'screenshotRegion': {
208
- const buf = await browser.screenshotRegion(params.x, params.y, params.width, params.height, params.padding);
209
- return { screenshot: buf.toString('base64') };
884
+ else {
885
+ const content = await renderSkillSingleFile({
886
+ type: skillType === 'studio' ? 'studio' : 'preset',
887
+ agent: opts.agent?.toLowerCase(),
888
+ placeholders: opts,
889
+ });
890
+ process.stdout.write(content);
210
891
  }
211
- case 'screenshotBySelector': {
212
- const r = await browser.screenshotBySelector(params.selector, params.outscale);
213
- return { buffer: r.buffer.toString('base64'), validation: r.validation };
892
+ }
893
+ catch (error) {
894
+ logger.error(error.message);
895
+ process.exit(1);
896
+ }
897
+ process.exit(0);
898
+ });
899
+ // ── init command ───────────────────────────────────────────────────
900
+ program
901
+ .command('init')
902
+ .description('Set up AutoKap: authenticate and install the AI skill in one step')
903
+ .option('--cli-key <key>', 'Your AutoKap CLI key')
904
+ .option('--project-id <id>', 'Your project ID (optional, can be set later)')
905
+ .option('--agent <name>', 'Target AI coding agent: claude, codex, cursor, windsurf, copilot (default: auto-detect)')
906
+ .option('--api-base-url <url>', `API base URL (env: ${API_BASE_URL_ENV_VAR})`, getDefaultApiBaseUrl())
907
+ .option('--ws-url <url>', `WebSocket server URL (env: ${WS_URL_ENV_VAR})`)
908
+ .action(async (opts) => {
909
+ const wsUrl = opts.wsUrl?.trim() || getDefaultWsUrl(opts.apiBaseUrl);
910
+ // If --cli-key not provided, prompt interactively
911
+ let cliKey = opts.cliKey;
912
+ if (!cliKey) {
913
+ const readline = await import('node:readline');
914
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
915
+ cliKey = await new Promise((resolve) => {
916
+ rl.question('Enter your AutoKap CLI key:', (answer) => {
917
+ rl.close();
918
+ resolve(answer.trim());
919
+ });
920
+ });
921
+ if (!cliKey) {
922
+ logger.error('CLI key is required. Get one at https://autokap.app/settings');
923
+ process.exit(1);
214
924
  }
215
- case 'screenshotByRegion': {
216
- const buf = await browser.screenshotByRegion(params.region, params.outscale);
217
- return { screenshot: buf.toString('base64') };
925
+ }
926
+ // Validate the key
927
+ logger.info('Validating CLI key...');
928
+ try {
929
+ const res = await fetch(`${opts.apiBaseUrl}/api/cli/validate`, {
930
+ headers: { Authorization: `Bearer ${cliKey}` },
931
+ });
932
+ if (!res.ok) {
933
+ logger.error('Invalid CLI key. Generate one at https://autokap.app/settings');
934
+ process.exit(1);
218
935
  }
219
- // Page state — serialize screenshots inside PageState
220
- case 'getPageState': {
221
- const state = await browser.getPageState(params);
222
- return {
223
- ...state,
224
- cleanScreenshot: state.cleanScreenshot.toString('base64'),
225
- screenshot: state.screenshot.toString('base64'),
226
- };
936
+ }
937
+ catch (err) {
938
+ logger.error(`Cannot reach API: ${err.message}`);
939
+ process.exit(1);
940
+ }
941
+ // Store credentials
942
+ await writeConfig({
943
+ apiKey: cliKey,
944
+ apiBaseUrl: opts.apiBaseUrl,
945
+ wsUrl,
946
+ });
947
+ logger.success(`Authenticated. Key stored in ${getConfigPath()}`);
948
+ // Determine agent target
949
+ let agentKey = opts.agent?.toLowerCase();
950
+ if (!agentKey) {
951
+ // Auto-detect from directory structure
952
+ const detectors = [
953
+ ['.claude', 'claude'],
954
+ ['.agents', 'codex'],
955
+ ['.cursor', 'cursor'],
956
+ ['.windsurf', 'windsurf'],
957
+ ['.github/instructions', 'copilot'],
958
+ ];
959
+ for (const [dir, key] of detectors) {
960
+ try {
961
+ await fs.access(dir);
962
+ agentKey = key;
963
+ break;
964
+ }
965
+ catch { /* try next */ }
227
966
  }
228
- case 'getPageStateLite':
229
- return await browser.getPageStateLite();
230
- case 'getAccessibilityTree':
231
- return await browser.getAccessibilityTree(params);
232
- case 'getInteractiveElements':
233
- return await browser.getInteractiveElements(params);
234
- case 'getSimplifiedDOM':
235
- return await browser.getSimplifiedDOM();
236
- case 'captureObservation':
237
- return await browser.captureObservation();
238
- case 'capturePageSignals':
239
- return await browser.capturePageSignals(params);
240
- case 'captureVerificationBundle': {
241
- const bundle = await browser.captureVerificationBundle(params);
242
- return { ...bundle, screenshot: bundle.screenshot.toString('base64') };
967
+ if (!agentKey)
968
+ agentKey = 'claude'; // Default
969
+ logger.info(`Detected agent: ${agentKey}`);
970
+ }
971
+ if (!AGENT_PATHS[agentKey]) {
972
+ logger.error(`Unknown agent "${agentKey}". Supported: ${Object.keys(AGENT_PATHS).join(', ')}`);
973
+ process.exit(1);
974
+ }
975
+ // Install skill file
976
+ const skillOutput = AGENT_PATHS[agentKey];
977
+ try {
978
+ const result = await writeSkillExport({
979
+ type: 'preset',
980
+ agent: agentKey,
981
+ outputPath: skillOutput,
982
+ placeholders: {
983
+ projectId: opts.projectId,
984
+ apiBaseUrl: opts.apiBaseUrl !== DEFAULT_API_BASE_URL ? opts.apiBaseUrl : undefined,
985
+ },
986
+ });
987
+ if (result.mode === 'bundle') {
988
+ logger.success(`Skill bundle installed to: ${path.dirname(skillOutput)}`);
243
989
  }
244
- case 'captureVideoVerificationBundle': {
245
- const bundle = await browser.captureVideoVerificationBundle(params);
246
- return { ...bundle, screenshot: bundle.screenshot.toString('base64') };
990
+ else {
991
+ logger.success(`Skill installed to: ${skillOutput}`);
247
992
  }
248
- // Interactions
249
- case 'clickByIndex':
250
- await browser.clickByIndex(params.index);
251
- return null;
252
- case 'clickBySelector':
253
- await browser.clickBySelector(params.selector, params);
254
- return null;
255
- case 'clickByCoordinates':
256
- await browser.clickByCoordinates(params.x, params.y);
257
- return null;
258
- case 'hoverByIndex':
259
- await browser.hoverByIndex(params.index);
260
- return null;
261
- case 'hoverBySelector':
262
- await browser.hoverBySelector(params.selector);
263
- return null;
264
- case 'hoverByCoordinates':
265
- await browser.hoverByCoordinates(params.x, params.y);
266
- return null;
267
- case 'typeText':
268
- await browser.typeText(params.text, params);
269
- return null;
270
- case 'selectOption':
271
- await browser.selectOption(params);
272
- return null;
273
- case 'scroll':
274
- await browser.scroll(params.direction, params.amount, params.selector);
275
- return null;
276
- case 'scrollElementIntoView':
277
- await browser.scrollElementIntoView(params.index, params);
278
- return null;
279
- case 'safeExpand':
280
- await browser.safeExpand(params);
281
- return null;
282
- case 'pressKey':
283
- await browser.pressKey(params.key);
284
- return null;
285
- case 'searchText': return await browser.searchText(params.query);
286
- // Reactions
287
- case 'waitForPageReaction':
288
- return await browser.waitForPageReaction(params.before, params);
289
- // UI manipulation
290
- case 'wait':
291
- await browser.wait(params.ms);
292
- return null;
293
- case 'dismissOverlays': return await browser.dismissOverlays();
294
- case 'setColorScheme':
295
- await browser.setColorScheme(params.scheme);
296
- return null;
297
- case 'setLanguage':
298
- await browser.setLanguage(params.lang);
299
- return null;
300
- case 'resizeViewport':
301
- await browser.resizeViewport(params.width, params.height);
302
- return null;
303
- case 'forceLoadLazyImages':
304
- await browser.forceLoadLazyImages(params);
305
- return null;
306
- // Storage
307
- case 'exportStorageState': return await browser.exportStorageState();
308
- case 'exportSessionStorage': return await browser.exportSessionStorage();
309
- case 'prepareSessionStorage':
310
- await browser.prepareSessionStorage(params.bundle, params);
311
- return null;
312
- // Network
313
- case 'observeNetworkRequests': return await browser.observeNetworkRequests(params.url, params.waitMs);
314
- case 'setupRouteInterception':
315
- await browser.setupRouteInterception(params.mocks);
316
- return null;
317
- case 'clearRouteInterception':
318
- await browser.clearRouteInterception();
319
- return null;
320
- default:
321
- throw new Error(`Unknown browser command: ${method}`);
322
993
  }
323
- }
994
+ catch (error) {
995
+ logger.error(error.message);
996
+ process.exit(1);
997
+ }
998
+ console.log('');
999
+ console.log('Setup complete! You can now:');
1000
+ console.log(` - Ask your AI agent to create presets using the autokap-preset skill`);
1001
+ console.log(` - Run captures locally: autokap run <preset-id>`);
1002
+ process.exit(0);
1003
+ });
1004
+ // ── proxy command ──────────────────────────────────────────────────
1005
+ const proxyCmd = program
1006
+ .command('proxy')
1007
+ .description('Manage capture proxy for edge caching');
1008
+ proxyCmd
1009
+ .command('register')
1010
+ .description('Register a user-side proxy for faster capture loading')
1011
+ .requiredOption('--project <id>', 'Project ID')
1012
+ .requiredOption('--proxy-url <url>', 'Proxy base URL (e.g. https://myapp.com/api/autokap/assets)')
1013
+ .option('--webhook-url <url>', 'Webhook URL for cache invalidation (e.g. https://myapp.com/api/autokap/webhook)')
1014
+ .option('--signing-secret <secret>', 'Webhook signing secret (auto-generated if omitted)')
1015
+ .action(async (opts) => {
1016
+ const cfg = await requireConfig();
1017
+ const body = {
1018
+ proxy_base_url: opts.proxyUrl,
1019
+ };
1020
+ if (opts.webhookUrl)
1021
+ body.webhook_url = opts.webhookUrl;
1022
+ if (opts.signingSecret)
1023
+ body.signing_secret = opts.signingSecret;
1024
+ const res = await fetch(`${cfg.apiBaseUrl}/api/v1/projects/${opts.project}/proxy`, {
1025
+ method: 'PATCH',
1026
+ headers: {
1027
+ Authorization: `Bearer ${cfg.apiKey}`,
1028
+ 'Content-Type': 'application/json',
1029
+ },
1030
+ body: JSON.stringify(body),
1031
+ });
1032
+ if (!res.ok) {
1033
+ const data = await res.json().catch(() => ({ error: res.statusText }));
1034
+ logger.error(`Failed to register proxy: ${data.error || res.statusText}`);
1035
+ process.exit(1);
1036
+ }
1037
+ const result = await res.json();
1038
+ logger.info(`Proxy registered: ${result.proxy_base_url}`);
1039
+ if (result.webhook_url) {
1040
+ logger.info(`Webhook configured: ${result.webhook_url}`);
1041
+ }
1042
+ if (result.signing_secret) {
1043
+ console.log('');
1044
+ console.log('Add this to your environment variables:');
1045
+ console.log(` AUTOKAP_WEBHOOK_SECRET=${result.signing_secret}`);
1046
+ console.log('');
1047
+ }
1048
+ process.exit(0);
1049
+ });
1050
+ proxyCmd
1051
+ .command('verify')
1052
+ .description('Verify the configured proxy for a project')
1053
+ .requiredOption('--project <id>', 'Project ID')
1054
+ .action(async (opts) => {
1055
+ const config = await requireConfig();
1056
+ const project = await loadProject(config, opts.project);
1057
+ if (!project.proxy_base_url) {
1058
+ fatal(`Project ${opts.project} has no proxy configured. Run "autokap proxy register" first.`);
1059
+ }
1060
+ const result = await requestJson(config, '/api/v1/proxy/verify', {
1061
+ method: 'POST',
1062
+ headers: authHeaders(config, { 'Content-Type': 'application/json' }),
1063
+ body: JSON.stringify({
1064
+ project_id: opts.project,
1065
+ proxy_base_url: project.proxy_base_url,
1066
+ }),
1067
+ }, 'Failed to verify proxy');
1068
+ printJson(result);
1069
+ if (!result.success) {
1070
+ process.exit(1);
1071
+ }
1072
+ process.exit(0);
1073
+ });
324
1074
  // ── Entry point ─────────────────────────────────────────────────────
325
1075
  const resolvedArgv = process.argv[1] && fs.realpath(process.argv[1]).catch(() => process.argv[1]);
326
- const isDirectExecution = resolvedArgv && await resolvedArgv.then(p => p.endsWith('/cli.js') || p.endsWith('/cli.ts'));
1076
+ const isDirectExecution = resolvedArgv && await resolvedArgv.then(p => {
1077
+ const base = path.basename(p);
1078
+ return base === 'cli.js' || base === 'cli.ts';
1079
+ });
327
1080
  if (isDirectExecution) {
328
1081
  program.parseAsync().catch(async (err) => {
329
1082
  logger.error(err.message);