@lnar/cli 0.0.1-dev.5da411d → 0.0.1-dev.632e82b

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 (105) hide show
  1. package/README.md +16 -54
  2. package/dist/api-client.d.ts +19 -0
  3. package/dist/api-client.js +40 -0
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/auth.d.ts +20 -0
  6. package/dist/auth.js +74 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/browser.d.ts +1 -0
  9. package/dist/browser.js +68 -0
  10. package/dist/browser.js.map +1 -0
  11. package/dist/capture-client.d.ts +13 -0
  12. package/dist/capture-client.js +37 -0
  13. package/dist/capture-client.js.map +1 -0
  14. package/dist/capture-worker.d.ts +14 -0
  15. package/dist/capture-worker.js +105 -0
  16. package/dist/capture-worker.js.map +1 -0
  17. package/dist/cli.js +21 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/daemon.d.ts +2 -0
  20. package/dist/commands/daemon.js +114 -1
  21. package/dist/commands/daemon.js.map +1 -1
  22. package/dist/commands/login.d.ts +2 -0
  23. package/dist/commands/login.js +19 -9
  24. package/dist/commands/login.js.map +1 -1
  25. package/dist/commands/record.d.ts +7 -0
  26. package/dist/commands/record.js +117 -0
  27. package/dist/commands/record.js.map +1 -0
  28. package/dist/commands/scan.js +12 -0
  29. package/dist/commands/scan.js.map +1 -1
  30. package/dist/commands/sync.js +3 -0
  31. package/dist/commands/sync.js.map +1 -1
  32. package/dist/commands/up.js +19 -0
  33. package/dist/commands/up.js.map +1 -1
  34. package/dist/machine-id.d.ts +2 -0
  35. package/dist/machine-id.js +48 -0
  36. package/dist/machine-id.js.map +1 -0
  37. package/dist/pending-client.d.ts +1 -1
  38. package/dist/recording/bundle.d.ts +25 -0
  39. package/dist/recording/bundle.js +55 -0
  40. package/dist/recording/bundle.js.map +1 -0
  41. package/dist/recording/capture.d.ts +53 -0
  42. package/dist/recording/capture.js +163 -0
  43. package/dist/recording/capture.js.map +1 -0
  44. package/dist/recording/session.d.ts +30 -0
  45. package/dist/recording/session.js +47 -0
  46. package/dist/recording/session.js.map +1 -0
  47. package/dist/recording/types.d.ts +59 -0
  48. package/dist/recording/types.js +8 -0
  49. package/dist/recording/types.js.map +1 -0
  50. package/dist/run-client.d.ts +19 -0
  51. package/dist/run-client.js +44 -0
  52. package/dist/run-client.js.map +1 -0
  53. package/dist/run-worker.d.ts +25 -0
  54. package/dist/run-worker.js +85 -0
  55. package/dist/run-worker.js.map +1 -0
  56. package/dist/runtime/actions.d.ts +13 -0
  57. package/dist/runtime/actions.js +107 -0
  58. package/dist/runtime/actions.js.map +1 -0
  59. package/dist/runtime/client.d.ts +3 -0
  60. package/dist/runtime/client.js +85 -0
  61. package/dist/runtime/client.js.map +1 -0
  62. package/dist/runtime/login.d.ts +20 -0
  63. package/dist/runtime/login.js +34 -0
  64. package/dist/runtime/login.js.map +1 -0
  65. package/dist/runtime/loop.d.ts +28 -0
  66. package/dist/runtime/loop.js +68 -0
  67. package/dist/runtime/loop.js.map +1 -0
  68. package/dist/runtime/playbook.d.ts +49 -0
  69. package/dist/runtime/playbook.js +73 -0
  70. package/dist/runtime/playbook.js.map +1 -0
  71. package/dist/runtime/runner.d.ts +20 -0
  72. package/dist/runtime/runner.js +54 -0
  73. package/dist/runtime/runner.js.map +1 -0
  74. package/dist/runtime/types.d.ts +69 -0
  75. package/dist/runtime/types.js +10 -0
  76. package/dist/runtime/types.js.map +1 -0
  77. package/dist/scanners/claude-code-skills.d.ts +6 -0
  78. package/dist/scanners/claude-code-skills.js +201 -0
  79. package/dist/scanners/claude-code-skills.js.map +1 -0
  80. package/dist/scanners/claude-code.js +10 -3
  81. package/dist/scanners/claude-code.js.map +1 -1
  82. package/dist/scanners/index.js +3 -2
  83. package/dist/scanners/index.js.map +1 -1
  84. package/dist/service/files.d.ts +14 -2
  85. package/dist/service/files.js +16 -4
  86. package/dist/service/files.js.map +1 -1
  87. package/dist/service/install.js +83 -16
  88. package/dist/service/install.js.map +1 -1
  89. package/dist/sse-client.d.ts +72 -0
  90. package/dist/sse-client.js +174 -0
  91. package/dist/sse-client.js.map +1 -0
  92. package/dist/types.d.ts +43 -3
  93. package/dist/types.js +34 -2
  94. package/dist/types.js.map +1 -1
  95. package/dist/writers/claude-code.js +31 -10
  96. package/dist/writers/claude-code.js.map +1 -1
  97. package/dist/writers/registry.js +0 -4
  98. package/dist/writers/registry.js.map +1 -1
  99. package/package.json +11 -1
  100. package/dist/scanners/chatgpt.d.ts +0 -20
  101. package/dist/scanners/chatgpt.js +0 -49
  102. package/dist/scanners/chatgpt.js.map +0 -1
  103. package/dist/writers/chatgpt.d.ts +0 -8
  104. package/dist/writers/chatgpt.js +0 -12
  105. package/dist/writers/chatgpt.js.map +0 -1
