@q-agent/agent 0.1.18 → 0.1.20

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.
@@ -1041,9 +1041,12 @@ async function processAuthoringJob(cfg, job) {
1041
1041
  }
1042
1042
  catch { /* best-effort on Windows */ }
1043
1043
  }
1044
+ // Prompt goes as the -p ARGUMENT (not stdin — headless `claude -p` reads the
1045
+ // prompt from argv). stream-json + verbose lets us surface every step live.
1044
1046
  const claudeArgs = [
1045
- "-p",
1046
- "--output-format", "json",
1047
+ "-p", job.taskPrompt,
1048
+ "--output-format", "stream-json",
1049
+ "--verbose",
1047
1050
  "--model", job.model,
1048
1051
  "--append-system-prompt-file", systemFile,
1049
1052
  "--allowedTools", "Bash", "Read", "Write", "Glob", "Grep",
@@ -1065,21 +1068,59 @@ async function processAuthoringJob(cfg, job) {
1065
1068
  claude = (0, child_process_1.spawn)((0, paths_1.claudeCli)(), claudeArgs, {
1066
1069
  cwd: workDir,
1067
1070
  env: claudeEnv,
1068
- stdio: ["pipe", "pipe", "pipe"],
1071
+ stdio: ["ignore", "pipe", "pipe"],
1069
1072
  });
1070
1073
  activeChild = claude;
1071
- let cout = "";
1074
+ // Surface each step to the agent console + the run WebSocket so the operator
1075
+ // can watch Claude drive browser-harness live.
1076
+ const emitStep = (line) => {
1077
+ const trimmed = line.length > 300 ? line.slice(0, 300) + "…" : line;
1078
+ console.log(`[authoring ${job.caseId}] ${trimmed}`);
1079
+ (0, bus_1.emit)("authoring-step", { caseId: job.caseId, line: trimmed });
1080
+ void api
1081
+ .postAuthoringEvent(cfg, job.sessionId, "authoring.progress", {
1082
+ case: job.caseId, phase: "step", message: trimmed,
1083
+ })
1084
+ .catch(() => { });
1085
+ };
1072
1086
  let cerr = "";
1073
- claude.stdout?.on("data", (d) => { cout += String(d); });
1087
+ let finalResult = "";
1088
+ let buf = "";
1089
+ const handleEvent = (ev) => {
1090
+ if (ev.type === "assistant" && ev.message?.content) {
1091
+ for (const c of ev.message.content) {
1092
+ if (c.type === "text" && typeof c.text === "string" && c.text.trim()) {
1093
+ emitStep(`Claude: ${c.text.trim()}`);
1094
+ }
1095
+ else if (c.type === "tool_use") {
1096
+ const inp = c.input || {};
1097
+ const detail = inp.command ?? inp.file_path ?? inp.path ?? inp.pattern ?? JSON.stringify(inp).slice(0, 200);
1098
+ emitStep(`▷ ${String(c.name)}: ${String(detail).replace(/\s+/g, " ").trim()}`);
1099
+ }
1100
+ }
1101
+ }
1102
+ else if (ev.type === "result" && typeof ev.result === "string") {
1103
+ finalResult = ev.result;
1104
+ }
1105
+ };
1106
+ claude.stdout?.on("data", (d) => {
1107
+ buf += String(d);
1108
+ let nl;
1109
+ while ((nl = buf.indexOf("\n")) >= 0) {
1110
+ const line = buf.slice(0, nl).trim();
1111
+ buf = buf.slice(nl + 1);
1112
+ if (!line)
1113
+ continue;
1114
+ try {
1115
+ handleEvent(JSON.parse(line));
1116
+ }
1117
+ catch { /* ignore non-JSON noise */ }
1118
+ }
1119
+ });
1074
1120
  claude.stderr?.on("data", (d) => { cerr += String(d); });
1075
- try {
1076
- claude.stdin?.write(job.taskPrompt);
1077
- claude.stdin?.end();
1078
- }
1079
- catch { }
1080
- await new Promise((resolve) => {
1081
- claude.on("close", () => resolve());
1082
- claude.on("error", (e) => { cerr += `\nclaude spawn error: ${e.message}`; resolve(); });
1121
+ const exitCode = await new Promise((resolve) => {
1122
+ claude.on("close", (c) => resolve(c ?? 0));
1123
+ claude.on("error", (e) => { cerr += `\nclaude spawn error: ${e.message}`; resolve(-1); });
1083
1124
  });
1084
1125
  // 3) Read emitted artifacts.
1085
1126
  const specPath = path.join(workDir, job.specFilename);
@@ -1092,19 +1133,19 @@ async function processAuthoringJob(cfg, job) {
1092
1133
  }
1093
1134
  catch { /* keep default */ }
1094
1135
  }
1095
- let summary = "";
1096
- try {
1097
- summary = JSON.parse(cout).result || "";
1098
- }
1099
- catch {
1100
- summary = (cout || cerr).slice(0, 800);
1101
- }
1102
- if (!code.trim() && !summary)
1103
- summary = "Live authoring produced no spec.";
1104
1136
  const ok = code.trim().length > 0;
1137
+ let summary = finalResult || "";
1138
+ if (!ok) {
1139
+ // Make failures diagnosable: include claude's exit + stderr tail.
1140
+ const errTail = cerr.trim().slice(-500);
1141
+ summary = `${summary || "Live authoring produced no spec."} (claude exit ${exitCode})` +
1142
+ (errTail ? `\n[claude stderr] ${errTail}` : "");
1143
+ }
1144
+ emitStep(ok ? "✓ spec authored" : `✗ no spec (claude exit ${exitCode})`);
1145
+ // Terminal event so the UI can close the live authoring trail.
1105
1146
  await api
1106
1147
  .postAuthoringEvent(cfg, job.sessionId, "authoring.progress", {
1107
- case: job.caseId, phase: ok ? "done" : "failed", message: summary.slice(0, 400),
1148
+ case: job.caseId, phase: ok ? "done" : "failed", message: (summary || "").slice(0, 300),
1108
1149
  })
1109
1150
  .catch(() => { });
1110
1151
  await finalize(code, discovered, summary, ok);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q-agent/agent",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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": {
@@ -75,36 +75,44 @@ async function waitForCDP(port, timeoutMs) {
75
75
  return false;
76
76
  }
77
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) {
78
+ // Load the saved sessionStorage map ({origin: {k:v}}), or null if unusable.
79
+ function loadSessionStorage() {
84
80
  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
81
  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;
82
+ const m = JSON.parse(fs.readFileSync(sessionStoragePath, 'utf-8'));
83
+ return m && typeof m === 'object' && Object.keys(m).length ? m : null;
84
+ } catch { return null; }
85
+ }
86
+
87
+ // Require Playwright if available (agent side); null in the API container.
88
+ function tryPlaywright() {
89
+ try { return require('playwright').chromium; } catch { return null; }
90
+ }
91
+
92
+ // Arm sessionStorage replay and navigate the VISIBLE tab to baseUrl WITH the
93
+ // token restored, so MSAL/SPA apps load authenticated. Critical ordering: the
94
+ // init script must be registered BEFORE the first navigation to the app (that's
95
+ // why Chrome is launched at about:blank, not baseUrl). Returns the connected
96
+ // Playwright browser (kept alive so the init-script registration + tab survive).
97
+ async function armAuthAndNavigate(chromium, port, byOrigin) {
98
+ const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
99
+ const ctx = browser.contexts()[0];
100
+ if (!ctx) return browser;
101
+ await ctx.addInitScript((data) => {
102
+ try {
103
+ const o = data && data[location.origin];
104
+ if (o) for (const k of Object.keys(o)) window.sessionStorage.setItem(k, o[k]);
105
+ } catch (e) {}
106
+ }, byOrigin);
107
+ const page = ctx.pages()[0] || (await ctx.newPage());
108
+ try {
109
+ await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
110
+ await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
104
111
  } catch (e) {
105
- console.error('authoring_browser: sessionStorage replay failed:', e && e.message);
106
- return null;
112
+ console.error('authoring_browser: navigate after replay failed:', e && e.message);
107
113
  }
114
+ console.error('authoring_browser: sessionStorage replay armed for', Object.keys(byOrigin).join(','));
115
+ return browser;
108
116
  }
109
117
 
110
118
  (async () => {
@@ -127,14 +135,22 @@ async function replaySessionStorage(port) {
127
135
  }
128
136
  } catch (e) { console.error('pref seed failed:', e && e.message); }
129
137
 
138
+ // If we can replay sessionStorage (agent side: Playwright resolvable + a saved
139
+ // map), launch to about:blank and let Playwright navigate AFTER arming the init
140
+ // script — so the app never loads before the token is restored. Otherwise launch
141
+ // straight to baseUrl (profile-only / API container).
142
+ const byOrigin = loadSessionStorage();
143
+ const chromium = byOrigin ? tryPlaywright() : null;
144
+ const launchUrl = chromium ? 'about:blank' : baseUrl;
145
+
130
146
  const child = spawn(exe, [
131
147
  `--remote-debugging-port=${PORT}`,
132
148
  `--user-data-dir=${profileDir}`,
133
149
  '--no-first-run', '--no-default-browser-check', '--new-window',
134
150
  ...containerFlags(),
135
- baseUrl,
151
+ launchUrl,
136
152
  ], { detached: false, stdio: 'ignore' });
137
- console.error('authoring_browser launched:', exe, 'port', PORT);
153
+ console.error('authoring_browser launched:', exe, 'port', PORT, 'replay:', Boolean(chromium));
138
154
 
139
155
  if (!(await waitForCDP(PORT, 20000))) {
140
156
  console.error('authoring_browser: CDP endpoint never came up on port', PORT);
@@ -142,9 +158,13 @@ async function replaySessionStorage(port) {
142
158
  process.exit(1);
143
159
  }
144
160
 
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);
161
+ // Arm sessionStorage replay + navigate the visible tab authenticated, BEFORE
162
+ // signalling readiness so browser-harness attaches to a logged-in tab.
163
+ let pw = null;
164
+ if (chromium) {
165
+ try { pw = await armAuthAndNavigate(chromium, PORT, byOrigin); }
166
+ catch (e) { console.error('authoring_browser: replay failed:', e && e.message); }
167
+ }
148
168
 
149
169
  // Signal readiness on stdout so the parent proceeds. The daemon resolves
150
170
  // BU_CDP_URL to the WS.