waygraph 0.16.0 → 0.16.2

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 (143) hide show
  1. package/dist/cli/commands/browser.d.ts +2 -0
  2. package/dist/cli/commands/browser.js +109 -0
  3. package/dist/cli/commands/codemod-stubs.js +90 -2
  4. package/dist/cli/commands/pilot.d.ts +2 -0
  5. package/dist/cli/commands/pilot.js +102 -0
  6. package/dist/cli/commands/session-ops.d.ts +3 -0
  7. package/dist/cli/commands/session-ops.js +257 -0
  8. package/dist/cli/commands/session.js +20 -461
  9. package/dist/cli/run-chain.js +2 -2
  10. package/dist/cli/usage.d.ts +1 -1
  11. package/dist/cli/util.js +1 -1
  12. package/dist/demo/narration/demo-pace.d.ts +2 -2
  13. package/dist/demo/narration/demo-pace.js +1 -1
  14. package/dist/demo/narration/device.d.ts +2 -2
  15. package/dist/demo/narration/merge.d.ts +5 -3
  16. package/dist/demo/narration/merge.js +3 -7
  17. package/dist/demo/narration/narration.d.ts +9 -13
  18. package/dist/demo/narration/narration.js +5 -17
  19. package/dist/demo/narration/stub-ctx.js +3 -3
  20. package/dist/demo/narration/todo-dock.d.ts +5 -5
  21. package/dist/demo/overlay/cursor.d.ts +9 -2
  22. package/dist/demo/overlay/cursor.js +1 -1
  23. package/dist/demo/overlay/device-stage.d.ts +15 -2
  24. package/dist/demo/overlay/inpage/banner.d.ts +2 -1
  25. package/dist/demo/overlay/inpage/banner.js +3 -3
  26. package/dist/demo/overlay/inpage/caption.d.ts +1 -1
  27. package/dist/demo/overlay/inpage/core.d.ts +2 -1
  28. package/dist/demo/overlay/inpage/core.js +1 -7
  29. package/dist/demo/overlay/inpage/device-chip.d.ts +1 -1
  30. package/dist/demo/overlay/inpage/device-chip.js +6 -6
  31. package/dist/demo/overlay/inpage/network.d.ts +3 -2
  32. package/dist/demo/overlay/inpage/network.js +1 -12
  33. package/dist/demo/overlay/inpage/panel-chrome.d.ts +1 -1
  34. package/dist/demo/overlay/inpage/panel-chrome.js +1 -1
  35. package/dist/demo/overlay/inpage/stamp.d.ts +1 -1
  36. package/dist/demo/overlay/inpage/stamp.js +1 -1
  37. package/dist/demo/overlay/inpage/swipe-zoom.d.ts +1 -1
  38. package/dist/demo/overlay/inpage/swipe-zoom.js +1 -1
  39. package/dist/demo/overlay/inpage/todos.d.ts +2 -1
  40. package/dist/demo/overlay/inpage/todos.js +1 -2
  41. package/dist/demo/overlay/network-capture.d.ts +2 -2
  42. package/dist/demo/overlay/network-capture.js +1 -1
  43. package/dist/demo/overlay/overlay-css.js +1 -55
  44. package/dist/demo/overlay/overlay-stage.d.ts +19 -11
  45. package/dist/demo/overlay/overlay.d.ts +2 -2
  46. package/dist/demo/overlay/overlay.js +3 -2
  47. package/dist/demo/overlay/rings.d.ts +38 -18
  48. package/dist/demo/overlay/rings.js +21 -62
  49. package/dist/demo/overlay/ui/overlay.css +40 -0
  50. package/dist/demo/stepper/after.js +11 -34
  51. package/dist/demo/stepper/banner-log.d.ts +4 -6
  52. package/dist/demo/stepper/banner-log.js +3 -1
  53. package/dist/demo/stepper/before.js +6 -4
  54. package/dist/demo/stepper/demo-log.d.ts +26 -9
  55. package/dist/demo/stepper/demo-log.js +10 -27
  56. package/dist/demo/stepper/interaction-patch.d.ts +3 -2
  57. package/dist/demo/stepper/interaction-patch.js +22 -34
  58. package/dist/demo/stepper/panel-after.d.ts +17 -0
  59. package/dist/demo/stepper/panel-after.js +166 -0
  60. package/dist/demo/stepper/panel-before.d.ts +12 -0
  61. package/dist/demo/stepper/panel-before.js +241 -0
  62. package/dist/demo/stepper/slides.d.ts +16 -1
  63. package/dist/demo/stepper/slides.js +21 -5
  64. package/dist/demo/stepper/step-panels.d.ts +57 -9
  65. package/dist/demo/stepper/step-panels.js +10 -427
  66. package/dist/demo/stepper/stepper.js +1 -1
  67. package/dist/demo/stepper/teardown.d.ts +4 -5
  68. package/dist/demo/stepper/teardown.js +4 -6
  69. package/dist/demo/stepper/todo-advance.d.ts +3 -2
  70. package/dist/demo/stepper/todo-advance.js +2 -2
  71. package/dist/demo/stepper/view.d.ts +4 -14
  72. package/dist/demo/types.d.ts +69 -1
  73. package/dist/demo/verbose-log.js +1 -1
  74. package/dist/engine/block/action-page.d.ts +53 -0
  75. package/dist/engine/block/action-page.js +1 -0
  76. package/dist/engine/block/block.d.ts +24 -90
  77. package/dist/engine/block/block.js +2 -77
  78. package/dist/engine/block/branch.d.ts +17 -0
  79. package/dist/engine/block/branch.js +40 -0
  80. package/dist/engine/block/dialog.d.ts +13 -0
  81. package/dist/engine/block/dialog.js +37 -0
  82. package/dist/engine/block/kinds/method.d.ts +0 -2
  83. package/dist/engine/block/kinds/method.js +0 -1
  84. package/dist/engine/block/kinds/nav.js +1 -1
  85. package/dist/engine/flow/map-branch.d.ts +9 -0
  86. package/dist/engine/flow/map-branch.js +57 -0
  87. package/dist/engine/flow/map-builder.d.ts +7 -22
  88. package/dist/engine/flow/map-builder.js +6 -145
  89. package/dist/engine/flow/map-steps.d.ts +30 -0
  90. package/dist/engine/flow/map-steps.js +87 -0
  91. package/dist/engine/run/run.d.ts +1 -19
  92. package/dist/engine/run/run.js +0 -88
  93. package/dist/engine/stub/ring.d.ts +2 -4
  94. package/dist/engine/stub/ring.js +1 -0
  95. package/dist/engine/stub/stub.d.ts +16 -8
  96. package/dist/engine/trait/trait.js +1 -1
  97. package/dist/index.d.ts +15 -12
  98. package/dist/index.js +9 -7
  99. package/dist/live/auto/auto.d.ts +3 -15
  100. package/dist/live/auto/auto.js +10 -420
  101. package/dist/live/auto/explore.d.ts +2 -2
  102. package/dist/live/auto/mem.d.ts +11 -0
  103. package/dist/live/auto/mem.js +85 -0
  104. package/dist/live/auto/menu.d.ts +30 -0
  105. package/dist/live/auto/menu.js +335 -0
  106. package/dist/live/auto-session/auto-session.d.ts +23 -103
  107. package/dist/live/auto-session/auto-session.js +100 -370
  108. package/dist/live/auto-session/console.d.ts +4 -0
  109. package/dist/live/auto-session/console.js +41 -0
  110. package/dist/live/auto-session/files.d.ts +44 -0
  111. package/dist/live/auto-session/files.js +145 -0
  112. package/dist/live/auto-session/helpers.d.ts +5 -2
  113. package/dist/live/auto-session/helpers.js +17 -0
  114. package/dist/live/auto-session/inspect.d.ts +4 -0
  115. package/dist/live/auto-session/inspect.js +75 -0
  116. package/dist/live/auto-session/ipc.d.ts +3 -22
  117. package/dist/live/auto-session/ipc.js +7 -147
  118. package/dist/live/auto-session/open-browser.d.ts +6 -0
  119. package/dist/live/auto-session/open-browser.js +47 -0
  120. package/dist/live/pilot/pilot.d.ts +1 -1
  121. package/dist/live/pilot/pilot.js +2 -1
  122. package/dist/live/traverse/traverse.js +6 -200
  123. package/dist/live/traverse/worker.d.ts +37 -0
  124. package/dist/live/traverse/worker.js +200 -0
  125. package/dist/runner/plan/find.js +2 -2
  126. package/dist/runner/plan/plan.js +1 -1
  127. package/dist/runner/plan/seed-mem.js +5 -5
  128. package/dist/runner/plan/spec.js +1 -1
  129. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  130. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +2 -1
  131. package/examples/saucedemo/src/flows/checkout.flow.ts +2 -1
  132. package/examples/saucedemo/src/flows/shop.flow.ts +2 -1
  133. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -43
  134. package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
  135. package/package.json +5 -1
  136. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  137. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +2 -1
  138. package/templates/quickstart/src/flows/checkout.flow.ts +2 -1
  139. package/templates/quickstart/src/flows/shop.flow.ts +2 -1
  140. package/templates/quickstart/src/stubs/device-showcase.ts +37 -43
  141. package/templates/quickstart/src/stubs/finish-order.ts +38 -0
  142. package/templates/scaffold/src/flows/home/hello.flow.ts +3 -0
  143. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