@@ -0,0 +1,48 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { z } from 'zod';
6
+ /**
7
+ * マシンに永続化される一意 ID。hostname は macOS のネットワーク状況で
8
+ * `xxx.local` ↔ `xxx.kkbnj1.kt.home.ne.jp` のように切り替わって別マシンと
9
+ * みなされるため、agent 同一性の判定には使えない。代わりに UUID v4 を生成して
10
+ * `~/.config/lnar/machine.json` に保存し、これをサーバー側 dedupe キーにする。
11
+ *
12
+ * - 初回呼び出し時に UUID を生成して書き込む
13
+ * - 2 回目以降は読み込んで返す
14
+ * - ファイル破損時は新規生成して上書き (機微情報は持たないので破壊的でよい)
15
+ */
16
+ const MachineFile = z.object({
17
+ machineId: z.string().uuid(),
18
+ createdAt: z.string(),
19
+ });
20
+ export const machineIdPath = (home = homedir()) => join(home, '.config', 'lnar', 'machine.json');
21
+ const tryRead = async (path) => {
22
+ try {
23
+ const text = await readFile(path, 'utf-8');
24
+ const parsed = MachineFile.parse(JSON.parse(text));
25
+ return parsed.machineId;
26
+ }
27
+ catch (err) {
28
+ const code = err.code;
29
+ if (code === 'ENOENT')
30
+ return null;
31
+ return null;
32
+ }
33
+ };
34
+ const write = async (path, machineId) => {
35
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
36
+ const payload = { machineId, createdAt: new Date().toISOString() };
37
+ await writeFile(path, JSON.stringify(payload, null, 2), { mode: 0o600 });
38
+ };
39
+ export const loadOrCreateMachineId = async (home = homedir()) => {
40
+ const path = machineIdPath(home);
41
+ const existing = await tryRead(path);
42
+ if (existing != null)
43
+ return existing;
44
+ const machineId = randomUUID();
45
+ await write(path, machineId);
46
+ return machineId;
47
+ };
48
+ //# sourceMappingURL=machine-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-id.js","sourceRoot":"","sources":["../src/machine-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC5B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,OAAe,OAAO,EAAE,EAAU,EAAE,CAChE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC;AAEhD,MAAM,OAAO,GAAG,KAAK,EAAE,IAAY,EAA0B,EAAE;IAC7D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACnD,OAAO,MAAM,CAAC,SAAS,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;QACjD,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,KAAK,GAAG,KAAK,EAAE,IAAY,EAAE,SAAiB,EAAiB,EAAE;IACrE,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,MAAM,OAAO,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;IACnE,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAC3E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EAAE,OAAe,OAAO,EAAE,EAAmB,EAAE;IACvF,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,QAAQ,CAAC;IACtC,MAAM,SAAS,GAAG,UAAU,EAAE,CAAC;IAC/B,MAAM,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC7B,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC"}
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export type PendingAction = 'add_server' | 'remove_server' | 'install_plugin' | 'uninstall_plugin' | 'enable_plugin' | 'disable_plugin';
11
11
  export type PendingStatus = 'pending' | 'applied' | 'failed' | 'cancelled';
12
- export type AgentKind = 'claude_code' | 'codex' | 'cursor' | 'gemini_cli' | 'chatgpt';
12
+ export type AgentKind = 'claude_code' | 'codex' | 'cursor' | 'gemini_cli';
13
13
  export type SourceScope = 'global' | 'project' | 'local' | 'remote';
14
14
  export type PluginScope = 'user' | 'local' | 'project';
