waygraph 0.15.11 → 0.15.15
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.
- package/README.md +67 -26
- package/dist/auto-explore.d.ts +7 -6
- package/dist/auto-explore.js +43 -32
- package/dist/auto-session-ipc.d.ts +13 -0
- package/dist/auto-session-ipc.js +119 -7
- package/dist/auto-session.d.ts +16 -13
- package/dist/auto-session.js +72 -53
- package/dist/block-inject.d.ts +15 -0
- package/dist/block-inject.js +91 -0
- package/dist/browser.d.ts +15 -0
- package/dist/browser.js +29 -0
- package/dist/cli.js +335 -47
- package/dist/coverage-gap.d.ts +11 -0
- package/dist/coverage-gap.js +119 -0
- package/dist/graph.d.ts +1 -0
- package/dist/graph.js +99 -91
- package/dist/pilot-overlay.d.ts +7 -1
- package/dist/pilot-overlay.js +59 -41
- package/dist/pilot.d.ts +7 -2
- package/dist/pilot.js +34 -7
- package/dist/practices-check.d.ts +14 -0
- package/dist/practices-check.js +115 -0
- package/examples/saucedemo/.waygraph-auto/0b937f26.log +4 -0
- package/examples/saucedemo/.waygraph-auto/2b608ed3.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2c13567b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2d9622a5.log +4 -0
- package/examples/saucedemo/.waygraph-auto/305f6e5b.log +4 -0
- package/examples/saucedemo/.waygraph-auto/418befbf.log +4 -0
- package/examples/saucedemo/.waygraph-auto/483bd763.log +4 -0
- package/examples/saucedemo/.waygraph-auto/48e3596a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4adfbe91.json +8 -0
- package/examples/saucedemo/.waygraph-auto/4adfbe91.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4f0e33e7.log +4 -0
- package/examples/saucedemo/.waygraph-auto/5bd8fedf.log +4 -0
- package/examples/saucedemo/.waygraph-auto/5dde8331.log +4 -0
- package/examples/saucedemo/.waygraph-auto/68903903.log +0 -0
- package/examples/saucedemo/.waygraph-auto/78bbf43c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7efa646f.log +4 -0
- package/examples/saucedemo/.waygraph-auto/9cecb0c8.log +4 -0
- package/examples/saucedemo/.waygraph-auto/aae57d51.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b61a08d8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c946f66c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cd0d5b50.log +4 -0
- package/examples/saucedemo/.waygraph-auto/cd5b203c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ea2df316.log +4 -0
- package/examples/saucedemo/.waygraph-auto/ee60ed24.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f2b6cb1b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f392cdc8.log +0 -0
- package/package.json +2 -2
- package/templates/scaffold/package.json +1 -1
- package/templates/skills/waygraph-blind-pilot.skill.md +30 -15
- package/templates/skills/waygraph-convention.skill.md +11 -1
- package/templates/skills/waygraph-pilot.skill.md +71 -4
package/README.md
CHANGED
|
@@ -296,6 +296,13 @@ nothing is blocked, nothing fails to build. `waygraph check [project]` is the
|
|
|
296
296
|
complementary whole-project sweep for contexts with no editor watching (CI, generated
|
|
297
297
|
code, an autonomous agent authoring Blocks) - it warns, never fails the process.
|
|
298
298
|
|
|
299
|
+
**`waygraph typecheck [project]`** runs `tsc --noEmit` then the same **bad-practice** scan
|
|
300
|
+
(wildcard `Checkpoint<string>`, assert blocks missing type args, multi-`.fill()` Methods,
|
|
301
|
+
fill+click in one Method). Practices warn only unless `tsc` fails; use `--no-practices` to
|
|
302
|
+
skip the scan. Opt out per file: `// waygraph-ignore-practices` or
|
|
303
|
+
`// waygraph-ignore: multi-input, combined-action`. Print full agent guidance:
|
|
304
|
+
`waygraph --skill-convention`.
|
|
305
|
+
|
|
299
306
|
### Demo narration
|
|
300
307
|
|
|
301
308
|
Automation rings are gray; authored stubs/slides can use
|
|
@@ -662,27 +669,52 @@ space is constrained to this project's own pre-verified Blocks (each with a real
|
|
|
662
669
|
already checked) instead of freeform DOM/pixel guessing - narrower, but far more accurate and
|
|
663
670
|
deterministic.
|
|
664
671
|
|
|
672
|
+
### auto vs browser vs pilot
|
|
673
|
+
|
|
674
|
+
| Layer | Command | What it is |
|
|
675
|
+
|-------|---------|------------|
|
|
676
|
+
| **auto** | `waygraph auto` | Interactive explore (picker / `--cli` menu). **Not** a persistent agent browser. |
|
|
677
|
+
| **browser** | `waygraph browser start` | New persistent Playwright session — **headful by default**, **about:blank by default**. |
|
|
678
|
+
| **pilot** | `waygraph pilot start` | Bootstrap only: `browser start` + whole-project `graph` + starting `snapshot` in one JSON payload. Does not plan or act. |
|
|
679
|
+
|
|
680
|
+
Session control (`send`, `status`, `highlight`, `dom`, …) works on **`auto`**, **`browser`**, and
|
|
681
|
+
**`pilot`** — same socket, same session id. Prefer the `browser`/`pilot` prefix when driving a
|
|
682
|
+
detached session; `auto` remains for the interactive picker and legacy `--cli --detach`.
|
|
683
|
+
|
|
684
|
+
**Session lifecycle is explicit** — list what's live before opening another:
|
|
685
|
+
|
|
665
686
|
```bash
|
|
666
|
-
waygraph
|
|
687
|
+
waygraph browser # show browser subcommands
|
|
688
|
+
waygraph browser sessions [--json] # list live sessions; --json for agents
|
|
689
|
+
waygraph browser start # always opens a new session
|
|
690
|
+
waygraph browser stop <sessionId> # shut one down
|
|
691
|
+
waygraph browser stop --all # shut every session for this project
|
|
692
|
+
waygraph browser attach <sessionId> # terminal picker on an existing session
|
|
693
|
+
waygraph pilot attach <sessionId> # graph + snapshot, no new browser
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
waygraph pilot start
|
|
667
698
|
# {"sessionId":"bb65d74c","socketPath":"...","headless":false,"graph":{...},"snapshot":{...}}
|
|
668
699
|
```
|
|
669
700
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
session
|
|
675
|
-
before Pilot did:
|
|
701
|
+
`pilot start` is one round trip for agents that need the whole graph immediately. For browser-only
|
|
702
|
+
work (no graph payload), use `browser start`. `auto --cli --detach` still works but is the older
|
|
703
|
+
explore entry point — prefer `browser start` for agent-driven sessions.
|
|
704
|
+
|
|
705
|
+
From a live session, drive one pick at a time:
|
|
676
706
|
|
|
677
707
|
```bash
|
|
678
|
-
waygraph
|
|
679
|
-
waygraph
|
|
680
|
-
waygraph
|
|
681
|
-
waygraph
|
|
682
|
-
waygraph
|
|
683
|
-
waygraph
|
|
708
|
+
waygraph browser status <sessionId> # read the current menu (no side effects)
|
|
709
|
+
waygraph browser send <sessionId> "<pick>" # run one pick, get the resulting menu back
|
|
710
|
+
waygraph browser dom <sessionId> # inspect the live DOM/ARIA tree
|
|
711
|
+
waygraph browser trace <sessionId> # read the Checkpoint/Block history so far
|
|
712
|
+
waygraph browser reach <sessionId> <Checkpoint> # run a whole multi-step route in one call
|
|
713
|
+
waygraph browser resync <sessionId> # force here to re-detect from the real page
|
|
684
714
|
```
|
|
685
715
|
|
|
716
|
+
(`auto …` and `pilot …` accept the same subcommands with the same `<sessionId>`.)
|
|
717
|
+
|
|
686
718
|
Worked example (run for real against `examples/saucedemo`, no new execution code involved): an
|
|
687
719
|
agent gets a plain request - "log me in and buy the backpack" - reads the graph and the starting
|
|
688
720
|
`status`, reasons out login -> add-to-cart -> cart -> checkout info -> submit -> finish as the
|
|
@@ -718,16 +750,18 @@ outright rather than left as a caveat.
|
|
|
718
750
|
|
|
719
751
|
**On-page indicator.** A headful (`--non-headless`) session looks like an ordinary browser
|
|
720
752
|
tab otherwise - no sign anything is driving it, which matters once a human might be watching
|
|
721
|
-
or co-driving the same visible session. Every session
|
|
753
|
+
or co-driving the same visible session. Every session injects a small badge (bottom-right
|
|
722
754
|
corner: `Waygraph Pilot - session <id> - <Checkpoint>`) that click-expands into a panel
|
|
723
755
|
listing the current live menu's real edges (block name, kind, target Checkpoint,
|
|
724
|
-
description) - the same data `auto status` returns, rendered in the page itself.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
756
|
+
description) - the same data `auto status` returns, rendered in the page itself. The overlay
|
|
757
|
+
shell is installed via `context.addInitScript`, so it **persists on `about:blank`** and
|
|
758
|
+
survives `goto` navigations (browser start defaults to blank; pilot navigates when a base URL
|
|
759
|
+
exists). Headful sessions paint the badge immediately on start — no need to call `status`
|
|
760
|
+
first. Bottom-left activity toasts label raw primitives as **`Running (no Block): …`**
|
|
761
|
+
(click/type/goto/upload outside the Block library); Block runs stay **`Running (Block): …`**.
|
|
762
|
+
Both refresh automatically after every `send`/`reach`/`reload`/`resync`/raw action.
|
|
763
|
+
Self-contained (`src/pilot-overlay.ts`), not built on `waygraph demo`'s own panel/banner
|
|
764
|
+
system.
|
|
731
765
|
|
|
732
766
|
**Pilot itself does no planning, resolving, narrating, or acting** - `pilotStart`'s only job is
|
|
733
767
|
starting the session and handing back its graph in one round trip; see `src/pilot.ts`. An
|
|
@@ -1021,17 +1055,24 @@ Flows are files (0.10.5+):
|
|
|
1021
1055
|
| Ad-hoc Blocks | `waygraph run --blocks "login then nav-cart" --data '{...}'` |
|
|
1022
1056
|
| Headed execute | `waygraph run --blocks shopFlow --non-headless --video` |
|
|
1023
1057
|
| Explore | `waygraph auto` / `waygraph auto --cli` |
|
|
1058
|
+
| Browser command reference | `waygraph browser` |
|
|
1059
|
+
| List live browser sessions | `waygraph browser sessions [--json]` |
|
|
1060
|
+
| Open a new persistent browser | `waygraph browser start` (headful, about:blank by default) |
|
|
1061
|
+
| Stop a browser session | `waygraph browser stop <sessionId>` / `stop --all` |
|
|
1062
|
+
| Pilot bootstrap (browser + graph + snapshot) | `waygraph pilot start` |
|
|
1063
|
+
| Attach to existing session (graph only) | `waygraph pilot attach <sessionId>` |
|
|
1024
1064
|
| Path-find | `waygraph auto --blocks LoginPage OrderComplete` |
|
|
1025
|
-
| Run `--cli` as a background session | `waygraph auto --cli --detach` -> `{sessionId, socketPath}` |
|
|
1026
|
-
| Drive a detached session (no TTY) | `waygraph
|
|
1027
|
-
| Read a detached session's state | `waygraph
|
|
1028
|
-
| Reattach a terminal to a detached session | `waygraph
|
|
1065
|
+
| Run `--cli` as a background session (legacy) | `waygraph auto --cli --detach` -> `{sessionId, socketPath}` |
|
|
1066
|
+
| Drive a detached session (no TTY) | `waygraph browser send <sessionId> "<pick>"` -> JSON state |
|
|
1067
|
+
| Read a detached session's state | `waygraph browser status <sessionId>` (no side effects) |
|
|
1068
|
+
| Reattach a terminal to a detached session | `waygraph browser attach <sessionId>` |
|
|
1029
1069
|
| Read the live page (aria, small/default) | `waygraph auto dom <sessionId>` |
|
|
1030
1070
|
| Read the live page (raw DOM, bounded) | `waygraph auto dom <sessionId> --mode full` |
|
|
1031
1071
|
| Scope either to one element | `waygraph auto dom <sessionId> --selector ".inventory_list"` |
|
|
1032
1072
|
| Detached session with a real visible browser | `waygraph auto --cli --detach --non-headless` |
|
|
1033
1073
|
| Read the session's Checkpoint/Block history | `waygraph auto trace <sessionId>` |
|
|
1034
1074
|
| Whole-project nav-escape + inline-selector + orphan sweep | `waygraph check [project]` |
|
|
1075
|
+
| `tsc --noEmit` + bad-practice warnings | `waygraph typecheck [project]` (`--no-practices` to skip scan) |
|
|
1035
1076
|
| Waygraph Map enforcement (verbatim url-vs-folder, exits 1 on a violation) | `waygraph map [project]` |
|
|
1036
1077
|
| Graph crawl | `waygraph traverse [project] --parallel N --min-edge-coverage 80%` |
|
|
1037
1078
|
| Coding-agent defs | `waygraph agent-dive --loop claude` |
|
|
@@ -1165,7 +1206,7 @@ it out from under you.
|
|
|
1165
1206
|
git clone git@github.com:deviate-dv8/waygraph.git
|
|
1166
1207
|
cd waygraph
|
|
1167
1208
|
npm install
|
|
1168
|
-
npm run typecheck # tsc --noEmit
|
|
1209
|
+
npm run typecheck # waygraph typecheck . (tsc --noEmit + practice warnings)
|
|
1169
1210
|
npm run test # playwright test
|
|
1170
1211
|
npm run build # tsc -p tsconfig.build.json, emits dist/
|
|
1171
1212
|
```
|
package/dist/auto-explore.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Page } from "@playwright/test";
|
|
2
2
|
import type { Block, Checkpoint, WaygraphInstanceOption } from "./types.js";
|
|
3
3
|
import { type WaygraphGraph, type WaygraphEdge } from "./graph.js";
|
|
4
|
+
export interface ExploreContextOpts {
|
|
5
|
+
blocksSelect?: import("./blocks-select.js").BlocksSelect;
|
|
6
|
+
/** Absolute paths of extra Block trees to merge (host projectDir is always first). */
|
|
7
|
+
inject?: string[];
|
|
8
|
+
}
|
|
4
9
|
export interface BlockEntry {
|
|
5
10
|
block: Block<Checkpoint<string>, Checkpoint<string>>;
|
|
6
11
|
exportName: string;
|
|
@@ -32,9 +37,7 @@ export interface ExploreMenu {
|
|
|
32
37
|
/**
|
|
33
38
|
* Import every `*.block.ts` export keyed by runtime Block `.name`.
|
|
34
39
|
*/
|
|
35
|
-
export declare function loadBlockLibrary(projectDir: string, opts?: {
|
|
36
|
-
blocksSelect?: import("./blocks-select.js").BlocksSelect;
|
|
37
|
-
}): Promise<{
|
|
40
|
+
export declare function loadBlockLibrary(projectDir: string, opts?: ExploreContextOpts): Promise<{
|
|
38
41
|
byName: Map<string, BlockEntry>;
|
|
39
42
|
navBlocks: BlockEntry[];
|
|
40
43
|
}>;
|
|
@@ -48,9 +51,7 @@ export declare function exploreChoices(graph: WaygraphGraph, here: string | null
|
|
|
48
51
|
export declare function buildExploreMenu(page: Page, graph: WaygraphGraph, library: {
|
|
49
52
|
byName: Map<string, BlockEntry>;
|
|
50
53
|
}, here: string | null): Promise<ExploreMenu>;
|
|
51
|
-
export declare function buildExploreContext(projectDir: string, opts?: {
|
|
52
|
-
blocksSelect?: import("./blocks-select.js").BlocksSelect;
|
|
53
|
-
}): Promise<{
|
|
54
|
+
export declare function buildExploreContext(projectDir: string, opts?: ExploreContextOpts): Promise<{
|
|
54
55
|
graph: WaygraphGraph;
|
|
55
56
|
library: Awaited<ReturnType<typeof loadBlockLibrary>>;
|
|
56
57
|
}>;
|
package/dist/auto-explore.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import { discoverGraph } from "./graph.js";
|
|
4
|
+
import { exploreRoots, injectTaggedFile } from "./block-inject.js";
|
|
4
5
|
function walkDir(dir, pattern) {
|
|
5
6
|
const results = [];
|
|
6
7
|
let entries;
|
|
@@ -105,42 +106,52 @@ export async function loadBlockLibrary(projectDir, opts) {
|
|
|
105
106
|
const { matchBlocksSelect } = await import("./blocks-select.js");
|
|
106
107
|
const byName = new Map();
|
|
107
108
|
const navBlocks = [];
|
|
109
|
+
const navSeen = new Set();
|
|
108
110
|
const select = opts?.blocksSelect;
|
|
109
|
-
for (const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let mod;
|
|
116
|
-
try {
|
|
117
|
-
mod = await importModule(file);
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
-
if (/second time|playwright/i.test(msg)) {
|
|
122
|
-
console.error(`waygraph auto: skip ${relFile}: ${msg.split("\n")[0]}`);
|
|
123
|
-
}
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
for (const [exportName, exported] of Object.entries(mod)) {
|
|
127
|
-
if (!isBlockLike(exported))
|
|
111
|
+
for (const root of exploreRoots(projectDir, opts?.inject)) {
|
|
112
|
+
for (const file of walkDir(root, /\.block\.ts$/)) {
|
|
113
|
+
const relFile = relative(root, file).replace(/\\/g, "/");
|
|
114
|
+
const taggedFile = injectTaggedFile(projectDir, root, relFile);
|
|
115
|
+
// Glob/bare: path filter before import. Regex: need block name — filter after.
|
|
116
|
+
if (select && select.kind !== "regex" && !matchBlocksSelect(select, { relFile })) {
|
|
128
117
|
continue;
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
}
|
|
119
|
+
let mod;
|
|
120
|
+
try {
|
|
121
|
+
mod = await importModule(file);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
125
|
+
if (/second time|playwright/i.test(msg)) {
|
|
126
|
+
console.error(`waygraph auto: skip ${relFile}: ${msg.split("\n")[0]}`);
|
|
127
|
+
}
|
|
131
128
|
continue;
|
|
132
129
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
130
|
+
for (const [exportName, exported] of Object.entries(mod)) {
|
|
131
|
+
if (!isBlockLike(exported))
|
|
132
|
+
continue;
|
|
133
|
+
if (select &&
|
|
134
|
+
!matchBlocksSelect(select, { relFile, blockName: exported.name })) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const kind = isNavBlockMarked(exported) ? "nav" : "action";
|
|
138
|
+
const entry = {
|
|
139
|
+
block: exported,
|
|
140
|
+
exportName,
|
|
141
|
+
file: taggedFile,
|
|
142
|
+
kind,
|
|
143
|
+
description: typeof exported.description === "string" ? exported.description : "",
|
|
144
|
+
};
|
|
145
|
+
if (byName.has(exported.name)) {
|
|
146
|
+
console.error(`waygraph: inject skipped duplicate Block name "${exported.name}" (${taggedFile})`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
byName.set(exported.name, entry);
|
|
150
|
+
if (kind === "nav" && !navSeen.has(exported.name)) {
|
|
151
|
+
navSeen.add(exported.name);
|
|
152
|
+
navBlocks.push(entry);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
navBlocks.sort((a, b) => {
|
|
@@ -54,6 +54,13 @@ export type HighlightResponse = {
|
|
|
54
54
|
ok: false;
|
|
55
55
|
error: string;
|
|
56
56
|
};
|
|
57
|
+
export declare function readSessionMeta(projectDir: string, sessionId: string): SessionMeta | null;
|
|
58
|
+
/**
|
|
59
|
+
* Resolve session metadata from a project hint (cwd) or the global registry.
|
|
60
|
+
* Fixes "session unreachable" when the agent calls status/send from a different
|
|
61
|
+
* directory or pane than the one that ran `pilot start`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveSessionMeta(sessionId: string, projectDirHint?: string): SessionMeta | null;
|
|
57
64
|
/**
|
|
58
65
|
* Client-side: sends one request to a running session and returns its
|
|
59
66
|
* response. Reports a stale/unreachable session as an error within
|
|
@@ -129,6 +136,12 @@ export declare function runAutoServeCommand(init: AutoSessionInit, sessionId: st
|
|
|
129
136
|
* than re-implementing that resolution here.
|
|
130
137
|
*/
|
|
131
138
|
export declare function spawnDetachedSession(init: AutoSessionInit, readyTimeoutMs?: number): Promise<SessionMeta>;
|
|
139
|
+
/** Stop one detached session (SIGTERM + cleanup metadata/socket). */
|
|
140
|
+
export declare function stopBrowserSession(projectDir: string, sessionId: string): boolean;
|
|
141
|
+
/** Stop every live session registered for a project. */
|
|
142
|
+
export declare function stopAllBrowserSessions(projectDir: string): number;
|
|
143
|
+
/** Live browser sessions for a project (metadata + socket + pid still present). */
|
|
144
|
+
export declare function listBrowserSessions(projectDir: string): SessionMeta[];
|
|
132
145
|
/**
|
|
133
146
|
* Client-only interactive loop against a running detached session - reopens
|
|
134
147
|
* the same rendering/parsing the foreground `--cli` loop uses, driven purely
|
package/dist/auto-session-ipc.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createServer, connect } from "node:net";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import {
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, openSync, } from "node:fs";
|
|
10
11
|
import { join, dirname } from "node:path";
|
|
11
12
|
import { tmpdir } from "node:os";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -20,6 +21,19 @@ function sessionDir(projectDir) {
|
|
|
20
21
|
function metaPath(projectDir, sessionId) {
|
|
21
22
|
return join(sessionDir(projectDir), `${sessionId}.json`);
|
|
22
23
|
}
|
|
24
|
+
/** Global index so session control works even when cwd ≠ the project that spawned the session. */
|
|
25
|
+
function globalMetaPath(sessionId) {
|
|
26
|
+
return join(socketDir(), `${sessionId}.meta.json`);
|
|
27
|
+
}
|
|
28
|
+
function isPidAlive(pid) {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* Deliberately NOT under the project directory (unlike meta/log files) -
|
|
25
39
|
* a real bug found while proving out openspec/changes/waygraph-map's own
|
|
@@ -44,7 +58,7 @@ function logPathFor(projectDir, sessionId) {
|
|
|
44
58
|
function generateSessionId() {
|
|
45
59
|
return randomBytes(4).toString("hex");
|
|
46
60
|
}
|
|
47
|
-
function readSessionMeta(projectDir, sessionId) {
|
|
61
|
+
export function readSessionMeta(projectDir, sessionId) {
|
|
48
62
|
const p = metaPath(projectDir, sessionId);
|
|
49
63
|
if (!existsSync(p))
|
|
50
64
|
return null;
|
|
@@ -55,9 +69,36 @@ function readSessionMeta(projectDir, sessionId) {
|
|
|
55
69
|
return null;
|
|
56
70
|
}
|
|
57
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve session metadata from a project hint (cwd) or the global registry.
|
|
74
|
+
* Fixes "session unreachable" when the agent calls status/send from a different
|
|
75
|
+
* directory or pane than the one that ran `pilot start`.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveSessionMeta(sessionId, projectDirHint) {
|
|
78
|
+
if (projectDirHint) {
|
|
79
|
+
const local = readSessionMeta(projectDirHint, sessionId);
|
|
80
|
+
if (local)
|
|
81
|
+
return local;
|
|
82
|
+
}
|
|
83
|
+
const globalPath = globalMetaPath(sessionId);
|
|
84
|
+
if (existsSync(globalPath)) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(globalPath, "utf-8"));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function writeGlobalSessionMeta(meta) {
|
|
95
|
+
mkdirSync(socketDir(), { recursive: true });
|
|
96
|
+
writeFileSync(globalMetaPath(meta.sessionId), JSON.stringify(meta, null, 2));
|
|
97
|
+
}
|
|
58
98
|
function removeSessionFiles(projectDir, sessionId) {
|
|
59
99
|
for (const p of [
|
|
60
100
|
metaPath(projectDir, sessionId),
|
|
101
|
+
globalMetaPath(sessionId),
|
|
61
102
|
socketPathFor(sessionId),
|
|
62
103
|
]) {
|
|
63
104
|
try {
|
|
@@ -110,9 +151,20 @@ function readOneMessage(sock, timeoutMs) {
|
|
|
110
151
|
});
|
|
111
152
|
}
|
|
112
153
|
export async function requestSession(projectDir, sessionId, request, timeoutMs = 15_000) {
|
|
113
|
-
const meta =
|
|
154
|
+
const meta = resolveSessionMeta(sessionId, projectDir);
|
|
114
155
|
if (!meta) {
|
|
115
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: `no such session "${sessionId}" (no metadata under ${projectDir}/.waygraph-auto/ or global registry) — ` +
|
|
159
|
+
"run from the project that started the session, or `waygraph pilot sessions` to list live ids",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!isPidAlive(meta.pid)) {
|
|
163
|
+
removeSessionFiles(meta.projectDir, sessionId);
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: `session "${sessionId}" is not running (pid ${meta.pid} exited) — start a new session with pilot/browser start`,
|
|
167
|
+
};
|
|
116
168
|
}
|
|
117
169
|
return new Promise((resolvePromise) => {
|
|
118
170
|
const sock = connect(meta.socketPath);
|
|
@@ -283,6 +335,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
|
|
|
283
335
|
headless,
|
|
284
336
|
};
|
|
285
337
|
writeFileSync(metaPath(projectDir, sessionId), JSON.stringify(meta, null, 2));
|
|
338
|
+
writeGlobalSessionMeta(meta);
|
|
286
339
|
process.on("SIGTERM", () => void shutdown());
|
|
287
340
|
process.on("SIGINT", () => void shutdown());
|
|
288
341
|
}
|
|
@@ -305,17 +358,31 @@ export async function runAutoServeCommand(init, sessionId) {
|
|
|
305
358
|
export async function spawnDetachedSession(init, readyTimeoutMs = 20_000) {
|
|
306
359
|
const sessionId = generateSessionId();
|
|
307
360
|
mkdirSync(sessionDir(init.projectDir), { recursive: true });
|
|
308
|
-
const
|
|
361
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
362
|
+
const require = createRequire(join(pkgRoot, "package.json"));
|
|
363
|
+
const tsxEsm = require.resolve("tsx/esm");
|
|
364
|
+
const cli = join(pkgRoot, "dist", "cli.js");
|
|
309
365
|
const logFd = openSync(logPathFor(init.projectDir, sessionId), "a");
|
|
310
|
-
const args = ["__auto-serve", init.projectDir, "--session-id", sessionId];
|
|
366
|
+
const args = ["--import", tsxEsm, cli, "__auto-serve", init.projectDir, "--session-id", sessionId];
|
|
311
367
|
if (init.baseURL)
|
|
312
368
|
args.push("--base-url", init.baseURL);
|
|
369
|
+
if (init.startUrl)
|
|
370
|
+
args.push("--goto", init.startUrl);
|
|
371
|
+
if (init.skipInitialNavigation)
|
|
372
|
+
args.push("--blank");
|
|
313
373
|
if (init.headless === false)
|
|
314
374
|
args.push("--non-headless");
|
|
315
|
-
|
|
375
|
+
if (init.headless === true)
|
|
376
|
+
args.push("--headless");
|
|
377
|
+
for (const p of init.inject ?? []) {
|
|
378
|
+
args.push("--inject", p);
|
|
379
|
+
}
|
|
380
|
+
// Spawn cli.js directly — not bin/waygraph (which would fork a second Node).
|
|
381
|
+
const child = spawn(process.execPath, args, {
|
|
316
382
|
detached: true,
|
|
317
383
|
stdio: ["ignore", logFd, logFd],
|
|
318
384
|
windowsHide: true,
|
|
385
|
+
env: process.env,
|
|
319
386
|
});
|
|
320
387
|
child.unref();
|
|
321
388
|
const deadline = Date.now() + readyTimeoutMs;
|
|
@@ -328,6 +395,51 @@ export async function spawnDetachedSession(init, readyTimeoutMs = 20_000) {
|
|
|
328
395
|
throw new Error(`session "${sessionId}" did not become ready within ${readyTimeoutMs}ms - ` +
|
|
329
396
|
`check ${logPathFor(init.projectDir, sessionId)} for errors`);
|
|
330
397
|
}
|
|
398
|
+
/** Stop one detached session (SIGTERM + cleanup metadata/socket). */
|
|
399
|
+
export function stopBrowserSession(projectDir, sessionId) {
|
|
400
|
+
const meta = resolveSessionMeta(sessionId, projectDir);
|
|
401
|
+
if (!meta)
|
|
402
|
+
return false;
|
|
403
|
+
try {
|
|
404
|
+
process.kill(meta.pid, "SIGTERM");
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
/* already gone */
|
|
408
|
+
}
|
|
409
|
+
removeSessionFiles(meta.projectDir, sessionId);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
/** Stop every live session registered for a project. */
|
|
413
|
+
export function stopAllBrowserSessions(projectDir) {
|
|
414
|
+
const sessions = listBrowserSessions(projectDir);
|
|
415
|
+
for (const meta of sessions) {
|
|
416
|
+
stopBrowserSession(meta.projectDir, meta.sessionId);
|
|
417
|
+
}
|
|
418
|
+
return sessions.length;
|
|
419
|
+
}
|
|
420
|
+
/** Live browser sessions for a project (metadata + socket + pid still present). */
|
|
421
|
+
export function listBrowserSessions(projectDir) {
|
|
422
|
+
const dir = sessionDir(projectDir);
|
|
423
|
+
if (!existsSync(dir))
|
|
424
|
+
return [];
|
|
425
|
+
const out = [];
|
|
426
|
+
for (const f of readdirSync(dir)) {
|
|
427
|
+
if (!f.endsWith(".json"))
|
|
428
|
+
continue;
|
|
429
|
+
const id = f.slice(0, -".json".length);
|
|
430
|
+
const meta = readSessionMeta(projectDir, id);
|
|
431
|
+
if (!meta)
|
|
432
|
+
continue;
|
|
433
|
+
if (!existsSync(socketPathFor(id)))
|
|
434
|
+
continue;
|
|
435
|
+
if (!isPidAlive(meta.pid)) {
|
|
436
|
+
removeSessionFiles(projectDir, id);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
out.push(meta);
|
|
440
|
+
}
|
|
441
|
+
return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
442
|
+
}
|
|
331
443
|
function printSnapshotMenu(snapshot) {
|
|
332
444
|
console.log("");
|
|
333
445
|
console.log(`You are here: ${snapshot.here ?? "Unknown (pick Start here)"}`);
|
package/dist/auto-session.d.ts
CHANGED
|
@@ -58,6 +58,10 @@ export interface AutoSessionInit {
|
|
|
58
58
|
baseURL?: string;
|
|
59
59
|
startUrl?: string;
|
|
60
60
|
blocksSelect?: import("./blocks-select.js").BlocksSelect;
|
|
61
|
+
/** Absolute paths — merged Block libraries (see `waygraph browser --inject`). */
|
|
62
|
+
inject?: string[];
|
|
63
|
+
/** When true, open about:blank instead of baseURL/startUrl (browser default). */
|
|
64
|
+
skipInitialNavigation?: boolean;
|
|
61
65
|
/** Default true. false launches a real visible browser window. */
|
|
62
66
|
headless?: boolean;
|
|
63
67
|
/**
|
|
@@ -175,6 +179,7 @@ export interface StorageSnapshot {
|
|
|
175
179
|
*/
|
|
176
180
|
export declare class AutoSession {
|
|
177
181
|
private readonly projectDir;
|
|
182
|
+
private readonly inject;
|
|
178
183
|
private readonly blocksSelect;
|
|
179
184
|
private graph;
|
|
180
185
|
private library;
|
|
@@ -189,6 +194,9 @@ export declare class AutoSession {
|
|
|
189
194
|
private lastRunNote;
|
|
190
195
|
private readonly trace;
|
|
191
196
|
private readonly consoleLog;
|
|
197
|
+
private readonly knownInteractionSelectors;
|
|
198
|
+
private readonly headless;
|
|
199
|
+
private lastOverlayInfo;
|
|
192
200
|
private constructor();
|
|
193
201
|
static start(init: AutoSessionInit): Promise<AutoSession>;
|
|
194
202
|
/** Pure getter - returns a copy, not the live array. */
|
|
@@ -198,21 +206,16 @@ export declare class AutoSession {
|
|
|
198
206
|
getConsoleLog(): ConsoleLogEntry[];
|
|
199
207
|
private pushConsoleLog;
|
|
200
208
|
private currentMenu;
|
|
209
|
+
/** Paint the overlay on about:blank / immediately after session start (headful browser). */
|
|
210
|
+
private bootstrapOverlay;
|
|
211
|
+
/** Re-apply cached overlay state after navigation (init script restores shell only). */
|
|
212
|
+
private repaintOverlayAfterNavigation;
|
|
201
213
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* had been doing by hand the whole time this feature was built. Compares
|
|
206
|
-
* every same-origin `<a href>` on the page against every NavBlock's own
|
|
207
|
-
* static url (`__waygraphNavUrl` - see `defineNavBlock`'s own comment for
|
|
208
|
-
* why only plain-string urls qualify), and `console.warn()`s each pathname
|
|
209
|
-
* with no NavBlock at all - surfaced through the same real console capture
|
|
210
|
-
* `auto console` already reads, not a separate channel. Deduped per path
|
|
211
|
-
* per page load (a `Set` living on `window`, so it survives repeated
|
|
212
|
-
* `currentMenu()` calls but resets on a real navigation) - once is enough
|
|
213
|
-
* to flag a gap, repeating it on every status/send call would just be noise.
|
|
214
|
+
* Flags same-origin `<a href>` paths and visible buttons with no NavBlock
|
|
215
|
+
* URL / Block selector in the loaded library — read via `auto console`.
|
|
216
|
+
* Deduped per page load so repeated status/send calls stay quiet.
|
|
214
217
|
*/
|
|
215
|
-
private
|
|
218
|
+
private warnUnmappedInteractions;
|
|
216
219
|
/** Pure getter - no Block runs, no mem/page mutation. */
|
|
217
220
|
currentSnapshot(): Promise<SessionSnapshot>;
|
|
218
221
|
/**
|