@@ -0,0 +1,44 @@
1
+ export interface SessionMeta {
2
+ sessionId: string;
3
+ socketPath: string;
4
+ pid: number;
5
+ projectDir: string;
6
+ startedAt: string;
7
+ headless: boolean;
8
+ }
9
+ export declare function sessionDir(projectDir: string): string;
10
+ export declare function metaPath(projectDir: string, sessionId: string): string;
11
+ /** Global index so session control works even when cwd ≠ the project that spawned the session. */
12
+ export declare function globalMetaPath(sessionId: string): string;
13
+ export declare function isPidAlive(pid: number): boolean;
14
+ /**
15
+ * Deliberately NOT under the project directory (unlike meta/log files) -
16
+ * a real bug found while proving out openspec/changes/waygraph-map's own
17
+ * "waypack" loading claim: a project loaded through a consumer's own deeply
18
+ * nested `node_modules/<pkg>` path can push the socket's absolute path past
19
+ * the OS's AF_UNIX `sun_path` limit (~108 bytes on Linux), failing with
20
+ * `listen EINVAL`. A short, fixed-location directory sidesteps this
21
+ * regardless of how deep `projectDir` itself is nested. `sessionId` alone
22
+ * (8 hex chars, `generateSessionId()`) is enough to keep this collision-safe
23
+ * across every project on the machine sharing this one flat directory - the
24
+ * same randomness space this package already relied on per-project.
25
+ */
26
+ export declare function socketDir(): string;
27
+ export declare function socketPathFor(sessionId: string): string;
28
+ export declare function logPathFor(projectDir: string, sessionId: string): string;
29
+ export declare function generateSessionId(): string;
30
+ export declare function readSessionMeta(projectDir: string, sessionId: string): SessionMeta | null;
31
+ /**
32
+ * Resolve session metadata from a project hint (cwd) or the global registry.
33
+ * Fixes "session unreachable" when the agent calls status/send from a different
34
+ * directory or pane than the one that ran `pilot start`.
35
+ */
36
+ export declare function resolveSessionMeta(sessionId: string, projectDirHint?: string): SessionMeta | null;
37
+ export declare function writeGlobalSessionMeta(meta: SessionMeta): void;
38
+ export declare function removeSessionFiles(projectDir: string, sessionId: string): void;
39
+ /** Stop one detached session (SIGTERM + cleanup metadata/socket). */
40
+ export declare function stopBrowserSession(projectDir: string, sessionId: string): boolean;
41
+ /** Stop every live session registered for a project. */
42
+ export declare function stopAllBrowserSessions(projectDir: string): number;
43
+ /** Live browser sessions for a project (metadata + socket + pid still present). */
44
+ export declare function listBrowserSessions(projectDir: string): SessionMeta[];
@@ -0,0 +1,145 @@
1
+ // Where a detached session lives on disk: its metadata (per project, plus a global copy keyed by id), its short
2
+ // socket path under the OS temp folder, its log; and listing, resolving and stopping sessions by id.
3
+ import { randomBytes } from "node:crypto";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ export function sessionDir(projectDir) {
8
+ return join(projectDir, ".waygraph-auto");
9
+ }
10
+ export function metaPath(projectDir, sessionId) {
11
+ return join(sessionDir(projectDir), `${sessionId}.json`);
12
+ }
13
+ /** Global index so session control works even when cwd ≠ the project that spawned the session. */
14
+ export function globalMetaPath(sessionId) {
15
+ return join(socketDir(), `${sessionId}.meta.json`);
16
+ }
17
+ export function isPidAlive(pid) {
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * Deliberately NOT under the project directory (unlike meta/log files) -
28
+ * a real bug found while proving out openspec/changes/waygraph-map's own
29
+ * "waypack" loading claim: a project loaded through a consumer's own deeply
30
+ * nested `node_modules/<pkg>` path can push the socket's absolute path past
31
+ * the OS's AF_UNIX `sun_path` limit (~108 bytes on Linux), failing with
32
+ * `listen EINVAL`. A short, fixed-location directory sidesteps this
33
+ * regardless of how deep `projectDir` itself is nested. `sessionId` alone
34
+ * (8 hex chars, `generateSessionId()`) is enough to keep this collision-safe
35
+ * across every project on the machine sharing this one flat directory - the
36
+ * same randomness space this package already relied on per-project.
37
+ */
38
+ export function socketDir() {
39
+ return join(tmpdir(), "waygraph-auto");
40
+ }
41
+ export function socketPathFor(sessionId) {
42
+ return join(socketDir(), `${sessionId}.sock`);
43
+ }
44
+ export function logPathFor(projectDir, sessionId) {
45
+ return join(sessionDir(projectDir), `${sessionId}.log`);
46
+ }
47
+ export function generateSessionId() {
48
+ return randomBytes(4).toString("hex");
49
+ }
50
+ export function readSessionMeta(projectDir, sessionId) {
51
+ const p = metaPath(projectDir, sessionId);
52
+ if (!existsSync(p))
53
+ return null;
54
+ try {
55
+ return JSON.parse(readFileSync(p, "utf-8"));
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Resolve session metadata from a project hint (cwd) or the global registry.
63
+ * Fixes "session unreachable" when the agent calls status/send from a different
64
+ * directory or pane than the one that ran `pilot start`.
65
+ */
66
+ export function resolveSessionMeta(sessionId, projectDirHint) {
67
+ if (projectDirHint) {
68
+ const local = readSessionMeta(projectDirHint, sessionId);
69
+ if (local)
70
+ return local;
71
+ }
72
+ const globalPath = globalMetaPath(sessionId);
73
+ if (existsSync(globalPath)) {
74
+ try {
75
+ return JSON.parse(readFileSync(globalPath, "utf-8"));
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ export function writeGlobalSessionMeta(meta) {
84
+ mkdirSync(socketDir(), { recursive: true });
85
+ writeFileSync(globalMetaPath(meta.sessionId), JSON.stringify(meta, null, 2));
86
+ }
87
+ export function removeSessionFiles(projectDir, sessionId) {
88
+ for (const p of [
89
+ metaPath(projectDir, sessionId),
90
+ globalMetaPath(sessionId),
91
+ socketPathFor(sessionId),
92
+ ]) {
93
+ try {
94
+ rmSync(p, { force: true });
95
+ }
96
+ catch {
97
+ /* ignore */
98
+ }
99
+ }
100
+ }
101
+ /** Stop one detached session (SIGTERM + cleanup metadata/socket). */
102
+ export function stopBrowserSession(projectDir, sessionId) {
103
+ const meta = resolveSessionMeta(sessionId, projectDir);
104
+ if (!meta)
105
+ return false;
106
+ try {
107
+ process.kill(meta.pid, "SIGTERM");
108
+ }
109
+ catch {
110
+ /* already gone */
111
+ }
112
+ removeSessionFiles(meta.projectDir, sessionId);
113
+ return true;
114
+ }
115
+ /** Stop every live session registered for a project. */
116
+ export function stopAllBrowserSessions(projectDir) {
117
+ const sessions = listBrowserSessions(projectDir);
118
+ for (const meta of sessions) {
119
+ stopBrowserSession(meta.projectDir, meta.sessionId);
120
+ }
121
+ return sessions.length;
122
+ }
123
+ /** Live browser sessions for a project (metadata + socket + pid still present). */
124
+ export function listBrowserSessions(projectDir) {
125
+ const dir = sessionDir(projectDir);
126
+ if (!existsSync(dir))
127
+ return [];
128
+ const out = [];
129
+ for (const f of readdirSync(dir)) {
130
+ if (!f.endsWith(".json"))
131
+ continue;
132
+ const id = f.slice(0, -".json".length);
133
+ const meta = readSessionMeta(projectDir, id);
134
+ if (!meta)
135
+ continue;
136
+ if (!existsSync(socketPathFor(id)))
137
+ continue;
138
+ if (!isPidAlive(meta.pid)) {
139
+ removeSessionFiles(projectDir, id);
140
+ continue;
141
+ }
142
+ out.push(meta);
143
+ }
144
+ return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
145
+ }
@@ -1,7 +1,8 @@
1
+ import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
1
2
  import type { StubPhaseResult } from "../../engine/stub/stub.js";
2
- import type { ExploreMenu } from "../auto/explore.js";
3
3
  import type { BlockEntry } from "../../manifest/project.js";
4
- import type { PickResult } from "../auto/auto.js";
4
+ import type { ExploreMenu } from "../auto/explore.js";
5
+ import type { PickResult } from "../auto/menu.js";
5
6
  /** True when a stub-phase result actually carries authored content worth keeping. */
6
7
  export declare function stubPhaseHasContent(r: StubPhaseResult): boolean;
7
8
  /** One available move in a session snapshot: a Block and the Checkpoint it leads to. */
@@ -211,4 +212,6 @@ export interface StorageSnapshot {
211
212
  serviceWorkers: string[];
212
213
  serviceWorkerRegistrations: ServiceWorkerRegistrationInfo[];
213
214
  }
215
+ /** What a highlight call paints, in a few words, for the session's activity line. */
216
+ export declare function describeHighlight(fixtures: PilotHighlightFixtures): string;
214
217
  export {};
@@ -1,3 +1,4 @@
1
+ // The session's shared shapes and pure helpers: snapshots, picks, trace / console entries, DOM walking, stub files.
1
2
  import { dirname, join, resolve } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  /** True when a stub-phase result actually carries authored content worth keeping. */
@@ -146,3 +147,19 @@ export function stubFilePath(kind) {
146
147
  return join(packageRoot, "assets", "stubs", names[kind]);
147
148
  }
148
149
  export const CONSOLE_LOG_MAX_ENTRIES = 200;
150
+ /** What a highlight call paints, in a few words, for the session's activity line. */
151
+ export function describeHighlight(fixtures) {
152
+ if (fixtures.clear === true)
153
+ return "clear";
154
+ return ([
155
+ fixtures.rings?.length ? `${fixtures.rings.length} ring(s)` : null,
156
+ fixtures.todos?.length ? `${fixtures.todos.length} todo(s)` : null,
157
+ fixtures.zoom && fixtures.zoom > 1.001 ? `zoom ${fixtures.zoom}` : null,
158
+ fixtures.device
159
+ ? `device ${typeof fixtures.device === "string" ? fixtures.device : fixtures.device.preset || "custom"}`
160
+ : null,
161
+ fixtures.rings?.some((r) => r.focus) ? "focus" : null,
162
+ ]
163
+ .filter(Boolean)
164
+ .join(", ") || "fixtures");
165
+ }
@@ -0,0 +1,4 @@
1
+ import type { BrowserContext, Page } from "@playwright/test";
2
+ import type { InspectDomOptions, InspectDomResult, StorageSnapshot } from "./helpers.js";
3
+ export declare function inspectDom(page: Page, opts: InspectDomOptions): Promise<InspectDomResult>;
4
+ export declare function inspectStorage(page: Page, context: BrowserContext): Promise<StorageSnapshot>;
@@ -0,0 +1,75 @@
1
+ import { showPilotActivity, showPilotVision } from "../../demo/overlay/pilot/shell.js";
2
+ import { ignorePageGone } from "../../engine/page-gone.js";
3
+ import { FULL_MODE_DEFAULT_DEPTH, FULL_MODE_MAX_NODES, FULL_MODE_MAX_TEXT_LENGTH, walkFullDom } from "./helpers.js";
4
+ export async function inspectDom(page, opts) {
5
+ const mode = opts.mode ?? "aria";
6
+ const ariaOptions = opts.depth !== undefined ? { mode: "ai", depth: opts.depth } : { mode: "ai" };
7
+ if (opts.selector) {
8
+ const locator = page.locator(opts.selector).first();
9
+ if ((await locator.count()) === 0) {
10
+ return { ok: false, error: `no element matches selector "${opts.selector}"` };
11
+ }
12
+ await showPilotVision(page, opts.selector, `dom (${mode}): ${opts.selector}`, "orange");
13
+ if (mode === "full") {
14
+ const caps = {
15
+ depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
16
+ maxNodes: FULL_MODE_MAX_NODES,
17
+ maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
18
+ };
19
+ const { tree, truncated } = await locator.evaluate(walkFullDom, caps);
20
+ return { ok: true, snapshot: { mode: "full", selector: opts.selector, truncated, tree } };
21
+ }
22
+ const tree = await locator.ariaSnapshotJSON(ariaOptions);
23
+ return { ok: true, snapshot: { mode: "aria", selector: opts.selector, truncated: false, tree } };
24
+ }
25
+ await showPilotActivity(page, `Running (Dom): ${mode}, whole page`);
26
+ if (mode === "full") {
27
+ // page.evaluate only ships the one function passed to it - it can't
28
+ // reach another named function by closure. Routing the whole-page case
29
+ // through the same `html` Locator lets both cases share one
30
+ // self-contained walkFullDom instead of two near-duplicate callbacks.
31
+ const caps = {
32
+ depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
33
+ maxNodes: FULL_MODE_MAX_NODES,
34
+ maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
35
+ };
36
+ const { tree, truncated } = await page.locator("html").first().evaluate(walkFullDom, caps);
37
+ return { ok: true, snapshot: { mode: "full", truncated, tree } };
38
+ }
39
+ const tree = await page.ariaSnapshotJSON(ariaOptions);
40
+ return { ok: true, snapshot: { mode: "aria", truncated: false, tree } };
41
+ }
42
+ export async function inspectStorage(page, context) {
43
+ const [localStorageEntries, sessionStorageEntries] = await page.evaluate(() => [
44
+ Object.entries(window.localStorage),
45
+ Object.entries(window.sessionStorage),
46
+ ]);
47
+ const cookies = await context.cookies();
48
+ const workers = context.serviceWorkers();
49
+ const registrations = await page
50
+ .evaluate(async () => {
51
+ if (!("serviceWorker" in navigator))
52
+ return [];
53
+ const regs = await navigator.serviceWorker.getRegistrations();
54
+ return regs.map((r) => ({
55
+ scope: r.scope,
56
+ ...(r.active?.scriptURL ? { activeUrl: r.active.scriptURL } : {}),
57
+ ...(r.active?.state ? { state: r.active.state } : {}),
58
+ }));
59
+ })
60
+ .catch(ignorePageGone);
61
+ return {
62
+ localStorage: Object.fromEntries(localStorageEntries),
63
+ sessionStorage: Object.fromEntries(sessionStorageEntries),
64
+ cookies: cookies.map((c) => ({
65
+ name: c.name,
66
+ value: c.value,
67
+ domain: c.domain,
68
+ path: c.path,
69
+ httpOnly: c.httpOnly,
70
+ secure: c.secure,
71
+ })),
72
+ serviceWorkers: workers.map((w) => w.url()),
73
+ serviceWorkerRegistrations: registrations ?? [],
74
+ };
75
+ }
@@ -1,14 +1,7 @@
1
- import { AutoSession } from "./auto-session.js";
2
- import { type AutoSessionInit, type SessionSnapshot, type InspectDomOptions, type DomSnapshot, type TraceStep, type ApplyPathResult, type ConsoleLogEntry, type StorageSnapshot, type StubFileKind } from "./helpers.js";
3
1
  import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
4
- export interface SessionMeta {
5
- sessionId: string;
6
- socketPath: string;
7
- pid: number;
8
- projectDir: string;
9
- startedAt: string;
10
- headless: boolean;
11
- }
2
+ import { AutoSession } from "./auto-session.js";
3
+ import { SessionMeta } from "./files.js";
4
+ import { type ApplyPathResult, type AutoSessionInit, type ConsoleLogEntry, type DomSnapshot, type InspectDomOptions, type SessionSnapshot, type StorageSnapshot, type StubFileKind, type TraceStep } from "./helpers.js";
12
5
  export type StatusOrSendResponse = {
13
6
  ok: true;
14
7
  snapshot: SessionSnapshot;
@@ -54,12 +47,6 @@ export type HighlightResponse = {
54
47
  ok: false;
55
48
  error: string;
56
49
  };
57
- /**
58
- * Resolve session metadata from a project hint (cwd) or the global registry.
59
- * Fixes "session unreachable" when the agent calls status/send from a different
60
- * directory or pane than the one that ran `pilot start`.
61
- */
62
- export declare function resolveSessionMeta(sessionId: string, projectDirHint?: string): SessionMeta | null;
63
50
  /**
64
51
  * Client-side: sends one request to a running session and returns its
65
52
  * response. Reports a stale/unreachable session as an error within
@@ -137,12 +124,6 @@ export declare function runAutoServeCommand(init: AutoSessionInit, sessionId: st
137
124
  * than re-implementing that resolution here.
138
125
  */
139
126
  export declare function spawnDetachedSession(init: AutoSessionInit, readyTimeoutMs?: number): Promise<SessionMeta>;
140
- /** Stop one detached session (SIGTERM + cleanup metadata/socket). */
141
- export declare function stopBrowserSession(projectDir: string, sessionId: string): boolean;
142
- /** Stop every live session registered for a project. */
143
- export declare function stopAllBrowserSessions(projectDir: string): number;
144
- /** Live browser sessions for a project (metadata + socket + pid still present). */
145
- export declare function listBrowserSessions(projectDir: string): SessionMeta[];
146
127
  /**
147
128
  * Client-only interactive loop against a running detached session - reopens
148
129
  * the same rendering/parsing the foreground `--cli` loop uses, driven purely
@@ -4,112 +4,17 @@
4
4
  * directory, mirroring the existing `.waygraph-traverse/` dotdir convention.
5
5
  * See openspec/changes/waygraph-auto-cli-session-control.
6
6
  */
7
- import { createServer, connect } from "node:net";
8
7
  import { spawn } from "node:child_process";
8
+ import { mkdirSync, openSync, rmSync, writeFileSync, } from "node:fs";
9
9
  import { createRequire } from "node:module";
10
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, openSync, } from "node:fs";
11
- import { join, dirname } from "node:path";
12
- import { tmpdir } from "node:os";
13
- import { fileURLToPath } from "node:url";
14
- import { randomBytes } from "node:crypto";
15
- import { createInterface } from "node:readline/promises";
10
+ import { connect, createServer } from "node:net";
11
+ import { dirname, join } from "node:path";
16
12
  import { stdin as input, stdout as output } from "node:process";
17
- import { AutoSession } from "./auto-session.js";
13
+ import { createInterface } from "node:readline/promises";
14
+ import { fileURLToPath } from "node:url";
18
15
  import { ignorePageGone } from "../../engine/page-gone.js";
19
- function sessionDir(projectDir) {
20
- return join(projectDir, ".waygraph-auto");
21
- }
22
- function metaPath(projectDir, sessionId) {
23
- return join(sessionDir(projectDir), `${sessionId}.json`);
24
- }
25
- /** Global index so session control works even when cwd ≠ the project that spawned the session. */
26
- function globalMetaPath(sessionId) {
27
- return join(socketDir(), `${sessionId}.meta.json`);
28
- }
29
- function isPidAlive(pid) {
30
- try {
31
- process.kill(pid, 0);
32
- return true;
33
- }
34
- catch {
35
- return false;
36
- }
37
- }
38
- /**
39
- * Deliberately NOT under the project directory (unlike meta/log files) -
40
- * a real bug found while proving out openspec/changes/waygraph-map's own
41
- * "waypack" loading claim: a project loaded through a consumer's own deeply
42
- * nested `node_modules/<pkg>` path can push the socket's absolute path past
43
- * the OS's AF_UNIX `sun_path` limit (~108 bytes on Linux), failing with
44
- * `listen EINVAL`. A short, fixed-location directory sidesteps this
45
- * regardless of how deep `projectDir` itself is nested. `sessionId` alone
46
- * (8 hex chars, `generateSessionId()`) is enough to keep this collision-safe
47
- * across every project on the machine sharing this one flat directory - the
48
- * same randomness space this package already relied on per-project.
49
- */
50
- function socketDir() {
51
- return join(tmpdir(), "waygraph-auto");
52
- }
53
- function socketPathFor(sessionId) {
54
- return join(socketDir(), `${sessionId}.sock`);
55
- }
56
- function logPathFor(projectDir, sessionId) {
57
- return join(sessionDir(projectDir), `${sessionId}.log`);
58
- }
59
- function generateSessionId() {
60
- return randomBytes(4).toString("hex");
61
- }
62
- function readSessionMeta(projectDir, sessionId) {
63
- const p = metaPath(projectDir, sessionId);
64
- if (!existsSync(p))
65
- return null;
66
- try {
67
- return JSON.parse(readFileSync(p, "utf-8"));
68
- }
69
- catch {
70
- return null;
71
- }
72
- }
73
- /**
74
- * Resolve session metadata from a project hint (cwd) or the global registry.
75
- * Fixes "session unreachable" when the agent calls status/send from a different
76
- * directory or pane than the one that ran `pilot start`.
77
- */
78
- export function resolveSessionMeta(sessionId, projectDirHint) {
79
- if (projectDirHint) {
80
- const local = readSessionMeta(projectDirHint, sessionId);
81
- if (local)
82
- return local;
83
- }
84
- const globalPath = globalMetaPath(sessionId);
85
- if (existsSync(globalPath)) {
86
- try {
87
- return JSON.parse(readFileSync(globalPath, "utf-8"));
88
- }
89
- catch {
90
- return null;
91
- }
92
- }
93
- return null;
94
- }
95
- function writeGlobalSessionMeta(meta) {
96
- mkdirSync(socketDir(), { recursive: true });
97
- writeFileSync(globalMetaPath(meta.sessionId), JSON.stringify(meta, null, 2));
98
- }
99
- function removeSessionFiles(projectDir, sessionId) {
100
- for (const p of [
101
- metaPath(projectDir, sessionId),
102
- globalMetaPath(sessionId),
103
- socketPathFor(sessionId),
104
- ]) {
105
- try {
106
- rmSync(p, { force: true });
107
- }
108
- catch {
109
- /* ignore */
110
- }
111
- }
112
- }
16
+ import { AutoSession } from "./auto-session.js";
17
+ import { generateSessionId, isPidAlive, logPathFor, metaPath, readSessionMeta, removeSessionFiles, resolveSessionMeta, sessionDir, socketDir, socketPathFor, writeGlobalSessionMeta } from "./files.js";
113
18
  /** Reads newline-delimited JSON off a socket, one message at a time. */
114
19
  function readOneMessage(sock, timeoutMs) {
115
20
  return new Promise((resolvePromise, reject) => {
@@ -412,51 +317,6 @@ export async function spawnDetachedSession(init, readyTimeoutMs = 20_000) {
412
317
  throw new Error(`session "${sessionId}" said it was ready but wrote no metadata - check ${log}`);
413
318
  return meta;
414
319
  }
415
- /** Stop one detached session (SIGTERM + cleanup metadata/socket). */
416
- export function stopBrowserSession(projectDir, sessionId) {
417
- const meta = resolveSessionMeta(sessionId, projectDir);
418
- if (!meta)
419
- return false;
420
- try {
421
- process.kill(meta.pid, "SIGTERM");
422
- }
423
- catch {
424
- /* already gone */
425
- }
426
- removeSessionFiles(meta.projectDir, sessionId);
427
- return true;
428
- }
429
- /** Stop every live session registered for a project. */
430
- export function stopAllBrowserSessions(projectDir) {
431
- const sessions = listBrowserSessions(projectDir);
432
- for (const meta of sessions) {
433
- stopBrowserSession(meta.projectDir, meta.sessionId);
434
- }
435
- return sessions.length;
436
- }
437
- /** Live browser sessions for a project (metadata + socket + pid still present). */
438
- export function listBrowserSessions(projectDir) {
439
- const dir = sessionDir(projectDir);
440
- if (!existsSync(dir))
441
- return [];
442
- const out = [];
443
- for (const f of readdirSync(dir)) {
444
- if (!f.endsWith(".json"))
445
- continue;
446
- const id = f.slice(0, -".json".length);
447
- const meta = readSessionMeta(projectDir, id);
448
- if (!meta)
449
- continue;
450
- if (!existsSync(socketPathFor(id)))
451
- continue;
452
- if (!isPidAlive(meta.pid)) {
453
- removeSessionFiles(projectDir, id);
454
- continue;
455
- }
456
- out.push(meta);
457
- }
458
- return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
459
- }
460
320
  function printSnapshotMenu(snapshot) {
461
321
  console.log("");
462
322
  console.log(`You are here: ${snapshot.here ?? "Unknown (pick Start here)"}`);
@@ -0,0 +1,6 @@
1
+ import type { BrowserContext, Page } from "@playwright/test";
2
+ export declare function openSessionBrowser(projectDir: string, headless: boolean, baseURL: string | undefined, startUrl: string | undefined): Promise<{
3
+ closeBrowser: () => Promise<void>;
4
+ context: BrowserContext;
5
+ page: Page;
6
+ }>;
@@ -0,0 +1,47 @@
1
+ import { instrumentPilotActions } from "../../demo/overlay/pilot/actions.js";
2
+ import { installPersistentPilotOverlay } from "../../demo/overlay/pilot/shell.js";
3
+ import { ignorePageGone, ignoreTimeout } from "../../engine/page-gone.js";
4
+ import { chromiumExecutablePath, launchContext } from "../../runtime/browser.js";
5
+ import { configuredExtensions } from "../../runtime/waygraph-config.js";
6
+ export async function openSessionBrowser(projectDir, headless, baseURL, startUrl) {
7
+ const { chromium } = await import("@playwright/test");
8
+ const executablePath = chromiumExecutablePath();
9
+ // Real gap found live: a headed (--non-headless) session kept a fixed
10
+ // 1280x720 content area even when the user maximized/fullscreened the
11
+ // actual window - `viewport: null` alone only makes the PAGE track the
12
+ // window; the window itself still launches at Chromium's own small
13
+ // default size unless told to start maximized. Same fix cli.ts's own
14
+ // --step/headed launch already uses (see its own comment there) -
15
+ // headless keeps the fixed viewport since there's no real window to size.
16
+ const launchOpts = {
17
+ headless,
18
+ // Keep Playwright's default --no-startup-window for headful: without it
19
+ // Chromium opens its own "New Tab" window AND our context.newPage() opens
20
+ // a second about:blank window. bringToFront() below surfaces the real page.
21
+ args: headless ? [] : ["--start-maximized"],
22
+ };
23
+ if (executablePath)
24
+ launchOpts.executablePath = executablePath;
25
+ const contextOpts = {
26
+ viewport: headless ? { width: 1280, height: 720 } : null,
27
+ };
28
+ if (baseURL)
29
+ contextOpts.baseURL = baseURL;
30
+ const { close: closeBrowser, context } = await launchContext({
31
+ launcher: chromium,
32
+ launch: launchOpts,
33
+ context: contextOpts,
34
+ extensions: await configuredExtensions(projectDir),
35
+ });
36
+ await installPersistentPilotOverlay(context);
37
+ const page = await context.newPage();
38
+ instrumentPilotActions(page, headless ? 0 : 450);
39
+ if (startUrl) {
40
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(ignorePageGone);
41
+ await page.waitForLoadState("load").catch(ignoreTimeout);
42
+ }
43
+ else {
44
+ await page.goto("about:blank").catch(ignorePageGone);
45
+ }
46
+ return { closeBrowser, context, page };
47
+ }
@@ -29,8 +29,8 @@
29
29
  * new execution primitive.
30
30
  */
31
31
  import { type WaygraphGraph } from "../../analysis/graph.js";
32
- import { type BrowserStartInit } from "../auto-session/start.js";
33
32
  import type { SessionSnapshot } from "../auto-session/helpers.js";
33
+ import { type BrowserStartInit } from "../auto-session/start.js";
34
34
  /** What `pilotStart` returns: the session id, socket path and first snapshot. */
35
35
  export interface PilotStartResult {
36
36
  sessionId: string;
@@ -29,7 +29,8 @@
29
29
  * new execution primitive.
30
30
  */
31
31
  import { discoverGraph } from "../../analysis/graph.js";
32
- import { requestSession, resolveSessionMeta, } from "../auto-session/ipc.js";
32
+ import { resolveSessionMeta } from "../auto-session/files.js";
33
+ import { requestSession, } from "../auto-session/ipc.js";
33
34
  import { browserStart } from "../auto-session/start.js";
34
35
  /**
35
36
  * Starts a detached session and reads back its own graph + starting