15
15
  export interface PendingSpec {
@@ -0,0 +1,25 @@
1
+ import type { Keyframe } from './capture.js';
2
+ import type { RecordedAction, RecordingManifest } from './types.js';
3
+ export interface BuildManifestParams {
4
+ readonly name: string;
5
+ readonly startUrl: string | null;
6
+ readonly createdAt: string;
7
+ readonly viewport: {
8
+ readonly width: number;
9
+ readonly height: number;
10
+ };
11
+ readonly actions: ReadonlyArray<RecordedAction>;
12
+ readonly screenshots: ReadonlyArray<string>;
13
+ }
14
+ /** manifest を組み立てる純関数。 */
15
+ export declare function buildManifest(params: BuildManifestParams): RecordingManifest;
16
+ export interface BundleContent {
17
+ readonly manifest: RecordingManifest;
18
+ readonly actions: ReadonlyArray<RecordedAction>;
19
+ readonly frames: ReadonlyArray<Keyframe>;
20
+ }
21
+ /**
22
+ * バンドルを tar.gz の Buffer として組み立てる (ディスク不使用)。
23
+ * 返り値をそのままアップロード body に使う。
24
+ */
25
+ export declare function packBundleToBuffer(content: BundleContent): Promise<Buffer>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 録画バンドルの組み立て。
3
+ *
4
+ * バンドルは tar.gz をメモリ上で組み立て、そのまま raw body でアップロードする。
5
+ * 録画内容 (キーフレーム画像) を一瞬でもディスクへ書かないため、ファイル経由
6
+ * (writeFile → packBundle) は使わず、すべて Buffer で完結させる。
7
+ *
8
+ * tar 構成 (サーバーが展開して読む):
9
+ * manifest.json メタデータ
10
+ * actions.json RecordedAction[]
11
+ * screenshots/NNNN.png キーフレーム
12
+ */
13
+ import { gzipSync } from 'node:zlib';
14
+ import { pack as tarPack } from 'tar-stream';
15
+ /** manifest を組み立てる純関数。 */
16
+ export function buildManifest(params) {
17
+ return {
18
+ version: 1,
19
+ name: params.name,
20
+ startUrl: params.startUrl,
21
+ createdAt: params.createdAt,
22
+ viewport: params.viewport,
23
+ actionCount: params.actions.length,
24
+ // 動画 / trace はディスクへ書かない方針のため常に未取得。
25
+ video: null,
26
+ trace: null,
27
+ screenshots: [...params.screenshots],
28
+ };
29
+ }
30
+ const jsonBuffer = (value) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf-8');
31
+ /**
32
+ * バンドルを tar.gz の Buffer として組み立てる (ディスク不使用)。
33
+ * 返り値をそのままアップロード body に使う。
34
+ */
35
+ export async function packBundleToBuffer(content) {
36
+ const pack = tarPack();
37
+ const addEntry = (name, bytes) => new Promise((resolve, reject) => {
38
+ pack.entry({ name }, bytes, (err) => (err ? reject(err) : resolve()));
39
+ });
40
+ const collected = new Promise((resolve, reject) => {
41
+ const chunks = [];
42
+ pack.on('data', (chunk) => chunks.push(chunk));
43
+ pack.on('error', reject);
44
+ pack.on('end', () => resolve(Buffer.concat(chunks)));
45
+ });
46
+ await addEntry('manifest.json', jsonBuffer(content.manifest));
47
+ await addEntry('actions.json', jsonBuffer(content.actions));
48
+ for (const frame of content.frames) {
49
+ await addEntry(`screenshots/${frame.name}`, frame.bytes);
50
+ }
51
+ pack.finalize();
52
+ const tarBytes = await collected;
53
+ return gzipSync(tarBytes);
54
+ }
55
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/recording/bundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAa7C,0BAA0B;AAC1B,MAAM,UAAU,aAAa,CAAC,MAA2B;IACvD,OAAO;QACL,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM;QAClC,mCAAmC;QACnC,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC;KACrC,CAAC;AACJ,CAAC;AAQD,MAAM,UAAU,GAAG,CAAC,KAAc,EAAU,EAAE,CAC5C,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAE9D;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAAsB;IAC7D,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IAEvB,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,KAAa,EAAiB,EAAE,CAC9D,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC9B,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEL,MAAM,SAAS,GAAoB,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACjE,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACzB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,MAAM,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,QAAQ,CAAC,eAAe,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC,QAAQ,EAAE,CAAC;IAEhB,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;IACjC,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * ローカルブラウザのユーザー操作をキャプチャする。
3
+ *
4
+ * 設計:
5
+ * - ページ内に capture-phase のリスナを注入し、click / change / submit / keydown を
6
+ * `window.__lnarRecord` (exposeBinding) 経由で Node 側へ送る。
7
+ * - Node 側 (Recorder) で録画開始からの相対時刻を付与し、キーフレームを保存する。
8
+ * - main frame のナビゲーションは Playwright の framenavigated で記録する。
9
+ *
10
+ * 機微フィールド (password 等) の値は **ページ内でマスク** してから送るため、生の
11
+ * 認証情報は Node 側にもバンドルにも残らない。
12
+ */
13
+ import type { BrowserContext, Page } from 'playwright';
14
+ import type { RecordedAction } from './types.js';
15
+ /** メモリ上に保持するキーフレーム (ディスクには一切書かない)。 */
16
+ export interface Keyframe {
17
+ readonly name: string;
18
+ readonly bytes: Buffer;
19
+ }
20
+ /** 全ページに注入する記録スクリプト (self-contained・依存なし)。 */
21
+ export declare const INIT_SCRIPT = "() => {\n if (window.__lnarRecorderInstalled) return;\n window.__lnarRecorderInstalled = true;\n var MAXV = 300;\n function esc(s){ return (window.CSS && CSS.escape) ? CSS.escape(s) : String(s).replace(/[^a-zA-Z0-9_-]/g, '\\\\$&'); }\n function selectorFor(el){\n if (!el || el.nodeType !== 1) return undefined;\n var tid = el.getAttribute && el.getAttribute('data-testid');\n if (tid) return '[data-testid=\"' + esc(tid) + '\"]';\n if (el.id) return '#' + esc(el.id);\n var parts = [], cur = el, depth = 0;\n while (cur && cur.nodeType === 1 && depth < 4) {\n var part = cur.tagName.toLowerCase();\n var parent = cur.parentElement;\n if (parent) {\n var sibs = Array.prototype.filter.call(parent.children, function(c){ return c.tagName === cur.tagName; });\n if (sibs.length > 1) part += ':nth-of-type(' + (sibs.indexOf(cur) + 1) + ')';\n }\n parts.unshift(part);\n cur = cur.parentElement;\n depth++;\n }\n return parts.join(' > ');\n }\n function labelFor(el){\n if (!el || !el.getAttribute) return undefined;\n var a = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('name');\n if (a) return String(a).slice(0, 80);\n var t = (el.innerText || el.value || '').trim();\n return t ? t.slice(0, 80) : undefined;\n }\n function isSensitive(el){\n var type = ((el.getAttribute && el.getAttribute('type')) || '').toLowerCase();\n if (type === 'password') return true;\n var ac = ((el.getAttribute && el.getAttribute('autocomplete')) || '').toLowerCase();\n if (/password|cc-|one-time-code/.test(ac)) return true;\n var idn = (((el.name || '') + ' ' + (el.id || '')).toLowerCase());\n return /pass|otp|secret|token|card|cvv|ssn/.test(idn);\n }\n function send(p){ try { window.__lnarRecord(JSON.stringify(p)); } catch (e) {} }\n document.addEventListener('click', function(e){\n var el = e.target;\n send({ kind: 'click', x: Math.round(e.clientX), y: Math.round(e.clientY), selector: selectorFor(el), label: labelFor(el) });\n }, true);\n document.addEventListener('change', function(e){\n var el = e.target;\n if (!el || !('value' in el)) return;\n var sens = isSensitive(el);\n send({ kind: 'input', selector: selectorFor(el), label: labelFor(el), sensitive: sens, value: sens ? '***' : String(el.value).slice(0, MAXV) });\n }, true);\n document.addEventListener('submit', function(e){\n send({ kind: 'submit', selector: selectorFor(e.target), label: labelFor(e.target) });\n }, true);\n document.addEventListener('keydown', function(e){\n if (e.key === 'Enter' || e.key === 'Tab' || e.key === 'Escape') {\n send({ kind: 'keydown', key: e.key, selector: selectorFor(e.target) });\n }\n }, true);\n}";
22
+ export interface RecorderOptions {
23
+ readonly maxScreenshots?: number;
24
+ /** テスト用の時刻ソース。既定は Date.now。 */
25
+ readonly now?: () => number;
26
+ }
27
+ /**
28
+ * 録画状態を保持し、ページ内イベント / ナビゲーションを RecordedAction に変換する。
29
+ *
30
+ * キーフレームは PNG の Buffer としてメモリ上にのみ保持し、ディスクには書かない
31
+ * (録画内容が一瞬でもローカルに残るのを防ぐ)。
32
+ */
33
+ export declare class Recorder {
34
+ readonly actions: RecordedAction[];
35
+ readonly frames: Keyframe[];
36
+ private readonly startMs;
37
+ private readonly now;
38
+ private readonly maxScreenshots;
39
+ private shotCount;
40
+ constructor(options?: RecorderOptions);
41
+ /** 記録済みキーフレームのファイル名一覧 (manifest 用)。 */
42
+ get screenshots(): string[];
43
+ private capture;
44
+ /** ページ内イベントを受けてアクションを記録する。 */
45
+ handleInPage(page: Page, payloadJson: string): Promise<void>;
46
+ /** main frame のナビゲーションを記録する。 */
47
+ handleNavigate(page: Page, url: string): Promise<void>;
48
+ }
49
+ /**
50
+ * context にレコーダを取り付ける。exposeBinding → addInitScript の順で行うこと。
51
+ * 取り付け後に作成/遷移したページが記録対象になる。
52
+ */
53
+ export declare function attachRecorder(context: BrowserContext, recorder: Recorder): Promise<void>;
@@ -0,0 +1,163 @@
1
+ /** 全ページに注入する記録スクリプト (self-contained・依存なし)。 */
2
+ export const INIT_SCRIPT = `() => {
3
+ if (window.__lnarRecorderInstalled) return;
4
+ window.__lnarRecorderInstalled = true;
5
+ var MAXV = 300;
6
+ function esc(s){ return (window.CSS && CSS.escape) ? CSS.escape(s) : String(s).replace(/[^a-zA-Z0-9_-]/g, '\\\\$&'); }
7
+ function selectorFor(el){
8
+ if (!el || el.nodeType !== 1) return undefined;
9
+ var tid = el.getAttribute && el.getAttribute('data-testid');
10
+ if (tid) return '[data-testid="' + esc(tid) + '"]';
11
+ if (el.id) return '#' + esc(el.id);
12
+ var parts = [], cur = el, depth = 0;
13
+ while (cur && cur.nodeType === 1 && depth < 4) {
14
+ var part = cur.tagName.toLowerCase();
15
+ var parent = cur.parentElement;
16
+ if (parent) {
17
+ var sibs = Array.prototype.filter.call(parent.children, function(c){ return c.tagName === cur.tagName; });
18
+ if (sibs.length > 1) part += ':nth-of-type(' + (sibs.indexOf(cur) + 1) + ')';
19
+ }
20
+ parts.unshift(part);
21
+ cur = cur.parentElement;
22
+ depth++;
23
+ }
24
+ return parts.join(' > ');
25
+ }
26
+ function labelFor(el){
27
+ if (!el || !el.getAttribute) return undefined;
28
+ var a = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('name');
29
+ if (a) return String(a).slice(0, 80);
30
+ var t = (el.innerText || el.value || '').trim();
31
+ return t ? t.slice(0, 80) : undefined;
32
+ }
33
+ function isSensitive(el){
34
+ var type = ((el.getAttribute && el.getAttribute('type')) || '').toLowerCase();
35
+ if (type === 'password') return true;
36
+ var ac = ((el.getAttribute && el.getAttribute('autocomplete')) || '').toLowerCase();
37
+ if (/password|cc-|one-time-code/.test(ac)) return true;
38
+ var idn = (((el.name || '') + ' ' + (el.id || '')).toLowerCase());
39
+ return /pass|otp|secret|token|card|cvv|ssn/.test(idn);
40
+ }
41
+ function send(p){ try { window.__lnarRecord(JSON.stringify(p)); } catch (e) {} }
42
+ document.addEventListener('click', function(e){
43
+ var el = e.target;
44
+ send({ kind: 'click', x: Math.round(e.clientX), y: Math.round(e.clientY), selector: selectorFor(el), label: labelFor(el) });
45
+ }, true);
46
+ document.addEventListener('change', function(e){
47
+ var el = e.target;
48
+ if (!el || !('value' in el)) return;
49
+ var sens = isSensitive(el);
50
+ send({ kind: 'input', selector: selectorFor(el), label: labelFor(el), sensitive: sens, value: sens ? '***' : String(el.value).slice(0, MAXV) });
51
+ }, true);
52
+ document.addEventListener('submit', function(e){
53
+ send({ kind: 'submit', selector: selectorFor(e.target), label: labelFor(e.target) });
54
+ }, true);
55
+ document.addEventListener('keydown', function(e){
56
+ if (e.key === 'Enter' || e.key === 'Tab' || e.key === 'Escape') {
57
+ send({ kind: 'keydown', key: e.key, selector: selectorFor(e.target) });
58
+ }
59
+ }, true);
60
+ }`;
61
+ /**
62
+ * 録画状態を保持し、ページ内イベント / ナビゲーションを RecordedAction に変換する。
63
+ *
64
+ * キーフレームは PNG の Buffer としてメモリ上にのみ保持し、ディスクには書かない
65
+ * (録画内容が一瞬でもローカルに残るのを防ぐ)。
66
+ */
67
+ export class Recorder {
68
+ actions = [];
69
+ frames = [];
70
+ startMs;
71
+ now;
72
+ maxScreenshots;
73
+ shotCount = 0;
74
+ constructor(options = {}) {
75
+ this.now = options.now ?? (() => Date.now());
76
+ this.maxScreenshots = options.maxScreenshots ?? 200;
77
+ this.startMs = this.now();
78
+ }
79
+ /** 記録済みキーフレームのファイル名一覧 (manifest 用)。 */
80
+ get screenshots() {
81
+ return this.frames.map((f) => f.name);
82
+ }
83
+ async capture(page) {
84
+ if (this.shotCount >= this.maxScreenshots)
85
+ return undefined;
86
+ // 採番は await の前に同期的に予約する。クリック/遷移/入力が近接して発火すると
87
+ // capture が並行実行されるため、ここで予約しないと同じ番号を取り合い、
88
+ // 同名フレームで上書きして manifest に重複が出る (実データで確認済みの不具合)。
89
+ const index = ++this.shotCount;
90
+ const name = `${String(index).padStart(4, '0')}.png`;
91
+ try {
92
+ const bytes = await page.screenshot({ type: 'png' });
93
+ this.frames.push({ name, bytes });
94
+ return name;
95
+ }
96
+ catch {
97
+ return undefined; // ナビゲーション中などは握り潰す (予約番号は欠番になる)
98
+ }
99
+ }
100
+ /** ページ内イベントを受けてアクションを記録する。 */
101
+ async handleInPage(page, payloadJson) {
102
+ let payload;
103
+ try {
104
+ payload = JSON.parse(payloadJson);
105
+ }
106
+ catch {
107
+ return;
108
+ }
109
+ const screenshot = await this.capture(page);
110
+ this.actions.push({
111
+ type: payload.kind,
112
+ atMs: this.now() - this.startMs,
113
+ url: page.url(),
114
+ selector: payload.selector,
115
+ label: payload.label,
116
+ value: payload.value,
117
+ sensitive: payload.sensitive,
118
+ x: payload.x,
119
+ y: payload.y,
120
+ key: payload.key,
121
+ screenshot,
122
+ });
123
+ }
124
+ /** main frame のナビゲーションを記録する。 */
125
+ async handleNavigate(page, url) {
126
+ const screenshot = await this.capture(page);
127
+ this.actions.push({
128
+ type: 'navigate',
129
+ atMs: this.now() - this.startMs,
130
+ url,
131
+ screenshot,
132
+ });
133
+ }
134
+ }
135
+ /**
136
+ * context にレコーダを取り付ける。exposeBinding → addInitScript の順で行うこと。
137
+ * 取り付け後に作成/遷移したページが記録対象になる。
138
+ */
139
+ export async function attachRecorder(context, recorder) {
140
+ await context.exposeBinding('__lnarRecord', (source, payloadJson) => {
141
+ // 戻り値を待たない (ページ側 send は fire-and-forget)。
142
+ void recorder.handleInPage(source.page, payloadJson);
143
+ });
144
+ // INIT_SCRIPT は関数式。addInitScript は文字列をソースとして評価するだけで呼び出さ
145
+ // ないため、自己実行する形で渡す。
146
+ await context.addInitScript({ content: `(${INIT_SCRIPT})()` });
147
+ const wirePage = (page) => {
148
+ let lastUrl = '';
149
+ page.on('framenavigated', (frame) => {
150
+ if (frame !== page.mainFrame())
151
+ return;
152
+ const url = frame.url();
153
+ if (!url || url === 'about:blank' || url === lastUrl)
154
+ return;
155
+ lastUrl = url;
156
+ void recorder.handleNavigate(page, url);
157
+ });
158
+ };
159
+ for (const page of context.pages())
160
+ wirePage(page);
161
+ context.on('page', wirePage);
162
+ }
163
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/recording/capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 録画セッションのオーケストレーション。
3
+ *
4
+ * ローカルブラウザを起動 → ユーザー操作を記録 → 停止シグナルで終了 → バンドル内容を返す。
5
+ * キーフレームはメモリ上の Buffer として保持し、ディスクには一切書かない。
6
+ * 停止条件 (`waitForStop`) は呼び出し側から注入し、本体をテスト可能に保つ。
7
+ */
8
+ import { type BrowserContext, type Page } from 'playwright';
9
+ import { type Keyframe } from './capture.js';
10
+ import type { RecordedAction, RecordingManifest } from './types.js';
11
+ export interface RecordSessionOptions {
12
+ readonly name: string;
13
+ readonly startUrl: string | null;
14
+ readonly profileDir: string;
15
+ readonly viewport?: {
16
+ readonly width: number;
17
+ readonly height: number;
18
+ };
19
+ readonly headless?: boolean;
20
+ readonly createdAt?: string;
21
+ /** 停止シグナル。resolve でバンドル化に進む。 */
22
+ readonly waitForStop: (page: Page, context: BrowserContext) => Promise<void>;
23
+ }
24
+ export interface RecordSessionResult {
25
+ readonly manifest: RecordingManifest;
26
+ readonly actions: ReadonlyArray<RecordedAction>;
27
+ readonly frames: ReadonlyArray<Keyframe>;
28
+ }
29
+ /** 録画を実行し、バンドル内容 (メモリ上) を返す。 */
30
+ export declare function recordSession(options: RecordSessionOptions): Promise<RecordSessionResult>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 録画セッションのオーケストレーション。
3
+ *
4
+ * ローカルブラウザを起動 → ユーザー操作を記録 → 停止シグナルで終了 → バンドル内容を返す。
5
+ * キーフレームはメモリ上の Buffer として保持し、ディスクには一切書かない。
6
+ * 停止条件 (`waitForStop`) は呼び出し側から注入し、本体をテスト可能に保つ。
7
+ */
8
+ import { chromium } from 'playwright';
9
+ import { buildManifest } from './bundle.js';
10
+ import { attachRecorder, Recorder } from './capture.js';
11
+ const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
12
+ /** 録画を実行し、バンドル内容 (メモリ上) を返す。 */
13
+ export async function recordSession(options) {
14
+ const viewport = options.viewport ?? DEFAULT_VIEWPORT;
15
+ // 動画 / trace は記録しない (Playwright が必ずディスクへ書き出すため、
16
+ // 録画内容を一瞬もローカルに残さない方針と両立しない)。
17
+ const context = await chromium.launchPersistentContext(options.profileDir, {
18
+ headless: options.headless ?? false,
19
+ viewport,
20
+ });
21
+ const recorder = new Recorder();
22
+ try {
23
+ await attachRecorder(context, recorder);
24
+ const page = context.pages()[0] ?? (await context.newPage());
25
+ if (options.startUrl) {
26
+ await page.goto(options.startUrl, { waitUntil: 'domcontentloaded' });
27
+ }
28
+ await options.waitForStop(page, context);
29
+ await context.close();
30
+ const manifest = buildManifest({
31
+ name: options.name,
32
+ startUrl: options.startUrl,
33
+ createdAt: options.createdAt ?? new Date().toISOString(),
34
+ viewport,
35
+ actions: recorder.actions,
36
+ screenshots: recorder.screenshots,
37
+ });
38
+ return { manifest, actions: recorder.actions, frames: recorder.frames };
39
+ }
40
+ finally {
41
+ // 例外時も確実に閉じる (close 済みなら no-op 相当)。
42
+ if (context.browser()?.isConnected()) {
43
+ await context.close().catch(() => { });
44
+ }
45
+ }
46
+ }
47
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/recording/session.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAuB,QAAQ,EAAa,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAiB,QAAQ,EAAE,MAAM,cAAc,CAAC;AAoBvE,MAAM,gBAAgB,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAW,CAAC;AAE/D,gCAAgC;AAChC,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAA6B;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,gBAAgB,CAAC;IAEtD,gDAAgD;IAChD,8BAA8B;IAC9B,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,uBAAuB,CAAC,OAAO,CAAC,UAAU,EAAE;QACzE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,KAAK;QACnC,QAAQ;KACT,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAExC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAC;QACvE,CAAC;QAED,MAAM,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAEzC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QAEtB,MAAM,QAAQ,GAAG,aAAa,CAAC;YAC7B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACxD,QAAQ;YACR,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,WAAW,EAAE,QAAQ,CAAC,WAAW;SAClC,CAAC,CAAC;QAEH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IAC1E,CAAC;YAAS,CAAC;QACT,oCAAoC;QACpC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,EAAE,CAAC;YACrC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * デモ録画 (Demo-to-MCP / Phase 1) の型。
3
+ *
4
+ * ユーザーがローカルブラウザで実演した操作を構造化して保存し、後段 (Phase 2) で
5
+ * gemini-3.5-flash が「タスク手順書 (playbook)」へ解析するための素材にする。
6
+ */
7
+ /** ユーザー操作 1 件。座標はキャプチャ時のビューポートピクセル。 */
8
+ export interface RecordedAction {
9
+ /** 操作種別。 */
10
+ readonly type: 'navigate' | 'click' | 'input' | 'submit' | 'keydown';
11
+ /** 録画開始からの相対ミリ秒。 */
12
+ readonly atMs: number;
13
+ /** 操作時点の URL。 */
14
+ readonly url: string;
15
+ /** 対象要素の CSS セレクタ (best-effort)。navigate では無い。 */
16
+ readonly selector?: string;
17
+ /** 対象要素の可視テキスト/ラベル (要素特定のヒント)。 */
18
+ readonly label?: string;
19
+ /** 入力値。password 等の機微フィールドは "***" にマスクされる。 */
20
+ readonly value?: string;
21
+ /** 入力フィールドが機微 (password 等) だったか。 */
22
+ readonly sensitive?: boolean;
23
+ /** click のビューポート座標。 */
24
+ readonly x?: number;
25
+ readonly y?: number;
26
+ /** keydown のキー。 */
27
+ readonly key?: string;
28
+ /** このアクション直後に保存したキーフレーム画像のファイル名。 */
29
+ readonly screenshot?: string;
30
+ }
31
+ /** 録画バンドルのメタデータ (manifest.json)。API へ JSON で送る。 */
32
+ export interface RecordingManifest {
33
+ /** スキーマバージョン。 */
34
+ readonly version: 1;
35
+ /** 録画の表示名。 */
36
+ readonly name: string;
37
+ /** 録画開始 URL。 */
38
+ readonly startUrl: string | null;
39
+ /** ISO8601 の作成時刻。 */
40
+ readonly createdAt: string;
41
+ /** 記録時のビューポート。座標スケールの基準。 */
42
+ readonly viewport: {
43
+ readonly width: number;
44
+ readonly height: number;
45
+ };
46
+ /** 記録したアクション総数。 */
47
+ readonly actionCount: number;
48
+ /** 動画ファイル名 (取得できた場合)。 */
49
+ readonly video: string | null;
50
+ /** Playwright trace.zip ファイル名 (取得できた場合)。 */
51
+ readonly trace: string | null;
52
+ /** キーフレーム画像ファイル名の一覧。 */
53
+ readonly screenshots: ReadonlyArray<string>;
54
+ }
55
+ /** 録画 1 回の成果物 (バンドル内容)。 */
56
+ export interface RecordingBundle {
57
+ readonly manifest: RecordingManifest;
58
+ readonly actions: ReadonlyArray<RecordedAction>;
59
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * デモ録画 (Demo-to-MCP / Phase 1) の型。
3
+ *
4
+ * ユーザーがローカルブラウザで実演した操作を構造化して保存し、後段 (Phase 2) で
5
+ * gemini-3.5-flash が「タスク手順書 (playbook)」へ解析するための素材にする。
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/recording/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,19 @@
1
+ export interface PendingRun {
2
+ id: string;
3
+ recording_id: string;
4
+ name: string;
5
+ /** 録画の開始 URL。ここからブラウザを開いて実行を始める。 */
6
+ start_url?: string | null;
7
+ /** 作業の目的。実行時の指示文に含める。 */
8
+ purpose?: string | null;
9
+ params: Record<string, unknown> | null;
10
+ /** 解析済み playbook (parsePlaybook で検証して使う)。 */
11
+ playbook: unknown;
12
+ }
13
+ /** 自分が実行すべき pending run の一覧 (playbook 同梱)。 */
14
+ export declare const listPendingRuns: (baseUrl: string, apiKey: string) => Promise<PendingRun[]>;
15
+ /** ローカル実行用の Gemini API キーを取得する (K1)。 */
16
+ export declare const getRunCredential: (baseUrl: string, apiKey: string) => Promise<string>;
17
+ export declare const claimRun: (baseUrl: string, apiKey: string, runId: string, hostname: string) => Promise<void>;
18
+ export declare const reportRunResult: (baseUrl: string, apiKey: string, runId: string, resultText: string) => Promise<void>;
19
+ export declare const reportRunFailed: (baseUrl: string, apiKey: string, runId: string, error: string) => Promise<void>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 録画 run-job の REST クライアント (Demo-to-MCP / ハイブリッド R2)。
3
+ *
4
+ * daemon が「自分宛の pending ジョブ取得 → claim → ローカル実行 → 結果報告」を行うための
5
+ * API 呼び出し群。Gemini 資格情報 (K1) もここで取得する。
6
+ */
7
+ import { ApiError } from './api-client.js';
8
+ const authHeaders = (apiKey) => ({
9
+ Authorization: `Bearer ${apiKey}`,
10
+ });
11
+ async function jsonOrThrow(response) {
12
+ if (!response.ok) {
13
+ throw new ApiError(response.status, await response.text().catch(() => ''));
14
+ }
15
+ return response.json();
16
+ }
17
+ /** 自分が実行すべき pending run の一覧 (playbook 同梱)。 */
18
+ export const listPendingRuns = async (baseUrl, apiKey) => {
19
+ const url = new URL('/v1/recordings/runs/pending', baseUrl).toString();
20
+ const response = await fetch(url, { headers: authHeaders(apiKey) });
21
+ return (await jsonOrThrow(response));
22
+ };
23
+ /** ローカル実行用の Gemini API キーを取得する (K1)。 */
24
+ export const getRunCredential = async (baseUrl, apiKey) => {
25
+ const url = new URL('/v1/recordings/runs/credential', baseUrl).toString();
26
+ const response = await fetch(url, { headers: authHeaders(apiKey) });
27
+ const body = (await jsonOrThrow(response));
28
+ return body.api_key;
29
+ };
30
+ const postRun = async (baseUrl, apiKey, path, body) => {
31
+ const url = new URL(path, baseUrl).toString();
32
+ const response = await fetch(url, {
33
+ method: 'POST',
34
+ headers: { ...authHeaders(apiKey), 'Content-Type': 'application/json' },
35
+ body: JSON.stringify(body),
36
+ });
37
+ await jsonOrThrow(response);
38
+ };
39
+ export const claimRun = (baseUrl, apiKey, runId, hostname) => postRun(baseUrl, apiKey, `/v1/recordings/runs/${runId}/claim`, { hostname });
40
+ export const reportRunResult = (baseUrl, apiKey, runId, resultText) => postRun(baseUrl, apiKey, `/v1/recordings/runs/${runId}/result`, {
41
+ result_text: resultText,
42
+ });
43
+ export const reportRunFailed = (baseUrl, apiKey, runId, error) => postRun(baseUrl, apiKey, `/v1/recordings/runs/${runId}/failed`, { error });
44
+ //# sourceMappingURL=run-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-client.js","sourceRoot":"","sources":["../src/run-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAe3C,MAAM,WAAW,GAAG,CAAC,MAAc,EAA0B,EAAE,CAAC,CAAC;IAC/D,aAAa,EAAE,UAAU,MAAM,EAAE;CAClC,CAAC,CAAC;AAEH,KAAK,UAAU,WAAW,CAAC,QAAkB;IAC3C,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,8CAA8C;AAC9C,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,OAAe,EAAE,MAAc,EAAyB,EAAE;IAC9F,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,6BAA6B,EAAE,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACpE,OAAO,CAAC,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAiB,CAAC;AACvD,CAAC,CAAC;AAEF,wCAAwC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,OAAe,EAAE,MAAc,EAAmB,EAAE;IACzF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,gCAAgC,EAAE,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC1E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,CAAC,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAwB,CAAC;IAClE,OAAO,IAAI,CAAC,OAAO,CAAC;AACtB,CAAC,CAAC;AAEF,MAAM,OAAO,GAAG,KAAK,EACnB,OAAe,EACf,MAAc,EACd,IAAY,EACZ,IAA6B,EACd,EAAE;IACjB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC9C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE;QACvE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;IACH,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;AAC9B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,OAAe,EACf,MAAc,EACd,KAAa,EACb,QAAgB,EACD,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,uBAAuB,KAAK,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;AAEjG,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,OAAe,EACf,MAAc,EACd,KAAa,EACb,UAAkB,EACH,EAAE,CACjB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,uBAAuB,KAAK,SAAS,EAAE;IAC9D,WAAW,EAAE,UAAU;CACxB,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,OAAe,EACf,MAAc,EACd,KAAa,EACb,KAAa,EACE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,uBAAuB,KAAK,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 録画 run-job のローカル実行ワーカー (Demo-to-MCP / ハイブリッド R2)。
3
+ *
4
+ * daemon から呼ばれ、自分宛の pending run を順に
5
+ * claim → 鍵取得(K1) → playbook をローカルブラウザ実行 → 結果/失敗を報告
6
+ * する。computer use の Gemini 呼び出しはローカルから直接行われ、スクショは
7
+ * Lnar を経由しない。
8
+ *
9
+ * 依存 (API クライアント / 実行器) は注入可能にしてテストしやすくしている。
10
+ */
11
+ import { claimRun, getRunCredential, listPendingRuns, type PendingRun, reportRunFailed, reportRunResult } from './run-client.js';
12
+ import { runPlaybook } from './runtime/runner.js';
13
+ export interface RunWorkerDeps {
14
+ listPendingRuns: typeof listPendingRuns;
15
+ claimRun: typeof claimRun;
16
+ getRunCredential: typeof getRunCredential;
17
+ reportRunResult: typeof reportRunResult;
18
+ reportRunFailed: typeof reportRunFailed;
19
+ runPlaybook: typeof runPlaybook;
20
+ log?: (message: string) => void;
21
+ }
22
+ /** pending run を 1 件実行して結果を報告する。 */
23
+ export declare function executeRun(baseUrl: string, token: string, hostname: string, run: PendingRun, deps: RunWorkerDeps): Promise<void>;
24
+ /** 自分宛の pending run をすべて実行する。実行件数を返す。 */
25
+ export declare function executePendingRuns(baseUrl: string, token: string, hostname: string, deps?: RunWorkerDeps): Promise<number>;