@nikelyh/offscript 1.5.0 → 1.6.1

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Your users don't follow a script. Neither do ours.**
8
8
 
9
- Current stable release: **v1.0.1** on [npm](https://www.npmjs.com/package/@nikelyh/offscript).
9
+ Current stable release: **v1.7.0** on [npm](https://www.npmjs.com/package/@nikelyh/offscript).
10
10
  Online documentation: [offscript.nikelyh.tech](https://offscript.nikelyh.tech).
11
11
 
12
12
  OFFSCRIPT brings multi-agent software exploration into a local developer workflow: configure a target and goal, choose artificial users, run a bounded session, and inspect observations, investigations and visual evidence in Studio or the terminal.
@@ -36,7 +36,7 @@ npm install -g @nikelyh/offscript
36
36
  Or run directly with `npx`:
37
37
 
38
38
  ```sh
39
- npx --yes @nikelyh/offscript@1.0.1 studio
39
+ npx --yes @nikelyh/offscript studio
40
40
  ```
41
41
 
42
42
  For browser exploration, install the required Chromium binary:
@@ -110,10 +110,10 @@ From the OFFSCRIPT checkout:
110
110
  npm pack
111
111
  ```
112
112
 
113
- This builds `nikelyh-offscript-1.0.1.tgz`. From your target project's directory:
113
+ This builds `nikelyh-offscript-*.tgz`. From your target project's directory:
114
114
 
115
115
  ```sh
116
- npm exec --yes --package="/absolute/path/nikelyh-offscript-1.0.1.tgz" -- offscript studio
116
+ npm exec --yes --package="/absolute/path/nikelyh-offscript-*.tgz" -- offscript studio
117
117
  ```
118
118
 
119
119
  The current directory becomes the project root. Use a real absolute archive path; Windows paths are supported. npm installs dependencies in its execution cache without adding OFFSCRIPT to the target's package manifest. The first run can need network access and native dependency setup.
@@ -22,6 +22,7 @@ export class BrowserManager {
22
22
  try {
23
23
  const browser = await chromium.launch({
24
24
  headless: this.headless,
25
+ args: this.headless ? [] : ["--start-maximized"],
25
26
  });
26
27
  if (this.closed) {
27
28
  await browser.close().catch(() => { });
@@ -50,6 +51,9 @@ export class BrowserManager {
50
51
  if (options.storageState) {
51
52
  contextOptions.storageState = options.storageState;
52
53
  }
54
+ if (!this.headless) {
55
+ contextOptions.viewport = null;
56
+ }
53
57
  const context = await browser.newContext(contextOptions);
54
58
  const session = await BrowserUserSession.create(context, options);
55
59
  this.sessions.add(session);
@@ -60,8 +60,10 @@ export class BrowserUserSession {
60
60
  try {
61
61
  await this.page.goto(resolvedUrl, {
62
62
  timeout: this.navigationTimeoutMs,
63
- waitUntil: "domcontentloaded",
63
+ waitUntil: "load",
64
64
  });
65
+ // Allow client-side frameworks (e.g. Next.js, React) to settle initial hydration
66
+ await this.page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 50))).catch(() => { });
65
67
  return { success: true, action: "navigate", url: this.page.url() };
66
68
  }
67
69
  catch (cause) {
@@ -13,9 +13,16 @@ export const simulationExecutionPhases = [
13
13
  "interrupted",
14
14
  ];
15
15
  export function parseStartSimulationRequest(value) {
16
- const input = record(value, "startRequest", ["mode"]);
16
+ const input = record(value, "startRequest", ["mode", "headless"]);
17
17
  const mode = oneOf(input.mode, simulationExecutionModes, "startRequest.mode");
18
- return Object.freeze({ mode });
18
+ let headless;
19
+ if (input.headless !== undefined) {
20
+ if (typeof input.headless !== "boolean") {
21
+ throw new ContractValidationError("startRequest.headless", "Expected a boolean.");
22
+ }
23
+ headless = input.headless;
24
+ }
25
+ return Object.freeze({ mode, ...(headless !== undefined ? { headless } : {}) });
19
26
  }
20
27
  export function parseSimulationExecutionStatus(value) {
21
28
  const input = record(value, "executionStatus", [
@@ -43,7 +43,7 @@ export function createBrowserExplorationSessionFactory(options = {}) {
43
43
  }
44
44
  const targetUrl = resolvedTargetUrl;
45
45
  const browserManager = createBrowserManager({
46
- headless: options.headless ?? true,
46
+ headless: context?.headless ?? options.headless ?? true,
47
47
  });
48
48
  const sessions = new Map();
49
49
  function getSession(id) {
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { executeAssignInvestigation } from "../application/index.js";
2
+ import { ApplicationError, executeAssignInvestigation } from "../application/index.js";
3
3
  import { decodeEvent, offscriptEventType } from "./event-bridge.js";
4
4
  import { situation } from "./situations.js";
5
5
  export function investigationReaction({ state, id, maxTurns, store, startTurn }) {
@@ -41,6 +41,12 @@ export function investigationReaction({ state, id, maxTurns, store, startTurn })
41
41
  }
42
42
  catch (cause) {
43
43
  state.reactions.delete(key);
44
+ if (cause instanceof ApplicationError && cause.code === "INVESTIGATION_CONFLICT") {
45
+ const reloaded = await store.findInvestigation(decoded.payload.investigation.id).catch(() => undefined);
46
+ if (reloaded && reloaded.investigation.lifecycle.status !== "requested") {
47
+ return;
48
+ }
49
+ }
44
50
  state.fail("investigation-assignment", id, cause);
45
51
  }
46
52
  finally {
@@ -89,7 +89,7 @@ export function executionCoordinator(dependencies) {
89
89
  await finalizeExecution(record, outcome, reason);
90
90
  });
91
91
  }
92
- async function start(id, mode) {
92
+ async function start(id, mode, options = {}) {
93
93
  if (closed)
94
94
  throw new ApplicationError("EXECUTION_START_FAILED", "Host is closing.");
95
95
  if (!factory)
@@ -137,6 +137,7 @@ export function executionCoordinator(dependencies) {
137
137
  mode,
138
138
  targetUrl: config.targetUrl,
139
139
  evidenceStore,
140
+ ...(options.headless !== undefined ? { headless: options.headless } : {}),
140
141
  }));
141
142
  record.session = session;
142
143
  record.phase = "running";
@@ -197,12 +197,12 @@ export function createLocalHost(options) {
197
197
  deleting.delete(id);
198
198
  }
199
199
  },
200
- async startSimulation(id, mode) {
200
+ async startSimulation(id, mode, options = {}) {
201
201
  if (phase !== "ready" || closing)
202
202
  throw new Error("Host is not accepting execution commands.");
203
203
  if (!coordinator)
204
204
  throw new Error("No execution session factory configured on this host.");
205
- return coordinator.start(simulationId(id), mode);
205
+ return coordinator.start(simulationId(id), mode, options);
206
206
  },
207
207
  async stopSimulation(id) {
208
208
  if (phase !== "ready" || closing)
@@ -44,8 +44,8 @@ export function configurationRoutes(server, services) {
44
44
  return value;
45
45
  });
46
46
  server.post("/api/simulations/:id/start", { bodyLimit: 4096 }, async (request, reply) => {
47
- const { mode } = parseStartSimulationRequest(request.body);
48
- const status = await services.startSimulation(simulationId(request.params.id), mode);
47
+ const { mode, headless } = parseStartSimulationRequest(request.body);
48
+ const status = await services.startSimulation(simulationId(request.params.id), mode, { headless });
49
49
  return reply.code(200).send(status);
50
50
  });
51
51
  server.get("/api/simulations/:id/execution", async (request) => {
@@ -7,10 +7,16 @@ import { startupError } from "./startup-error.js";
7
7
  process.env.MOZAIK_API_KEY ??= "";
8
8
  const originalWarn = console.warn;
9
9
  console.warn = (...args) => {
10
- if (typeof args[0] === "string" && args[0].includes("mozaik cloud: no API key"))
10
+ if (typeof args[0] === "string" && args[0].includes("mozaik cloud"))
11
11
  return;
12
12
  originalWarn.apply(console, args);
13
13
  };
14
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
15
+ process.stderr.write = ((chunk, ...rest) => {
16
+ if (typeof chunk === "string" && chunk.includes("mozaik cloud"))
17
+ return true;
18
+ return originalStderrWrite(chunk, ...rest);
19
+ });
14
20
  let requestStop = () => { };
15
21
  const stopping = new Promise(resolve => { requestStop = resolve; });
16
22
  let interrupted = false;
@@ -47,6 +53,8 @@ try {
47
53
  });
48
54
  }
49
55
  }
56
+ console.log("AI Mode: Local Autonomous Runner (offline, no API key required).");
57
+ console.log("Browser exploration requires Chromium: npx playwright install chromium");
50
58
  console.log("Press Ctrl+C to stop. No exploration starts automatically.");
51
59
  await stopping;
52
60
  }