@q-agent/agent 0.1.16 → 0.1.18
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/dist/src/ensureTooling.js +180 -0
- package/dist/src/paths.js +26 -0
- package/dist/src/runner.js +62 -33
- package/package.json +2 -1
- package/vendor/authoring_browser.cjs +51 -5
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.browserHarnessBinDir = browserHarnessBinDir;
|
|
37
|
+
exports.ensureBrowserHarness = ensureBrowserHarness;
|
|
38
|
+
/**
|
|
39
|
+
* Ensure the `browser-harness` CLI is available for live spec-authoring (#400),
|
|
40
|
+
* provisioning it on first run so the user never installs anything by hand —
|
|
41
|
+
* the same "download once, then it's ready" contract as `ensureChromium`.
|
|
42
|
+
*
|
|
43
|
+
* browser-harness is a Python CLI, so we can't just add an npm dep. Instead we
|
|
44
|
+
* self-provision it with `uv` (a single static binary that also installs its own
|
|
45
|
+
* Python): find `uv` on PATH, else download the platform build once into the
|
|
46
|
+
* agent's config dir, then `uv tool install browser-harness` into a known bin dir
|
|
47
|
+
* we can resolve and prepend to the authoring subprocess PATH.
|
|
48
|
+
*
|
|
49
|
+
* Everything is best-effort and heavily logged: a failure returns `{ok:false}`
|
|
50
|
+
* with a message the caller surfaces, rather than throwing.
|
|
51
|
+
*/
|
|
52
|
+
const child_process_1 = require("child_process");
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const config_1 = require("./config");
|
|
56
|
+
const TOOLS_DIR = path.join((0, config_1.configDir)(), "tools");
|
|
57
|
+
const UV_DIR = path.join(TOOLS_DIR, "uv"); // downloaded uv binary lives here
|
|
58
|
+
const TOOL_BIN = path.join(TOOLS_DIR, "bin"); // UV_TOOL_BIN_DIR — browser-harness lands here
|
|
59
|
+
const TOOL_DATA = path.join(TOOLS_DIR, "data"); // UV_TOOL_DIR — tool venvs
|
|
60
|
+
const isWin = process.platform === "win32";
|
|
61
|
+
const exe = (n) => (isWin ? `${n}.exe` : n);
|
|
62
|
+
/** Directory holding the installed `browser-harness` executable (prepended to the
|
|
63
|
+
* authoring subprocess PATH so the agent's `claude` can run `browser-harness`). */
|
|
64
|
+
function browserHarnessBinDir() {
|
|
65
|
+
return TOOL_BIN;
|
|
66
|
+
}
|
|
67
|
+
/** Run a command to completion, returning its exit code (null on spawn error).
|
|
68
|
+
* Inherits stdio so first-run downloads/installs are visible in the agent log. */
|
|
69
|
+
function run(cmd, args, env) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const child = (0, child_process_1.spawn)(cmd, args, { stdio: "inherit", env: { ...process.env, ...env } });
|
|
72
|
+
child.on("close", resolve);
|
|
73
|
+
child.on("error", () => resolve(null));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** True if `browser-harness` is already runnable (in our bin dir or on PATH). */
|
|
77
|
+
async function harnessPresent() {
|
|
78
|
+
if (fs.existsSync(path.join(TOOL_BIN, exe("browser-harness"))))
|
|
79
|
+
return true;
|
|
80
|
+
const code = await run(exe("browser-harness"), ["--version"]);
|
|
81
|
+
return code === 0;
|
|
82
|
+
}
|
|
83
|
+
/** Resolve a usable `uv`: our downloaded copy, then PATH, else download it once. */
|
|
84
|
+
async function ensureUv() {
|
|
85
|
+
const local = path.join(UV_DIR, exe("uv"));
|
|
86
|
+
if (fs.existsSync(local))
|
|
87
|
+
return local;
|
|
88
|
+
if ((await run(exe("uv"), ["--version"])) === 0)
|
|
89
|
+
return exe("uv"); // on PATH
|
|
90
|
+
const asset = uvAsset();
|
|
91
|
+
if (!asset) {
|
|
92
|
+
console.error(`ensureUv: unsupported platform ${process.platform}/${process.arch}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
fs.mkdirSync(UV_DIR, { recursive: true });
|
|
96
|
+
const url = `https://github.com/astral-sh/uv/releases/latest/download/${asset}`;
|
|
97
|
+
const archive = path.join(UV_DIR, asset);
|
|
98
|
+
console.log(`Downloading uv (one-time) from ${url} ...`);
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(url);
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
throw new Error(`HTTP ${res.status}`);
|
|
103
|
+
fs.writeFileSync(archive, Buffer.from(await res.arrayBuffer()));
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error("ensureUv: download failed:", e.message);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// Extract with the system `tar` (bsdtar on Win10+ handles .zip too; unix uses .tar.gz).
|
|
110
|
+
if ((await run("tar", ["-xf", archive, "-C", UV_DIR])) !== 0) {
|
|
111
|
+
console.error("ensureUv: extract failed (need `tar` on PATH)");
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// Archives may nest the binary in a subdir — find it.
|
|
115
|
+
const found = findFile(UV_DIR, exe("uv"));
|
|
116
|
+
if (found && found !== local) {
|
|
117
|
+
try {
|
|
118
|
+
fs.copyFileSync(found, local);
|
|
119
|
+
fs.chmodSync(local, 0o755);
|
|
120
|
+
}
|
|
121
|
+
catch { /* best-effort */ }
|
|
122
|
+
}
|
|
123
|
+
return fs.existsSync(local) ? local : found;
|
|
124
|
+
}
|
|
125
|
+
/** The uv release asset name for this platform/arch, or null if unsupported. */
|
|
126
|
+
function uvAsset() {
|
|
127
|
+
const a = process.arch;
|
|
128
|
+
if (isWin)
|
|
129
|
+
return a === "arm64" ? "uv-aarch64-pc-windows-msvc.zip" : "uv-x86_64-pc-windows-msvc.zip";
|
|
130
|
+
if (process.platform === "darwin")
|
|
131
|
+
return a === "arm64" ? "uv-aarch64-apple-darwin.tar.gz" : "uv-x86_64-apple-darwin.tar.gz";
|
|
132
|
+
if (process.platform === "linux")
|
|
133
|
+
return a === "arm64" ? "uv-aarch64-unknown-linux-gnu.tar.gz" : "uv-x86_64-unknown-linux-gnu.tar.gz";
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/** Shallow recursive search for a file named `name` under `dir`. */
|
|
137
|
+
function findFile(dir, name) {
|
|
138
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
139
|
+
const full = path.join(dir, entry.name);
|
|
140
|
+
if (entry.isDirectory()) {
|
|
141
|
+
const hit = findFile(full, name);
|
|
142
|
+
if (hit)
|
|
143
|
+
return hit;
|
|
144
|
+
}
|
|
145
|
+
else if (entry.name === name) {
|
|
146
|
+
return full;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Ensure `browser-harness` is installed. Returns the bin dir to prepend to PATH.
|
|
153
|
+
* Fast no-op once present; on first run downloads uv (if needed) and installs
|
|
154
|
+
* browser-harness (uv fetches its own Python). Never throws.
|
|
155
|
+
*/
|
|
156
|
+
async function ensureBrowserHarness() {
|
|
157
|
+
try {
|
|
158
|
+
if (await harnessPresent())
|
|
159
|
+
return { ok: true, binDir: TOOL_BIN };
|
|
160
|
+
const uv = await ensureUv();
|
|
161
|
+
if (!uv) {
|
|
162
|
+
return { ok: false, binDir: TOOL_BIN, error: "could not obtain `uv` to install browser-harness" };
|
|
163
|
+
}
|
|
164
|
+
fs.mkdirSync(TOOL_BIN, { recursive: true });
|
|
165
|
+
fs.mkdirSync(TOOL_DATA, { recursive: true });
|
|
166
|
+
console.log("Installing browser-harness (one-time) via uv ...");
|
|
167
|
+
const code = await run(uv, ["tool", "install", "--force", "browser-harness"], {
|
|
168
|
+
UV_TOOL_BIN_DIR: TOOL_BIN,
|
|
169
|
+
UV_TOOL_DIR: TOOL_DATA,
|
|
170
|
+
});
|
|
171
|
+
if (code !== 0 || !(await harnessPresent())) {
|
|
172
|
+
return { ok: false, binDir: TOOL_BIN, error: "browser-harness install via uv failed" };
|
|
173
|
+
}
|
|
174
|
+
console.log("browser-harness ready.");
|
|
175
|
+
return { ok: true, binDir: TOOL_BIN };
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
return { ok: false, binDir: TOOL_BIN, error: e.message };
|
|
179
|
+
}
|
|
180
|
+
}
|
package/dist/src/paths.js
CHANGED
|
@@ -40,6 +40,7 @@ exports.packagedRoot = packagedRoot;
|
|
|
40
40
|
exports.nodeBin = nodeBin;
|
|
41
41
|
exports.agentNodeModules = agentNodeModules;
|
|
42
42
|
exports.playwrightCli = playwrightCli;
|
|
43
|
+
exports.claudeCli = claudeCli;
|
|
43
44
|
exports.vendorCaptureScript = vendorCaptureScript;
|
|
44
45
|
exports.vendorExploreScript = vendorExploreScript;
|
|
45
46
|
exports.vendorAuthoringScript = vendorAuthoringScript;
|
|
@@ -140,6 +141,31 @@ function playwrightCli() {
|
|
|
140
141
|
// Resolve via Node so it works whether npm nested or hoisted `playwright`.
|
|
141
142
|
return path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
|
|
142
143
|
}
|
|
144
|
+
/** The bundled Claude Code CLI (`@anthropic-ai/claude-code`), so no separate
|
|
145
|
+
* `claude` install / PATH shim is needed (#400 — live-authoring drives it on the
|
|
146
|
+
* agent). NOTE: the package ships a NATIVE binary (`bin/claude[.exe]`), not a JS
|
|
147
|
+
* entry — spawn it DIRECTLY (not via node). Resolves the package's `bin` entry
|
|
148
|
+
* across source/npx and the packaged bundle. */
|
|
149
|
+
function claudeCli() {
|
|
150
|
+
const relFromDir = (dir) => {
|
|
151
|
+
const fallback = path.join(dir, "bin", process.platform === "win32" ? "claude.exe" : "claude");
|
|
152
|
+
try {
|
|
153
|
+
const bin = require(path.join(dir, "package.json")).bin;
|
|
154
|
+
const rel = typeof bin === "string" ? bin : bin ? Object.values(bin)[0] : null;
|
|
155
|
+
return rel ? path.join(dir, rel) : fallback;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return fallback;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const root = packagedRoot();
|
|
162
|
+
if (root) {
|
|
163
|
+
const dir = path.join(root, "node_modules", "@anthropic-ai", "claude-code");
|
|
164
|
+
if (fs.existsSync(dir))
|
|
165
|
+
return relFromDir(dir);
|
|
166
|
+
}
|
|
167
|
+
return relFromDir(path.dirname(require.resolve("@anthropic-ai/claude-code/package.json")));
|
|
168
|
+
}
|
|
143
169
|
/** The vendored headed-login capture script (`capture_auth.cjs`). */
|
|
144
170
|
function vendorCaptureScript() {
|
|
145
171
|
const root = packagedRoot();
|
package/dist/src/runner.js
CHANGED
|
@@ -56,6 +56,7 @@ const api = __importStar(require("./api"));
|
|
|
56
56
|
const bus_1 = require("./bus");
|
|
57
57
|
const ensureBrowser_1 = require("./ensureBrowser");
|
|
58
58
|
const paths_1 = require("./paths");
|
|
59
|
+
const ensureTooling_1 = require("./ensureTooling");
|
|
59
60
|
const playwrightConfig_1 = require("./playwrightConfig");
|
|
60
61
|
const report_1 = require("./report");
|
|
61
62
|
const session_1 = require("./session");
|
|
@@ -935,22 +936,6 @@ async function getFreePort() {
|
|
|
935
936
|
});
|
|
936
937
|
});
|
|
937
938
|
}
|
|
938
|
-
/** Poll Chrome's /json/version until it responds (CDP is up) or we time out. */
|
|
939
|
-
async function waitForCdp(port, timeoutMs) {
|
|
940
|
-
const deadline = Date.now() + timeoutMs;
|
|
941
|
-
while (Date.now() < deadline) {
|
|
942
|
-
try {
|
|
943
|
-
const r = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
944
|
-
if (r.ok)
|
|
945
|
-
return true;
|
|
946
|
-
}
|
|
947
|
-
catch {
|
|
948
|
-
/* endpoint not up yet */
|
|
949
|
-
}
|
|
950
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
951
|
-
}
|
|
952
|
-
return false;
|
|
953
|
-
}
|
|
954
939
|
/**
|
|
955
940
|
* Author one spec live on this machine (#403): launch the dedicated,
|
|
956
941
|
* pre-authenticated Chrome (vendored launcher), point the local `browser-harness`
|
|
@@ -996,16 +981,37 @@ async function processAuthoringJob(cfg, job) {
|
|
|
996
981
|
case: job.caseId, phase: "launching", message: "Starting authenticated browser",
|
|
997
982
|
})
|
|
998
983
|
.catch(() => { });
|
|
999
|
-
// 1) Dedicated pre-auth Chrome (vendored launcher
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
984
|
+
// 1) Dedicated pre-auth Chrome (vendored launcher). Pass the saved
|
|
985
|
+
// sessionStorage path so the launcher replays MSAL/SPA tokens into the
|
|
986
|
+
// Chrome browser-harness attaches to; NODE_PATH lets the launcher resolve
|
|
987
|
+
// the bundled Playwright it uses for that replay. Wait for its READY line
|
|
988
|
+
// (emitted AFTER the replay is armed) so browser-harness never navigates
|
|
989
|
+
// before the session is restored.
|
|
990
|
+
const nm = (0, paths_1.agentNodeModules)();
|
|
991
|
+
launcher = (0, child_process_1.spawn)((0, paths_1.nodeBin)(), [(0, paths_1.vendorAuthoringScript)(), job.baseUrl, String(port), profileDir, sess ? sess.sessionStoragePath : ""], { env: nodePathEnv(nm), stdio: ["pipe", "pipe", "pipe"] });
|
|
1004
992
|
activeChild = launcher;
|
|
1005
993
|
let launcherErr = "";
|
|
1006
994
|
launcher.stderr?.on("data", (d) => { launcherErr += String(d); });
|
|
1007
|
-
|
|
1008
|
-
|
|
995
|
+
const ready = await new Promise((resolve) => {
|
|
996
|
+
const to = setTimeout(() => resolve(false), AUTHORING_CDP_READY_TIMEOUT_MS);
|
|
997
|
+
launcher.stdout?.on("data", (d) => {
|
|
998
|
+
if (String(d).includes("AUTHORING_BROWSER_READY")) {
|
|
999
|
+
clearTimeout(to);
|
|
1000
|
+
resolve(true);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
launcher.on("exit", () => { clearTimeout(to); resolve(false); });
|
|
1004
|
+
});
|
|
1005
|
+
if (!ready) {
|
|
1006
|
+
await finalize("", { routes: [], selectors: [] }, `Authoring browser did not become ready on port ${port}. ${launcherErr.trim()}`.trim(), false);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
// Ensure browser-harness is installed (first run provisions it via uv). Its
|
|
1010
|
+
// bin dir is prepended to the claude subprocess PATH so `browser-harness`
|
|
1011
|
+
// resolves for the Bash calls Claude makes.
|
|
1012
|
+
const bh = await (0, ensureTooling_1.ensureBrowserHarness)();
|
|
1013
|
+
if (!bh.ok) {
|
|
1014
|
+
await finalize("", { routes: [], selectors: [] }, `browser-harness unavailable: ${bh.error || "unknown"}`, false);
|
|
1009
1015
|
return;
|
|
1010
1016
|
}
|
|
1011
1017
|
await api
|
|
@@ -1015,28 +1021,51 @@ async function processAuthoringJob(cfg, job) {
|
|
|
1015
1021
|
.catch(() => { });
|
|
1016
1022
|
// 2) Local agentic Claude drives browser-harness (BU_CDP_URL → our Chrome),
|
|
1017
1023
|
// writing the spec + discovered.json into workDir. System prompt via a
|
|
1018
|
-
// file (avoids a huge argv); task prompt via stdin.
|
|
1024
|
+
// file (avoids a huge argv); task prompt via stdin. Invoked as
|
|
1025
|
+
// `nodeBin() <bundled claude cli.js>` so no `claude` install/PATH shim is
|
|
1026
|
+
// needed (and no Windows .cmd shell hack).
|
|
1019
1027
|
const systemFile = path.join(workDir, "system-prompt.txt");
|
|
1020
1028
|
fs.writeFileSync(systemFile, job.systemPrompt, "utf-8");
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
|
|
1029
|
+
// Use the app's saved Claude credential (shipped in the claim) so we don't
|
|
1030
|
+
// need a separate `claude login` on this machine. Write it locked-down into
|
|
1031
|
+
// the temp workspace (removed with workDir); empty ⇒ fall back to the agent's
|
|
1032
|
+
// own login (no CLAUDE_CONFIG_DIR override).
|
|
1033
|
+
let claudeConfigDir = "";
|
|
1034
|
+
if (job.claudeCredentials) {
|
|
1035
|
+
claudeConfigDir = path.join(workDir, ".claude-config");
|
|
1036
|
+
fs.mkdirSync(claudeConfigDir, { recursive: true });
|
|
1037
|
+
const credFile = path.join(claudeConfigDir, ".credentials.json");
|
|
1038
|
+
fs.writeFileSync(credFile, job.claudeCredentials, "utf-8");
|
|
1039
|
+
try {
|
|
1040
|
+
fs.chmodSync(credFile, 0o600);
|
|
1041
|
+
}
|
|
1042
|
+
catch { /* best-effort on Windows */ }
|
|
1043
|
+
}
|
|
1025
1044
|
const claudeArgs = [
|
|
1026
1045
|
"-p",
|
|
1027
1046
|
"--output-format", "json",
|
|
1028
1047
|
"--model", job.model,
|
|
1029
|
-
"--append-system-prompt-file",
|
|
1048
|
+
"--append-system-prompt-file", systemFile,
|
|
1030
1049
|
"--allowedTools", "Bash", "Read", "Write", "Glob", "Grep",
|
|
1031
1050
|
"--dangerously-skip-permissions",
|
|
1032
|
-
"--add-dir",
|
|
1051
|
+
"--add-dir", workDir,
|
|
1033
1052
|
"--max-budget-usd", String(job.maxBudgetUsd),
|
|
1034
1053
|
];
|
|
1035
|
-
|
|
1054
|
+
// Prepend browser-harness's bin dir to PATH (overwrite the same-case key so
|
|
1055
|
+
// Windows doesn't end up with both Path and PATH).
|
|
1056
|
+
const pathVar = Object.keys(process.env).find((k) => k.toLowerCase() === "path") || "PATH";
|
|
1057
|
+
const claudeEnv = {
|
|
1058
|
+
...process.env,
|
|
1059
|
+
BU_CDP_URL: `http://127.0.0.1:${port}`,
|
|
1060
|
+
[pathVar]: `${bh.binDir}${path.delimiter}${process.env[pathVar] || ""}`,
|
|
1061
|
+
};
|
|
1062
|
+
if (claudeConfigDir)
|
|
1063
|
+
claudeEnv.CLAUDE_CONFIG_DIR = claudeConfigDir;
|
|
1064
|
+
// Spawn the native `claude` binary directly (it is not a JS entry).
|
|
1065
|
+
claude = (0, child_process_1.spawn)((0, paths_1.claudeCli)(), claudeArgs, {
|
|
1036
1066
|
cwd: workDir,
|
|
1037
|
-
env:
|
|
1067
|
+
env: claudeEnv,
|
|
1038
1068
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1039
|
-
shell: useShell,
|
|
1040
1069
|
});
|
|
1041
1070
|
activeChild = claude;
|
|
1042
1071
|
let cout = "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q-agent/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Q-Agent Local Agent — claims execution jobs from the Q-Agent server and runs Playwright locally, so manual login/MFA happens on the user's own machine and session credentials never leave it.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dist:desktop": "npm run build && electron-builder --win"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@anthropic-ai/claude-code": "^2.1.0",
|
|
35
36
|
"@playwright/test": "^1.61.1",
|
|
36
37
|
"commander": "^12.1.0",
|
|
37
38
|
"electron-updater": "^6.3.9",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Long-lived, pre-authenticated automation Chrome for live spec-authoring (#400).
|
|
2
|
-
// Args: baseUrl, port, profileDir.
|
|
2
|
+
// Args: baseUrl, port, profileDir, [sessionStoragePath].
|
|
3
3
|
//
|
|
4
4
|
// Launches a real Chrome/Edge (NOT via Playwright's launcher, so no automation
|
|
5
5
|
// fingerprint) on a FIXED --remote-debugging-port using a DEDICATED, persistent
|
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
// the Chrome "Allow remote debugging" popup / default-profile lockdown (see
|
|
9
9
|
// browser_harness/daemon.py:128-131,148). Auth is inherited from the persistent
|
|
10
10
|
// profile — reuse the capture `browser-profile` dir (already logged in via the
|
|
11
|
-
// manual-login capture flow), so
|
|
11
|
+
// manual-login capture flow), so cookies + localStorage are present.
|
|
12
|
+
//
|
|
13
|
+
// sessionStorage (where MSAL/SPA auth tokens live) is NEVER persisted to a Chrome
|
|
14
|
+
// profile on disk, so a profile-only relaunch lands unauthenticated for such apps.
|
|
15
|
+
// When a `sessionStoragePath` is given AND Playwright is resolvable (agent side —
|
|
16
|
+
// the API container ships no Playwright and passes no path), we attach over CDP
|
|
17
|
+
// and register an init script that replays the saved sessionStorage for the
|
|
18
|
+
// matching origin before app code runs — the same trick the run/explore paths use.
|
|
19
|
+
// The Playwright connection is kept alive for the whole session so the init-script
|
|
20
|
+
// registration persists for the tabs browser-harness opens.
|
|
12
21
|
//
|
|
13
22
|
// Unlike capture_auth.cjs (a short snapshot loop) this stays ALIVE for the whole
|
|
14
23
|
// authoring session and only tears Chrome down when the parent closes our stdin
|
|
@@ -19,7 +28,7 @@
|
|
|
19
28
|
const { spawn } = require('child_process');
|
|
20
29
|
const fs = require('fs');
|
|
21
30
|
const path = require('path');
|
|
22
|
-
const [, , baseUrl, portArg, profileDir] = process.argv;
|
|
31
|
+
const [, , baseUrl, portArg, profileDir, sessionStoragePath] = process.argv;
|
|
23
32
|
const PORT = parseInt(portArg, 10);
|
|
24
33
|
|
|
25
34
|
process.on('unhandledRejection', (e) => console.error('authoring_browser unhandledRejection:', e && (e.message || e)));
|
|
@@ -66,6 +75,38 @@ async function waitForCDP(port, timeoutMs) {
|
|
|
66
75
|
return false;
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
// Replay saved sessionStorage into the running Chrome so MSAL/SPA apps that keep
|
|
79
|
+
// their token in sessionStorage are authenticated. Playwright is OPTIONAL: if it
|
|
80
|
+
// can't be required (the API container ships none) or no path was given, this is
|
|
81
|
+
// a no-op. Returns the connected Playwright browser (kept alive so the init-script
|
|
82
|
+
// registration survives) or null.
|
|
83
|
+
async function replaySessionStorage(port) {
|
|
84
|
+
if (!sessionStoragePath) return null;
|
|
85
|
+
let byOrigin;
|
|
86
|
+
try { byOrigin = JSON.parse(fs.readFileSync(sessionStoragePath, 'utf-8')); }
|
|
87
|
+
catch { return null; }
|
|
88
|
+
if (!byOrigin || typeof byOrigin !== 'object' || !Object.keys(byOrigin).length) return null;
|
|
89
|
+
let chromium;
|
|
90
|
+
try { ({ chromium } = require('playwright')); } catch { return null; }
|
|
91
|
+
try {
|
|
92
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
93
|
+
const ctx = browser.contexts()[0];
|
|
94
|
+
if (ctx) {
|
|
95
|
+
await ctx.addInitScript((data) => {
|
|
96
|
+
try {
|
|
97
|
+
const o = data && data[location.origin];
|
|
98
|
+
if (o) for (const k of Object.keys(o)) window.sessionStorage.setItem(k, o[k]);
|
|
99
|
+
} catch (e) {}
|
|
100
|
+
}, byOrigin);
|
|
101
|
+
console.error('authoring_browser: sessionStorage replay armed for', Object.keys(byOrigin).join(','));
|
|
102
|
+
}
|
|
103
|
+
return browser;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error('authoring_browser: sessionStorage replay failed:', e && e.message);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
69
110
|
(async () => {
|
|
70
111
|
if (!PORT || Number.isNaN(PORT)) { console.error('authoring_browser: invalid port', portArg); process.exit(1); }
|
|
71
112
|
const exe = findBrowser();
|
|
@@ -101,14 +142,19 @@ async function waitForCDP(port, timeoutMs) {
|
|
|
101
142
|
process.exit(1);
|
|
102
143
|
}
|
|
103
144
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
145
|
+
// Arm sessionStorage replay (best-effort) BEFORE signalling readiness, so the
|
|
146
|
+
// token is restored before browser-harness navigates.
|
|
147
|
+
const pw = await replaySessionStorage(PORT);
|
|
148
|
+
|
|
149
|
+
// Signal readiness on stdout so the parent proceeds. The daemon resolves
|
|
150
|
+
// BU_CDP_URL to the WS.
|
|
106
151
|
console.log(`AUTHORING_BROWSER_READY ${PORT}`);
|
|
107
152
|
|
|
108
153
|
let shuttingDown = false;
|
|
109
154
|
const shutdown = (code) => {
|
|
110
155
|
if (shuttingDown) return;
|
|
111
156
|
shuttingDown = true;
|
|
157
|
+
try { if (pw) pw.close(); } catch {}
|
|
112
158
|
try { child.kill(); } catch {}
|
|
113
159
|
process.exit(code || 0);
|
|
114
160
|
};
|