@charzhu/openjaw-agent 0.2.9 → 0.3.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/dist/main.js CHANGED
@@ -137,7 +137,11 @@ function loadAgentConfig() {
137
137
  feishu: parsed?.feishu ?? void 0,
138
138
  wechat: parsed?.wechat ?? void 0,
139
139
  features: {
140
- skill_auto_suggest: parsed?.features?.skill_auto_suggest ?? true
140
+ skill_auto_suggest: parsed?.features?.skill_auto_suggest ?? true,
141
+ dynamic_workflows: {
142
+ ...DEFAULT_DYNAMIC_WORKFLOWS,
143
+ ...parsed?.features?.dynamic_workflows ?? {}
144
+ }
141
145
  }
142
146
  };
143
147
  if (config.llm.provider === "anthropic") {
@@ -199,12 +203,20 @@ function updateBridgeConfig(name, values) {
199
203
  );
200
204
  return next;
201
205
  }
202
- var DEFAULT_COPILOT_OAUTH_CLIENT_ID, DEFAULT_CONFIG, configWriteChain;
206
+ var DEFAULT_COPILOT_OAUTH_CLIENT_ID, DEFAULT_DYNAMIC_WORKFLOWS, DEFAULT_CONFIG, configWriteChain;
203
207
  var init_config = __esm({
204
208
  "src/config.ts"() {
205
209
  "use strict";
206
210
  init_packageRoot();
207
211
  DEFAULT_COPILOT_OAUTH_CLIENT_ID = "Iv1.b507a08c87ecfe98";
212
+ DEFAULT_DYNAMIC_WORKFLOWS = {
213
+ enabled: true,
214
+ planner_mode: "adaptive",
215
+ hard_max_workers: 1024,
216
+ hard_max_concurrent_workers: 128,
217
+ worker_timeout_ms: 18e4,
218
+ persist_history: true
219
+ };
208
220
  DEFAULT_CONFIG = {
209
221
  llm: {
210
222
  provider: "anthropic",
@@ -856,7 +868,7 @@ Start-Sleep -Milliseconds 50
856
868
  }
857
869
  }
858
870
  async function wait(duration) {
859
- await new Promise((resolve5) => setTimeout(resolve5, duration * 1e3));
871
+ await new Promise((resolve6) => setTimeout(resolve6, duration * 1e3));
860
872
  return { output: `Waited ${duration} seconds` };
861
873
  }
862
874
  function getDisplayDimensions() {
@@ -2393,9 +2405,6 @@ var init_copilot = __esm({
2393
2405
  }
2394
2406
  async chat(options) {
2395
2407
  const modelInfo = await this.resolveModelInfo(this.config.model);
2396
- if (hasImageContent(options.messages) && modelInfo?.supportsVision === false) {
2397
- throw new Error(`model ${this.config.model} does not support image input; switch to a vision-capable model before submitting an image`);
2398
- }
2399
2408
  if (this.shouldRouteToResponses(modelInfo)) {
2400
2409
  return this.chatResponses(options, modelInfo);
2401
2410
  }
@@ -2554,7 +2563,7 @@ var init_copilot = __esm({
2554
2563
  handshakeTimeout: RESPONSES_WEBSOCKET_CONNECT_TIMEOUT_MS
2555
2564
  });
2556
2565
  const request = JSON.stringify(this.buildResponsesWebSocketRequest(requestBody));
2557
- return new Promise((resolve5, reject) => {
2566
+ return new Promise((resolve6, reject) => {
2558
2567
  const accumulator = { sawTextDelta: false, text: null, toolCalls: [] };
2559
2568
  let settled = false;
2560
2569
  const timeout = setTimeout(() => {
@@ -2568,7 +2577,7 @@ var init_copilot = __esm({
2568
2577
  settled = true;
2569
2578
  clearTimeout(timeout);
2570
2579
  ws.close();
2571
- resolve5(value);
2580
+ resolve6(value);
2572
2581
  }, "finish");
2573
2582
  const fail = /* @__PURE__ */ __name((error) => {
2574
2583
  if (settled) return;
@@ -4008,7 +4017,7 @@ function categoryForTool(toolName) {
4008
4017
  if (toolName.startsWith("word_") || toolName.startsWith("excel_") || toolName.startsWith("powerpoint_") || toolName.startsWith("office_")) return "office";
4009
4018
  if (toolName.startsWith("memory_") || toolName === "todo_write") return "memory";
4010
4019
  if (toolName.startsWith("file_") || toolName.startsWith("image_") || toolName === "grep" || toolName === "glob") return "files";
4011
- if (toolName.startsWith("system_") || toolName.startsWith("clipboard_") || ["code_execute", "web_fetch", "web_search", "notify", "sleep", "ask_user", "config"].includes(toolName)) return "system";
4020
+ if (toolName.startsWith("system_") || toolName.startsWith("clipboard_") || ["code_execute", "web_fetch", "web_search", "web_extract", "notify", "sleep", "ask_user", "config"].includes(toolName)) return "system";
4012
4021
  return "mcp";
4013
4022
  }
4014
4023
  var DEFAULT_OPENAI_MAX_TOOLS, MCP_AUTO_GROW_HARD_CAP, BUILTIN_HEADROOM, FOUNDATION_TOOL_NAMES, PROFILE_CATEGORIES, CATEGORY_KEYWORDS;
@@ -4034,9 +4043,9 @@ var init_tool_exposure = __esm({
4034
4043
  CATEGORY_KEYWORDS = [
4035
4044
  { category: "email", patterns: [/\b(email|mail|outlook|inbox|calendar|schedule|meeting|invite|today|tomorrow)\b/i] },
4036
4045
  { category: "teams", patterns: [/\b(teams|chat|channel|message|dm|meeting|standup|today|mention)\b/i] },
4037
- { category: "browser", patterns: [/\b(browser|page|website|web|navigate|click|screenshot|search online)\b/i] },
4046
+ { category: "browser", patterns: [/\b(browser|page|website|web|navigate|click|screenshot|snapshot|console|image|search online)\b/i] },
4038
4047
  { category: "files", patterns: [/\b(file|folder|directory|read|write|edit|grep|glob|find in repo|codebase)\b/i] },
4039
- { category: "system", patterns: [/\b(shell|command|terminal|run|execute|clipboard|notify|sleep|web search|fetch url)\b/i] },
4048
+ { category: "system", patterns: [/\b(shell|command|terminal|run|execute|clipboard|notify|sleep|web search|fetch url|extract url|read url|article|docs?|paper|source page|news|latest|headlines|current events|breaking news)\b/i] },
4040
4049
  { category: "office", patterns: [/\b(word|excel|powerpoint|spreadsheet|document|presentation|slide)\b/i] },
4041
4050
  { category: "wechat", patterns: [/\b(wechat|weixin)\b/i] },
4042
4051
  { category: "memory", patterns: [/\b(memory|remember|recall|todo|preference)\b/i] }
@@ -5094,9 +5103,7 @@ ${summary}
5094
5103
  if (imageData) {
5095
5104
  const contentBlocks = [
5096
5105
  { type: "image", source: { type: "base64", media_type: imageData.mimeType, data: imageData.base64 } },
5097
- { type: "text", text: `${taskText}
5098
-
5099
- [ATTACHED IMAGE]: The image is attached as visual input in this message. Inspect it directly; do not search the filesystem for an uploaded image path unless the user explicitly asks for a file.` }
5106
+ { type: "text", text: taskText }
5100
5107
  ];
5101
5108
  this.conversationHistory.push({ role: "user", content: contentBlocks });
5102
5109
  } else {
@@ -5477,18 +5484,18 @@ ${summary}
5477
5484
  content: parsed.question,
5478
5485
  choices: parsed.choices ?? void 0
5479
5486
  };
5480
- const userResponse = await new Promise((resolve5) => {
5487
+ const userResponse = await new Promise((resolve6) => {
5481
5488
  if (this._pendingAskUserResponse !== null) {
5482
5489
  const buffered = this._pendingAskUserResponse;
5483
5490
  this._pendingAskUserResponse = null;
5484
- resolve5(buffered);
5491
+ resolve6(buffered);
5485
5492
  return;
5486
5493
  }
5487
- this._askUserResolver = resolve5;
5494
+ this._askUserResolver = resolve6;
5488
5495
  setTimeout(() => {
5489
- if (this._askUserResolver === resolve5) {
5496
+ if (this._askUserResolver === resolve6) {
5490
5497
  this._askUserResolver = null;
5491
- resolve5("[No response from user \u2014 timed out after 5 minutes]");
5498
+ resolve6("[No response from user \u2014 timed out after 5 minutes]");
5492
5499
  }
5493
5500
  }, 5 * 60 * 1e3);
5494
5501
  });
@@ -5838,18 +5845,135 @@ var init_logger = __esm({
5838
5845
  }
5839
5846
  });
5840
5847
 
5848
+ // ../openjaw-mcp/dist/tools/url-safety.js
5849
+ import { isIP } from "node:net";
5850
+ function hasTokenLikeSecret(value) {
5851
+ const decoded = safeDecode(value);
5852
+ return TOKEN_PATTERNS.some((pattern) => pattern.test(value) || pattern.test(decoded));
5853
+ }
5854
+ function safeDecode(value) {
5855
+ try {
5856
+ return decodeURIComponent(value);
5857
+ } catch {
5858
+ return value;
5859
+ }
5860
+ }
5861
+ function normalizeHostname(hostname) {
5862
+ return hostname.replace(/^\[|\]$/g, "").replace(/\.+$/g, "").toLowerCase();
5863
+ }
5864
+ function isPrivateIPv4(host) {
5865
+ const parts = host.split(".").map((part) => Number(part));
5866
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
5867
+ return false;
5868
+ const [a, b] = parts;
5869
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 127 || a === 169 && b === 254 || a === 0;
5870
+ }
5871
+ function ipv4FromMappedIPv6(host) {
5872
+ const lower = host.toLowerCase();
5873
+ const dotted = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
5874
+ if (dotted)
5875
+ return dotted[1];
5876
+ const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower);
5877
+ if (!hex)
5878
+ return void 0;
5879
+ const high = Number.parseInt(hex[1], 16);
5880
+ const low = Number.parseInt(hex[2], 16);
5881
+ if (!Number.isFinite(high) || !Number.isFinite(low))
5882
+ return void 0;
5883
+ return [high >> 8, high & 255, low >> 8, low & 255].join(".");
5884
+ }
5885
+ function isPrivateIPv6(host) {
5886
+ const lower = host.toLowerCase();
5887
+ const mapped = ipv4FromMappedIPv6(lower);
5888
+ if (mapped)
5889
+ return isPrivateIPv4(mapped);
5890
+ return lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80:");
5891
+ }
5892
+ function isMetadataUrl(input) {
5893
+ const url = typeof input === "string" ? new URL(input) : input;
5894
+ const host = normalizeHostname(url.hostname);
5895
+ const mapped = ipv4FromMappedIPv6(host);
5896
+ return METADATA_HOSTS.has(host) || METADATA_IPS.has(host) || (mapped ? METADATA_IPS.has(mapped) : false);
5897
+ }
5898
+ function isPrivateHost(hostname) {
5899
+ const host = normalizeHostname(hostname);
5900
+ if (host === "localhost" || host.endsWith(".localhost"))
5901
+ return true;
5902
+ const ipKind = isIP(host);
5903
+ if (ipKind === 4)
5904
+ return isPrivateIPv4(host);
5905
+ if (ipKind === 6)
5906
+ return isPrivateIPv6(host);
5907
+ return false;
5908
+ }
5909
+ function validateHttpUrl(input, options = {}) {
5910
+ let url;
5911
+ try {
5912
+ url = new URL(input);
5913
+ } catch {
5914
+ return { ok: false, error: "Invalid URL" };
5915
+ }
5916
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
5917
+ return { ok: false, error: "Only http:// and https:// URLs are supported" };
5918
+ }
5919
+ if (url.username || url.password) {
5920
+ return { ok: false, error: "Blocked: URL contains embedded credentials" };
5921
+ }
5922
+ if (hasTokenLikeSecret(url.href)) {
5923
+ return { ok: false, error: "Blocked: URL contains what appears to be an API key, token, password, or secret" };
5924
+ }
5925
+ if (isMetadataUrl(url)) {
5926
+ return { ok: false, error: "Blocked: URL targets a cloud metadata endpoint" };
5927
+ }
5928
+ if (!options.allowPrivate && isPrivateHost(url.hostname)) {
5929
+ return { ok: false, error: "Blocked: URL targets a private, loopback, or internal network address" };
5930
+ }
5931
+ return { ok: true, url };
5932
+ }
5933
+ var TOKEN_PATTERNS, METADATA_HOSTS, METADATA_IPS;
5934
+ var init_url_safety = __esm({
5935
+ "../openjaw-mcp/dist/tools/url-safety.js"() {
5936
+ "use strict";
5937
+ TOKEN_PATTERNS = [
5938
+ /\b(?:sk|ghp|github_pat|gho|ghu|ghs|glpat|xox[baprs]|ya29|AIza)[A-Za-z0-9_\-]{12,}\b/i,
5939
+ /(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer|secret|password|passwd|pwd|token)=([^&\s]{8,})/i
5940
+ ];
5941
+ METADATA_HOSTS = /* @__PURE__ */ new Set([
5942
+ "metadata.google.internal",
5943
+ "metadata.azure.internal"
5944
+ ]);
5945
+ METADATA_IPS = /* @__PURE__ */ new Set([
5946
+ "169.254.169.254",
5947
+ "100.100.100.200"
5948
+ ]);
5949
+ __name(hasTokenLikeSecret, "hasTokenLikeSecret");
5950
+ __name(safeDecode, "safeDecode");
5951
+ __name(normalizeHostname, "normalizeHostname");
5952
+ __name(isPrivateIPv4, "isPrivateIPv4");
5953
+ __name(ipv4FromMappedIPv6, "ipv4FromMappedIPv6");
5954
+ __name(isPrivateIPv6, "isPrivateIPv6");
5955
+ __name(isMetadataUrl, "isMetadataUrl");
5956
+ __name(isPrivateHost, "isPrivateHost");
5957
+ __name(validateHttpUrl, "validateHttpUrl");
5958
+ }
5959
+ });
5960
+
5841
5961
  // ../openjaw-mcp/dist/channels/browser.js
5842
5962
  import * as chromeLauncher from "chrome-launcher";
5843
5963
  import CDP from "chrome-remote-interface";
5844
5964
  function escapeForJs(str) {
5845
5965
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\0/g, "\\0").replace(/</g, "\\x3c").replace(/>/g, "\\x3e");
5846
5966
  }
5847
- var BrowserChannel;
5967
+ var MAX_CONSOLE_MESSAGES, MAX_JS_ERRORS, MAX_CONSOLE_TEXT, BrowserChannel;
5848
5968
  var init_browser = __esm({
5849
5969
  "../openjaw-mcp/dist/channels/browser.js"() {
5850
5970
  "use strict";
5851
5971
  init_logger();
5972
+ init_url_safety();
5852
5973
  __name(escapeForJs, "escapeForJs");
5974
+ MAX_CONSOLE_MESSAGES = 200;
5975
+ MAX_JS_ERRORS = 100;
5976
+ MAX_CONSOLE_TEXT = 4e3;
5853
5977
  BrowserChannel = class {
5854
5978
  static {
5855
5979
  __name(this, "BrowserChannel");
@@ -5859,6 +5983,10 @@ var init_browser = __esm({
5859
5983
  client = null;
5860
5984
  page = null;
5861
5985
  launchPromise = null;
5986
+ currentSnapshotId = null;
5987
+ currentRefs = /* @__PURE__ */ new Map();
5988
+ consoleBuffer = [];
5989
+ jsErrorBuffer = [];
5862
5990
  constructor(config) {
5863
5991
  this.config = config;
5864
5992
  }
@@ -5905,6 +6033,8 @@ var init_browser = __esm({
5905
6033
  this.client = null;
5906
6034
  this.page = null;
5907
6035
  this.launchPromise = null;
6036
+ this.consoleBuffer = [];
6037
+ this.jsErrorBuffer = [];
5908
6038
  logger_default.info("Browser connection state reset");
5909
6039
  }
5910
6040
  /**
@@ -5957,33 +6087,99 @@ var init_browser = __esm({
5957
6087
  DOM.enable(),
5958
6088
  Network.enable()
5959
6089
  ]);
6090
+ this.attachRuntimeEventBuffers(Runtime);
6091
+ await this.attachRequestSafetyInterception(this.client.Fetch);
5960
6092
  this.page = { Runtime, Page, DOM, Input };
5961
6093
  logger_default.info("Browser launched", { port: this.chrome.port });
5962
6094
  }
6095
+ async attachRequestSafetyInterception(Fetch) {
6096
+ const fetchDomain = Fetch;
6097
+ if (!fetchDomain?.enable || !fetchDomain.requestPaused || !fetchDomain.failRequest || !fetchDomain.continueRequest) {
6098
+ logger_default.debug("Browser Fetch domain unavailable; navigation safety falls back to pre/final URL checks");
6099
+ return;
6100
+ }
6101
+ fetchDomain.requestPaused((event) => {
6102
+ void (async () => {
6103
+ try {
6104
+ const url = new URL(event.request.url);
6105
+ if (url.protocol === "http:" || url.protocol === "https:") {
6106
+ const safety = validateHttpUrl(url.href, { allowPrivate: true });
6107
+ if (!safety.ok) {
6108
+ await fetchDomain.failRequest({ requestId: event.requestId, errorReason: "BlockedByClient" });
6109
+ return;
6110
+ }
6111
+ }
6112
+ await fetchDomain.continueRequest({ requestId: event.requestId });
6113
+ } catch {
6114
+ await fetchDomain.continueRequest({ requestId: event.requestId }).catch(() => void 0);
6115
+ }
6116
+ })();
6117
+ });
6118
+ await fetchDomain.enable({ patterns: [{ urlPattern: "*", requestStage: "Request" }] });
6119
+ }
6120
+ attachRuntimeEventBuffers(Runtime) {
6121
+ const runtime = Runtime;
6122
+ runtime.consoleAPICalled?.((event) => {
6123
+ const text = (event.args ?? []).map((arg) => String(arg.value ?? arg.description ?? "")).join(" ").slice(0, MAX_CONSOLE_TEXT);
6124
+ this.consoleBuffer.push({
6125
+ type: event.type ?? "log",
6126
+ text,
6127
+ timestamp: event.timestamp ?? Date.now()
6128
+ });
6129
+ if (this.consoleBuffer.length > MAX_CONSOLE_MESSAGES) {
6130
+ this.consoleBuffer.splice(0, this.consoleBuffer.length - MAX_CONSOLE_MESSAGES);
6131
+ }
6132
+ });
6133
+ runtime.exceptionThrown?.((event) => {
6134
+ const details = event.exceptionDetails ?? {};
6135
+ this.jsErrorBuffer.push({
6136
+ message: String(details.exception?.description ?? details.exception?.value ?? details.text ?? "JavaScript exception").slice(0, MAX_CONSOLE_TEXT),
6137
+ url: details.url,
6138
+ line: details.lineNumber,
6139
+ column: details.columnNumber,
6140
+ timestamp: event.timestamp ?? Date.now()
6141
+ });
6142
+ if (this.jsErrorBuffer.length > MAX_JS_ERRORS) {
6143
+ this.jsErrorBuffer.splice(0, this.jsErrorBuffer.length - MAX_JS_ERRORS);
6144
+ }
6145
+ });
6146
+ }
5963
6147
  async navigate(options) {
5964
6148
  await this.ensureBrowser();
5965
6149
  const { Page, Runtime } = this.page;
5966
- await Page.navigate({ url: options.url });
6150
+ const initialSafety = validateHttpUrl(options.url, { allowPrivate: true });
6151
+ if (!initialSafety.ok) {
6152
+ return { url: options.url, title: "", snapshot: "", error: initialSafety.error };
6153
+ }
6154
+ await Page.navigate({ url: initialSafety.url.href });
5967
6155
  if (options.waitFor === "load") {
5968
6156
  await Page.loadEventFired();
5969
6157
  } else if (options.waitFor === "domcontentloaded") {
5970
6158
  await Page.domContentEventFired();
5971
6159
  } else if (options.waitFor === "networkidle") {
5972
6160
  await Page.loadEventFired();
5973
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
6161
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
5974
6162
  }
5975
6163
  const result = await Runtime.evaluate({
5976
6164
  expression: "document.title"
5977
6165
  });
5978
- return {
5979
- url: options.url,
5980
- title: result.result.value
5981
- };
6166
+ const url = await this.getCurrentUrl() ?? initialSafety.url.href;
6167
+ const finalSafety = validateHttpUrl(url, { allowPrivate: true });
6168
+ if (!finalSafety.ok) {
6169
+ return { url, title: result.result.value, snapshot: "", error: `Blocked final URL: ${finalSafety.error}` };
6170
+ }
6171
+ const title = result.result.value;
6172
+ const snapshot = await this.snapshot({ full: false });
6173
+ return { ...snapshot, url, title };
5982
6174
  }
5983
6175
  async click(options) {
5984
6176
  await this.ensureBrowser();
5985
6177
  const { Runtime } = this.page;
5986
- const findExpr = options.selector ? `document.querySelector('${escapeForJs(options.selector)}')` : options.text ? `(() => {
6178
+ const resolvedSelector = options.ref ? this.resolveRef(options.ref, options.snapshotId) : options.selector;
6179
+ if (options.ref && !resolvedSelector) {
6180
+ return { success: false, element: `stale or unknown ref ${options.ref}; call browser_snapshot again` };
6181
+ }
6182
+ const findExpr = resolvedSelector ? `document.querySelector('${escapeForJs(resolvedSelector)}')` : options.text ? `(() => {
5987
6183
  const isVis = (e) => {
5988
6184
  if (!e.offsetParent && e.tagName !== 'BODY' && e.tagName !== 'HTML') return false;
5989
6185
  const s = getComputedStyle(e);
@@ -6107,9 +6303,13 @@ var init_browser = __esm({
6107
6303
  async type(options) {
6108
6304
  await this.ensureBrowser();
6109
6305
  const { Runtime } = this.page;
6306
+ const resolvedSelector = options.ref ? this.resolveRef(options.ref, options.snapshotId) : options.selector;
6307
+ if (!resolvedSelector) {
6308
+ return { success: false, error: options.ref ? `stale or unknown ref ${options.ref}; call browser_snapshot again` : "No selector or ref provided" };
6309
+ }
6110
6310
  const script = `
6111
6311
  (() => {
6112
- const el = document.querySelector('${escapeForJs(options.selector)}');
6312
+ const el = document.querySelector('${escapeForJs(resolvedSelector)}');
6113
6313
  if (el) {
6114
6314
  ${options.clear ? "el.value = '';" : ""}
6115
6315
  el.value = '${escapeForJs(options.text)}';
@@ -6191,6 +6391,141 @@ var init_browser = __esm({
6191
6391
  const result = await Runtime.evaluate({ expression: script, returnByValue: true });
6192
6392
  return { content: result.result.value };
6193
6393
  }
6394
+ async snapshot(options) {
6395
+ await this.ensureBrowser();
6396
+ const { Runtime } = this.page;
6397
+ const scope = options?.selector ? `'${escapeForJs(options.selector)}'` : `'body'`;
6398
+ const maxElements = Math.min(Math.max(options?.maxElements ?? 80, 1), 200);
6399
+ const textLimit = options?.full ? 8e3 : 1200;
6400
+ const script = `(() => {
6401
+ const root = document.querySelector(${scope});
6402
+ const cssEscape = (value) => globalThis.CSS?.escape
6403
+ ? globalThis.CSS.escape(String(value))
6404
+ : String(value).replace(/[^a-zA-Z0-9_-]/g, ch => '\\\\' + ch.charCodeAt(0).toString(16) + ' ');
6405
+ const selectorFor = (el) => {
6406
+ if (el.id) return '#' + cssEscape(el.id);
6407
+ const parts = [];
6408
+ let cur = el;
6409
+ while (cur && cur.nodeType === 1 && cur !== document.body) {
6410
+ let part = cur.tagName.toLowerCase();
6411
+ const parent = cur.parentElement;
6412
+ if (!parent) break;
6413
+ const same = Array.from(parent.children).filter(child => child.tagName === cur.tagName);
6414
+ if (same.length > 1) part += ':nth-of-type(' + (same.indexOf(cur) + 1) + ')';
6415
+ parts.unshift(part);
6416
+ cur = parent;
6417
+ }
6418
+ return parts.length ? parts.join(' > ') : 'body';
6419
+ };
6420
+ const isVisible = (el) => {
6421
+ const rect = el.getBoundingClientRect();
6422
+ const style = getComputedStyle(el);
6423
+ return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
6424
+ };
6425
+ const disabled = (el) => Boolean(el.disabled || el.getAttribute('aria-disabled') === 'true');
6426
+ if (!root) return JSON.stringify({ url: location.href, title: document.title, elements: [], text: '' });
6427
+ const nodes = Array.from(root.querySelectorAll('button, a[href], input, select, textarea, summary, [role="button"], [role="link"], [role="textbox"], [onclick], [tabindex]'));
6428
+ const elements = [];
6429
+ for (const el of nodes) {
6430
+ if (elements.length >= ${maxElements}) break;
6431
+ const tag = el.tagName.toLowerCase();
6432
+ const type = el.getAttribute('type') || '';
6433
+ const role = el.getAttribute('role') || (type ? tag + '[' + type + ']' : tag);
6434
+ const text = (el.innerText || el.textContent || '').replace(/s+/g, ' ').trim().slice(0, 120);
6435
+ const label = (el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || text || el.getAttribute('value') || '').replace(/s+/g, ' ').trim().slice(0, 120);
6436
+ elements.push({
6437
+ ref: '@e' + (elements.length + 1),
6438
+ selector: selectorFor(el),
6439
+ role,
6440
+ tag,
6441
+ label,
6442
+ text,
6443
+ visible: isVisible(el),
6444
+ disabled: disabled(el),
6445
+ });
6446
+ }
6447
+ const pageText = (root.innerText || root.textContent || '').replace(/s+/g, ' ').trim().slice(0, ${textLimit});
6448
+ return JSON.stringify({ url: location.href, title: document.title, elements, text: pageText });
6449
+ })()`;
6450
+ const result = await Runtime.evaluate({ expression: script, returnByValue: true });
6451
+ const parsed = JSON.parse(result.result.value || "{}");
6452
+ const snapshotId = `snap_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
6453
+ const elements = parsed.elements ?? [];
6454
+ const refs = {};
6455
+ this.currentRefs.clear();
6456
+ for (const element of elements) {
6457
+ refs[element.ref] = element.selector;
6458
+ this.currentRefs.set(element.ref, element.selector);
6459
+ }
6460
+ this.currentSnapshotId = snapshotId;
6461
+ const lines = [
6462
+ `URL: ${parsed.url ?? ""}`,
6463
+ `Title: ${parsed.title ?? ""}`,
6464
+ `Snapshot: ${snapshotId}`,
6465
+ "",
6466
+ `Interactive elements (${elements.length}):`,
6467
+ ...elements.map((element) => `${element.ref} <${element.role}>${element.visible ? "" : " [hidden]"}${element.disabled ? " [disabled]" : ""} ${JSON.stringify(element.label || element.text || element.selector)}`),
6468
+ "",
6469
+ "Visible text:",
6470
+ parsed.text ?? ""
6471
+ ];
6472
+ return {
6473
+ url: parsed.url ?? "",
6474
+ title: parsed.title ?? "",
6475
+ snapshot: lines.join("\n").trim(),
6476
+ snapshot_id: snapshotId,
6477
+ refs,
6478
+ elements,
6479
+ text: parsed.text ?? "",
6480
+ element_count: elements.length
6481
+ };
6482
+ }
6483
+ resolveRef(ref, snapshotId) {
6484
+ if (snapshotId && this.currentSnapshotId && snapshotId !== this.currentSnapshotId)
6485
+ return void 0;
6486
+ return this.currentRefs.get(ref);
6487
+ }
6488
+ async back() {
6489
+ await this.ensureBrowser();
6490
+ const { Page, Runtime } = this.page;
6491
+ const history = await Page.getNavigationHistory();
6492
+ if (history.currentIndex > 0) {
6493
+ await Page.navigateToHistoryEntry({ entryId: history.entries[history.currentIndex - 1].id });
6494
+ await Page.loadEventFired().catch(() => void 0);
6495
+ }
6496
+ const title = await Runtime.evaluate({ expression: "document.title", returnByValue: true });
6497
+ return { url: await this.getCurrentUrl(), title: title.result.value };
6498
+ }
6499
+ async press(key) {
6500
+ await this.sendKey(key);
6501
+ return { success: true, key };
6502
+ }
6503
+ async getImages() {
6504
+ await this.ensureBrowser();
6505
+ const { Runtime } = this.page;
6506
+ const script = `JSON.stringify(Array.from(document.images).slice(0, 100).map(img => {
6507
+ const rect = img.getBoundingClientRect();
6508
+ return { src: img.currentSrc || img.src, alt: img.alt || '', width: img.naturalWidth || rect.width || 0, height: img.naturalHeight || rect.height || 0, visible: rect.width > 0 && rect.height > 0 };
6509
+ }).filter(img => img.src && !img.src.startsWith('data:')))`;
6510
+ const result = await Runtime.evaluate({ expression: script, returnByValue: true });
6511
+ const images = JSON.parse(result.result.value || "[]");
6512
+ return { images, count: images.length };
6513
+ }
6514
+ async consoleMessages(clear2 = false) {
6515
+ const consoleMessages = [...this.consoleBuffer];
6516
+ const jsErrors = [...this.jsErrorBuffer];
6517
+ if (clear2) {
6518
+ this.consoleBuffer = [];
6519
+ this.jsErrorBuffer = [];
6520
+ }
6521
+ return {
6522
+ console_messages: consoleMessages,
6523
+ js_errors: jsErrors,
6524
+ total_messages: consoleMessages.length,
6525
+ total_errors: jsErrors.length,
6526
+ note: clear2 ? "Returned and cleared buffered console messages." : "Returned buffered console messages."
6527
+ };
6528
+ }
6194
6529
  async evaluate(script) {
6195
6530
  await this.ensureBrowser();
6196
6531
  const { Runtime } = this.page;
@@ -6594,7 +6929,7 @@ var init_browser = __esm({
6594
6929
  if (exists) {
6595
6930
  return true;
6596
6931
  }
6597
- await new Promise((resolve5) => setTimeout(resolve5, 200));
6932
+ await new Promise((resolve6) => setTimeout(resolve6, 200));
6598
6933
  }
6599
6934
  return false;
6600
6935
  }
@@ -6625,18 +6960,48 @@ function createBrowseTools(config, sharedBrowser) {
6625
6960
  required: ["url"]
6626
6961
  },
6627
6962
  execute: /* @__PURE__ */ __name(async (input) => {
6963
+ const safety = validateHttpUrl(input.url, { allowPrivate: true });
6964
+ if (!safety.ok)
6965
+ return { error: safety.error, url: input.url };
6628
6966
  return await browser.navigate({
6629
6967
  url: input.url,
6630
6968
  waitFor: input.wait_for ?? "load"
6631
6969
  });
6632
6970
  }, "execute")
6633
6971
  },
6972
+ {
6973
+ name: "browser_snapshot",
6974
+ description: "Get a compact snapshot of the current page with stable refs like @e1 for browser_click and browser_type. Use after navigation or after interactions that changed the page.",
6975
+ parameters: {
6976
+ type: "object",
6977
+ properties: {
6978
+ full: { type: "boolean", description: "Include more visible page text (default false)", default: false },
6979
+ selector: { type: "string", description: "Optional CSS selector to scope the snapshot" },
6980
+ max_elements: { type: "number", description: "Maximum interactive elements to include (default 80, max 200)" }
6981
+ }
6982
+ },
6983
+ execute: /* @__PURE__ */ __name(async (input) => {
6984
+ return await browser.snapshot({
6985
+ full: input.full === true,
6986
+ selector: input.selector,
6987
+ maxElements: input.max_elements
6988
+ });
6989
+ }, "execute")
6990
+ },
6634
6991
  {
6635
6992
  name: "browser_click",
6636
- description: "Click an element on the page",
6993
+ description: "Click an element on the page. Prefer ref from browser_snapshot (e.g. @e5). Legacy selector/text targeting is still supported.",
6637
6994
  parameters: {
6638
6995
  type: "object",
6639
6996
  properties: {
6997
+ ref: {
6998
+ type: "string",
6999
+ description: "Element ref from browser_snapshot, e.g. @e5"
7000
+ },
7001
+ snapshot_id: {
7002
+ type: "string",
7003
+ description: "Optional snapshot_id associated with the ref. If stale, refresh with browser_snapshot."
7004
+ },
6640
7005
  selector: {
6641
7006
  type: "string",
6642
7007
  description: "CSS selector of element to click"
@@ -6649,6 +7014,8 @@ function createBrowseTools(config, sharedBrowser) {
6649
7014
  },
6650
7015
  execute: /* @__PURE__ */ __name(async (input) => {
6651
7016
  return await browser.click({
7017
+ ref: input.ref,
7018
+ snapshotId: input.snapshot_id,
6652
7019
  selector: input.selector,
6653
7020
  text: input.text
6654
7021
  });
@@ -6656,10 +7023,18 @@ function createBrowseTools(config, sharedBrowser) {
6656
7023
  },
6657
7024
  {
6658
7025
  name: "browser_type",
6659
- description: "Type text into an input element",
7026
+ description: "Type text into an input element. Prefer ref from browser_snapshot (e.g. @e3). Legacy selector targeting is still supported.",
6660
7027
  parameters: {
6661
7028
  type: "object",
6662
7029
  properties: {
7030
+ ref: {
7031
+ type: "string",
7032
+ description: "Element ref from browser_snapshot, e.g. @e3"
7033
+ },
7034
+ snapshot_id: {
7035
+ type: "string",
7036
+ description: "Optional snapshot_id associated with the ref. If stale, refresh with browser_snapshot."
7037
+ },
6663
7038
  selector: {
6664
7039
  type: "string",
6665
7040
  description: "CSS selector of input element"
@@ -6679,10 +7054,12 @@ function createBrowseTools(config, sharedBrowser) {
6679
7054
  default: false
6680
7055
  }
6681
7056
  },
6682
- required: ["selector", "text"]
7057
+ required: ["text"]
6683
7058
  },
6684
7059
  execute: /* @__PURE__ */ __name(async (input) => {
6685
7060
  return await browser.type({
7061
+ ref: input.ref,
7062
+ snapshotId: input.snapshot_id,
6686
7063
  selector: input.selector,
6687
7064
  text: input.text,
6688
7065
  clear: input.clear ?? true,
@@ -6690,6 +7067,24 @@ function createBrowseTools(config, sharedBrowser) {
6690
7067
  });
6691
7068
  }, "execute")
6692
7069
  },
7070
+ {
7071
+ name: "browser_back",
7072
+ description: "Navigate back to the previous page in browser history.",
7073
+ parameters: { type: "object", properties: {} },
7074
+ execute: /* @__PURE__ */ __name(async () => await browser.back(), "execute")
7075
+ },
7076
+ {
7077
+ name: "browser_press",
7078
+ description: "Press a keyboard key in the browser, e.g. Enter, Tab, Escape, ArrowDown.",
7079
+ parameters: {
7080
+ type: "object",
7081
+ properties: {
7082
+ key: { type: "string", description: "Key to press, e.g. Enter, Tab, Escape, ArrowDown" }
7083
+ },
7084
+ required: ["key"]
7085
+ },
7086
+ execute: /* @__PURE__ */ __name(async (input) => await browser.press(input.key), "execute")
7087
+ },
6693
7088
  {
6694
7089
  name: "browser_extract",
6695
7090
  description: "Extract text content from the page",
@@ -6734,6 +7129,51 @@ function createBrowseTools(config, sharedBrowser) {
6734
7129
  return await browser.evaluate(input.script);
6735
7130
  }, "execute")
6736
7131
  },
7132
+ {
7133
+ name: "browser_console",
7134
+ description: "Read browser console logs and JavaScript errors. This tool is read-only; use browser_evaluate for JavaScript execution.",
7135
+ parameters: {
7136
+ type: "object",
7137
+ properties: {
7138
+ clear: { type: "boolean", description: "Clear buffered logs after reading, if buffering is active", default: false }
7139
+ }
7140
+ },
7141
+ execute: /* @__PURE__ */ __name(async (input) => await browser.consoleMessages(input.clear === true), "execute")
7142
+ },
7143
+ {
7144
+ name: "browser_get_images",
7145
+ description: "List images on the current page with URLs, alt text, dimensions, and visibility. Use before visual analysis or downloading images.",
7146
+ parameters: { type: "object", properties: {} },
7147
+ execute: /* @__PURE__ */ __name(async () => await browser.getImages(), "execute")
7148
+ },
7149
+ {
7150
+ name: "browser_vision",
7151
+ description: "Take a browser screenshot for visual inspection. Returns a screenshot path plus page metadata. Use for visual layouts, CAPTCHAs, screenshots, or image-heavy pages.",
7152
+ parameters: {
7153
+ type: "object",
7154
+ properties: {
7155
+ question: { type: "string", description: "What you want to inspect visually" },
7156
+ annotate: { type: "boolean", description: "Reserved for future ref overlays; currently returns a normal screenshot", default: false }
7157
+ }
7158
+ },
7159
+ execute: /* @__PURE__ */ __name(async (input) => {
7160
+ const { join: join48 } = await import("node:path");
7161
+ const { tmpdir: tmpdir13 } = await import("node:os");
7162
+ const { randomUUID: randomUUID15 } = await import("node:crypto");
7163
+ const screenshotPath = join48(tmpdir13(), `openjaw-browser-${randomUUID15().slice(0, 8)}.png`);
7164
+ const screenshot = await browser.screenshot({ fullPage: false, path: screenshotPath });
7165
+ const snapshot = await browser.snapshot({ full: false });
7166
+ return {
7167
+ message: "Browser screenshot captured for visual inspection",
7168
+ question: input.question,
7169
+ snapshot_id: snapshot.snapshot_id,
7170
+ url: snapshot.url,
7171
+ title: snapshot.title,
7172
+ screenshotPath: screenshot.path ?? screenshotPath,
7173
+ imagePayload: "not_attached: screenshot path returned to avoid sending images to non-vision providers"
7174
+ };
7175
+ }, "execute")
7176
+ },
6737
7177
  {
6738
7178
  name: "browser_wait",
6739
7179
  description: "Wait for an element to appear, text to change, or element to hide. Use before interacting with dynamic/slow-loading pages.",
@@ -6819,6 +7259,7 @@ var init_browse = __esm({
6819
7259
  "../openjaw-mcp/dist/tools/browse.js"() {
6820
7260
  "use strict";
6821
7261
  init_browser();
7262
+ init_url_safety();
6822
7263
  __name(createBrowseTools, "createBrowseTools");
6823
7264
  }
6824
7265
  });
@@ -7414,7 +7855,7 @@ var init_outlook_desktop = __esm({
7414
7855
  }
7415
7856
  }
7416
7857
  sleep(ms) {
7417
- return new Promise((resolve5) => setTimeout(resolve5, ms));
7858
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
7418
7859
  }
7419
7860
  };
7420
7861
  }
@@ -8012,7 +8453,7 @@ var init_outlook_web = __esm({
8012
8453
  await this.browser.typeChars(text);
8013
8454
  }
8014
8455
  sleep(ms) {
8015
- return new Promise((resolve5) => setTimeout(resolve5, ms));
8456
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
8016
8457
  }
8017
8458
  };
8018
8459
  }
@@ -8246,8 +8687,8 @@ var init_token_pool = __esm({
8246
8687
  if (!existsSync8(legacyDir))
8247
8688
  return;
8248
8689
  try {
8249
- const { readdirSync: readdirSync7 } = __require("node:fs");
8250
- const files = readdirSync7(legacyDir);
8690
+ const { readdirSync: readdirSync8 } = __require("node:fs");
8691
+ const files = readdirSync8(legacyDir);
8251
8692
  for (const file2 of files) {
8252
8693
  if (!file2.endsWith(".json"))
8253
8694
  continue;
@@ -8427,7 +8868,7 @@ var init_cdp_token_extractor = __esm({
8427
8868
  }
8428
8869
  logger_default.info("CDP: reloading tab for token refresh", { url: targetTab.url, audience });
8429
8870
  await this.reloadTab(targetTab);
8430
- await new Promise((resolve5) => setTimeout(resolve5, PAGE_RELOAD_WAIT_MS));
8871
+ await new Promise((resolve6) => setTimeout(resolve6, PAGE_RELOAD_WAIT_MS));
8431
8872
  if (!targetTab.webSocketDebuggerUrl)
8432
8873
  return [];
8433
8874
  const freshPages = await this.listPages();
@@ -8544,19 +8985,19 @@ var init_cdp_token_extractor = __esm({
8544
8985
  * Evaluate a JS expression in a tab and return the string result.
8545
8986
  */
8546
8987
  async evaluateInTab(wsUrl, expression) {
8547
- return new Promise((resolve5) => {
8988
+ return new Promise((resolve6) => {
8548
8989
  let ws;
8549
8990
  try {
8550
8991
  ws = new WebSocket(wsUrl);
8551
8992
  } catch (err) {
8552
8993
  logger_default.warn("CDP: WebSocket constructor failed", { wsUrl: wsUrl.substring(0, 60), error: String(err) });
8553
- resolve5(null);
8994
+ resolve6(null);
8554
8995
  return;
8555
8996
  }
8556
8997
  const timer = setTimeout(() => {
8557
8998
  logger_default.warn("CDP: evaluateInTab timeout", { wsUrl: wsUrl.substring(0, 60) });
8558
8999
  ws.close();
8559
- resolve5(null);
9000
+ resolve6(null);
8560
9001
  }, CDP_TIMEOUT_MS);
8561
9002
  ws.on("open", () => {
8562
9003
  ws.send(JSON.stringify({
@@ -8570,18 +9011,18 @@ var init_cdp_token_extractor = __esm({
8570
9011
  if (resp.id === 1) {
8571
9012
  clearTimeout(timer);
8572
9013
  ws.close();
8573
- resolve5(resp.result?.result?.value ?? null);
9014
+ resolve6(resp.result?.result?.value ?? null);
8574
9015
  }
8575
9016
  });
8576
9017
  ws.on("error", (err) => {
8577
9018
  logger_default.warn("CDP: evaluateInTab WS error", { error: String(err), wsUrl: wsUrl.substring(0, 60) });
8578
9019
  clearTimeout(timer);
8579
- resolve5(null);
9020
+ resolve6(null);
8580
9021
  });
8581
9022
  });
8582
9023
  }
8583
9024
  async extractFromTab(wsUrl) {
8584
- return new Promise((resolve5, reject) => {
9025
+ return new Promise((resolve6, reject) => {
8585
9026
  const ws = new WebSocket(wsUrl);
8586
9027
  const timer = setTimeout(() => {
8587
9028
  ws.close();
@@ -8601,14 +9042,14 @@ var init_cdp_token_extractor = __esm({
8601
9042
  ws.close();
8602
9043
  const value = resp.result?.result?.value;
8603
9044
  if (!value) {
8604
- resolve5([]);
9045
+ resolve6([]);
8605
9046
  return;
8606
9047
  }
8607
9048
  try {
8608
9049
  const tokens = JSON.parse(value);
8609
- resolve5(tokens);
9050
+ resolve6(tokens);
8610
9051
  } catch {
8611
- resolve5([]);
9052
+ resolve6([]);
8612
9053
  }
8613
9054
  }
8614
9055
  });
@@ -8624,11 +9065,11 @@ var init_cdp_token_extractor = __esm({
8624
9065
  async reloadTab(page) {
8625
9066
  if (!page.webSocketDebuggerUrl)
8626
9067
  return;
8627
- return new Promise((resolve5, reject) => {
9068
+ return new Promise((resolve6, reject) => {
8628
9069
  const ws = new WebSocket(page.webSocketDebuggerUrl);
8629
9070
  const timer = setTimeout(() => {
8630
9071
  ws.close();
8631
- resolve5();
9072
+ resolve6();
8632
9073
  }, 1e4);
8633
9074
  ws.on("open", () => {
8634
9075
  ws.send(JSON.stringify({
@@ -8642,7 +9083,7 @@ var init_cdp_token_extractor = __esm({
8642
9083
  if (resp.id === 1) {
8643
9084
  clearTimeout(timer);
8644
9085
  ws.close();
8645
- resolve5();
9086
+ resolve6();
8646
9087
  }
8647
9088
  });
8648
9089
  ws.on("error", (err) => {
@@ -10303,13 +10744,13 @@ function createMemoryTools(config) {
10303
10744
  const todos = input.todos;
10304
10745
  try {
10305
10746
  const { appendFile: appendFile2, mkdir: mkdir5 } = await import("node:fs/promises");
10306
- const { existsSync: existsSync33 } = await import("node:fs");
10307
- const { join: join47 } = await import("node:path");
10308
- const { homedir: homedir31 } = await import("node:os");
10309
- const memoryDir = join47(homedir31(), ".openjaw", "memory");
10310
- if (!existsSync33(memoryDir))
10747
+ const { existsSync: existsSync34 } = await import("node:fs");
10748
+ const { join: join48 } = await import("node:path");
10749
+ const { homedir: homedir32 } = await import("node:os");
10750
+ const memoryDir = join48(homedir32(), ".openjaw", "memory");
10751
+ if (!existsSync34(memoryDir))
10311
10752
  await mkdir5(memoryDir, { recursive: true });
10312
- const todoPath = join47(memoryDir, "TODOS.md");
10753
+ const todoPath = join48(memoryDir, "TODOS.md");
10313
10754
  const { writeFile: writeFile5 } = await import("node:fs/promises");
10314
10755
  await writeFile5(todoPath, `# Session Todos
10315
10756
 
@@ -11630,7 +12071,7 @@ var init_teams_desktop = __esm({
11630
12071
  }
11631
12072
  }
11632
12073
  sleep(ms) {
11633
- return new Promise((resolve5) => setTimeout(resolve5, ms));
12074
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
11634
12075
  }
11635
12076
  /**
11636
12077
  * Get the current Teams window state
@@ -12497,7 +12938,7 @@ var init_teams_web = __esm({
12497
12938
  }
12498
12939
  }
12499
12940
  sleep(ms) {
12500
- return new Promise((resolve5) => setTimeout(resolve5, ms));
12941
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
12501
12942
  }
12502
12943
  };
12503
12944
  }
@@ -13895,7 +14336,7 @@ var init_teams_chat_monitor = __esm({
13895
14336
  return this.sentMessages.has(normalized);
13896
14337
  }
13897
14338
  sleep(ms) {
13898
- return new Promise((resolve5) => setTimeout(resolve5, ms));
14339
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
13899
14340
  }
13900
14341
  /**
13901
14342
  * Attempt to reconnect the browser after detecting a disconnection.
@@ -14682,7 +15123,7 @@ ${lines.join("\n")}`;
14682
15123
  globalThis.__teamsSeenMessages.set(chatName, seenIds);
14683
15124
  }
14684
15125
  if (syncMode) {
14685
- return new Promise((resolve5) => {
15126
+ return new Promise((resolve6) => {
14686
15127
  const timer2 = setInterval(async () => {
14687
15128
  try {
14688
15129
  const current = await channel.readCurrentChatMessages();
@@ -14691,7 +15132,7 @@ ${lines.join("\n")}`;
14691
15132
  if (!seenIds.has(msgId) && msg.sender !== currentUserName) {
14692
15133
  clearInterval(timer2);
14693
15134
  seenIds.add(msgId);
14694
- resolve5({
15135
+ resolve6({
14695
15136
  success: true,
14696
15137
  channel: channelType,
14697
15138
  sync: true,
@@ -14768,7 +15209,7 @@ ${lines.join("\n")}`;
14768
15209
  globalThis.__teamsSeenMessages.set(chatName, seenIds);
14769
15210
  }
14770
15211
  if (syncMode) {
14771
- return new Promise((resolve5) => {
15212
+ return new Promise((resolve6) => {
14772
15213
  const timer2 = setInterval(async () => {
14773
15214
  try {
14774
15215
  const current = await channel.readCurrentChatMessages();
@@ -14777,7 +15218,7 @@ ${lines.join("\n")}`;
14777
15218
  if (!seenIds.has(msgId) && msg.sender !== currentUserName) {
14778
15219
  clearInterval(timer2);
14779
15220
  seenIds.add(msgId);
14780
- resolve5({
15221
+ resolve6({
14781
15222
  success: true,
14782
15223
  channel: channelType,
14783
15224
  sync: true,
@@ -15135,7 +15576,7 @@ ${lines.join("\n")}`;
15135
15576
  return allTools;
15136
15577
  }
15137
15578
  function sleep2(ms) {
15138
- return new Promise((resolve5) => setTimeout(resolve5, ms));
15579
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
15139
15580
  }
15140
15581
  var MAX_STORED_ENTRIES, ENTRY_TTL_MS;
15141
15582
  var init_chat = __esm({
@@ -15750,6 +16191,175 @@ function urlMatchesDomain(url, domain) {
15750
16191
  return false;
15751
16192
  }
15752
16193
  }
16194
+ function normalizeSearchResults(results, limit) {
16195
+ return results.slice(0, limit).map((result, index) => ({
16196
+ ...result,
16197
+ snippet: result.snippet ?? "",
16198
+ description: result.snippet ?? "",
16199
+ position: index + 1
16200
+ }));
16201
+ }
16202
+ function clampNumber(value, fallback, min, max) {
16203
+ const numberValue = typeof value === "number" ? value : Number(value);
16204
+ if (!Number.isFinite(numberValue))
16205
+ return fallback;
16206
+ return Math.min(Math.max(Math.trunc(numberValue), min), max);
16207
+ }
16208
+ function extractHtmlTitle(html) {
16209
+ const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
16210
+ if (!match)
16211
+ return void 0;
16212
+ return DECODE_ENTITIES(match[1].replace(/\s+/g, " ").trim()) || void 0;
16213
+ }
16214
+ function cleanHtmlForMarkdown(html) {
16215
+ return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, "").replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, "").replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "").replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "").replace(/<header[^>]*>[\s\S]*?<\/header>/gi, "");
16216
+ }
16217
+ function htmlToText(html) {
16218
+ return DECODE_ENTITIES(cleanHtmlForMarkdown(html).replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/h[1-6]>/gi, "\n\n").replace(/<[^>]+>/g, " ").replace(/[ \t]+/g, " ").replace(/\n\s+/g, "\n").replace(/\n{3,}/g, "\n\n").trim());
16219
+ }
16220
+ function isPdfLike(url, contentType) {
16221
+ if (/application\/pdf/i.test(contentType))
16222
+ return true;
16223
+ try {
16224
+ return new URL(url).pathname.toLowerCase().endsWith(".pdf");
16225
+ } catch {
16226
+ return /\.pdf(?:$|[?#])/i.test(url);
16227
+ }
16228
+ }
16229
+ async function htmlToMarkdown(html) {
16230
+ try {
16231
+ const TurndownModule = await import("turndown");
16232
+ const TurndownService = TurndownModule.default || TurndownModule;
16233
+ const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
16234
+ return turndown.turndown(cleanHtmlForMarkdown(html));
16235
+ } catch {
16236
+ return cleanHtmlForMarkdown(html).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
16237
+ }
16238
+ }
16239
+ function paginateContent(text, startIndex, maxLength) {
16240
+ const safeStart = clampNumber(startIndex, 0, 0, Math.max(0, text.length));
16241
+ const safeMax = clampNumber(maxLength, 8e3, 1, 1e5);
16242
+ const content = text.slice(safeStart, safeStart + safeMax);
16243
+ const nextStartIndex = safeStart + content.length < text.length ? safeStart + content.length : void 0;
16244
+ return {
16245
+ content,
16246
+ contentLength: text.length,
16247
+ truncated: nextStartIndex !== void 0,
16248
+ startIndex: safeStart,
16249
+ ...nextStartIndex !== void 0 && { nextStartIndex }
16250
+ };
16251
+ }
16252
+ async function fetchWithValidatedRedirects(startUrl, options) {
16253
+ let current = startUrl;
16254
+ const maxRedirects = options.maxRedirects ?? 10;
16255
+ for (let redirects = 0; redirects <= maxRedirects; redirects++) {
16256
+ const response = await fetch(current.href, {
16257
+ signal: options.signal,
16258
+ headers: options.headers,
16259
+ redirect: "manual"
16260
+ });
16261
+ if (response.status < 300 || response.status >= 400) {
16262
+ return { response, finalUrl: current.href };
16263
+ }
16264
+ const location = response.headers.get("location");
16265
+ if (!location) {
16266
+ return { response, finalUrl: current.href };
16267
+ }
16268
+ const nextUrl = new URL(location, current.href);
16269
+ const nextSafety = validateHttpUrl(nextUrl.href, { allowPrivate: options.allowPrivate });
16270
+ if (!nextSafety.ok) {
16271
+ return { finalUrl: nextUrl.href, error: `Blocked redirect: ${nextSafety.error}` };
16272
+ }
16273
+ current = nextSafety.url;
16274
+ }
16275
+ return { finalUrl: current.href, error: `Too many redirects (>${maxRedirects})` };
16276
+ }
16277
+ async function extractUrlContent(inputUrl, options) {
16278
+ const initial = validateHttpUrl(inputUrl, { allowPrivate: options.allowPrivate });
16279
+ if (!initial.ok) {
16280
+ return {
16281
+ url: inputUrl,
16282
+ finalUrl: inputUrl,
16283
+ contentType: "",
16284
+ content: "",
16285
+ contentLength: 0,
16286
+ truncated: false,
16287
+ startIndex: options.startIndex,
16288
+ error: initial.error
16289
+ };
16290
+ }
16291
+ const controller = new AbortController();
16292
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 2e4);
16293
+ try {
16294
+ const fetched = await fetchWithValidatedRedirects(initial.url, {
16295
+ signal: controller.signal,
16296
+ headers: { "User-Agent": "OpenJaw-Agent/1.0" },
16297
+ allowPrivate: options.allowPrivate
16298
+ });
16299
+ const finalUrl = fetched.finalUrl;
16300
+ if (fetched.error || !fetched.response) {
16301
+ return {
16302
+ url: inputUrl,
16303
+ finalUrl,
16304
+ contentType: "",
16305
+ content: "",
16306
+ contentLength: 0,
16307
+ truncated: false,
16308
+ startIndex: options.startIndex,
16309
+ error: fetched.error ?? "Fetch failed before response"
16310
+ };
16311
+ }
16312
+ const response = fetched.response;
16313
+ if (!response.ok) {
16314
+ return {
16315
+ url: inputUrl,
16316
+ finalUrl,
16317
+ contentType: response.headers.get("content-type") || "",
16318
+ content: "",
16319
+ contentLength: 0,
16320
+ truncated: false,
16321
+ startIndex: options.startIndex,
16322
+ error: `HTTP ${response.status}: ${response.statusText}`
16323
+ };
16324
+ }
16325
+ const contentType = response.headers.get("content-type") || "";
16326
+ if (isPdfLike(finalUrl, contentType)) {
16327
+ return {
16328
+ url: inputUrl,
16329
+ finalUrl,
16330
+ contentType,
16331
+ content: "",
16332
+ contentLength: 0,
16333
+ truncated: false,
16334
+ startIndex: options.startIndex,
16335
+ error: "PDF extraction is not yet supported"
16336
+ };
16337
+ }
16338
+ const rawText = await response.text();
16339
+ const title = contentType.includes("html") ? extractHtmlTitle(rawText) : void 0;
16340
+ const text = contentType.includes("html") ? options.format === "text" ? htmlToText(rawText) : await htmlToMarkdown(rawText) : rawText;
16341
+ return {
16342
+ url: inputUrl,
16343
+ finalUrl,
16344
+ title,
16345
+ contentType,
16346
+ ...paginateContent(text, options.startIndex, options.maxLength)
16347
+ };
16348
+ } catch (err) {
16349
+ return {
16350
+ url: inputUrl,
16351
+ finalUrl: inputUrl,
16352
+ contentType: "",
16353
+ content: "",
16354
+ contentLength: 0,
16355
+ truncated: false,
16356
+ startIndex: options.startIndex,
16357
+ error: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`
16358
+ };
16359
+ } finally {
16360
+ clearTimeout(timeout);
16361
+ }
16362
+ }
15753
16363
  function createShellTools(_config, hooks) {
15754
16364
  return [
15755
16365
  {
@@ -15790,11 +16400,11 @@ function createShellTools(_config, hooks) {
15790
16400
  const shell = input.shell ?? true;
15791
16401
  if (input.background) {
15792
16402
  const { tmpdir: tmpdir13 } = await import("node:os");
15793
- const { join: join47 } = await import("node:path");
15794
- const { randomUUID: randomUUID14 } = await import("node:crypto");
16403
+ const { join: join48 } = await import("node:path");
16404
+ const { randomUUID: randomUUID15 } = await import("node:crypto");
15795
16405
  const { createWriteStream: createWriteStream2 } = await import("node:fs");
15796
- const taskId = randomUUID14().slice(0, 8);
15797
- const outputPath = join47(tmpdir13(), `oj-bg-${taskId}.log`);
16406
+ const taskId = randomUUID15().slice(0, 8);
16407
+ const outputPath = join48(tmpdir13(), `oj-bg-${taskId}.log`);
15798
16408
  const detached = spawn(command, [], {
15799
16409
  shell,
15800
16410
  cwd,
@@ -15814,7 +16424,7 @@ function createShellTools(_config, hooks) {
15814
16424
  message: `Command started in background (PID: ${detached.pid}). Output: ${outputPath}`
15815
16425
  };
15816
16426
  }
15817
- return new Promise((resolve5) => {
16427
+ return new Promise((resolve6) => {
15818
16428
  const proc = spawn(command, [], {
15819
16429
  shell,
15820
16430
  cwd,
@@ -15832,7 +16442,7 @@ function createShellTools(_config, hooks) {
15832
16442
  proc.on("close", (code) => {
15833
16443
  const stdoutResult = truncateOutput(stdout.trim());
15834
16444
  const stderrResult = truncateOutput(stderr.trim());
15835
- resolve5({
16445
+ resolve6({
15836
16446
  command,
15837
16447
  exitCode: code,
15838
16448
  stdout: stdoutResult.text,
@@ -15843,7 +16453,7 @@ function createShellTools(_config, hooks) {
15843
16453
  });
15844
16454
  });
15845
16455
  proc.on("error", (error) => {
15846
- resolve5({
16456
+ resolve6({
15847
16457
  command,
15848
16458
  exitCode: -1,
15849
16459
  stdout: "",
@@ -15877,10 +16487,10 @@ function createShellTools(_config, hooks) {
15877
16487
  },
15878
16488
  requiresConfirmation: false,
15879
16489
  execute: /* @__PURE__ */ __name(async (input) => {
15880
- const { writeFileSync: writeFileSync22, unlinkSync: unlinkSync9 } = await import("node:fs");
15881
- const { join: join47 } = await import("node:path");
16490
+ const { writeFileSync: writeFileSync23, unlinkSync: unlinkSync9 } = await import("node:fs");
16491
+ const { join: join48 } = await import("node:path");
15882
16492
  const { tmpdir: tmpdir13 } = await import("node:os");
15883
- const { randomUUID: randomUUID14 } = await import("node:crypto");
16493
+ const { randomUUID: randomUUID15 } = await import("node:crypto");
15884
16494
  const { execFile: execFile3 } = await import("node:child_process");
15885
16495
  const code = input.code;
15886
16496
  const language = input.language;
@@ -15897,14 +16507,14 @@ function createShellTools(_config, hooks) {
15897
16507
  const interpreter = interpreterMap[language];
15898
16508
  if (!interpreter)
15899
16509
  return { error: `Unsupported language: ${language}` };
15900
- const tmpFile = join47(tmpdir13(), `oj-code-${randomUUID14().slice(0, 8)}${ext}`);
16510
+ const tmpFile = join48(tmpdir13(), `oj-code-${randomUUID15().slice(0, 8)}${ext}`);
15901
16511
  try {
15902
- writeFileSync22(tmpFile, code, "utf-8");
16512
+ writeFileSync23(tmpFile, code, "utf-8");
15903
16513
  const startTime = Date.now();
15904
- const result = await new Promise((resolve5) => {
16514
+ const result = await new Promise((resolve6) => {
15905
16515
  execFile3(interpreter.cmd, [...interpreter.args, tmpFile], { timeout, maxBuffer: 10 * 1024 * 1024 }, (error, stdout2, stderr2) => {
15906
16516
  const exitCode = error ? typeof error.code === "number" ? error.code : 1 : 0;
15907
- resolve5({ exitCode, stdout: stdout2 ?? "", stderr: stderr2 ?? "" });
16517
+ resolve6({ exitCode, stdout: stdout2 ?? "", stderr: stderr2 ?? "" });
15908
16518
  });
15909
16519
  });
15910
16520
  const executionTimeMs = Date.now() - startTime;
@@ -15997,7 +16607,7 @@ function createShellTools(_config, hooks) {
15997
16607
  required: ["title", "message"]
15998
16608
  },
15999
16609
  execute: /* @__PURE__ */ __name(async (input) => {
16000
- return new Promise((resolve5) => {
16610
+ return new Promise((resolve6) => {
16001
16611
  notifier.notify({
16002
16612
  title: input.title,
16003
16613
  message: input.message,
@@ -16005,9 +16615,9 @@ function createShellTools(_config, hooks) {
16005
16615
  sound: true
16006
16616
  }, (err) => {
16007
16617
  if (err) {
16008
- resolve5({ error: err.message });
16618
+ resolve6({ error: err.message });
16009
16619
  } else {
16010
- resolve5({ success: true });
16620
+ resolve6({ success: true });
16011
16621
  }
16012
16622
  });
16013
16623
  });
@@ -16016,58 +16626,86 @@ function createShellTools(_config, hooks) {
16016
16626
  // ─── Web Fetch tool (headless URL content extraction) ───
16017
16627
  {
16018
16628
  name: "web_fetch",
16019
- description: "Fetch content from a URL and return it as text. Use this for quick content retrieval without opening a browser. Supports HTML pages (converted to markdown), JSON APIs, and plain text.",
16629
+ description: "Fetch raw content from a URL. Use for APIs, JSON, plain text, and small pages. For articles/docs/pages that need cleanup, prefer web_extract.",
16020
16630
  parameters: {
16021
16631
  type: "object",
16022
16632
  properties: {
16023
16633
  url: { type: "string", description: "The URL to fetch" },
16024
16634
  max_length: { type: "number", description: "Maximum characters to return (default: 5000)" },
16025
- start_index: { type: "number", description: "Start index for content pagination (default: 0). Use to continue reading truncated content." }
16635
+ start_index: { type: "number", description: "Start index for content pagination (default: 0). Use to continue reading truncated content." },
16636
+ allow_private: { type: "boolean", description: "Allow private, loopback, or internal network URLs. Default false." }
16026
16637
  },
16027
16638
  required: ["url"]
16028
16639
  },
16029
16640
  execute: /* @__PURE__ */ __name(async (input) => {
16030
16641
  const url = input.url;
16031
- const maxLength = input.max_length || 5e3;
16032
- const startIndex = input.start_index || 0;
16033
- try {
16034
- const controller = new AbortController();
16035
- const timeout = setTimeout(() => controller.abort(), 15e3);
16036
- const response = await fetch(url, {
16037
- signal: controller.signal,
16038
- headers: { "User-Agent": "OpenJaw-Agent/1.0" }
16039
- });
16040
- clearTimeout(timeout);
16041
- if (!response.ok) {
16042
- return { error: `HTTP ${response.status}: ${response.statusText}`, url };
16043
- }
16044
- const contentType = response.headers.get("content-type") || "";
16045
- let text = await response.text();
16046
- if (contentType.includes("html")) {
16047
- try {
16048
- const TurndownModule = await import("turndown");
16049
- const TurndownService = TurndownModule.default || TurndownModule;
16050
- const turndown = new TurndownService({
16051
- headingStyle: "atx",
16052
- codeBlockStyle: "fenced"
16053
- });
16054
- const cleaned = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "").replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "");
16055
- text = turndown.turndown(cleaned);
16056
- } catch {
16057
- text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
16058
- }
16059
- }
16060
- if (startIndex > 0) {
16061
- text = text.slice(startIndex);
16062
- }
16063
- if (text.length > maxLength) {
16064
- text = text.slice(0, maxLength);
16065
- return { url, contentType, length: text.length, content: text, truncated: true, nextIndex: startIndex + maxLength, hint: `Content truncated. Use start_index: ${startIndex + maxLength} to continue reading.` };
16642
+ const maxLength = clampNumber(input.max_length, 5e3, 1, 1e5);
16643
+ const startIndex = clampNumber(input.start_index, 0, 0, Number.MAX_SAFE_INTEGER);
16644
+ const allowPrivate = input.allow_private === true;
16645
+ const extracted = await extractUrlContent(url, { maxLength, startIndex, allowPrivate, timeoutMs: 15e3, format: "markdown" });
16646
+ if (extracted.error)
16647
+ return { error: extracted.error, url: extracted.url, finalUrl: extracted.finalUrl };
16648
+ return {
16649
+ url: extracted.url,
16650
+ finalUrl: extracted.finalUrl,
16651
+ contentType: extracted.contentType,
16652
+ title: extracted.title,
16653
+ length: extracted.content.length,
16654
+ contentLength: extracted.contentLength,
16655
+ content: extracted.content,
16656
+ truncated: extracted.truncated,
16657
+ ...extracted.nextStartIndex !== void 0 && {
16658
+ nextIndex: extracted.nextStartIndex,
16659
+ nextStartIndex: extracted.nextStartIndex,
16660
+ hint: `Content truncated. Use start_index: ${extracted.nextStartIndex} to continue reading.`
16066
16661
  }
16067
- return { url, contentType, length: text.length, content: text };
16068
- } catch (err) {
16069
- return { error: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`, url };
16070
- }
16662
+ };
16663
+ }, "execute")
16664
+ },
16665
+ // ─── Web Extract tool (selected-source reading) ───
16666
+ {
16667
+ name: "web_extract",
16668
+ description: "Extract readable content from selected web page URLs. Use after web_search when you need to read source pages, articles, docs, or papers. Returns markdown/text with metadata and pagination. For raw APIs or plain JSON, web_fetch is lighter.",
16669
+ parameters: {
16670
+ type: "object",
16671
+ properties: {
16672
+ urls: { type: "array", items: { type: "string" }, description: "List of URLs to extract content from (max 5)" },
16673
+ format: { type: "string", enum: ["markdown", "text"], description: "Output format. HTML is converted to markdown by default.", default: "markdown" },
16674
+ max_length: { type: "number", description: "Maximum characters per URL to return (default: 8000, max: 100000)" },
16675
+ start_index: { type: "number", description: "Start index for content pagination (default: 0). Applies to each URL." },
16676
+ allow_private: { type: "boolean", description: "Allow private, loopback, or internal network URLs. Default false." },
16677
+ use_llm_processing: { type: "boolean", description: "Reserved for future LLM summarization. Currently deterministic extraction only." }
16678
+ },
16679
+ required: ["urls"]
16680
+ },
16681
+ execute: /* @__PURE__ */ __name(async (input) => {
16682
+ const rawUrls = Array.isArray(input.urls) ? input.urls.filter((url) => typeof url === "string") : [];
16683
+ const urls = rawUrls.slice(0, 5);
16684
+ if (urls.length === 0)
16685
+ return { error: "web_extract requires at least one URL", results: [] };
16686
+ const maxLength = clampNumber(input.max_length, 8e3, 1, 1e5);
16687
+ const startIndex = clampNumber(input.start_index, 0, 0, Number.MAX_SAFE_INTEGER);
16688
+ const allowPrivate = input.allow_private === true;
16689
+ const format2 = input.format === "text" ? "text" : "markdown";
16690
+ const results = await Promise.all(urls.map((url) => extractUrlContent(url, { maxLength, startIndex, allowPrivate, format: format2 })));
16691
+ return {
16692
+ results: results.map((result) => ({
16693
+ url: result.url,
16694
+ finalUrl: result.finalUrl,
16695
+ title: result.title,
16696
+ contentType: result.contentType,
16697
+ format: format2,
16698
+ content: result.content,
16699
+ contentLength: result.contentLength,
16700
+ truncated: result.truncated,
16701
+ startIndex: result.startIndex,
16702
+ ...result.nextStartIndex !== void 0 && { nextStartIndex: result.nextStartIndex },
16703
+ ...result.error && { error: result.error }
16704
+ })),
16705
+ count: results.length,
16706
+ ...rawUrls.length > urls.length && { warning: `Only the first ${urls.length} URLs were extracted (max 5 per call).` },
16707
+ ...input.use_llm_processing === true && { llmProcessing: "unavailable: deterministic extraction only in this build" }
16708
+ };
16071
16709
  }, "execute")
16072
16710
  },
16073
16711
  // ─── Sleep tool ───
@@ -16083,7 +16721,7 @@ function createShellTools(_config, hooks) {
16083
16721
  },
16084
16722
  execute: /* @__PURE__ */ __name(async (input) => {
16085
16723
  const seconds = Math.min(Math.max(0.1, input.seconds), 60);
16086
- await new Promise((resolve5) => setTimeout(resolve5, seconds * 1e3));
16724
+ await new Promise((resolve6) => setTimeout(resolve6, seconds * 1e3));
16087
16725
  return { waited: seconds, message: `Waited ${seconds} seconds` };
16088
16726
  }, "execute")
16089
16727
  },
@@ -16162,12 +16800,14 @@ function createShellTools(_config, hooks) {
16162
16800
  signal: controller.signal
16163
16801
  });
16164
16802
  if (native && Array.isArray(native.results) && native.results.length > 0) {
16803
+ const results = normalizeSearchResults(native.results, maxResults);
16165
16804
  return {
16166
16805
  query,
16167
- resultCount: native.results.length,
16168
- results: native.results,
16806
+ resultCount: results.length,
16807
+ results,
16169
16808
  summary: native.summary,
16170
16809
  provider: native.provider,
16810
+ backend: native.provider,
16171
16811
  durationSeconds: native.durationSeconds
16172
16812
  };
16173
16813
  }
@@ -16218,11 +16858,13 @@ function createShellTools(_config, hooks) {
16218
16858
  results = results.filter((r) => !blockedDomains.some((d) => urlMatchesDomain(r.url, d)));
16219
16859
  }
16220
16860
  if (results.length > 0) {
16861
+ const normalizedResults = normalizeSearchResults(results, maxResults);
16221
16862
  return {
16222
16863
  query,
16223
- resultCount: results.length,
16224
- results,
16864
+ resultCount: normalizedResults.length,
16865
+ results: normalizedResults,
16225
16866
  provider: `duckduckgo-${ep.kind}`,
16867
+ backend: `duckduckgo-${ep.kind}`,
16226
16868
  ...nativeError ? { nativeSearchError: nativeError } : {}
16227
16869
  };
16228
16870
  }
@@ -16287,12 +16929,23 @@ var init_shell = __esm({
16287
16929
  "../openjaw-mcp/dist/tools/shell.js"() {
16288
16930
  "use strict";
16289
16931
  init_web_search_types();
16932
+ init_url_safety();
16290
16933
  __name(truncateOutput, "truncateOutput");
16291
16934
  DECODE_ENTITIES = /* @__PURE__ */ __name((s) => s.replace(/&amp;/g, "&").replace(/&nbsp;/g, " ").replace(/&#x27;/g, "'").replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">"), "DECODE_ENTITIES");
16292
16935
  __name(parseLiteResults, "parseLiteResults");
16293
16936
  __name(parseHtmlResults, "parseHtmlResults");
16294
16937
  __name(isAnomalyPage, "isAnomalyPage");
16295
16938
  __name(urlMatchesDomain, "urlMatchesDomain");
16939
+ __name(normalizeSearchResults, "normalizeSearchResults");
16940
+ __name(clampNumber, "clampNumber");
16941
+ __name(extractHtmlTitle, "extractHtmlTitle");
16942
+ __name(cleanHtmlForMarkdown, "cleanHtmlForMarkdown");
16943
+ __name(htmlToText, "htmlToText");
16944
+ __name(isPdfLike, "isPdfLike");
16945
+ __name(htmlToMarkdown, "htmlToMarkdown");
16946
+ __name(paginateContent, "paginateContent");
16947
+ __name(fetchWithValidatedRedirects, "fetchWithValidatedRedirects");
16948
+ __name(extractUrlContent, "extractUrlContent");
16296
16949
  __name(createShellTools, "createShellTools");
16297
16950
  }
16298
16951
  });
@@ -17180,7 +17833,7 @@ var init_office_desktop = __esm({
17180
17833
  return Array.isArray(parsed) ? parsed : [parsed];
17181
17834
  }
17182
17835
  sleep(ms) {
17183
- return new Promise((resolve5) => setTimeout(resolve5, ms));
17836
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
17184
17837
  }
17185
17838
  };
17186
17839
  }
@@ -18861,7 +19514,7 @@ public class Win32Send {
18861
19514
  }
18862
19515
  }
18863
19516
  sleep(ms) {
18864
- return new Promise((resolve5) => setTimeout(resolve5, ms));
19517
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
18865
19518
  }
18866
19519
  };
18867
19520
  }
@@ -18926,7 +19579,7 @@ function getActiveMonitors() {
18926
19579
  return result;
18927
19580
  }
18928
19581
  function sleep3(ms) {
18929
- return new Promise((resolve5) => setTimeout(resolve5, ms));
19582
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
18930
19583
  }
18931
19584
  async function fileHash(filePath) {
18932
19585
  const data = fs.readFileSync(filePath);
@@ -19667,6 +20320,7 @@ var init_categories = __esm({
19667
20320
  ["clipboard_", "system"],
19668
20321
  ["web_fetch", "system"],
19669
20322
  ["web_search", "system"],
20323
+ ["web_extract", "system"],
19670
20324
  ["notify", "system"],
19671
20325
  ["sleep", "system"],
19672
20326
  ["ask_user", "system"],
@@ -20248,6 +20902,8 @@ function parseSkillFile(content, filename) {
20248
20902
  whenToUse: meta.whenToUse || meta.when_to_use,
20249
20903
  tools: Array.isArray(meta.tools) ? meta.tools : meta.tools ? parseYamlArray(meta.tools) : void 0,
20250
20904
  model: meta.model,
20905
+ execution: meta.execution === "fork" ? "fork" : meta.execution === "inline" ? "inline" : void 0,
20906
+ timeoutMs: typeof meta.timeoutMs === "number" ? meta.timeoutMs : typeof meta.timeout_ms === "number" ? meta.timeout_ms : void 0,
20251
20907
  version: meta.version,
20252
20908
  author: meta.author,
20253
20909
  platforms: Array.isArray(meta.platforms) ? meta.platforms : void 0,
@@ -20396,11 +21052,11 @@ function loadFlatSkillsFromDir(dir2, source, priority, out) {
20396
21052
  function loadPackagedSkillsFromDir(dir2, out) {
20397
21053
  for (const entry of safeReadDir(dir2)) {
20398
21054
  if (!entry.isDirectory()) continue;
20399
- const rootDir = join22(dir2, entry.name);
20400
- const entrypoint = findPackageEntrypoint(rootDir);
21055
+ const rootDir2 = join22(dir2, entry.name);
21056
+ const entrypoint = findPackageEntrypoint(rootDir2);
20401
21057
  if (!entrypoint) continue;
20402
- const filePath = join22(rootDir, entrypoint);
20403
- const skill = parseSkillAtPath(filePath, rootDir, entrypoint, "user", `${entry.name}.md`);
21058
+ const filePath = join22(rootDir2, entrypoint);
21059
+ const skill = parseSkillAtPath(filePath, rootDir2, entrypoint, "user", `${entry.name}.md`);
20404
21060
  if (skill) putSkill(out, skill, 2);
20405
21061
  }
20406
21062
  }
@@ -20413,7 +21069,7 @@ function findPackageEntrypoint(dir2) {
20413
21069
  const markdown = entries.filter((entry) => isMarkdown(entry.name));
20414
21070
  return markdown.length === 1 ? markdown[0].name : null;
20415
21071
  }
20416
- function parseSkillAtPath(filePath, rootDir, entrypoint, source, fallbackFilename) {
21072
+ function parseSkillAtPath(filePath, rootDir2, entrypoint, source, fallbackFilename) {
20417
21073
  try {
20418
21074
  const content = readFileSync14(filePath, "utf-8").trim();
20419
21075
  if (!content) return null;
@@ -20424,7 +21080,7 @@ function parseSkillAtPath(filePath, rootDir, entrypoint, source, fallbackFilenam
20424
21080
  name,
20425
21081
  meta: { ...parsed.meta, name },
20426
21082
  filePath,
20427
- rootDir,
21083
+ rootDir: rootDir2,
20428
21084
  entrypoint,
20429
21085
  source,
20430
21086
  hasFrontmatter: parsed.hasFrontmatter
@@ -20527,8 +21183,10 @@ function getSkillsSection() {
20527
21183
  const content = `# Available Skills
20528
21184
 
20529
21185
  Use the \`invoke_skill\` tool to execute any skill listed below.
20530
- When a skill matches the user's request, invoke it instead of doing the task manually.
20531
- Skills run in an isolated context with their own conversation \u2014 they won't pollute your main conversation.
21186
+ Skills are for specialized, multi-step workflows that materially benefit from the skill's process.
21187
+ Do NOT invoke a skill for simple one-off questions, quick factual/current-info lookups, basic summaries, or tasks a visible built-in/MCP tool can answer in 1-2 calls.
21188
+ For news/latest/current events, use \`web_search\` directly. For selected URLs, use \`web_extract\` directly.
21189
+ When a skill truly matches a complex workflow, invoke it. Most skills load inline into the current turn so you can continue with visible tools. Only skills marked for fork execution run as isolated sub-agents and may take longer than ordinary tools.
20532
21190
 
20533
21191
  To create new skills or improve existing ones, use \`invoke_skill("skill-creator")\`.
20534
21192
  New skills must be saved to \`~/.openjaw-agent/skills/\` (user skills directory).
@@ -21079,12 +21737,12 @@ var init_telegram = __esm({
21079
21737
  Options: ${chunk.choices.join(" | ")}` : "";
21080
21738
  await this.bot.sendMessage(chatId, `\u2753 ${question}${choicesText}`);
21081
21739
  updateStatus("\u2753 Waiting for your response...");
21082
- const userReply = await new Promise((resolve5) => {
21083
- this._pendingReplyResolver = resolve5;
21740
+ const userReply = await new Promise((resolve6) => {
21741
+ this._pendingReplyResolver = resolve6;
21084
21742
  setTimeout(() => {
21085
- if (this._pendingReplyResolver === resolve5) {
21743
+ if (this._pendingReplyResolver === resolve6) {
21086
21744
  this._pendingReplyResolver = null;
21087
- resolve5("[No response \u2014 timed out]");
21745
+ resolve6("[No response \u2014 timed out]");
21088
21746
  }
21089
21747
  }, 5 * 60 * 1e3);
21090
21748
  });
@@ -21682,7 +22340,7 @@ async function promptConsent(servers) {
21682
22340
  input: process.stdin,
21683
22341
  output: process.stderr
21684
22342
  });
21685
- const ask = /* @__PURE__ */ __name((question) => new Promise((resolve5) => rl.question(question, resolve5)), "ask");
22343
+ const ask = /* @__PURE__ */ __name((question) => new Promise((resolve6) => rl.question(question, resolve6)), "ask");
21686
22344
  const w = process.stderr.columns || 80;
21687
22345
  const inner = w - 4;
21688
22346
  const hLine = "\u2500".repeat(inner);
@@ -23737,7 +24395,7 @@ try {
23737
24395
  }
23738
24396
  `;
23739
24397
  const encoded = Buffer.from(script, "utf16le").toString("base64");
23740
- return new Promise((resolve5) => {
24398
+ return new Promise((resolve6) => {
23741
24399
  const proc = spawn3("powershell.exe", ["-NoProfile", "-STA", "-EncodedCommand", encoded], {
23742
24400
  stdio: ["pipe", "pipe", "pipe"],
23743
24401
  windowsHide: true
@@ -23752,22 +24410,22 @@ try {
23752
24410
  });
23753
24411
  const timer = setTimeout(() => {
23754
24412
  if (!proc.killed) proc.kill();
23755
- resolve5(null);
24413
+ resolve6(null);
23756
24414
  }, (timeoutSeconds + 5) * 1e3);
23757
24415
  proc.on("exit", () => {
23758
24416
  clearTimeout(timer);
23759
24417
  const output = stdout.replace(/#< CLIXML[\s\S]*/m, "").trim();
23760
24418
  if (output === "NO_SPEECH" || output.startsWith("ERROR") || !output) {
23761
- resolve5(null);
24419
+ resolve6(null);
23762
24420
  return;
23763
24421
  }
23764
24422
  const parts = output.split("|");
23765
24423
  const text = parts[0]?.trim();
23766
24424
  const confidence = parseFloat(parts[1] || "0");
23767
24425
  if (text) {
23768
- resolve5({ text, confidence: isNaN(confidence) ? 0.5 : confidence });
24426
+ resolve6({ text, confidence: isNaN(confidence) ? 0.5 : confidence });
23769
24427
  } else {
23770
- resolve5(null);
24428
+ resolve6(null);
23771
24429
  }
23772
24430
  try {
23773
24431
  if (existsSync21(resultFile)) unlinkSync5(resultFile);
@@ -24104,6 +24762,7 @@ var init_PromptInput = __esm({
24104
24762
  { name: "/repl", description: "\u{1F527} Start interactive code REPL" },
24105
24763
  { name: "/voice", description: "\u{1F50A} Toggle voice output (TTS)" },
24106
24764
  { name: "/fork", description: "\u{1F500} Spawn background sub-agent" },
24765
+ { name: "/workflow", description: "\u{1F9ED} Run advisory dynamic workflow" },
24107
24766
  { name: "/tasks", description: "List background tasks" },
24108
24767
  { name: "/clear", description: "Clear conversation history" },
24109
24768
  { name: "/compact", description: "Summarize old messages to free context" },
@@ -24136,6 +24795,12 @@ var init_PromptInput = __esm({
24136
24795
  { name: "pause", description: "Pause a task: /schedule pause <id>" },
24137
24796
  { name: "resume", description: "Resume a task: /schedule resume <id>" }
24138
24797
  ],
24798
+ "/workflow": [
24799
+ { name: "status", description: "Open live worker status" },
24800
+ { name: "list", description: "List recent workflows" },
24801
+ { name: "show <id>", description: "Show workflow summary" },
24802
+ { name: "cancel <id>", description: "Cancel a run or worker" }
24803
+ ],
24139
24804
  "/repl": [
24140
24805
  { name: "python", description: "Python interactive shell" },
24141
24806
  { name: "node", description: "Node.js interactive shell" },
@@ -25339,8 +26004,8 @@ function isSensitivePath(resolved) {
25339
26004
  if (re.test(normalized)) return true;
25340
26005
  }
25341
26006
  for (const re of SENSITIVE_FILE_PATTERNS) {
25342
- const basename4 = path2.basename(resolved);
25343
- if (re.test(basename4)) return true;
26007
+ const basename5 = path2.basename(resolved);
26008
+ if (re.test(basename5)) return true;
25344
26009
  }
25345
26010
  return false;
25346
26011
  }
@@ -25458,34 +26123,34 @@ ${output}
25458
26123
  }
25459
26124
  }
25460
26125
  function expandUrl(ref, warnings) {
25461
- return new Promise((resolve5) => {
26126
+ return new Promise((resolve6) => {
25462
26127
  const url = ref.target;
25463
26128
  const mod = url.startsWith("https") ? https : http;
25464
26129
  const req = mod.get(url, { timeout: 15e3 }, (res) => {
25465
26130
  if (res.statusCode && (res.statusCode >= 300 && res.statusCode < 400) && res.headers.location) {
25466
26131
  const redirectMod = res.headers.location.startsWith("https") ? https : http;
25467
26132
  const req2 = redirectMod.get(res.headers.location, { timeout: 15e3 }, (res2) => {
25468
- collectResponse(res2, ref, warnings, resolve5);
26133
+ collectResponse(res2, ref, warnings, resolve6);
25469
26134
  });
25470
26135
  req2.on("error", (e) => {
25471
26136
  warnings.push(`\u26A0\uFE0F @url fetch error: ${e.message}`);
25472
- resolve5(null);
26137
+ resolve6(null);
25473
26138
  });
25474
26139
  return;
25475
26140
  }
25476
- collectResponse(res, ref, warnings, resolve5);
26141
+ collectResponse(res, ref, warnings, resolve6);
25477
26142
  });
25478
26143
  req.on("error", (e) => {
25479
26144
  warnings.push(`\u26A0\uFE0F @url fetch error: ${e.message}`);
25480
- resolve5(null);
26145
+ resolve6(null);
25481
26146
  });
25482
26147
  });
25483
26148
  }
25484
- function collectResponse(res, ref, warnings, resolve5) {
26149
+ function collectResponse(res, ref, warnings, resolve6) {
25485
26150
  if (res.statusCode && res.statusCode >= 400) {
25486
26151
  warnings.push(`\u26A0\uFE0F @url returned HTTP ${res.statusCode}: ${ref.target}`);
25487
26152
  res.resume();
25488
- resolve5(null);
26153
+ resolve6(null);
25489
26154
  return;
25490
26155
  }
25491
26156
  const chunks = [];
@@ -25497,14 +26162,14 @@ function collectResponse(res, ref, warnings, resolve5) {
25497
26162
  text = text.slice(0, 5e4) + "\n\u2026 (truncated)";
25498
26163
  }
25499
26164
  const tokens = estimateTokens2(text);
25500
- resolve5(`\u{1F310} @url:${ref.target} (${tokens} tokens)
26165
+ resolve6(`\u{1F310} @url:${ref.target} (${tokens} tokens)
25501
26166
  \`\`\`
25502
26167
  ${text}
25503
26168
  \`\`\``);
25504
26169
  });
25505
26170
  res.on("error", (e) => {
25506
26171
  warnings.push(`\u26A0\uFE0F @url fetch error: ${e.message}`);
25507
- resolve5(null);
26172
+ resolve6(null);
25508
26173
  });
25509
26174
  }
25510
26175
  function stripHtml(html) {
@@ -26005,13 +26670,13 @@ Type /resume <id> to resume.` });
26005
26670
  if (input === "/export") {
26006
26671
  try {
26007
26672
  const { writeFile: writeFile5, mkdir: mkdir5 } = await import("node:fs/promises");
26008
- const { join: join47 } = await import("node:path");
26009
- const { homedir: homedir31 } = await import("node:os");
26010
- const { existsSync: existsSync33 } = await import("node:fs");
26011
- const exportDir = join47(homedir31(), ".openjaw-agent", "exports");
26012
- if (!existsSync33(exportDir)) await mkdir5(exportDir, { recursive: true });
26673
+ const { join: join48 } = await import("node:path");
26674
+ const { homedir: homedir32 } = await import("node:os");
26675
+ const { existsSync: existsSync34 } = await import("node:fs");
26676
+ const exportDir = join48(homedir32(), ".openjaw-agent", "exports");
26677
+ if (!existsSync34(exportDir)) await mkdir5(exportDir, { recursive: true });
26013
26678
  const filename = `session-${agentLoop.sessionId}.md`;
26014
- const filepath = join47(exportDir, filename);
26679
+ const filepath = join48(exportDir, filename);
26015
26680
  const lines = [
26016
26681
  `# OpenJaw Agent Session ${agentLoop.sessionId}`,
26017
26682
  `Date: ${(/* @__PURE__ */ new Date()).toISOString()}`,
@@ -26230,9 +26895,9 @@ ${list}` });
26230
26895
  const cmd = lang === "python" ? "python" : lang === "node" ? "node" : "pwsh";
26231
26896
  const args = lang === "python" ? ["-i", "-u"] : lang === "node" ? ["-i"] : ["-NoProfile", "-NoLogo"];
26232
26897
  try {
26233
- const { spawn: spawn9 } = await import("node:child_process");
26898
+ const { spawn: spawn8 } = await import("node:child_process");
26234
26899
  if (replRef.current) replRef.current.kill();
26235
- const proc = spawn9(cmd, args, {
26900
+ const proc = spawn8(cmd, args, {
26236
26901
  stdio: ["pipe", "pipe", "pipe"],
26237
26902
  windowsHide: true
26238
26903
  });
@@ -26272,7 +26937,7 @@ ${list}` });
26272
26937
  if (replRef.current && replLangRef.current && !input.startsWith("/")) {
26273
26938
  const proc = replRef.current;
26274
26939
  proc.stdin?.write(input + "\n");
26275
- await new Promise((resolve5) => setTimeout(resolve5, 800));
26940
+ await new Promise((resolve6) => setTimeout(resolve6, 800));
26276
26941
  const flush = proc._ojFlush;
26277
26942
  const output = flush ? flush() : "";
26278
26943
  if (output.trim()) {
@@ -26586,14 +27251,14 @@ Options: ${chunk.choices.join(" | ")}` : chunk.content;
26586
27251
  waitingForAskUserRef.current = true;
26587
27252
  setIsRunning(false);
26588
27253
  setWaitingForAskUser(true);
26589
- await new Promise((resolve5) => {
27254
+ await new Promise((resolve6) => {
26590
27255
  const checkInterval = setInterval(() => {
26591
27256
  if (!waitingForAskUserRef.current || !agentLoop.isWaitingForAskUser) {
26592
27257
  clearInterval(checkInterval);
26593
27258
  waitingForAskUserRef.current = false;
26594
27259
  setWaitingForAskUser(false);
26595
27260
  setIsRunning(true);
26596
- resolve5();
27261
+ resolve6();
26597
27262
  }
26598
27263
  }, 200);
26599
27264
  });
@@ -26869,10 +27534,38 @@ __export(skill_tool_exports, {
26869
27534
  createSkillTool: () => createSkillTool
26870
27535
  });
26871
27536
  import { readFileSync as readFileSync23, writeFileSync as writeFileSync15 } from "node:fs";
27537
+ function resolveSkillTimeoutMs() {
27538
+ const raw = process.env["OPENJAW_SKILL_TIMEOUT_MS"];
27539
+ const parsed = raw ? Number(raw) : NaN;
27540
+ if (Number.isFinite(parsed) && parsed > 0) {
27541
+ return Math.min(Math.max(Math.floor(parsed), 1e4), 6e5);
27542
+ }
27543
+ return DEFAULT_SKILL_TIMEOUT_MS;
27544
+ }
27545
+ function resolveForkTimeoutMs(skill) {
27546
+ const configured = skill.meta.timeoutMs;
27547
+ if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
27548
+ return Math.min(Math.max(Math.floor(configured), 1e4), 6e5);
27549
+ }
27550
+ return resolveSkillTimeoutMs();
27551
+ }
27552
+ async function withTimeout(promise, ms) {
27553
+ let timeoutId;
27554
+ try {
27555
+ return await Promise.race([
27556
+ promise,
27557
+ new Promise((resolve6) => {
27558
+ timeoutId = setTimeout(() => resolve6(SKILL_TIMEOUT), ms);
27559
+ })
27560
+ ]);
27561
+ } finally {
27562
+ if (timeoutId) clearTimeout(timeoutId);
27563
+ }
27564
+ }
26872
27565
  function createSkillTool(config, toolRegistry, systemPromptFn) {
26873
27566
  return {
26874
27567
  name: "invoke_skill",
26875
- description: "Execute a skill from the available skills list. Skills provide specialized multi-step workflows. Use this when the user's request matches a listed skill.",
27568
+ description: "Execute a skill from the available skills list. Skills are for specialized multi-step workflows. Do not use for simple one-off lookups, quick current-news/factual questions, or tasks a direct built-in tool can answer in 1-2 calls.",
26876
27569
  parameters: {
26877
27570
  type: "object",
26878
27571
  properties: {
@@ -26883,6 +27576,11 @@ function createSkillTool(config, toolRegistry, systemPromptFn) {
26883
27576
  args: {
26884
27577
  type: "string",
26885
27578
  description: "Optional arguments or context for the skill"
27579
+ },
27580
+ mode: {
27581
+ type: "string",
27582
+ enum: ["inline", "fork"],
27583
+ description: "Execution mode. Default inline loads instructions into this turn; fork runs an isolated sub-agent for skills that explicitly support long-running execution."
26886
27584
  }
26887
27585
  },
26888
27586
  required: ["skill"]
@@ -26890,6 +27588,7 @@ function createSkillTool(config, toolRegistry, systemPromptFn) {
26890
27588
  execute: /* @__PURE__ */ __name(async (input) => {
26891
27589
  const skillName = input.skill.trim().replace(/^\//, "");
26892
27590
  const args = input.args || "";
27591
+ const requestedMode = input.mode === "fork" ? "fork" : input.mode === "inline" ? "inline" : void 0;
26893
27592
  const skill = findSkill(skillName);
26894
27593
  if (!skill) {
26895
27594
  clearSkillRegistry();
@@ -26897,17 +27596,39 @@ function createSkillTool(config, toolRegistry, systemPromptFn) {
26897
27596
  if (!retrySkill) {
26898
27597
  return { success: false, error: `Skill "${skillName}" not found. Check /skills for available skills.` };
26899
27598
  }
26900
- return await executeSkill(retrySkill.name, args, config, toolRegistry, systemPromptFn);
27599
+ return await executeSkill(retrySkill, args, requestedMode, config, toolRegistry, systemPromptFn);
26901
27600
  }
26902
- return await executeSkill(skill.name, args, config, toolRegistry, systemPromptFn);
27601
+ return await executeSkill(skill, args, requestedMode, config, toolRegistry, systemPromptFn);
26903
27602
  }, "execute")
26904
27603
  };
26905
27604
  }
26906
- async function executeSkill(skillName, args, config, toolRegistry, systemPromptFn) {
27605
+ async function executeSkill(skill, args, requestedMode, config, toolRegistry, systemPromptFn) {
27606
+ const skillName = skill.name;
26907
27607
  const body = loadSkillPrompt(skillName);
26908
27608
  if (!body) {
26909
27609
  return { success: false, error: `Could not load skill content for "${skillName}"` };
26910
27610
  }
27611
+ const mode = requestedMode ?? skill.meta.execution ?? "inline";
27612
+ if (mode === "fork") {
27613
+ return await executeForkedSkill(skill, body, args, config, toolRegistry, systemPromptFn);
27614
+ }
27615
+ return loadInlineSkill(skill, body, args);
27616
+ }
27617
+ function loadInlineSkill(skill, body, args) {
27618
+ const skillBlock = args ? `${body}
27619
+
27620
+ # User instructions for this skill
27621
+ ${args}` : body;
27622
+ return {
27623
+ success: true,
27624
+ skill: skill.name,
27625
+ mode: "inline",
27626
+ message: "Skill instructions loaded inline. Continue the task in the current conversation using these instructions and visible tools.",
27627
+ instructions: skillBlock
27628
+ };
27629
+ }
27630
+ async function executeForkedSkill(skill, body, args, config, toolRegistry, systemPromptFn) {
27631
+ const skillName = skill.name;
26911
27632
  const baseSections = await systemPromptFn();
26912
27633
  const staticSections = baseSections.slice(0, 4).filter(Boolean);
26913
27634
  const skillPrompt = [...staticSections, body].join("\n\n");
@@ -26917,7 +27638,34 @@ async function executeSkill(skillName, args, config, toolRegistry, systemPromptF
26917
27638
  let lastAnswer = "";
26918
27639
  let thinking = "";
26919
27640
  const allChunks = [];
26920
- for await (const chunk of forkLoop.run(userMessage, skillPrompt)) {
27641
+ const timeoutMs = resolveForkTimeoutMs(skill);
27642
+ const deadline = Date.now() + timeoutMs;
27643
+ const iterator = forkLoop.run(userMessage, skillPrompt)[Symbol.asyncIterator]();
27644
+ while (true) {
27645
+ const remaining = deadline - Date.now();
27646
+ if (remaining <= 0) {
27647
+ forkLoop.abort();
27648
+ return {
27649
+ success: false,
27650
+ skill: skillName,
27651
+ timeoutMs,
27652
+ error: `Skill "${skillName}" exceeded ${Math.round(timeoutMs / 1e3)}s. Use direct tools for simple requests, or rerun with narrower instructions.`,
27653
+ partial: lastAnswer || thinking || void 0
27654
+ };
27655
+ }
27656
+ const next = await withTimeout(iterator.next(), remaining);
27657
+ if (next === SKILL_TIMEOUT) {
27658
+ forkLoop.abort();
27659
+ return {
27660
+ success: false,
27661
+ skill: skillName,
27662
+ timeoutMs,
27663
+ error: `Skill "${skillName}" exceeded ${Math.round(timeoutMs / 1e3)}s. Use direct tools for simple requests, or rerun with narrower instructions.`,
27664
+ partial: lastAnswer || thinking || void 0
27665
+ };
27666
+ }
27667
+ if (next.done) break;
27668
+ const chunk = next.value;
26921
27669
  if (chunk.type === "answer" && chunk.content) lastAnswer = chunk.content;
26922
27670
  if (chunk.type === "thinking" && chunk.content) thinking += chunk.content;
26923
27671
  allChunks.push({ type: chunk.type, content: chunk.content });
@@ -26925,14 +27673,14 @@ async function executeSkill(skillName, args, config, toolRegistry, systemPromptF
26925
27673
  const result = lastAnswer || thinking || "Skill completed (no output)";
26926
27674
  const compressed = result.length > 2e3 ? result.slice(0, 2e3) + `
26927
27675
  ...(${result.length} chars total, truncated)` : result;
26928
- const skill = findSkill(skillName);
26929
- if (skill?.filePath && skill.source === "user") {
27676
+ const currentSkill = findSkill(skillName);
27677
+ if (currentSkill?.filePath && currentSkill.source === "user") {
26930
27678
  const lessons = extractLessons(allChunks);
26931
27679
  if (lessons) {
26932
- appendLessonsLearned(skill.filePath, lessons);
27680
+ appendLessonsLearned(currentSkill.filePath, lessons);
26933
27681
  }
26934
27682
  }
26935
- return { success: true, skill: skillName, result: compressed };
27683
+ return { success: true, skill: skillName, mode: "fork", result: compressed };
26936
27684
  } catch (err) {
26937
27685
  return { success: false, skill: skillName, error: err instanceof Error ? err.message : String(err) };
26938
27686
  }
@@ -26973,13 +27721,21 @@ function appendLessonsLearned(skillPath, lessons) {
26973
27721
  } catch {
26974
27722
  }
26975
27723
  }
27724
+ var DEFAULT_SKILL_TIMEOUT_MS, SKILL_TIMEOUT;
26976
27725
  var init_skill_tool = __esm({
26977
27726
  "src/tools/skill-tool.ts"() {
26978
27727
  "use strict";
26979
27728
  init_agent_loop();
26980
27729
  init_registry2();
27730
+ DEFAULT_SKILL_TIMEOUT_MS = 12e4;
27731
+ SKILL_TIMEOUT = /* @__PURE__ */ Symbol("skill-timeout");
27732
+ __name(resolveSkillTimeoutMs, "resolveSkillTimeoutMs");
27733
+ __name(resolveForkTimeoutMs, "resolveForkTimeoutMs");
27734
+ __name(withTimeout, "withTimeout");
26981
27735
  __name(createSkillTool, "createSkillTool");
26982
27736
  __name(executeSkill, "executeSkill");
27737
+ __name(loadInlineSkill, "loadInlineSkill");
27738
+ __name(executeForkedSkill, "executeForkedSkill");
26983
27739
  __name(extractLessons, "extractLessons");
26984
27740
  __name(appendLessonsLearned, "appendLessonsLearned");
26985
27741
  }
@@ -27409,12 +28165,12 @@ var init_teams = __esm({
27409
28165
  Options: ${chunk.choices.join(" | ")}` : "";
27410
28166
  await this.sendToSelfChat(`\u2753 ${question}${choicesText}`);
27411
28167
  if (!answerSent) await updateStatus("\u{1F916} OpenJaw Agent \u2014 \u2753 Waiting for your response...");
27412
- const userReply = await new Promise((resolve5) => {
27413
- this._pendingReplyResolver = resolve5;
28168
+ const userReply = await new Promise((resolve6) => {
28169
+ this._pendingReplyResolver = resolve6;
27414
28170
  setTimeout(() => {
27415
- if (this._pendingReplyResolver === resolve5) {
28171
+ if (this._pendingReplyResolver === resolve6) {
27416
28172
  this._pendingReplyResolver = null;
27417
- resolve5("[No response \u2014 timed out]");
28173
+ resolve6("[No response \u2014 timed out]");
27418
28174
  }
27419
28175
  }, 5 * 60 * 1e3);
27420
28176
  });
@@ -27832,13 +28588,13 @@ var init_feishu = __esm({
27832
28588
  if (stat2.isFile() && stat2.size < 30 * 1024 * 1024) {
27833
28589
  const fileType = this.getFeishuFileType(fileName);
27834
28590
  this.emit({ type: "system", content: `\u{1F4E4} Uploading to Feishu: ${fileName} (${(stat2.size / 1024).toFixed(0)}KB, type=${fileType})` });
27835
- const withTimeout = /* @__PURE__ */ __name((promise, ms, label) => Promise.race([
28591
+ const withTimeout2 = /* @__PURE__ */ __name((promise, ms, label) => Promise.race([
27836
28592
  promise,
27837
28593
  new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1e3}s`)), ms))
27838
28594
  ]), "withTimeout");
27839
28595
  const { createReadStream: crs } = await import("node:fs");
27840
28596
  const t0 = Date.now();
27841
- const uploadResp = await withTimeout(
28597
+ const uploadResp = await withTimeout2(
27842
28598
  this.client.im.file.create({
27843
28599
  data: {
27844
28600
  file_type: fileType,
@@ -27856,7 +28612,7 @@ var init_feishu = __esm({
27856
28612
  this.emit({ type: "system", content: `\u{1F4E4} Upload done in ${(uploadMs / 1e3).toFixed(1)}s: code=${respCode}, file_key=${fileKey ? "\u2713" : "\u2717"}` });
27857
28613
  if (fileKey) {
27858
28614
  const t1 = Date.now();
27859
- const sendResp = await withTimeout(
28615
+ const sendResp = await withTimeout2(
27860
28616
  this.client.im.message.create({
27861
28617
  params: { receive_id_type: "chat_id" },
27862
28618
  data: {
@@ -27898,12 +28654,12 @@ ${err.stack?.split("\n").slice(0, 3).join("\n")}` : String(err);
27898
28654
  }
27899
28655
  /** Wait for the next user message (used by ask_user flow) */
27900
28656
  waitForUserReply(_chatId) {
27901
- return new Promise((resolve5) => {
27902
- this._pendingReplyResolver = resolve5;
28657
+ return new Promise((resolve6) => {
28658
+ this._pendingReplyResolver = resolve6;
27903
28659
  setTimeout(() => {
27904
- if (this._pendingReplyResolver === resolve5) {
28660
+ if (this._pendingReplyResolver === resolve6) {
27905
28661
  this._pendingReplyResolver = null;
27906
- resolve5("[No response \u2014 timed out]");
28662
+ resolve6("[No response \u2014 timed out]");
27907
28663
  }
27908
28664
  }, 5 * 60 * 1e3);
27909
28665
  });
@@ -28194,9 +28950,9 @@ var init_wechat2 = __esm({
28194
28950
  try {
28195
28951
  const qrTerminal = await import("qrcode-terminal");
28196
28952
  const mod = qrTerminal.default || qrTerminal;
28197
- const qrAscii = await new Promise((resolve5, reject) => {
28953
+ const qrAscii = await new Promise((resolve6, reject) => {
28198
28954
  try {
28199
- mod.generate(qrUrl, { small: true }, (out) => resolve5(out));
28955
+ mod.generate(qrUrl, { small: true }, (out) => resolve6(out));
28200
28956
  } catch (e) {
28201
28957
  reject(e);
28202
28958
  }
@@ -28462,12 +29218,12 @@ Scan URL manually: ${qrUrl}` });
28462
29218
  const choicesText = chunk.choices?.length ? `
28463
29219
  \u9009\u9879: ${chunk.choices.join(" | ")}` : "";
28464
29220
  await this.sendText(userId, `\u2753 ${question}${choicesText}`, contextToken);
28465
- const userReply = await new Promise((resolve5) => {
28466
- this._pendingReplyResolver = resolve5;
29221
+ const userReply = await new Promise((resolve6) => {
29222
+ this._pendingReplyResolver = resolve6;
28467
29223
  setTimeout(() => {
28468
- if (this._pendingReplyResolver === resolve5) {
29224
+ if (this._pendingReplyResolver === resolve6) {
28469
29225
  this._pendingReplyResolver = null;
28470
- resolve5("[No response \u2014 timed out]");
29226
+ resolve6("[No response \u2014 timed out]");
28471
29227
  }
28472
29228
  }, 5 * 60 * 1e3);
28473
29229
  });
@@ -29968,8 +30724,8 @@ function createPromptCollector(bus, getSessionId) {
29968
30724
  }, "emit");
29969
30725
  const register = /* @__PURE__ */ __name((kind) => {
29970
30726
  const requestId = randomUUID12();
29971
- const promise = new Promise((resolve5) => {
29972
- pending.set(requestId, { kind, resolve: resolve5 });
30727
+ const promise = new Promise((resolve6) => {
30728
+ pending.set(requestId, { kind, resolve: resolve6 });
29973
30729
  });
29974
30730
  return { promise, requestId };
29975
30731
  }, "register");
@@ -30949,11 +31705,11 @@ function detectTerminal() {
30949
31705
  }
30950
31706
  return process.env.TERM ?? null;
30951
31707
  }
30952
- function supportsOsc52Clipboard(terminal = env.terminal) {
30953
- return OSC52_CAPABLE_TERMINALS.includes(terminal ?? "");
31708
+ function supportsOsc52Clipboard(terminal2 = env.terminal) {
31709
+ return OSC52_CAPABLE_TERMINALS.includes(terminal2 ?? "");
30954
31710
  }
30955
31711
  function execFileNoThrow(file2, args, options = {}) {
30956
- return new Promise((resolve5) => {
31712
+ return new Promise((resolve6) => {
30957
31713
  const child = spawn4(file2, args, {
30958
31714
  cwd: options.useCwd ? process.cwd() : void 0,
30959
31715
  env: options.env,
@@ -30976,13 +31732,13 @@ function execFileNoThrow(file2, args, options = {}) {
30976
31732
  if (timer) {
30977
31733
  clearTimeout(timer);
30978
31734
  }
30979
- resolve5({ stdout, stderr, code: 1, error: String(error) });
31735
+ resolve6({ stdout, stderr, code: 1, error: String(error) });
30980
31736
  });
30981
31737
  child.on("close", (code) => {
30982
31738
  if (timer) {
30983
31739
  clearTimeout(timer);
30984
31740
  }
30985
- resolve5({ stdout, stderr, code: timedOut ? 124 : code ?? 0 });
31741
+ resolve6({ stdout, stderr, code: timedOut ? 124 : code ?? 0 });
30986
31742
  });
30987
31743
  if (options.input) {
30988
31744
  child.stdin?.write(options.input);
@@ -31014,7 +31770,7 @@ function shouldEmitClipboardSequence(env2 = process.env) {
31014
31770
  }
31015
31771
  return !!env2["SSH_CONNECTION"] || !env2["TMUX"] && !env2["STY"];
31016
31772
  }
31017
- function shouldUseNativeClipboard(env2 = process.env, terminal = env.terminal) {
31773
+ function shouldUseNativeClipboard(env2 = process.env, terminal2 = env.terminal) {
31018
31774
  if (env2.SSH_CONNECTION) {
31019
31775
  return false;
31020
31776
  }
@@ -31024,7 +31780,7 @@ function shouldUseNativeClipboard(env2 = process.env, terminal = env.terminal) {
31024
31780
  if (!shouldEmitClipboardSequence(env2)) {
31025
31781
  return true;
31026
31782
  }
31027
- return !supportsOsc52Clipboard(terminal);
31783
+ return !supportsOsc52Clipboard(terminal2);
31028
31784
  }
31029
31785
  function tmuxPassthrough(payload) {
31030
31786
  return `${ESC}Ptmux;${payload.replaceAll(ESC, ESC + ESC)}${ST}`;
@@ -32460,7 +33216,7 @@ function needsAltScreenResizeScrollbackClear(env2 = process.env) {
32460
33216
  function supportsExtendedKeys() {
32461
33217
  return EXTENDED_KEYS_TERMINALS.includes(env.terminal ?? "");
32462
33218
  }
32463
- function writeDiffToTerminal(terminal, diff2, skipSyncMarkers = false, onDrain) {
33219
+ function writeDiffToTerminal(terminal2, diff2, skipSyncMarkers = false, onDrain) {
32464
33220
  if (diff2.length === 0) {
32465
33221
  return { bytes: 0, backpressure: false };
32466
33222
  }
@@ -32505,7 +33261,7 @@ function writeDiffToTerminal(terminal, diff2, skipSyncMarkers = false, onDrain)
32505
33261
  if (useSync) {
32506
33262
  buffer += ESU;
32507
33263
  }
32508
- const wrote = onDrain ? terminal.stdout.write(buffer, () => onDrain()) : terminal.stdout.write(buffer);
33264
+ const wrote = onDrain ? terminal2.stdout.write(buffer, () => onDrain()) : terminal2.stdout.write(buffer);
32509
33265
  return { bytes: Buffer.byteLength(buffer, "utf8"), backpressure: !wrote };
32510
33266
  }
32511
33267
  function logForDebugging(_message, _options = {}) {
@@ -35955,8 +36711,8 @@ function setTerminalFocused(v) {
35955
36711
  cb();
35956
36712
  }
35957
36713
  if (!v) {
35958
- for (const resolve5 of resolvers) {
35959
- resolve5();
36714
+ for (const resolve6 of resolvers) {
36715
+ resolve6();
35960
36716
  }
35961
36717
  resolvers.clear();
35962
36718
  }
@@ -42194,11 +42950,11 @@ $ npm install --save-dev react-devtools-core
42194
42950
  * and the terminal doesn't respond, the promise remains pending.
42195
42951
  */
42196
42952
  send(query) {
42197
- return new Promise((resolve5) => {
42953
+ return new Promise((resolve6) => {
42198
42954
  this.queue.push({
42199
42955
  kind: "query",
42200
42956
  match: query.match,
42201
- resolve: /* @__PURE__ */ __name((r) => resolve5(r), "resolve")
42957
+ resolve: /* @__PURE__ */ __name((r) => resolve6(r), "resolve")
42202
42958
  });
42203
42959
  this.stdout.write(query.request);
42204
42960
  });
@@ -42213,8 +42969,8 @@ $ npm install --save-dev react-devtools-core
42213
42969
  * Safe to call with no pending queries — still waits for a round-trip.
42214
42970
  */
42215
42971
  flush() {
42216
- return new Promise((resolve5) => {
42217
- this.queue.push({ kind: "sentinel", resolve: resolve5 });
42972
+ return new Promise((resolve6) => {
42973
+ this.queue.push({ kind: "sentinel", resolve: resolve6 });
42218
42974
  this.stdout.write(SENTINEL);
42219
42975
  });
42220
42976
  }
@@ -44694,8 +45450,8 @@ $ npm install --save-dev react-devtools-core
44694
45450
  }
44695
45451
  }
44696
45452
  async waitUntilExit() {
44697
- this.exitPromise ||= new Promise((resolve5, reject) => {
44698
- this.resolveExitPromise = resolve5;
45453
+ this.exitPromise ||= new Promise((resolve6, reject) => {
45454
+ this.resolveExitPromise = resolve6;
44699
45455
  this.rejectExitPromise = reject;
44700
45456
  });
44701
45457
  return this.exitPromise;
@@ -45186,10 +45942,10 @@ async function writeClipboardText(text, platform2 = process.platform, start = sp
45186
45942
  const candidates = writeClipboardCommands(platform2, env2);
45187
45943
  for (const { cmd, args } of candidates) {
45188
45944
  try {
45189
- const ok = await new Promise((resolve5) => {
45945
+ const ok = await new Promise((resolve6) => {
45190
45946
  const child = start(cmd, [...args], { stdio: ["pipe", "ignore", "ignore"], windowsHide: true });
45191
- child.once("error", () => resolve5(false));
45192
- child.once("close", (code) => resolve5(code === 0));
45947
+ child.once("error", () => resolve6(false));
45948
+ child.once("close", (code) => resolve6(code === 0));
45193
45949
  child.stdin?.end(text);
45194
45950
  });
45195
45951
  if (ok) {
@@ -45248,8 +46004,8 @@ async function readOsc52Clipboard(querier, timeoutMs = 500) {
45248
46004
  if (!querier) {
45249
46005
  return null;
45250
46006
  }
45251
- const timeout = new Promise((resolve5) => {
45252
- setTimeout(() => resolve5(void 0), timeoutMs);
46007
+ const timeout = new Promise((resolve6) => {
46008
+ setTimeout(() => resolve6(void 0), timeoutMs);
45253
46009
  });
45254
46010
  const query = querier.send({
45255
46011
  request: buildOsc52ClipboardQuery(),
@@ -45406,12 +46162,12 @@ async function backupFile(filePath, ops) {
45406
46162
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
45407
46163
  await ops.copyFile(filePath, `${filePath}.backup.${stamp}`);
45408
46164
  }
45409
- async function configureTerminalKeybindings(terminal, options) {
46165
+ async function configureTerminalKeybindings(terminal2, options) {
45410
46166
  const env2 = options?.env ?? process.env;
45411
46167
  const platform2 = options?.platform ?? process.platform;
45412
46168
  const homeDir = options?.homeDir ?? homedir25();
45413
46169
  const ops = { ...DEFAULT_FILE_OPS, ...options?.fileOps ?? {} };
45414
- const meta = TERMINAL_META[terminal];
46170
+ const meta = TERMINAL_META[terminal2];
45415
46171
  if (isRemoteShellSession(env2)) {
45416
46172
  return {
45417
46173
  success: false,
@@ -45606,6 +46362,7 @@ var init_overlayStore = __esm({
45606
46362
  buildOverlayState = /* @__PURE__ */ __name(() => ({
45607
46363
  agents: false,
45608
46364
  agentsInitialHistoryIndex: 0,
46365
+ agentsWorkflowId: null,
45609
46366
  approval: null,
45610
46367
  clarify: null,
45611
46368
  confirm: null,
@@ -45628,6 +46385,7 @@ var init_overlayStore = __esm({
45628
46385
  ...buildOverlayState(),
45629
46386
  agents: $overlayState.get().agents,
45630
46387
  agentsInitialHistoryIndex: $overlayState.get().agentsInitialHistoryIndex,
46388
+ agentsWorkflowId: $overlayState.get().agentsWorkflowId,
45631
46389
  mcpHub: $overlayState.get().mcpHub,
45632
46390
  modelPicker: $overlayState.get().modelPicker,
45633
46391
  modelPickerMode: $overlayState.get().modelPickerMode,
@@ -46427,13 +47185,25 @@ var init_debug = __esm({
46427
47185
  }
46428
47186
  });
46429
47187
 
47188
+ // src/app/workflowStore.ts
47189
+ import { atom as atom3 } from "nanostores";
47190
+ var $workflowSnapshots, setWorkflowSnapshot;
47191
+ var init_workflowStore = __esm({
47192
+ "src/app/workflowStore.ts"() {
47193
+ "use strict";
47194
+ $workflowSnapshots = atom3({});
47195
+ setWorkflowSnapshot = /* @__PURE__ */ __name((snapshot) => $workflowSnapshots.set({ ...$workflowSnapshots.get(), [snapshot.id]: snapshot }), "setWorkflowSnapshot");
47196
+ }
47197
+ });
47198
+
46430
47199
  // src/app/slash/commands/openjaw.ts
46431
- var fmt, money, stub, eventLine, parseScheduleInput, showUsage, openjawCommands;
47200
+ var fmt, money, stub, eventLine, parseScheduleInput, asWorkflowSnapshot, workflowLine, workflowRows, showUsage, openjawCommands;
46432
47201
  var init_openjaw = __esm({
46433
47202
  "src/app/slash/commands/openjaw.ts"() {
46434
47203
  "use strict";
46435
47204
  init_usage();
46436
47205
  init_overlayStore();
47206
+ init_workflowStore();
46437
47207
  fmt = /* @__PURE__ */ __name((n) => (n ?? 0).toLocaleString(), "fmt");
46438
47208
  money = /* @__PURE__ */ __name((n) => `$${(n ?? 0).toFixed(4)}`, "money");
46439
47209
  stub = /* @__PURE__ */ __name((ctx, command, message) => {
@@ -46473,6 +47243,14 @@ var init_openjaw = __esm({
46473
47243
  }
46474
47244
  return null;
46475
47245
  }, "parseScheduleInput");
47246
+ asWorkflowSnapshot = /* @__PURE__ */ __name((run) => run ? run : null, "asWorkflowSnapshot");
47247
+ workflowLine = /* @__PURE__ */ __name((run) => {
47248
+ const active = run.workers.filter((worker) => worker.status === "running" || worker.status === "queued").length;
47249
+ const done = run.workers.filter((worker) => worker.status === "completed").length;
47250
+ return `${run.id} \xB7 ${run.status} \xB7 ${done}/${run.workers.length} done${active ? ` \xB7 ${active} active` : ""} \xB7 ${run.goal}`;
47251
+ }, "workflowLine");
47252
+ workflowRows = /* @__PURE__ */ __name((runs) => runs.map((run) => [run.id, `${run.status} \xB7 ${run.workerCount}/${run.plannedWorkerCount} workers
47253
+ ${run.goal}`]), "workflowRows");
46476
47254
  showUsage = /* @__PURE__ */ __name((ctx, render2) => {
46477
47255
  ctx.gateway.rpc("session.usage", { session_id: ctx.sid }).then(ctx.guarded(render2)).catch(ctx.guardedErr);
46478
47256
  }, "showUsage");
@@ -46494,6 +47272,10 @@ var init_openjaw = __esm({
46494
47272
  if (!r.messages?.length) {
46495
47273
  ctx.transcript.sys("connect: no output");
46496
47274
  }
47275
+ if (!ctx.sid && r.config_updates) {
47276
+ ctx.transcript.sys("provider connected \u2014 starting OpenJaw session\u2026");
47277
+ ctx.session.newSession();
47278
+ }
46497
47279
  })
46498
47280
  ).catch(ctx.guardedErr);
46499
47281
  }, "run")
@@ -46587,6 +47369,70 @@ var init_openjaw = __esm({
46587
47369
  ).catch(ctx.guardedErr);
46588
47370
  }, "run")
46589
47371
  },
47372
+ {
47373
+ aliases: ["wf"],
47374
+ help: "start or inspect an advisory dynamic workflow",
47375
+ name: "workflow",
47376
+ usage: "/workflow <goal> | /workflow status [id] | list | show [id] | cancel <id>",
47377
+ run: /* @__PURE__ */ __name((arg, ctx) => {
47378
+ const text = arg.trim();
47379
+ if (!text) {
47380
+ return ctx.transcript.sys("usage: /workflow <goal> | /workflow status [id] | list | show [id] | cancel <id>");
47381
+ }
47382
+ const [subRaw, ...rest] = text.split(/\s+/);
47383
+ const sub = subRaw?.toLowerCase() ?? "";
47384
+ const remainder = rest.join(" ").trim();
47385
+ if (sub === "status") {
47386
+ return ctx.gateway.rpc("workflow.status", { id: remainder, session_id: ctx.sid }).then(
47387
+ ctx.guarded((r) => {
47388
+ const snapshot = asWorkflowSnapshot(r.run);
47389
+ if (!snapshot) return ctx.transcript.sys(r.error || "no workflows yet");
47390
+ setWorkflowSnapshot(snapshot);
47391
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 0, agentsWorkflowId: snapshot.id });
47392
+ ctx.transcript.sys(`workflow status \xB7 ${workflowLine(snapshot)}`);
47393
+ })
47394
+ ).catch(ctx.guardedErr);
47395
+ }
47396
+ if (sub === "list" || sub === "ls") {
47397
+ return ctx.gateway.rpc("workflow.list", { limit: 30, session_id: ctx.sid }).then(
47398
+ ctx.guarded((r) => {
47399
+ const runs = r.runs ?? [];
47400
+ if (!runs.length) return ctx.transcript.sys("no workflows yet");
47401
+ ctx.transcript.panel("Workflows", [{ rows: workflowRows(runs) }]);
47402
+ })
47403
+ ).catch(ctx.guardedErr);
47404
+ }
47405
+ if (sub === "show") {
47406
+ return ctx.gateway.rpc("workflow.show", { id: remainder, session_id: ctx.sid }).then(
47407
+ ctx.guarded((r) => {
47408
+ const snapshot = asWorkflowSnapshot(r.run);
47409
+ if (!snapshot) return ctx.transcript.sys(r.error || "no workflows yet");
47410
+ setWorkflowSnapshot(snapshot);
47411
+ ctx.transcript.page(snapshot.summary || workflowLine(snapshot), `Workflow ${snapshot.id}`);
47412
+ })
47413
+ ).catch(ctx.guardedErr);
47414
+ }
47415
+ if (sub === "cancel" || sub === "stop") {
47416
+ if (!remainder) return ctx.transcript.sys("usage: /workflow cancel <runId|workerId>");
47417
+ return ctx.gateway.rpc("workflow.cancel", { id: remainder, session_id: ctx.sid }).then(
47418
+ ctx.guarded((r) => {
47419
+ const snapshot = asWorkflowSnapshot(r.run);
47420
+ if (snapshot) setWorkflowSnapshot(snapshot);
47421
+ ctx.transcript.sys(r.ok ? `workflow cancel requested: ${remainder}` : r.error || `not found: ${remainder}`);
47422
+ })
47423
+ ).catch(ctx.guardedErr);
47424
+ }
47425
+ return ctx.gateway.rpc("workflow.start", { goal: text, session_id: ctx.sid }).then(
47426
+ ctx.guarded((r) => {
47427
+ const snapshot = asWorkflowSnapshot(r.run);
47428
+ if (!snapshot) return ctx.transcript.sys(r.error || "workflow failed to start");
47429
+ setWorkflowSnapshot(snapshot);
47430
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 0, agentsWorkflowId: snapshot.id });
47431
+ ctx.transcript.sys(`workflow ${snapshot.id} started \xB7 ${snapshot.plannedWorkerCount} workers \xB7 concurrency ${snapshot.concurrency}`);
47432
+ })
47433
+ ).catch(ctx.guardedErr);
47434
+ }, "run")
47435
+ },
46590
47436
  {
46591
47437
  help: "schedule a recurring prompt",
46592
47438
  name: "schedule",
@@ -46719,7 +47565,7 @@ var init_openjaw = __esm({
46719
47565
  });
46720
47566
 
46721
47567
  // src/app/delegationStore.ts
46722
- import { atom as atom3 } from "nanostores";
47568
+ import { atom as atom4 } from "nanostores";
46723
47569
  var buildState, $delegationState, getDelegationState, patchDelegationState, $overlaySectionsOpen, toggleOverlaySection, applyDelegationStatus;
46724
47570
  var init_delegationStore = __esm({
46725
47571
  "src/app/delegationStore.ts"() {
@@ -46730,10 +47576,10 @@ var init_delegationStore = __esm({
46730
47576
  paused: false,
46731
47577
  updatedAt: null
46732
47578
  }), "buildState");
46733
- $delegationState = atom3(buildState());
47579
+ $delegationState = atom4(buildState());
46734
47580
  getDelegationState = /* @__PURE__ */ __name(() => $delegationState.get(), "getDelegationState");
46735
47581
  patchDelegationState = /* @__PURE__ */ __name((next) => $delegationState.set({ ...$delegationState.get(), ...next }), "patchDelegationState");
46736
- $overlaySectionsOpen = atom3({});
47582
+ $overlaySectionsOpen = atom4({});
46737
47583
  toggleOverlaySection = /* @__PURE__ */ __name((title, defaultOpen) => {
46738
47584
  const state = $overlaySectionsOpen.get();
46739
47585
  const current = title in state ? state[title] : defaultOpen;
@@ -46759,7 +47605,7 @@ var init_delegationStore = __esm({
46759
47605
  });
46760
47606
 
46761
47607
  // src/app/spawnHistoryStore.ts
46762
- import { atom as atom4 } from "nanostores";
47608
+ import { atom as atom5 } from "nanostores";
46763
47609
  function summarizeLabel(subagents) {
46764
47610
  const top = subagents.filter((s) => s.parentId == null || subagents.every((o) => o.id !== s.parentId)).slice(0, 2).map((s) => s.goal || "subagent").join(" \xB7 ");
46765
47611
  return top || `${subagents.length} agent${subagents.length === 1 ? "" : "s"}`;
@@ -46802,8 +47648,8 @@ var init_spawnHistoryStore = __esm({
46802
47648
  "src/app/spawnHistoryStore.ts"() {
46803
47649
  "use strict";
46804
47650
  HISTORY_LIMIT = 10;
46805
- $spawnHistory = atom4([]);
46806
- $spawnDiff = atom4(null);
47651
+ $spawnHistory = atom5([]);
47652
+ $spawnDiff = atom5(null);
46807
47653
  getSpawnHistory = /* @__PURE__ */ __name(() => $spawnHistory.get(), "getSpawnHistory");
46808
47654
  clearDiffPair = /* @__PURE__ */ __name(() => $spawnDiff.set(null), "clearDiffPair");
46809
47655
  setDiffPair = /* @__PURE__ */ __name((pair) => $spawnDiff.set(pair), "setDiffPair");
@@ -47113,15 +47959,15 @@ var init_ops = __esm({
47113
47959
  }
47114
47960
  const [a, b] = parts;
47115
47961
  const history = getSpawnHistory();
47116
- const resolve5 = /* @__PURE__ */ __name((token) => {
47962
+ const resolve6 = /* @__PURE__ */ __name((token) => {
47117
47963
  const n = parseInt(token, 10);
47118
47964
  if (Number.isFinite(n) && n >= 1 && n <= history.length) {
47119
47965
  return history[n - 1] ?? null;
47120
47966
  }
47121
47967
  return null;
47122
47968
  }, "resolve");
47123
- const baseline = resolve5(a);
47124
- const candidate = resolve5(b);
47969
+ const baseline = resolve6(a);
47970
+ const candidate = resolve6(b);
47125
47971
  if (!baseline || !candidate) {
47126
47972
  return ctx.transcript.sys(`replay-diff: could not resolve indices \xB7 history has ${history.length} entries`);
47127
47973
  }
@@ -47994,76 +48840,20 @@ var init_session2 = __esm({
47994
48840
  }
47995
48841
  });
47996
48842
 
47997
- // src/lib/externalCli.ts
47998
- import { spawn as spawn6 } from "node:child_process";
47999
- var resolveHermesBin, launchHermesCommand;
48000
- var init_externalCli = __esm({
48001
- "src/lib/externalCli.ts"() {
48002
- "use strict";
48003
- resolveHermesBin = /* @__PURE__ */ __name(() => process.env.OPENJAW_BIN?.trim() || "hermes", "resolveHermesBin");
48004
- launchHermesCommand = /* @__PURE__ */ __name((args) => new Promise((resolve5) => {
48005
- const child = spawn6(resolveHermesBin(), args, { stdio: "inherit" });
48006
- child.on("error", (err) => resolve5({ code: null, error: err.message }));
48007
- child.on("exit", (code) => resolve5({ code }));
48008
- }), "launchHermesCommand");
48009
- }
48010
- });
48011
-
48012
- // src/app/setupHandoff.ts
48013
- async function runExternalSetup({ args, ctx, done, launcher, suspend }) {
48014
- const { gateway, session, transcript } = ctx;
48015
- transcript.sys(`launching \`hermes ${args.join(" ")}\`\u2026`);
48016
- patchUiState({ status: "setup running\u2026" });
48017
- let result = { code: null };
48018
- await suspend(async () => {
48019
- result = await launcher(args);
48020
- });
48021
- if (result.error) {
48022
- transcript.sys(`error launching hermes: ${result.error}`);
48023
- patchUiState({ status: "setup required" });
48024
- return;
48025
- }
48026
- if (result.code !== 0) {
48027
- transcript.sys(`hermes ${args[0]} exited with code ${result.code}`);
48028
- patchUiState({ status: "setup required" });
48029
- return;
48030
- }
48031
- const setup = await gateway.rpc("setup.status", {});
48032
- if (setup?.provider_configured === false) {
48033
- transcript.sys("still no provider configured");
48034
- patchUiState({ status: "setup required" });
48035
- return;
48036
- }
48037
- transcript.sys(done);
48038
- session.newSession();
48039
- }
48040
- var init_setupHandoff = __esm({
48041
- "src/app/setupHandoff.ts"() {
48042
- "use strict";
48043
- init_uiStore();
48044
- __name(runExternalSetup, "runExternalSetup");
48045
- }
48046
- });
48047
-
48048
48843
  // src/app/slash/commands/setup.ts
48049
48844
  var setupCommands;
48050
48845
  var init_setup = __esm({
48051
48846
  "src/app/slash/commands/setup.ts"() {
48052
48847
  "use strict";
48053
- init_entry_exports();
48054
- init_externalCli();
48055
- init_setupHandoff();
48848
+ init_overlayStore();
48056
48849
  setupCommands = [
48057
48850
  {
48058
- help: "run full setup wizard (launches `hermes setup`)",
48851
+ help: "first-run setup help; opens provider connection picker",
48059
48852
  name: "setup",
48060
- run: /* @__PURE__ */ __name((arg, ctx) => void runExternalSetup({
48061
- args: ["setup", ...arg.split(/\s+/).filter(Boolean)],
48062
- ctx,
48063
- done: "setup complete \u2014 starting session\u2026",
48064
- launcher: launchHermesCommand,
48065
- suspend: withInkSuspended
48066
- }), "run")
48853
+ run: /* @__PURE__ */ __name((_arg, ctx) => {
48854
+ ctx.transcript.sys("OpenJaw setup: run /connect to set up a provider, then /model to choose a model.");
48855
+ patchOverlayState({ modelPicker: true, modelPickerMode: "connect" });
48856
+ }, "run")
48067
48857
  }
48068
48858
  ];
48069
48859
  }
@@ -48219,17 +49009,856 @@ var init_catalog = __esm({
48219
49009
  }
48220
49010
  });
48221
49011
 
48222
- // src/rpcHandlers.ts
48223
- import { spawn as spawn7 } from "node:child_process";
48224
- import { randomUUID as randomUUID13 } from "node:crypto";
48225
- import { existsSync as existsSync31, mkdirSync as mkdirSync17, readFileSync as readFileSync28, rmSync, statSync as statSync4, writeFileSync as writeFileSync19 } from "node:fs";
49012
+ // src/workflows/planner.ts
49013
+ function resolveDynamicWorkflowConfig(config) {
49014
+ const raw = config.features?.dynamic_workflows ?? {};
49015
+ return {
49016
+ enabled: raw.enabled ?? DEFAULT_DYNAMIC_WORKFLOW_CONFIG.enabled,
49017
+ plannerMode: "adaptive",
49018
+ hardMaxWorkers: clampInt(raw.hard_max_workers, DEFAULT_DYNAMIC_WORKFLOW_CONFIG.hardMaxWorkers, 1, 1e4),
49019
+ hardMaxConcurrentWorkers: clampInt(
49020
+ raw.hard_max_concurrent_workers,
49021
+ DEFAULT_DYNAMIC_WORKFLOW_CONFIG.hardMaxConcurrentWorkers,
49022
+ 1,
49023
+ 2048
49024
+ ),
49025
+ workerTimeoutMs: clampInt(raw.worker_timeout_ms, DEFAULT_DYNAMIC_WORKFLOW_CONFIG.workerTimeoutMs, 1e4, 36e5),
49026
+ persistHistory: raw.persist_history ?? DEFAULT_DYNAMIC_WORKFLOW_CONFIG.persistHistory
49027
+ };
49028
+ }
49029
+ function initialConcurrency(plannedWorkers, limits) {
49030
+ if (plannedWorkers <= 0) return 0;
49031
+ return Math.min(Math.ceil(Math.sqrt(plannedWorkers) * 2), plannedWorkers, limits.hardMaxConcurrentWorkers);
49032
+ }
49033
+ function adjustConcurrency(current, plannedWorkers, limits, event) {
49034
+ if (plannedWorkers <= 0) return 0;
49035
+ if (event === "rate_limited") return Math.max(1, Math.floor(current / 2));
49036
+ return Math.min(current + 1, plannedWorkers, limits.hardMaxConcurrentWorkers);
49037
+ }
49038
+ function planWorkflow(goal, runId, limits) {
49039
+ const taskGoals = deriveTaskGoals(goal);
49040
+ const specs = [];
49041
+ const maxWorkers = limits.hardMaxWorkers;
49042
+ for (const taskGoal of taskGoals) {
49043
+ if (specs.length >= maxWorkers) break;
49044
+ const id = `${runId}-w${specs.length + 1}`;
49045
+ specs.push({
49046
+ depth: 0,
49047
+ goal: taskGoal,
49048
+ id,
49049
+ index: specs.length,
49050
+ parentId: null,
49051
+ prompt: buildTaskPrompt(goal, taskGoal),
49052
+ role: "task"
49053
+ });
49054
+ }
49055
+ const taskSpecs = [...specs];
49056
+ const verifierEvery = taskSpecs.length >= 8 ? 4 : 3;
49057
+ for (let i = 0; i < taskSpecs.length && specs.length < maxWorkers; i += verifierEvery) {
49058
+ const batch = taskSpecs.slice(i, i + verifierEvery);
49059
+ const id = `${runId}-w${specs.length + 1}`;
49060
+ specs.push({
49061
+ depth: 1,
49062
+ dependsOn: batch.map((item) => item.id),
49063
+ goal: `Verify findings from workers ${batch.map((item) => item.index + 1).join(", ")}`,
49064
+ id,
49065
+ index: specs.length,
49066
+ parentId: batch[0]?.id ?? null,
49067
+ prompt: buildVerifierPrompt(goal, batch),
49068
+ role: "verifier"
49069
+ });
49070
+ }
49071
+ return { concurrency: initialConcurrency(specs.length, limits), specs };
49072
+ }
49073
+ function deriveTaskGoals(goal) {
49074
+ const explicit = extractExplicitTasks(goal);
49075
+ const words = goal.trim().split(/\s+/).filter(Boolean).length;
49076
+ const complexity = Math.min(12, Math.max(3, Math.ceil(words / 35)));
49077
+ const inferred = inferLanes(goal);
49078
+ const seeds = explicit.length > 1 ? explicit : inferred;
49079
+ const out = [];
49080
+ for (const item of seeds) {
49081
+ pushUnique(out, item);
49082
+ }
49083
+ while (out.length < complexity) {
49084
+ const next = DEFAULT_LANES[out.length % DEFAULT_LANES.length];
49085
+ pushUnique(out, next);
49086
+ if (out.length >= DEFAULT_LANES.length && explicit.length <= 1) break;
49087
+ }
49088
+ return out.slice(0, Math.max(1, out.length));
49089
+ }
49090
+ function extractExplicitTasks(goal) {
49091
+ return goal.split(/\r?\n/).map((line) => line.trim().replace(/^[-*+]\s+/, "").replace(/^\d+[.)]\s+/, "").trim()).filter((line) => line.length >= 12);
49092
+ }
49093
+ function inferLanes(goal) {
49094
+ const lower = goal.toLowerCase();
49095
+ const lanes = ["Map the current state and locate relevant files or evidence"];
49096
+ if (/code|repo|branch|test|build|bug|feature|implement/.test(lower)) {
49097
+ lanes.push("Review implementation risks, edge cases, and integration points");
49098
+ lanes.push("Inspect validation strategy and likely test coverage");
49099
+ }
49100
+ if (/research|latest|current|external|claude|compare|similar/.test(lower)) {
49101
+ lanes.push("Research external context and comparable workflow behavior");
49102
+ }
49103
+ if (/ui|status|overlay|navigate|progress|worker/.test(lower)) {
49104
+ lanes.push("Evaluate user experience, status visibility, and navigation requirements");
49105
+ }
49106
+ if (/security|safe|permission|mutat|write|approval/.test(lower)) {
49107
+ lanes.push("Analyze safety boundaries, permissions, and failure modes");
49108
+ }
49109
+ lanes.push("Draft concrete recommendations and acceptance criteria");
49110
+ return lanes;
49111
+ }
49112
+ function pushUnique(items, value) {
49113
+ const normalized = value.trim();
49114
+ if (!normalized) return;
49115
+ if (!items.some((item) => item.toLowerCase() === normalized.toLowerCase())) items.push(normalized);
49116
+ }
49117
+ function buildTaskPrompt(overallGoal, taskGoal) {
49118
+ return [
49119
+ `Overall workflow goal: ${overallGoal}`,
49120
+ `Your assigned read-only worker goal: ${taskGoal}`,
49121
+ "",
49122
+ "Work independently. Use only read/search/analysis tools. Do not modify files, send messages, update memory, or run shell/code execution.",
49123
+ "Return concise findings with evidence, confidence, and open questions."
49124
+ ].join("\n");
49125
+ }
49126
+ function buildVerifierPrompt(overallGoal, batch) {
49127
+ return [
49128
+ `Overall workflow goal: ${overallGoal}`,
49129
+ `Verify the likely claims and gaps for these worker lanes: ${batch.map((item) => item.goal).join("; ")}`,
49130
+ "",
49131
+ "Use read-only checks. Label findings as verified, partially verified, or unverified. Do not modify anything."
49132
+ ].join("\n");
49133
+ }
49134
+ var DEFAULT_DYNAMIC_WORKFLOW_CONFIG, clampInt, DEFAULT_LANES;
49135
+ var init_planner = __esm({
49136
+ "src/workflows/planner.ts"() {
49137
+ "use strict";
49138
+ DEFAULT_DYNAMIC_WORKFLOW_CONFIG = {
49139
+ enabled: true,
49140
+ plannerMode: "adaptive",
49141
+ hardMaxWorkers: 1024,
49142
+ hardMaxConcurrentWorkers: 128,
49143
+ workerTimeoutMs: 18e4,
49144
+ persistHistory: true
49145
+ };
49146
+ clampInt = /* @__PURE__ */ __name((value, fallback, min, max) => {
49147
+ const parsed = typeof value === "number" ? value : Number(value);
49148
+ if (!Number.isFinite(parsed)) return fallback;
49149
+ return Math.min(max, Math.max(min, Math.floor(parsed)));
49150
+ }, "clampInt");
49151
+ __name(resolveDynamicWorkflowConfig, "resolveDynamicWorkflowConfig");
49152
+ __name(initialConcurrency, "initialConcurrency");
49153
+ __name(adjustConcurrency, "adjustConcurrency");
49154
+ __name(planWorkflow, "planWorkflow");
49155
+ __name(deriveTaskGoals, "deriveTaskGoals");
49156
+ __name(extractExplicitTasks, "extractExplicitTasks");
49157
+ __name(inferLanes, "inferLanes");
49158
+ DEFAULT_LANES = [
49159
+ "Map the current state and locate relevant files or evidence",
49160
+ "Review implementation risks, edge cases, and integration points",
49161
+ "Inspect validation strategy and likely test coverage",
49162
+ "Analyze safety boundaries, permissions, and failure modes",
49163
+ "Draft concrete recommendations and acceptance criteria"
49164
+ ];
49165
+ __name(pushUnique, "pushUnique");
49166
+ __name(buildTaskPrompt, "buildTaskPrompt");
49167
+ __name(buildVerifierPrompt, "buildVerifierPrompt");
49168
+ }
49169
+ });
49170
+
49171
+ // src/workflows/persistence.ts
49172
+ import { existsSync as existsSync31, mkdirSync as mkdirSync17, readdirSync as readdirSync7, readFileSync as readFileSync28, writeFileSync as writeFileSync19 } from "node:fs";
48226
49173
  import { homedir as homedir28 } from "node:os";
48227
- import { basename as basename3, extname as extname4, join as join42 } from "node:path";
49174
+ import { basename as basename3, join as join42, resolve as resolve4 } from "node:path";
49175
+ function ensureDir(path3) {
49176
+ if (!existsSync31(path3)) mkdirSync17(path3, { recursive: true });
49177
+ }
49178
+ function safeSegment(value) {
49179
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 80) || "default";
49180
+ }
49181
+ function readJson(path3) {
49182
+ try {
49183
+ return JSON.parse(readFileSync28(path3, "utf8"));
49184
+ } catch {
49185
+ return null;
49186
+ }
49187
+ }
49188
+ function saveWorkflowSnapshot(snapshot) {
49189
+ ensureDir(workflowDir());
49190
+ const path3 = join42(workflowDir(), `${safeSegment(snapshot.id)}.json`);
49191
+ writeFileSync19(path3, `${JSON.stringify(snapshot, null, 2)}
49192
+ `, "utf8");
49193
+ return path3;
49194
+ }
49195
+ function loadWorkflowSnapshot(id) {
49196
+ const path3 = join42(workflowDir(), `${safeSegment(id)}.json`);
49197
+ return readJson(path3);
49198
+ }
49199
+ function listWorkflowSnapshots(limit = 30) {
49200
+ if (!existsSync31(workflowDir())) return [];
49201
+ const entries = [];
49202
+ for (const entry of readdirSync7(workflowDir(), { withFileTypes: true })) {
49203
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
49204
+ const path3 = join42(workflowDir(), entry.name);
49205
+ const snapshot = readJson(path3);
49206
+ if (!snapshot) continue;
49207
+ entries.push({
49208
+ finishedAt: snapshot.finishedAt,
49209
+ goal: snapshot.goal,
49210
+ id: snapshot.id,
49211
+ path: path3,
49212
+ plannedWorkerCount: snapshot.plannedWorkerCount,
49213
+ startedAt: snapshot.startedAt,
49214
+ status: snapshot.status,
49215
+ workerCount: snapshot.workers.length
49216
+ });
49217
+ }
49218
+ return entries.sort((a, b) => (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt)).slice(0, Math.max(1, limit));
49219
+ }
49220
+ function saveSpawnTreeSnapshot(input) {
49221
+ const sessionId = safeSegment(input.session_id ?? "default");
49222
+ const dir2 = join42(spawnTreeDir(), sessionId);
49223
+ ensureDir(dir2);
49224
+ const stamp = new Date((input.finished_at ?? Date.now() / 1e3) * 1e3).toISOString().replace(/[:.]/g, "-");
49225
+ const path3 = join42(dir2, `${stamp}-${Math.random().toString(36).slice(2, 8)}.json`);
49226
+ const snapshot = {
49227
+ count: input.subagents?.length ?? 0,
49228
+ finished_at: input.finished_at,
49229
+ label: input.label,
49230
+ session_id: input.session_id,
49231
+ started_at: input.started_at,
49232
+ subagents: input.subagents ?? []
49233
+ };
49234
+ writeFileSync19(path3, `${JSON.stringify(snapshot, null, 2)}
49235
+ `, "utf8");
49236
+ return path3;
49237
+ }
49238
+ function listSpawnTreeSnapshots(sessionId = "default", limit = 30) {
49239
+ const dir2 = join42(spawnTreeDir(), safeSegment(sessionId));
49240
+ if (!existsSync31(dir2)) return [];
49241
+ const entries = [];
49242
+ for (const entry of readdirSync7(dir2, { withFileTypes: true })) {
49243
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
49244
+ const path3 = join42(dir2, entry.name);
49245
+ const snapshot = readJson(path3);
49246
+ if (!snapshot) continue;
49247
+ entries.push({
49248
+ count: snapshot.count,
49249
+ finished_at: snapshot.finished_at,
49250
+ label: snapshot.label,
49251
+ path: path3,
49252
+ session_id: snapshot.session_id,
49253
+ started_at: snapshot.started_at
49254
+ });
49255
+ }
49256
+ return entries.sort((a, b) => (b.finished_at ?? 0) - (a.finished_at ?? 0)).slice(0, Math.max(1, limit));
49257
+ }
49258
+ function loadSpawnTreeSnapshot(path3) {
49259
+ const root = resolve4(spawnTreeDir());
49260
+ const resolved = resolve4(path3);
49261
+ if (!resolved.startsWith(root)) return null;
49262
+ const snapshot = readJson(resolved);
49263
+ return snapshot ? { ...snapshot, path: resolved } : null;
49264
+ }
49265
+ var rootDir, workflowDir, spawnTreeDir;
49266
+ var init_persistence = __esm({
49267
+ "src/workflows/persistence.ts"() {
49268
+ "use strict";
49269
+ rootDir = /* @__PURE__ */ __name(() => join42(homedir28(), ".openjaw-agent"), "rootDir");
49270
+ workflowDir = /* @__PURE__ */ __name(() => join42(rootDir(), "workflows"), "workflowDir");
49271
+ spawnTreeDir = /* @__PURE__ */ __name(() => join42(rootDir(), "spawn-trees"), "spawnTreeDir");
49272
+ __name(ensureDir, "ensureDir");
49273
+ __name(safeSegment, "safeSegment");
49274
+ __name(readJson, "readJson");
49275
+ __name(saveWorkflowSnapshot, "saveWorkflowSnapshot");
49276
+ __name(loadWorkflowSnapshot, "loadWorkflowSnapshot");
49277
+ __name(listWorkflowSnapshots, "listWorkflowSnapshots");
49278
+ __name(saveSpawnTreeSnapshot, "saveSpawnTreeSnapshot");
49279
+ __name(listSpawnTreeSnapshots, "listSpawnTreeSnapshots");
49280
+ __name(loadSpawnTreeSnapshot, "loadSpawnTreeSnapshot");
49281
+ }
49282
+ });
49283
+
49284
+ // src/workflows/readOnlyTools.ts
49285
+ function isWorkflowReadOnlyTool(name) {
49286
+ return READ_ONLY_TOOLS.has(name);
49287
+ }
49288
+ var READ_ONLY_TOOLS, ReadOnlyToolRuntime;
49289
+ var init_readOnlyTools = __esm({
49290
+ "src/workflows/readOnlyTools.ts"() {
49291
+ "use strict";
49292
+ READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
49293
+ "browser_snapshot",
49294
+ "browser_extract",
49295
+ "file_info",
49296
+ "file_list",
49297
+ "file_read",
49298
+ "glob",
49299
+ "grep",
49300
+ "image_view",
49301
+ "web_extract",
49302
+ "web_fetch",
49303
+ "web_search"
49304
+ ]);
49305
+ __name(isWorkflowReadOnlyTool, "isWorkflowReadOnlyTool");
49306
+ ReadOnlyToolRuntime = class {
49307
+ constructor(inner) {
49308
+ this.inner = inner;
49309
+ }
49310
+ inner;
49311
+ static {
49312
+ __name(this, "ReadOnlyToolRuntime");
49313
+ }
49314
+ listTools() {
49315
+ return this.inner.listTools().filter((tool) => isWorkflowReadOnlyTool(tool.name));
49316
+ }
49317
+ async execute(name, input) {
49318
+ if (!isWorkflowReadOnlyTool(name)) {
49319
+ throw new Error(`Tool ${name} is not available in advisory workflow workers`);
49320
+ }
49321
+ return await this.inner.execute(name, input);
49322
+ }
49323
+ };
49324
+ }
49325
+ });
49326
+
49327
+ // src/workflows/manager.ts
49328
+ import { randomUUID as randomUUID13 } from "node:crypto";
49329
+ function buildPlannerSystemPrompt(limits) {
49330
+ return [
49331
+ "You are the OpenJaw dynamic workflow planner.",
49332
+ "Return ONLY valid JSON. Do not include markdown fences or prose.",
49333
+ "Create a task graph for advisory read-only worker agents. Do not answer the user task.",
49334
+ "Use enough workers to represent genuinely different research lanes, viewpoints, critics, verifiers, and a final synthesizer. Do not default to a small fixed count.",
49335
+ "For panel-review prompts, create separate panelist workers for each requested philosophy and a critic/risk worker before the synthesizer.",
49336
+ `Physical caps: at most ${limits.hardMaxWorkers} total workers and ${limits.hardMaxConcurrentWorkers} concurrent workers. Normal plans should be smaller than the cap but may contain dozens of workers when useful.`,
49337
+ 'Schema: {"language":"optional output language","workers":[{"id":"kebab-id","role":"researcher|panelist|critic|verifier|synthesizer","philosophy":"optional","goal":"specific worker goal","depends_on":["worker-id"]}]}',
49338
+ "Always include exactly one synthesizer worker that depends on the most important research, panelist, critic, and verifier workers."
49339
+ ].join("\n");
49340
+ }
49341
+ function parseModelPlan(content, goal, runId, limits) {
49342
+ const parsed = JSON.parse(extractJsonObject(content));
49343
+ const workers = Array.isArray(parsed.workers) ? parsed.workers : [];
49344
+ const specs = [];
49345
+ const usedIds = /* @__PURE__ */ new Set();
49346
+ for (const worker of workers.slice(0, limits.hardMaxWorkers)) {
49347
+ const rawGoal = String(worker.goal ?? "").trim();
49348
+ if (!rawGoal) continue;
49349
+ const id = uniqueWorkerId(runId, worker.id || rawGoal, usedIds, specs.length + 1);
49350
+ const role = normalizeRole(worker.role);
49351
+ const dependsOn = Array.isArray(worker.depends_on) ? worker.depends_on.map(String).map((value) => scopedWorkerId(runId, value)).filter((value) => usedIds.has(value)) : [];
49352
+ specs.push({
49353
+ depth: dependsOn.length > 0 || role !== "task" ? 1 : 0,
49354
+ dependsOn,
49355
+ goal: rawGoal,
49356
+ id,
49357
+ index: specs.length,
49358
+ parentId: dependsOn[0] ?? null,
49359
+ prompt: buildModelWorkerPrompt(goal, rawGoal, worker.role, worker.philosophy, parsed.language),
49360
+ role
49361
+ });
49362
+ }
49363
+ return ensureSynthesizer({ concurrency: 0, specs }, goal, runId, limits);
49364
+ }
49365
+ function ensureSynthesizer(plan, goal, runId, limits) {
49366
+ const specs = plan.specs.slice(0, limits.hardMaxWorkers);
49367
+ const existing = specs.find((spec) => spec.role === "synthesizer");
49368
+ const dependencies = specs.filter((spec) => spec.role !== "synthesizer").map((spec) => spec.id);
49369
+ if (existing) {
49370
+ existing.dependsOn = existing.dependsOn?.length ? existing.dependsOn : dependencies;
49371
+ existing.depth = Math.max(existing.depth, 1);
49372
+ existing.parentId = existing.dependsOn[0] ?? null;
49373
+ } else if (specs.length < limits.hardMaxWorkers) {
49374
+ specs.push({
49375
+ depth: 1,
49376
+ dependsOn: dependencies,
49377
+ goal: "Synthesize the panel and research outputs into the final answer",
49378
+ id: `${runId}-synthesizer`,
49379
+ index: specs.length,
49380
+ parentId: dependencies[0] ?? null,
49381
+ prompt: buildModelWorkerPrompt(goal, "Synthesize all completed worker outputs into the final answer for the user.", "synthesizer", void 0, void 0),
49382
+ role: "synthesizer"
49383
+ });
49384
+ }
49385
+ return { concurrency: Math.min(Math.ceil(Math.sqrt(specs.length) * 2), specs.length, limits.hardMaxConcurrentWorkers), specs };
49386
+ }
49387
+ function buildModelWorkerPrompt(overallGoal, workerGoal, role, philosophy, language) {
49388
+ return [
49389
+ `Overall workflow goal: ${overallGoal}`,
49390
+ `Worker role: ${role || "researcher"}`,
49391
+ philosophy ? `Investment/work philosophy: ${philosophy}` : "",
49392
+ language ? `Final language preference: ${language}` : "",
49393
+ `Assigned goal: ${workerGoal}`,
49394
+ "",
49395
+ "Use only read/search/analysis tools. Do not modify files, send messages, update memory, or run shell/code execution.",
49396
+ "If prior worker outputs are provided below, use them as context and explicitly resolve agreements, disagreements, and uncertainty.",
49397
+ "Return evidence-backed findings. A synthesizer must produce the final answer directly for the user."
49398
+ ].filter(Boolean).join("\n");
49399
+ }
49400
+ function normalizeRole(role) {
49401
+ const lower = String(role ?? "").toLowerCase();
49402
+ if (lower.includes("synth") || lower.includes("chair")) return "synthesizer";
49403
+ if (lower.includes("verify") || lower.includes("critic") || lower.includes("risk")) return "verifier";
49404
+ return "task";
49405
+ }
49406
+ function extractJsonObject(content) {
49407
+ const trimmed = content.trim().replace(/^```(?:json)?\s*/i, "").replace(/```$/i, "").trim();
49408
+ const start = trimmed.indexOf("{");
49409
+ const end = trimmed.lastIndexOf("}");
49410
+ if (start < 0 || end < start) throw new Error("planner response did not contain a JSON object");
49411
+ return trimmed.slice(start, end + 1);
49412
+ }
49413
+ function uniqueWorkerId(runId, value, used, index) {
49414
+ const base = scopedWorkerId(runId, value) || `${runId}-w${index}`;
49415
+ let candidate = base;
49416
+ let suffix = 2;
49417
+ while (used.has(candidate)) {
49418
+ candidate = `${base}-${suffix}`;
49419
+ suffix += 1;
49420
+ }
49421
+ used.add(candidate);
49422
+ return candidate;
49423
+ }
49424
+ function scopedWorkerId(runId, value) {
49425
+ const slug = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
49426
+ return slug.startsWith(`${runId}-`) ? slug : `${runId}-${slug || "worker"}`;
49427
+ }
49428
+ function firstParagraph(value) {
49429
+ return value.split(/\n\s*\n/)[0]?.trim().slice(0, 800) || value.trim().slice(0, 800);
49430
+ }
49431
+ function inferVerificationState(value) {
49432
+ const lower = value.toLowerCase();
49433
+ if (lower.includes("partially verified") || lower.includes("partial")) return "partial";
49434
+ if (lower.includes("unverified") || lower.includes("not verified")) return "unverified";
49435
+ if (lower.includes("verified")) return "verified";
49436
+ return "pending";
49437
+ }
49438
+ function dependencyOutputContext(run, spec) {
49439
+ const dependencyIds = spec.dependsOn ?? [];
49440
+ if (dependencyIds.length === 0) return "";
49441
+ const blocks = dependencyIds.map((id) => run.workers.find((worker) => worker.id === id)).filter((worker) => Boolean(worker)).map((worker) => [
49442
+ `## Worker ${worker.id}`,
49443
+ `Role: ${worker.workerRole ?? "task"}`,
49444
+ `Goal: ${worker.goal}`,
49445
+ `Status: ${worker.status}`,
49446
+ worker.summary ? `Summary: ${worker.summary}` : "",
49447
+ worker.details ? `Details:
49448
+ ${worker.details}` : ""
49449
+ ].filter(Boolean).join("\n"));
49450
+ return blocks.length ? `# Prior worker outputs
49451
+ ${blocks.join("\n\n")}` : "";
49452
+ }
49453
+ var terminal, WorkflowManager;
49454
+ var init_manager = __esm({
49455
+ "src/workflows/manager.ts"() {
49456
+ "use strict";
49457
+ init_agent_loop();
49458
+ init_providers();
49459
+ init_planner();
49460
+ init_persistence();
49461
+ init_readOnlyTools();
49462
+ terminal = /* @__PURE__ */ new Set(["cancelled", "completed", "failed"]);
49463
+ WorkflowManager = class {
49464
+ constructor(config, toolRuntime, systemPromptFn, bus, options = {}) {
49465
+ this.config = config;
49466
+ this.toolRuntime = toolRuntime;
49467
+ this.systemPromptFn = systemPromptFn;
49468
+ this.bus = bus;
49469
+ this.planner = options.planner;
49470
+ this.runner = options.runner ?? this.defaultRunner;
49471
+ }
49472
+ config;
49473
+ toolRuntime;
49474
+ systemPromptFn;
49475
+ bus;
49476
+ static {
49477
+ __name(this, "WorkflowManager");
49478
+ }
49479
+ runs = /* @__PURE__ */ new Map();
49480
+ planner;
49481
+ runner;
49482
+ async start(goal, sessionId) {
49483
+ const limits = resolveDynamicWorkflowConfig(this.config);
49484
+ if (!limits.enabled) {
49485
+ throw new Error("dynamic workflows are disabled in config");
49486
+ }
49487
+ const id = `wf-${randomUUID13().slice(0, 8)}`;
49488
+ const plan = await this.buildPlan(goal, id, limits);
49489
+ const now2 = Date.now();
49490
+ const run = {
49491
+ abortController: new AbortController(),
49492
+ activeAborts: /* @__PURE__ */ new Map(),
49493
+ concurrency: plan.concurrency,
49494
+ config: limits,
49495
+ goal,
49496
+ id,
49497
+ plannedWorkerCount: plan.specs.length,
49498
+ startedAt: now2,
49499
+ status: "running",
49500
+ workers: plan.specs.map((spec) => this.workerFromSpec(spec, id, plan.specs.length, now2))
49501
+ };
49502
+ this.runs.set(id, run);
49503
+ this.emitWorkflow("workflow.start", run, sessionId);
49504
+ for (const worker of run.workers) {
49505
+ this.emitSubagent("subagent.spawn_requested", run, worker, sessionId);
49506
+ }
49507
+ void this.executeRun(run, plan.specs, sessionId);
49508
+ return { run: this.snapshot(run), started: true };
49509
+ }
49510
+ status(id) {
49511
+ const run = id ? this.runs.get(id) : this.latestRun();
49512
+ if (run) return this.snapshot(run);
49513
+ if (id) return loadWorkflowSnapshot(id);
49514
+ const first = listWorkflowSnapshots(1)[0];
49515
+ return first ? loadWorkflowSnapshot(first.id) : null;
49516
+ }
49517
+ list(limit = 30) {
49518
+ const live = [...this.runs.values()].map((run) => ({
49519
+ finishedAt: run.finishedAt,
49520
+ goal: run.goal,
49521
+ id: run.id,
49522
+ plannedWorkerCount: run.plannedWorkerCount,
49523
+ startedAt: run.startedAt,
49524
+ status: run.status,
49525
+ workerCount: run.workers.length
49526
+ }));
49527
+ const persisted = listWorkflowSnapshots(limit);
49528
+ const seen = new Set(live.map((entry) => entry.id));
49529
+ return [...live, ...persisted.filter((entry) => !seen.has(entry.id))].sort((a, b) => (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt)).slice(0, Math.max(1, limit));
49530
+ }
49531
+ cancel(id) {
49532
+ const run = this.runs.get(id);
49533
+ if (run) {
49534
+ this.cancelRun(run);
49535
+ return { found: true, run: this.snapshot(run) };
49536
+ }
49537
+ for (const candidate of this.runs.values()) {
49538
+ const worker = candidate.workers.find((item) => item.id === id);
49539
+ if (!worker) continue;
49540
+ candidate.activeAborts.get(worker.id)?.();
49541
+ this.updateWorker(candidate, worker.id, {
49542
+ currentStep: "cancel requested",
49543
+ status: "interrupted"
49544
+ });
49545
+ return { found: true, run: this.snapshot(candidate), workerId: worker.id };
49546
+ }
49547
+ return { found: false };
49548
+ }
49549
+ async buildPlan(goal, runId, limits) {
49550
+ if (this.planner) {
49551
+ return ensureSynthesizer(await this.planner(goal, runId, limits), goal, runId, limits);
49552
+ }
49553
+ try {
49554
+ const provider = createProvider(this.config);
49555
+ const controller = new AbortController();
49556
+ const timer = setTimeout(() => controller.abort(), Math.min(6e4, limits.workerTimeoutMs));
49557
+ try {
49558
+ const result = await provider.chat({
49559
+ systemPrompt: buildPlannerSystemPrompt(limits),
49560
+ messages: [{ role: "user", content: goal }],
49561
+ tools: [],
49562
+ signal: controller.signal
49563
+ });
49564
+ const parsed = parseModelPlan(result.text ?? "", goal, runId, limits);
49565
+ if (parsed.specs.length > 0) return parsed;
49566
+ } finally {
49567
+ clearTimeout(timer);
49568
+ }
49569
+ } catch {
49570
+ }
49571
+ return ensureSynthesizer(planWorkflow(goal, runId, limits), goal, runId, limits);
49572
+ }
49573
+ async executeRun(run, specs, sessionId) {
49574
+ const queue = [...specs];
49575
+ const active = /* @__PURE__ */ new Map();
49576
+ let stableCompletions = 0;
49577
+ try {
49578
+ while ((queue.length > 0 || active.size > 0) && !run.abortController.signal.aborted) {
49579
+ const ready = queue.filter((spec) => this.dependenciesFinished(run, spec));
49580
+ while (ready.length > 0 && active.size < run.concurrency && !run.abortController.signal.aborted) {
49581
+ const spec = ready.shift();
49582
+ queue.splice(queue.indexOf(spec), 1);
49583
+ const promise = this.executeWorker(run, spec, sessionId).then(() => {
49584
+ stableCompletions += 1;
49585
+ if (stableCompletions % Math.max(2, run.concurrency) === 0) {
49586
+ run.concurrency = adjustConcurrency(run.concurrency, run.plannedWorkerCount, run.config, "stable_completion");
49587
+ }
49588
+ }).catch((err) => {
49589
+ const message = err instanceof Error ? err.message : String(err);
49590
+ if (/rate|429|quota|throttle/i.test(message)) {
49591
+ run.concurrency = adjustConcurrency(run.concurrency, run.plannedWorkerCount, run.config, "rate_limited");
49592
+ }
49593
+ }).finally(() => active.delete(spec.id));
49594
+ active.set(spec.id, promise);
49595
+ }
49596
+ if (active.size === 0) break;
49597
+ await Promise.race(active.values());
49598
+ }
49599
+ if (run.abortController.signal.aborted) {
49600
+ for (const worker of run.workers) {
49601
+ if (worker.status === "queued" || worker.status === "running") {
49602
+ this.updateWorker(run, worker.id, { currentStep: "cancelled", status: "interrupted" });
49603
+ }
49604
+ }
49605
+ run.status = "cancelled";
49606
+ run.summary = this.summarize(run, "cancelled");
49607
+ } else if (run.workers.some((worker) => worker.status === "failed")) {
49608
+ run.status = "failed";
49609
+ run.summary = this.summarize(run, "failed");
49610
+ } else {
49611
+ run.status = "completed";
49612
+ run.summary = this.summarize(run, "completed");
49613
+ }
49614
+ } catch (err) {
49615
+ run.status = "failed";
49616
+ run.summary = `Workflow failed: ${err instanceof Error ? err.message : String(err)}`;
49617
+ } finally {
49618
+ run.finishedAt = Date.now();
49619
+ if (run.config.persistHistory) saveWorkflowSnapshot(this.snapshot(run));
49620
+ this.emitWorkflow(run.status === "failed" ? "workflow.error" : "workflow.complete", run, sessionId);
49621
+ }
49622
+ }
49623
+ async executeWorker(run, spec, sessionId) {
49624
+ if (run.abortController.signal.aborted) return;
49625
+ this.updateWorker(run, spec.id, {
49626
+ currentStep: "starting",
49627
+ startedAt: Date.now(),
49628
+ status: "running"
49629
+ });
49630
+ this.emitSubagent("subagent.start", run, this.worker(run, spec.id), sessionId);
49631
+ const controller = new AbortController();
49632
+ const timeoutMs = spec.role === "synthesizer" ? run.config.workerTimeoutMs * 3 : run.config.workerTimeoutMs;
49633
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
49634
+ run.activeAborts.set(spec.id, () => controller.abort());
49635
+ const signal = AbortSignal.any([controller.signal, run.abortController.signal]);
49636
+ try {
49637
+ const result = await this.runner({
49638
+ config: this.config,
49639
+ run: this.snapshot(run),
49640
+ signal,
49641
+ spec,
49642
+ systemPromptFn: this.systemPromptFn,
49643
+ toolRuntime: new ReadOnlyToolRuntime(this.toolRuntime),
49644
+ update: /* @__PURE__ */ __name((patch) => {
49645
+ this.updateWorker(run, spec.id, patch);
49646
+ this.emitSubagent("subagent.progress", run, this.worker(run, spec.id), sessionId);
49647
+ }, "update")
49648
+ });
49649
+ this.updateWorker(run, spec.id, {
49650
+ currentStep: "complete",
49651
+ details: result,
49652
+ durationSeconds: (Date.now() - (this.worker(run, spec.id).startedAt ?? Date.now())) / 1e3,
49653
+ status: signal.aborted ? "interrupted" : "completed",
49654
+ summary: firstParagraph(result),
49655
+ verificationState: spec.role === "verifier" ? inferVerificationState(result) : "not_applicable"
49656
+ });
49657
+ } catch (err) {
49658
+ const message = err instanceof Error ? err.message : String(err);
49659
+ this.updateWorker(run, spec.id, {
49660
+ currentStep: signal.aborted ? "interrupted" : "failed",
49661
+ details: message,
49662
+ durationSeconds: (Date.now() - (this.worker(run, spec.id).startedAt ?? Date.now())) / 1e3,
49663
+ status: signal.aborted ? "interrupted" : "failed",
49664
+ summary: message,
49665
+ verificationState: "unverified"
49666
+ });
49667
+ if (!signal.aborted) throw err;
49668
+ } finally {
49669
+ clearTimeout(timeout);
49670
+ run.activeAborts.delete(spec.id);
49671
+ this.emitSubagent("subagent.complete", run, this.worker(run, spec.id), sessionId);
49672
+ if (run.config.persistHistory) saveWorkflowSnapshot(this.snapshot(run));
49673
+ }
49674
+ }
49675
+ defaultRunner = /* @__PURE__ */ __name(async (ctx) => {
49676
+ const workerConfig = {
49677
+ ...ctx.config,
49678
+ features: { ...ctx.config.features, skill_auto_suggest: false }
49679
+ };
49680
+ const loop = new AgentLoop(workerConfig, ctx.toolRuntime);
49681
+ const sections = await ctx.systemPromptFn();
49682
+ const systemPrompt = [
49683
+ ...sections,
49684
+ "# Advisory Workflow Worker Rules",
49685
+ "You are a read-only workflow worker. Never modify files, execute shell/code, send messages, update memory, or perform browser actions that change state.",
49686
+ "Use available read/search tools only. Return concise evidence-backed findings."
49687
+ ].filter(Boolean).join("\n\n");
49688
+ ctx.signal.addEventListener("abort", () => loop.abort(), { once: true });
49689
+ let answer = "";
49690
+ let streamedText = "";
49691
+ let outputTail = [];
49692
+ let toolCount = 0;
49693
+ const userPrompt = [ctx.spec.prompt, dependencyOutputContext(ctx.run, ctx.spec)].filter(Boolean).join("\n\n");
49694
+ for await (const chunk of loop.run(userPrompt, systemPrompt)) {
49695
+ if (ctx.signal.aborted) {
49696
+ loop.abort();
49697
+ break;
49698
+ }
49699
+ if (chunk.type === "thinking" && chunk.content.trim()) {
49700
+ streamedText += chunk.content;
49701
+ ctx.update({ currentStep: chunk.content.trim().slice(0, 180), thinking: [chunk.content.trim().slice(0, 500)] });
49702
+ }
49703
+ if (chunk.type === "tool_call") {
49704
+ toolCount += 1;
49705
+ ctx.update({ currentStep: `using ${chunk.toolName ?? "tool"}`, toolCount, tools: [`${chunk.toolName ?? "tool"}(...)`] });
49706
+ }
49707
+ if (chunk.type === "tool_result") {
49708
+ outputTail = [...outputTail, { isError: /error/i.test(chunk.content), preview: chunk.content.slice(0, 240), tool: chunk.toolName ?? "tool" }].slice(-8);
49709
+ ctx.update({ outputTail });
49710
+ }
49711
+ if (chunk.type === "answer" && chunk.content.trim()) answer = chunk.content.trim();
49712
+ }
49713
+ const result = answer || streamedText.trim();
49714
+ if (ctx.signal.aborted) {
49715
+ if (result) {
49716
+ return `${result}
49717
+
49718
+ [Partial output preserved after worker interruption.]`;
49719
+ }
49720
+ throw new Error("worker interrupted");
49721
+ }
49722
+ return result || "Worker completed with no final answer.";
49723
+ }, "defaultRunner");
49724
+ workerFromSpec(spec, workflowId, taskCount, now2) {
49725
+ return {
49726
+ currentStep: "queued",
49727
+ depth: spec.depth,
49728
+ goal: spec.goal,
49729
+ id: spec.id,
49730
+ index: spec.index,
49731
+ notes: [],
49732
+ parentId: spec.parentId,
49733
+ startedAt: now2,
49734
+ status: "queued",
49735
+ taskCount,
49736
+ thinking: [],
49737
+ toolCount: 0,
49738
+ tools: [],
49739
+ verificationState: spec.role === "verifier" ? "pending" : "not_applicable",
49740
+ workflowId,
49741
+ workerRole: spec.role
49742
+ };
49743
+ }
49744
+ dependenciesFinished(run, spec) {
49745
+ return (spec.dependsOn ?? []).every((id) => {
49746
+ const status = run.workers.find((worker) => worker.id === id)?.status;
49747
+ return status === "completed" || status === "failed" || status === "interrupted";
49748
+ });
49749
+ }
49750
+ updateWorker(run, id, patch) {
49751
+ run.workers = run.workers.map((worker) => worker.id === id ? { ...worker, ...patch } : worker);
49752
+ }
49753
+ worker(run, id) {
49754
+ return run.workers.find((worker) => worker.id === id) ?? run.workers[0];
49755
+ }
49756
+ snapshot(run) {
49757
+ return {
49758
+ concurrency: run.concurrency,
49759
+ finishedAt: run.finishedAt,
49760
+ goal: run.goal,
49761
+ id: run.id,
49762
+ plannedWorkerCount: run.plannedWorkerCount,
49763
+ startedAt: run.startedAt,
49764
+ status: run.status,
49765
+ summary: run.summary,
49766
+ workers: run.workers.map((worker) => ({ ...worker }))
49767
+ };
49768
+ }
49769
+ latestRun() {
49770
+ const runs = [...this.runs.values()].sort((a, b) => b.startedAt - a.startedAt);
49771
+ return runs[0] ?? null;
49772
+ }
49773
+ cancelRun(run) {
49774
+ if (terminal.has(run.status)) return;
49775
+ run.abortController.abort();
49776
+ for (const abort of run.activeAborts.values()) abort();
49777
+ run.status = "cancelled";
49778
+ }
49779
+ summarize(run, status) {
49780
+ const synthesizer = run.workers.find((worker) => worker.workerRole === "synthesizer" && worker.details && worker.details !== "worker interrupted");
49781
+ if ((status === "completed" || status === "cancelled" || status === "failed") && synthesizer?.details) {
49782
+ return [synthesizer.details, "No files were modified by advisory workflow workers."].join("\n\n");
49783
+ }
49784
+ const counts = run.workers.reduce((acc, worker) => {
49785
+ acc[worker.status] = (acc[worker.status] ?? 0) + 1;
49786
+ return acc;
49787
+ }, {});
49788
+ const highlights = run.workers.filter((worker) => worker.summary).slice(0, 8).map((worker) => `- ${worker.goal}: ${worker.summary}`).join("\n");
49789
+ return [
49790
+ `Workflow ${status}: ${run.goal}`,
49791
+ `Workers: ${run.workers.length} planned \xB7 ${counts.completed ?? 0} completed \xB7 ${counts.failed ?? 0} failed \xB7 ${counts.interrupted ?? 0} interrupted.`,
49792
+ "No files were modified by advisory workflow workers.",
49793
+ highlights ? `
49794
+ Key worker summaries:
49795
+ ${highlights}` : ""
49796
+ ].filter(Boolean).join("\n");
49797
+ }
49798
+ emitWorkflow(type, run, sessionId) {
49799
+ this.bus.emitEvent({
49800
+ payload: this.snapshot(run),
49801
+ session_id: sessionId ?? void 0,
49802
+ type
49803
+ });
49804
+ }
49805
+ emitSubagent(type, run, worker, sessionId) {
49806
+ this.bus.emitEvent({
49807
+ payload: {
49808
+ cost_usd: worker.costUsd,
49809
+ current_step: worker.currentStep,
49810
+ depth: worker.depth,
49811
+ details: worker.details,
49812
+ duration_seconds: worker.durationSeconds,
49813
+ files_read: worker.filesRead,
49814
+ files_written: worker.filesWritten,
49815
+ goal: worker.goal,
49816
+ input_tokens: worker.inputTokens,
49817
+ output_tail: worker.outputTail?.map((entry) => ({ is_error: entry.isError, preview: entry.preview, tool: entry.tool })),
49818
+ output_tokens: worker.outputTokens,
49819
+ parent_id: worker.parentId,
49820
+ status: worker.status,
49821
+ subagent_id: worker.id,
49822
+ summary: worker.summary,
49823
+ task_count: worker.taskCount,
49824
+ task_index: worker.index,
49825
+ text: worker.currentStep ?? worker.summary,
49826
+ tool_count: worker.toolCount,
49827
+ verification_state: worker.verificationState,
49828
+ workflow_id: run.id,
49829
+ worker_role: worker.workerRole
49830
+ },
49831
+ session_id: sessionId ?? void 0,
49832
+ type
49833
+ });
49834
+ }
49835
+ };
49836
+ __name(buildPlannerSystemPrompt, "buildPlannerSystemPrompt");
49837
+ __name(parseModelPlan, "parseModelPlan");
49838
+ __name(ensureSynthesizer, "ensureSynthesizer");
49839
+ __name(buildModelWorkerPrompt, "buildModelWorkerPrompt");
49840
+ __name(normalizeRole, "normalizeRole");
49841
+ __name(extractJsonObject, "extractJsonObject");
49842
+ __name(uniqueWorkerId, "uniqueWorkerId");
49843
+ __name(scopedWorkerId, "scopedWorkerId");
49844
+ __name(firstParagraph, "firstParagraph");
49845
+ __name(inferVerificationState, "inferVerificationState");
49846
+ __name(dependencyOutputContext, "dependencyOutputContext");
49847
+ }
49848
+ });
49849
+
49850
+ // src/rpcHandlers.ts
49851
+ import { spawn as spawn6 } from "node:child_process";
49852
+ import { randomUUID as randomUUID14 } from "node:crypto";
49853
+ import { existsSync as existsSync32, mkdirSync as mkdirSync18, readFileSync as readFileSync29, rmSync, statSync as statSync4, writeFileSync as writeFileSync20 } from "node:fs";
49854
+ import { homedir as homedir29 } from "node:os";
49855
+ import { basename as basename4, extname as extname4, join as join43 } from "node:path";
48228
49856
  function registerRpcHandlers(options) {
48229
49857
  const { agentConfig, agentLoop, bridgeEmitter, bridgeManager, bus, mcpManager, systemPromptFn, toolRegistry, voiceManager } = options;
48230
49858
  let currentRun = null;
48231
49859
  const pendingResponders = /* @__PURE__ */ new Map();
48232
49860
  const promptCollector = createPromptCollector(bus, () => agentLoop.sessionId);
49861
+ const workflowManager = new WorkflowManager(agentConfig, toolRegistry, systemPromptFn, bus);
48233
49862
  const OAUTH_FLOW_TTL_MS = 15 * 60 * 1e3;
48234
49863
  const oauthFlows = /* @__PURE__ */ new Map();
48235
49864
  const cleanupOAuthFlow = /* @__PURE__ */ __name((flowId, abort) => {
@@ -48248,7 +49877,7 @@ function registerRpcHandlers(options) {
48248
49877
  const user = bridgeUser(rawEvent, source);
48249
49878
  const text = formatBridgeText(rawEvent, source, user);
48250
49879
  bus.emitEvent({
48251
- payload: { source, task_id: randomUUID13(), text, user },
49880
+ payload: { source, task_id: randomUUID14(), text, user },
48252
49881
  session_id: agentLoop.sessionId,
48253
49882
  type: "bridge.message"
48254
49883
  });
@@ -48257,7 +49886,7 @@ function registerRpcHandlers(options) {
48257
49886
  bus.registerRpc("setup.status", () => {
48258
49887
  const hasConfigKey = Boolean(agentConfig.llm.api_key && agentConfig.llm.api_key !== "proxy-token");
48259
49888
  const hasEnvKey = Boolean(
48260
- process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GITHUB_TOKEN
49889
+ process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GITHUB_COPILOT_TOKEN || process.env.GITHUB_TOKEN
48261
49890
  );
48262
49891
  const hasStoredCredential = PROVIDERS2.some((p) => {
48263
49892
  try {
@@ -48605,12 +50234,12 @@ ${helpMessage}` : field.label;
48605
50234
  const id = String(params.session_id ?? agentLoop.sessionId) || agentLoop.sessionId;
48606
50235
  const data = loadSession(id);
48607
50236
  const messages = data?.messages ?? agentLoop.history;
48608
- const exportDir = join42(homedir28(), ".openjaw-agent", "exports");
48609
- if (!existsSync31(exportDir)) {
48610
- mkdirSync17(exportDir, { recursive: true });
50237
+ const exportDir = join43(homedir29(), ".openjaw-agent", "exports");
50238
+ if (!existsSync32(exportDir)) {
50239
+ mkdirSync18(exportDir, { recursive: true });
48611
50240
  }
48612
50241
  const safeId = id.replace(/[^a-zA-Z0-9_.-]/g, "_") || agentLoop.sessionId;
48613
- const file2 = join42(exportDir, `session-${safeId}.md`);
50242
+ const file2 = join43(exportDir, `session-${safeId}.md`);
48614
50243
  const lines = [
48615
50244
  `# OpenJaw Agent Session ${id}`,
48616
50245
  `Date: ${(/* @__PURE__ */ new Date()).toISOString()}`,
@@ -48623,7 +50252,7 @@ ${helpMessage}` : field.label;
48623
50252
  for (const message of messages) {
48624
50253
  lines.push(...sessionMessageToMarkdown(message));
48625
50254
  }
48626
- writeFileSync19(file2, lines.join("\n"), "utf-8");
50255
+ writeFileSync20(file2, lines.join("\n"), "utf-8");
48627
50256
  return { file: file2, message_count: messages.length };
48628
50257
  });
48629
50258
  bus.registerRpc("session.interrupt", () => {
@@ -48640,8 +50269,8 @@ ${helpMessage}` : field.label;
48640
50269
  return { deleted: "" };
48641
50270
  }
48642
50271
  try {
48643
- const file2 = join42(homedir28(), ".openjaw-agent", "sessions", `${id}.json`);
48644
- if (existsSync31(file2)) {
50272
+ const file2 = join43(homedir29(), ".openjaw-agent", "sessions", `${id}.json`);
50273
+ if (existsSync32(file2)) {
48645
50274
  rmSync(file2);
48646
50275
  }
48647
50276
  return { deleted: id };
@@ -48666,26 +50295,14 @@ ${helpMessage}` : field.label;
48666
50295
  bus.log("info", `session.steer ${String(params.text ?? "").slice(0, 200)}`);
48667
50296
  return { status: "queued", text: String(params.text ?? "") };
48668
50297
  });
48669
- let pendingImageSeq = 0;
48670
50298
  let pendingImage = null;
48671
- const nextImageAttachmentId = /* @__PURE__ */ __name(() => `img-${Date.now().toString(36)}-${++pendingImageSeq}`, "nextImageAttachmentId");
48672
50299
  bus.registerRpc("prompt.submit", async (params) => {
48673
50300
  const text = String(params.text ?? "");
48674
50301
  if (!text) return { ok: false };
48675
50302
  const systemPromptArr = await systemPromptFn();
48676
50303
  const systemPrompt = systemPromptArr.join("\n\n");
48677
- const requestedImageId = String(params.image_attachment_id ?? "");
48678
- const imageForSubmit = pendingImage && (!requestedImageId || pendingImage.attachmentId === requestedImageId) ? pendingImage : null;
48679
- if (imageForSubmit) {
48680
- const modelInfo = agentLoop.getActiveModelMetadata() ?? await agentLoop.listModels().then(() => agentLoop.getActiveModelMetadata()).catch(() => void 0);
48681
- if (modelInfo?.supportsVision === false) {
48682
- throw new Error(`model ${agentLoop.model} does not support image input; switch to a vision-capable model before submitting an image`);
48683
- }
48684
- }
48685
- const imageData = imageForSubmit ? { base64: imageForSubmit.base64, mimeType: imageForSubmit.mimeType } : void 0;
48686
- if (imageForSubmit) {
48687
- pendingImage = null;
48688
- }
50304
+ const imageData = pendingImage ? { base64: pendingImage.base64, mimeType: pendingImage.mimeType } : void 0;
50305
+ pendingImage = null;
48689
50306
  currentRun = { abort: /* @__PURE__ */ __name(() => agentLoop.abort(), "abort") };
48690
50307
  void streamAgentRun({ agentLoop, bus, systemPrompt, text, imageData }).finally(() => {
48691
50308
  currentRun = null;
@@ -48726,11 +50343,11 @@ ${helpMessage}` : field.label;
48726
50343
  bus.registerRpc("shell.exec", async (params) => {
48727
50344
  const command = String(params.command ?? "");
48728
50345
  if (!command) return { code: -1, stderr: "empty command" };
48729
- return await new Promise((resolve5) => {
50346
+ return await new Promise((resolve6) => {
48730
50347
  const isWin = process.platform === "win32";
48731
50348
  const shell = isWin ? "powershell.exe" : "sh";
48732
50349
  const args = isWin ? ["-NoProfile", "-Command", command] : ["-c", command];
48733
- const child = spawn7(shell, args, { timeout: 3e4 });
50350
+ const child = spawn6(shell, args, { timeout: 3e4 });
48734
50351
  let stdout = "";
48735
50352
  let stderr = "";
48736
50353
  let truncated = false;
@@ -48753,40 +50370,14 @@ ${helpMessage}` : field.label;
48753
50370
  if (truncated) {
48754
50371
  stderr += "\n[output truncated to 1MB]";
48755
50372
  }
48756
- resolve5({ code: code ?? -1, stderr, stdout });
50373
+ resolve6({ code: code ?? -1, stderr, stdout });
48757
50374
  });
48758
50375
  child.on("error", (err) => {
48759
- resolve5({ code: -1, stderr: err.message });
50376
+ resolve6({ code: -1, stderr: err.message });
48760
50377
  });
48761
50378
  });
48762
50379
  });
48763
50380
  bus.registerRpc("clipboard.paste", async () => {
48764
- try {
48765
- if (clipboardHasImage()) {
48766
- const img = readClipboardImage();
48767
- if (img) {
48768
- const attachmentId = nextImageAttachmentId();
48769
- pendingImage = {
48770
- attachmentId,
48771
- base64: img.base64,
48772
- mimeType: img.mimeType,
48773
- name: "clipboard.png"
48774
- };
48775
- const byteLength = Math.ceil(img.base64.length * 3 / 4);
48776
- const tokenEstimate = Math.max(1, Math.ceil(byteLength / 750));
48777
- return {
48778
- attached: true,
48779
- attachment_id: attachmentId,
48780
- count: 1,
48781
- height: img.height,
48782
- name: "clipboard.png",
48783
- token_estimate: tokenEstimate,
48784
- width: img.width
48785
- };
48786
- }
48787
- }
48788
- } catch {
48789
- }
48790
50381
  try {
48791
50382
  const text = await readClipboardText();
48792
50383
  return { attached: false, message: text ?? "" };
@@ -48904,7 +50495,7 @@ ${helpMessage}` : field.label;
48904
50495
  return agentConfig.llm.copilot_enterprise_url;
48905
50496
  })();
48906
50497
  const flow = await startCopilotDeviceFlow(clientId, enterpriseUrl);
48907
- const flowId = randomUUID13();
50498
+ const flowId = randomUUID14();
48908
50499
  const controller = new AbortController();
48909
50500
  const timer = setTimeout(() => {
48910
50501
  cleanupOAuthFlow(flowId, true);
@@ -49068,7 +50659,7 @@ ${helpMessage}` : field.label;
49068
50659
  Object.assign(agentConfig.llm, result.configUpdates);
49069
50660
  bus.emitEvent({ payload: sessionInfoSnapshot(agentLoop, toolRegistry), session_id: agentLoop.sessionId, type: "session.info" });
49070
50661
  }
49071
- return { messages };
50662
+ return { config_updates: result.configUpdates, messages };
49072
50663
  });
49073
50664
  bus.registerRpc("provider.disconnect", (params) => {
49074
50665
  const provider = String(params.provider ?? "").trim();
@@ -49128,6 +50719,32 @@ ${helpMessage}` : field.label;
49128
50719
  tasks: forks.map((task) => ({ id: task.id, prompt: task.prompt, status: task.status }))
49129
50720
  };
49130
50721
  });
50722
+ bus.registerRpc("workflow.start", async (params) => {
50723
+ const goal = String(params.goal ?? params.prompt ?? "").trim();
50724
+ if (!goal) return { error: "usage: /workflow <goal>", ok: false };
50725
+ const result = await workflowManager.start(goal, agentLoop.sessionId);
50726
+ return { ok: true, run: result.run };
50727
+ });
50728
+ bus.registerRpc("workflow.status", (params) => {
50729
+ const id = String(params.id ?? params.run_id ?? "").trim();
50730
+ const run = workflowManager.status(id || void 0);
50731
+ return run ? { ok: true, run } : { error: id ? `workflow not found: ${id}` : "no workflows yet", ok: false };
50732
+ });
50733
+ bus.registerRpc("workflow.list", (params) => {
50734
+ const limit = Number(params.limit ?? 30);
50735
+ return { ok: true, runs: workflowManager.list(Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 30) };
50736
+ });
50737
+ bus.registerRpc("workflow.show", (params) => {
50738
+ const id = String(params.id ?? params.run_id ?? "").trim();
50739
+ const run = workflowManager.status(id || void 0);
50740
+ return run ? { ok: true, run, summary: run.summary ?? "" } : { error: id ? `workflow not found: ${id}` : "no workflows yet", ok: false };
50741
+ });
50742
+ bus.registerRpc("workflow.cancel", (params) => {
50743
+ const id = String(params.id ?? params.run_id ?? params.worker_id ?? "").trim();
50744
+ if (!id) return { error: "usage: /workflow cancel <runId|workerId>", ok: false };
50745
+ const result = workflowManager.cancel(id);
50746
+ return result.found ? { ok: true, ...result } : { error: `workflow or worker not found: ${id}`, ok: false };
50747
+ });
49131
50748
  bus.registerRpc("schedule.add", (params) => {
49132
50749
  const prompt = String(params.prompt ?? "").trim();
49133
50750
  const raw = String(params.schedule ?? "").trim();
@@ -49398,43 +51015,33 @@ ${helpMessage}` : field.label;
49398
51015
  const firstSpace = raw.search(/\s/);
49399
51016
  const path3 = firstSpace > 0 ? raw.slice(0, firstSpace) : raw;
49400
51017
  const remainder = firstSpace > 0 ? raw.slice(firstSpace).trim() : "";
49401
- if (!existsSync31(path3)) {
51018
+ if (!existsSync32(path3)) {
49402
51019
  throw new Error(`image.attach: file not found: ${path3}`);
49403
51020
  }
49404
51021
  let buffer;
49405
51022
  let fileSize = 0;
49406
51023
  try {
49407
- buffer = readFileSync28(path3);
51024
+ buffer = readFileSync29(path3);
49408
51025
  fileSize = statSync4(path3).size;
49409
51026
  } catch (err) {
49410
51027
  throw new Error(`image.attach: ${err instanceof Error ? err.message : String(err)}`);
49411
51028
  }
49412
51029
  const ext = extname4(path3).toLowerCase().replace(/^\./, "");
49413
51030
  const mimeType = ext === "png" ? "image/png" : ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/png";
49414
- const attachmentId = nextImageAttachmentId();
49415
51031
  pendingImage = {
49416
- attachmentId,
49417
51032
  base64: buffer.toString("base64"),
49418
51033
  mimeType,
49419
- name: basename3(path3)
51034
+ name: basename4(path3)
49420
51035
  };
49421
51036
  const tokenEstimate = Math.max(1, Math.ceil(buffer.byteLength / 750));
49422
51037
  return {
49423
- attachment_id: attachmentId,
49424
51038
  height: 0,
49425
- name: basename3(path3),
51039
+ name: basename4(path3),
49426
51040
  remainder,
49427
51041
  token_estimate: tokenEstimate,
49428
51042
  width: 0
49429
51043
  };
49430
51044
  });
49431
- bus.registerRpc("image.clear", (params) => {
49432
- const attachmentId = String(params.attachment_id ?? "");
49433
- if (!attachmentId || pendingImage?.attachmentId === attachmentId) {
49434
- pendingImage = null;
49435
- }
49436
- return { ok: true };
49437
- });
49438
51045
  bus.registerRpc("process.stop", () => {
49439
51046
  const wasRunning = agentLoop.isRunning;
49440
51047
  agentLoop.abort();
@@ -49460,13 +51067,13 @@ ${helpMessage}` : field.label;
49460
51067
  }
49461
51068
  });
49462
51069
  bus.registerRpc("reload.env", () => {
49463
- const envPath = join42(homedir28(), ".openjaw-agent", ".env");
49464
- if (!existsSync31(envPath)) {
51070
+ const envPath = join43(homedir29(), ".openjaw-agent", ".env");
51071
+ if (!existsSync32(envPath)) {
49465
51072
  return { updated: 0 };
49466
51073
  }
49467
51074
  let updated = 0;
49468
51075
  try {
49469
- const raw = readFileSync28(envPath, "utf-8");
51076
+ const raw = readFileSync29(envPath, "utf-8");
49470
51077
  for (const line of raw.split(/\r?\n/)) {
49471
51078
  const trimmed = line.trim();
49472
51079
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -49506,8 +51113,25 @@ ${helpMessage}` : field.label;
49506
51113
  error: "checkpoints are not enabled in this build",
49507
51114
  success: false
49508
51115
  }));
49509
- bus.registerRpc("spawn_tree.list", () => ({ entries: [] }));
49510
- bus.registerRpc("spawn_tree.load", () => ({ subagents: [] }));
51116
+ bus.registerRpc("spawn_tree.save", (params) => ({
51117
+ path: saveSpawnTreeSnapshot({
51118
+ finished_at: typeof params.finished_at === "number" ? params.finished_at : Date.now() / 1e3,
51119
+ label: typeof params.label === "string" ? params.label : void 0,
51120
+ session_id: typeof params.session_id === "string" ? params.session_id : agentLoop.sessionId ?? "default",
51121
+ started_at: typeof params.started_at === "number" || params.started_at === null ? params.started_at : null,
51122
+ subagents: Array.isArray(params.subagents) ? params.subagents : []
51123
+ })
51124
+ }));
51125
+ bus.registerRpc("spawn_tree.list", (params) => ({
51126
+ entries: listSpawnTreeSnapshots(
51127
+ typeof params.session_id === "string" ? params.session_id : agentLoop.sessionId ?? "default",
51128
+ typeof params.limit === "number" ? params.limit : 30
51129
+ )
51130
+ }));
51131
+ bus.registerRpc("spawn_tree.load", (params) => {
51132
+ const path3 = String(params.path ?? "").trim();
51133
+ return path3 ? loadSpawnTreeSnapshot(path3) ?? { subagents: [] } : { subagents: [] };
51134
+ });
49511
51135
  bus.registerRpc("skills.reload", async () => {
49512
51136
  try {
49513
51137
  clearSkillsCache();
@@ -49591,9 +51215,19 @@ ${helpMessage}` : field.label;
49591
51215
  }
49592
51216
  return { output: name ? `unknown command: /${name}` : "(no command)", type: "exec" };
49593
51217
  });
49594
- bus.registerRpc("delegation.status", () => ({ delegated: [], paused: false }));
51218
+ bus.registerRpc("delegation.status", () => ({
51219
+ active: [],
51220
+ max_concurrent_children: agentConfig.features?.dynamic_workflows?.hard_max_concurrent_workers ?? 128,
51221
+ max_spawn_depth: agentConfig.features?.dynamic_workflows?.hard_max_workers ?? 1024,
51222
+ paused: false
51223
+ }));
49595
51224
  bus.registerRpc("delegation.pause", (params) => ({ paused: Boolean(params.paused) }));
49596
- bus.registerRpc("subagent.interrupt", () => ({ ok: true }));
51225
+ bus.registerRpc("subagent.interrupt", (params) => {
51226
+ const id = String(params.subagent_id ?? "").trim();
51227
+ if (!id) return { found: false };
51228
+ const result = workflowManager.cancel(id);
51229
+ return { found: result.found, subagent_id: id };
51230
+ });
49597
51231
  syncMcpTools(mcpManager, toolRegistry);
49598
51232
  bus.emitEvent({
49599
51233
  payload: sessionInfoSnapshot(agentLoop, toolRegistry),
@@ -49634,7 +51268,6 @@ var init_rpcHandlers = __esm({
49634
51268
  init_uiStore();
49635
51269
  init_catalog();
49636
51270
  init_clipboard();
49637
- init_clipboard_image();
49638
51271
  init_models_static();
49639
51272
  init_providers();
49640
51273
  init_types();
@@ -49642,6 +51275,8 @@ var init_rpcHandlers = __esm({
49642
51275
  init_registry2();
49643
51276
  init_usage();
49644
51277
  init_usageSnapshot();
51278
+ init_manager();
51279
+ init_persistence();
49645
51280
  init_registry3();
49646
51281
  PROVIDERS2 = ["anthropic", "openai", "github-copilot"];
49647
51282
  PROVIDER_LABELS = {
@@ -49827,8 +51462,8 @@ var init_rpcHandlers = __esm({
49827
51462
  ${raw}`;
49828
51463
  return `${header}: ${raw}`;
49829
51464
  }, "formatBridgeText");
49830
- runProcess = /* @__PURE__ */ __name((command, args, timeout = 2e4) => new Promise((resolve5) => {
49831
- const child = spawn7(command, args, { timeout, windowsHide: true });
51465
+ runProcess = /* @__PURE__ */ __name((command, args, timeout = 2e4) => new Promise((resolve6) => {
51466
+ const child = spawn6(command, args, { timeout, windowsHide: true });
49832
51467
  let stdout = "";
49833
51468
  let stderr = "";
49834
51469
  let truncated = false;
@@ -49851,9 +51486,9 @@ ${raw}`;
49851
51486
  if (truncated) {
49852
51487
  stderr += "\n[output truncated to 1MB]";
49853
51488
  }
49854
- resolve5({ code: code ?? -1, stderr, stdout });
51489
+ resolve6({ code: code ?? -1, stderr, stdout });
49855
51490
  });
49856
- child.on("error", (err) => resolve5({ code: -1, stderr: err.message, stdout }));
51491
+ child.on("error", (err) => resolve6({ code: -1, stderr: err.message, stdout }));
49857
51492
  }), "runProcess");
49858
51493
  contentToText = /* @__PURE__ */ __name((content) => {
49859
51494
  if (typeof content === "string") return content;
@@ -50011,14 +51646,14 @@ var init_memoryMonitor = __esm({
50011
51646
  });
50012
51647
 
50013
51648
  // src/lib/openExternalUrl.ts
50014
- import { spawn as spawn8 } from "node:child_process";
51649
+ import { spawn as spawn7 } from "node:child_process";
50015
51650
  import { platform } from "node:os";
50016
51651
  function openExternalUrl(rawUrl, dependencies = {}) {
50017
51652
  const url = parseSafeUrl(rawUrl);
50018
51653
  if (!url) {
50019
51654
  return false;
50020
51655
  }
50021
- const spawnFn = dependencies.spawn ?? spawn8;
51656
+ const spawnFn = dependencies.spawn ?? spawn7;
50022
51657
  const platformId = dependencies.platform?.() ?? platform();
50023
51658
  const command = openCommand(platformId);
50024
51659
  if (!command) {
@@ -50808,7 +52443,7 @@ async function terminalParityHints(env2 = process.env, options) {
50808
52443
  hints.push({
50809
52444
  key: "remote",
50810
52445
  tone: "warn",
50811
- message: "SSH session detected \xB7 text clipboard can bridge via OSC52, but image clipboard and local screenshot paths still depend on the machine running Hermes"
52446
+ message: "SSH session detected \xB7 text clipboard can bridge via OSC52, but image clipboard and local screenshot paths still depend on the machine running OpenJaw"
50812
52447
  });
50813
52448
  }
50814
52449
  return hints;
@@ -50913,13 +52548,13 @@ var init_setup2 = __esm({
50913
52548
  SETUP_REQUIRED_TITLE = "Setup Required";
50914
52549
  buildSetupRequiredSections = /* @__PURE__ */ __name(() => [
50915
52550
  {
50916
- text: "Hermes needs a model provider before the TUI can start a session."
52551
+ text: "OpenJaw needs a model provider before the TUI can start a session. Run /connect to set up a provider, then /model to choose a model."
50917
52552
  },
50918
52553
  {
50919
52554
  rows: [
50920
- ["/model", "configure provider + model in-place"],
50921
- ["/setup", "run full first-time setup wizard in-place"],
50922
- ["Ctrl+C", "exit and run `hermes setup` manually"]
52555
+ ["/connect", "set up provider credentials in-place"],
52556
+ ["/model", "choose or switch model after connecting"],
52557
+ ["Ctrl+C", "exit and restart OpenJaw after setup if needed"]
50923
52558
  ],
50924
52559
  title: "Actions"
50925
52560
  }
@@ -51222,7 +52857,7 @@ var init_reasoning2 = __esm({
51222
52857
  });
51223
52858
 
51224
52859
  // src/app/turnStore.ts
51225
- import { atom as atom5 } from "nanostores";
52860
+ import { atom as atom6 } from "nanostores";
51226
52861
  import { useSyncExternalStore as useSyncExternalStore4 } from "react";
51227
52862
  var buildTurnState, $turnState, getTurnState, subscribeTurn, useTurnSelector, patchTurnState, toggleTodoCollapsed, archiveDoneTodos, archiveTodosAtTurnEnd, resetTurnState;
51228
52863
  var init_turnStore = __esm({
@@ -51246,7 +52881,7 @@ var init_turnStore = __esm({
51246
52881
  tools: [],
51247
52882
  turnTrail: []
51248
52883
  }), "buildTurnState");
51249
- $turnState = atom5(buildTurnState());
52884
+ $turnState = atom6(buildTurnState());
51250
52885
  getTurnState = /* @__PURE__ */ __name(() => $turnState.get(), "getTurnState");
51251
52886
  subscribeTurn = /* @__PURE__ */ __name((cb) => $turnState.listen(() => cb()), "subscribeTurn");
51252
52887
  useTurnSelector = /* @__PURE__ */ __name((selector) => useSyncExternalStore4(
@@ -51846,7 +53481,9 @@ ${stripped}
51846
53481
  ...base,
51847
53482
  apiCalls: p.api_calls ?? base.apiCalls,
51848
53483
  costUsd: p.cost_usd ?? base.costUsd,
53484
+ currentStep: p.current_step ?? base.currentStep,
51849
53485
  depth: p.depth ?? base.depth,
53486
+ details: p.details ?? base.details,
51850
53487
  filesRead: p.files_read ?? base.filesRead,
51851
53488
  filesWritten: p.files_written ?? base.filesWritten,
51852
53489
  goal: p.goal || base.goal,
@@ -51860,6 +53497,9 @@ ${stripped}
51860
53497
  taskCount: p.task_count ?? base.taskCount,
51861
53498
  toolCount: p.tool_count ?? base.toolCount,
51862
53499
  toolsets: p.toolsets ?? base.toolsets,
53500
+ verificationState: p.verification_state ?? base.verificationState,
53501
+ workflowId: p.workflow_id ?? base.workflowId,
53502
+ workerRole: p.worker_role ?? base.workerRole,
51863
53503
  ...patch(base)
51864
53504
  };
51865
53505
  const subagents = existing ? state.subagents.map((item) => item.id === id ? next : item) : [...state.subagents, next].sort((a, b) => a.depth - b.depth || a.index - b.index);
@@ -51946,7 +53586,7 @@ function createGatewayEventHandler(ctx) {
51946
53586
  setTimeout(async () => {
51947
53587
  let sid = getUiState().sid;
51948
53588
  for (let i = 0; !sid && i < 40; i += 1) {
51949
- await new Promise((resolve5) => setTimeout(resolve5, 100));
53589
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
51950
53590
  sid = getUiState().sid;
51951
53591
  }
51952
53592
  if (!sid) {
@@ -52293,6 +53933,26 @@ function createGatewayEventHandler(ctx) {
52293
53933
  { createIfMissing: false }
52294
53934
  );
52295
53935
  return;
53936
+ case "workflow.start":
53937
+ case "workflow.progress":
53938
+ setWorkflowSnapshot(ev.payload);
53939
+ return;
53940
+ case "workflow.complete": {
53941
+ setWorkflowSnapshot(ev.payload);
53942
+ const text = String(ev.payload?.summary ?? "").trim();
53943
+ if (text) {
53944
+ appendMessage({ role: "assistant", text });
53945
+ }
53946
+ return;
53947
+ }
53948
+ case "workflow.error": {
53949
+ setWorkflowSnapshot(ev.payload);
53950
+ const text = String(ev.payload?.summary ?? "").trim();
53951
+ if (text) {
53952
+ sys(text);
53953
+ }
53954
+ return;
53955
+ }
52296
53956
  case "message.delta":
52297
53957
  turnController.recordMessageDelta(ev.payload ?? {});
52298
53958
  return;
@@ -52327,7 +53987,7 @@ function createGatewayEventHandler(ctx) {
52327
53987
  }
52328
53988
  };
52329
53989
  }
52330
- var NO_PROVIDER_RE, statusFromBusy, applySkin, dropBgTask, pushUnique, pushThinking, pushNote, pushTool;
53990
+ var NO_PROVIDER_RE, statusFromBusy, applySkin, dropBgTask, pushUnique2, pushThinking, pushNote, pushTool;
52331
53991
  var init_createGatewayEventHandler = __esm({
52332
53992
  "src/app/createGatewayEventHandler.ts"() {
52333
53993
  "use strict";
@@ -52342,6 +54002,7 @@ var init_createGatewayEventHandler = __esm({
52342
54002
  init_overlayStore();
52343
54003
  init_turnController();
52344
54004
  init_uiStore();
54005
+ init_workflowStore();
52345
54006
  NO_PROVIDER_RE = /\bNo (?:LLM|inference) provider configured\b/i;
52346
54007
  statusFromBusy = /* @__PURE__ */ __name(() => getUiState().busy ? "running\u2026" : "ready", "statusFromBusy");
52347
54008
  applySkin = /* @__PURE__ */ __name((s) => patchUiState({
@@ -52359,10 +54020,10 @@ var init_createGatewayEventHandler = __esm({
52359
54020
  next.delete(taskId);
52360
54021
  return { ...state, bgTasks: next };
52361
54022
  }), "dropBgTask");
52362
- pushUnique = /* @__PURE__ */ __name((max) => (xs, x) => xs.at(-1) === x ? xs : [...xs, x].slice(-max), "pushUnique");
52363
- pushThinking = pushUnique(6);
52364
- pushNote = pushUnique(6);
52365
- pushTool = pushUnique(8);
54023
+ pushUnique2 = /* @__PURE__ */ __name((max) => (xs, x) => xs.at(-1) === x ? xs : [...xs, x].slice(-max), "pushUnique");
54024
+ pushThinking = pushUnique2(6);
54025
+ pushNote = pushUnique2(6);
54026
+ pushTool = pushUnique2(8);
52366
54027
  __name(createGatewayEventHandler, "createGatewayEventHandler");
52367
54028
  }
52368
54029
  });
@@ -52506,12 +54167,12 @@ var init_createSlashHandler = __esm({
52506
54167
  });
52507
54168
 
52508
54169
  // src/app/inputSelectionStore.ts
52509
- import { atom as atom6 } from "nanostores";
54170
+ import { atom as atom7 } from "nanostores";
52510
54171
  var $inputSelection, setInputSelection, getInputSelection;
52511
54172
  var init_inputSelectionStore = __esm({
52512
54173
  "src/app/inputSelectionStore.ts"() {
52513
54174
  "use strict";
52514
- $inputSelection = atom6(null);
54175
+ $inputSelection = atom7(null);
52515
54176
  setInputSelection = /* @__PURE__ */ __name((next) => $inputSelection.set(next), "setInputSelection");
52516
54177
  getInputSelection = /* @__PURE__ */ __name(() => $inputSelection.get(), "getInputSelection");
52517
54178
  }
@@ -52672,21 +54333,21 @@ var init_useCompletion = __esm({
52672
54333
  });
52673
54334
 
52674
54335
  // src/lib/history.ts
52675
- import { appendFileSync as appendFileSync4, existsSync as existsSync32, mkdirSync as mkdirSync18, readFileSync as readFileSync29 } from "node:fs";
52676
- import { homedir as homedir29 } from "node:os";
52677
- import { join as join43 } from "node:path";
54336
+ import { appendFileSync as appendFileSync4, existsSync as existsSync33, mkdirSync as mkdirSync19, readFileSync as readFileSync30 } from "node:fs";
54337
+ import { homedir as homedir30 } from "node:os";
54338
+ import { join as join44 } from "node:path";
52678
54339
  function load() {
52679
54340
  if (cache3) {
52680
54341
  return cache3;
52681
54342
  }
52682
54343
  try {
52683
- if (!existsSync32(file)) {
54344
+ if (!existsSync33(file)) {
52684
54345
  cache3 = [];
52685
54346
  return cache3;
52686
54347
  }
52687
54348
  const entries = [];
52688
54349
  let current = [];
52689
- for (const line of readFileSync29(file, "utf8").split("\n")) {
54350
+ for (const line of readFileSync30(file, "utf8").split("\n")) {
52690
54351
  if (line.startsWith("+")) {
52691
54352
  current.push(line.slice(1));
52692
54353
  } else if (current.length) {
@@ -52717,8 +54378,8 @@ function append(line) {
52717
54378
  items.splice(0, items.length - MAX);
52718
54379
  }
52719
54380
  try {
52720
- if (!existsSync32(dir)) {
52721
- mkdirSync18(dir, { recursive: true });
54381
+ if (!existsSync33(dir)) {
54382
+ mkdirSync19(dir, { recursive: true });
52722
54383
  }
52723
54384
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
52724
54385
  const encoded = trimmed.split("\n").map((l) => `+${l}`).join("\n");
@@ -52734,8 +54395,8 @@ var init_history = __esm({
52734
54395
  "src/lib/history.ts"() {
52735
54396
  "use strict";
52736
54397
  MAX = 1e3;
52737
- dir = process.env.OPENJAW_HOME ?? join43(homedir29(), ".openjaw-agent");
52738
- file = join43(dir, ".openjaw-agent_history");
54398
+ dir = process.env.OPENJAW_HOME ?? join44(homedir30(), ".openjaw-agent");
54399
+ file = join44(dir, ".openjaw-agent_history");
52739
54400
  cache3 = null;
52740
54401
  __name(load, "load");
52741
54402
  __name(append, "append");
@@ -52829,7 +54490,7 @@ var init_useQueue = __esm({
52829
54490
 
52830
54491
  // src/lib/editor.ts
52831
54492
  import { accessSync, constants } from "node:fs";
52832
- import { delimiter, join as join44 } from "node:path";
54493
+ import { delimiter, join as join45 } from "node:path";
52833
54494
  var FALLBACKS, isExecutable, resolveEditor;
52834
54495
  var init_editor = __esm({
52835
54496
  "src/lib/editor.ts"() {
@@ -52852,7 +54513,7 @@ var init_editor = __esm({
52852
54513
  return ["notepad.exe"];
52853
54514
  }
52854
54515
  const dirs = (env2.PATH ?? "").split(delimiter).filter(Boolean);
52855
- const found = FALLBACKS.flatMap((name) => dirs.map((d) => join44(d, name))).find(isExecutable);
54516
+ const found = FALLBACKS.flatMap((name) => dirs.map((d) => join45(d, name))).find(isExecutable);
52856
54517
  return [found ?? "vi"];
52857
54518
  }, "resolveEditor");
52858
54519
  }
@@ -52860,9 +54521,9 @@ var init_editor = __esm({
52860
54521
 
52861
54522
  // src/app/useComposerState.ts
52862
54523
  import { spawnSync } from "node:child_process";
52863
- import { mkdtempSync, readFileSync as readFileSync30, rmSync as rmSync2, writeFileSync as writeFileSync20 } from "node:fs";
54524
+ import { mkdtempSync, readFileSync as readFileSync31, rmSync as rmSync2, writeFileSync as writeFileSync21 } from "node:fs";
52864
54525
  import { tmpdir as tmpdir12 } from "node:os";
52865
- import { join as join45 } from "node:path";
54526
+ import { join as join46 } from "node:path";
52866
54527
  import { useStore } from "@nanostores/react";
52867
54528
  import { useCallback as useCallback7, useMemo as useMemo6, useState as useState12 } from "react";
52868
54529
  function insertAtCursor(value, cursor, text) {
@@ -52896,7 +54557,6 @@ function useComposerState({
52896
54557
  }) {
52897
54558
  const [input, setInput] = useState12("");
52898
54559
  const [inputBuf, setInputBuf] = useState12([]);
52899
- const [attachedImage, setAttachedImage] = useState12(null);
52900
54560
  const [pasteSnips, setPasteSnips] = useState12([]);
52901
54561
  const isBlocked = useStore($isBlocked);
52902
54562
  const { querier } = use_stdin_default();
@@ -52914,25 +54574,14 @@ function useComposerState({
52914
54574
  } = useQueue();
52915
54575
  const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory();
52916
54576
  const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, isBlocked, gw);
52917
- const clearAttachedImage = useCallback7(() => {
52918
- const attachmentId = attachedImage?.attachment_id;
52919
- setAttachedImage(null);
52920
- if (attachmentId) {
52921
- void gw.request("image.clear", { attachment_id: attachmentId }).catch(() => {
52922
- });
52923
- }
52924
- }, [attachedImage?.attachment_id, gw]);
52925
- const clearIn = useCallback7((options = {}) => {
54577
+ const clearIn = useCallback7(() => {
52926
54578
  setInput("");
52927
54579
  setInputBuf([]);
52928
54580
  setPasteSnips([]);
52929
- if (!options.keepAttachedImage) {
52930
- clearAttachedImage();
52931
- }
52932
54581
  setQueueEdit(null);
52933
54582
  setHistoryIdx(null);
52934
54583
  historyDraftRef.current = "";
52935
- }, [clearAttachedImage, historyDraftRef, setQueueEdit, setHistoryIdx]);
54584
+ }, [historyDraftRef, setQueueEdit, setHistoryIdx]);
52936
54585
  const handleResolvedPaste = useCallback7(
52937
54586
  async ({
52938
54587
  bracketed,
@@ -52955,13 +54604,6 @@ function useComposerState({
52955
54604
  session_id: sid
52956
54605
  });
52957
54606
  if (attached?.name) {
52958
- setAttachedImage({
52959
- attachment_id: attached.attachment_id,
52960
- height: attached.height,
52961
- name: attached.name,
52962
- token_estimate: attached.token_estimate,
52963
- width: attached.width
52964
- });
52965
54607
  onImageAttached?.(attached);
52966
54608
  const remainder = attached.remainder?.trim() ?? "";
52967
54609
  if (!remainder) {
@@ -53014,7 +54656,7 @@ function useComposerState({
53014
54656
  }) => {
53015
54657
  if (hotkey) {
53016
54658
  const preferOsc52 = isRemoteShellSession(process.env);
53017
- const readPreferredText = /* @__PURE__ */ __name(() => preferOsc52 ? readOsc52Clipboard(querier).then(async (osc52Text) => {
54659
+ const readPreferredText = preferOsc52 ? readOsc52Clipboard(querier).then(async (osc52Text) => {
53018
54660
  if (isUsableClipboardText(osc52Text)) {
53019
54661
  return osc52Text;
53020
54662
  }
@@ -53024,12 +54666,8 @@ function useComposerState({
53024
54666
  return clipText;
53025
54667
  }
53026
54668
  return readOsc52Clipboard(querier);
53027
- }), "readPreferredText");
53028
- return Promise.resolve(onClipboardPaste(true)).then(async (imageAttached) => {
53029
- if (imageAttached) {
53030
- return null;
53031
- }
53032
- const preferredText = await readPreferredText();
54669
+ });
54670
+ return readPreferredText.then(async (preferredText) => {
53033
54671
  if (isUsableClipboardText(preferredText)) {
53034
54672
  return handleResolvedPaste({ bracketed: false, cursor, text: preferredText, value });
53035
54673
  }
@@ -53042,10 +54680,10 @@ function useComposerState({
53042
54680
  [handleResolvedPaste, onClipboardPaste, querier]
53043
54681
  );
53044
54682
  const openEditor = useCallback7(async () => {
53045
- const dir2 = mkdtempSync(join45(tmpdir12(), "hermes-"));
53046
- const file2 = join45(dir2, "prompt.md");
54683
+ const dir2 = mkdtempSync(join46(tmpdir12(), "hermes-"));
54684
+ const file2 = join46(dir2, "prompt.md");
53047
54685
  const [cmd, ...args] = resolveEditor();
53048
- writeFileSync20(file2, [...inputBuf, input].join("\n"));
54686
+ writeFileSync21(file2, [...inputBuf, input].join("\n"));
53049
54687
  let exitCode = null;
53050
54688
  await withInkSuspended(async () => {
53051
54689
  exitCode = spawnSync(cmd, [...args, file2], { stdio: "inherit" }).status;
@@ -53054,7 +54692,7 @@ function useComposerState({
53054
54692
  if (exitCode !== 0) {
53055
54693
  return;
53056
54694
  }
53057
- const text = readFileSync30(file2, "utf8").trimEnd();
54695
+ const text = readFileSync31(file2, "utf8").trimEnd();
53058
54696
  if (!text) {
53059
54697
  return;
53060
54698
  }
@@ -53067,7 +54705,6 @@ function useComposerState({
53067
54705
  }, [input, inputBuf, submitRef]);
53068
54706
  const actions = useMemo6(
53069
54707
  () => ({
53070
- clearAttachedImage,
53071
54708
  clearIn,
53072
54709
  dequeue,
53073
54710
  enqueue,
@@ -53078,7 +54715,6 @@ function useComposerState({
53078
54715
  replaceQueue: replaceQ,
53079
54716
  setCompIdx,
53080
54717
  setHistoryIdx,
53081
- setAttachedImage,
53082
54718
  setInput,
53083
54719
  setInputBuf,
53084
54720
  setPasteSnips,
@@ -53086,7 +54722,6 @@ function useComposerState({
53086
54722
  syncQueue
53087
54723
  }),
53088
54724
  [
53089
- clearAttachedImage,
53090
54725
  clearIn,
53091
54726
  dequeue,
53092
54727
  enqueue,
@@ -53113,7 +54748,6 @@ function useComposerState({
53113
54748
  );
53114
54749
  const state = useMemo6(
53115
54750
  () => ({
53116
- attachedImage,
53117
54751
  compIdx,
53118
54752
  compReplace,
53119
54753
  completions,
@@ -53124,7 +54758,7 @@ function useComposerState({
53124
54758
  queueEditIdx,
53125
54759
  queuedDisplay
53126
54760
  }),
53127
- [attachedImage, compIdx, compReplace, completions, historyIdx, input, inputBuf, pasteSnips, queueEditIdx, queuedDisplay]
54761
+ [compIdx, compReplace, completions, historyIdx, input, inputBuf, pasteSnips, queueEditIdx, queuedDisplay]
53128
54762
  );
53129
54763
  return {
53130
54764
  actions,
@@ -53476,11 +55110,11 @@ function applyVoiceRecordResponse(response, starting, voice, sys) {
53476
55110
  }
53477
55111
  }
53478
55112
  function useInputHandlers(ctx) {
53479
- const { actions, composer, gateway, terminal, voice, wheelStep } = ctx;
55113
+ const { actions, composer, gateway, terminal: terminal2, voice, wheelStep } = ctx;
53480
55114
  const { actions: cActions, refs: cRefs, state: cState } = composer;
53481
55115
  const overlay = useStore2($overlayState);
53482
55116
  const isBlocked = useStore2($isBlocked);
53483
- const pagerPageSize = Math.max(5, (terminal.stdout?.rows ?? 24) - 6);
55117
+ const pagerPageSize = Math.max(5, (terminal2.stdout?.rows ?? 24) - 6);
53484
55118
  const scrollIdleTimer = useRef10(null);
53485
55119
  const wheelAccelRef = useRef10(initWheelAccelForHost());
53486
55120
  const precisionWheelRef = useRef10(initPrecisionWheel());
@@ -53495,13 +55129,13 @@ function useInputHandlers(ctx) {
53495
55129
  turnController.relaxStreaming();
53496
55130
  }, TYPING_IDLE_MS);
53497
55131
  }
53498
- terminal.scrollWithSelection(delta);
55132
+ terminal2.scrollWithSelection(delta);
53499
55133
  }, "scrollTranscript");
53500
55134
  const copySelection = /* @__PURE__ */ __name(() => {
53501
- terminal.selection.copySelection();
55135
+ terminal2.selection.copySelection();
53502
55136
  }, "copySelection");
53503
55137
  const clearSelection2 = /* @__PURE__ */ __name(() => {
53504
- terminal.selection.clearSelection();
55138
+ terminal2.selection.clearSelection();
53505
55139
  }, "clearSelection");
53506
55140
  const cancelOverlayFromCtrlC = /* @__PURE__ */ __name(() => {
53507
55141
  if (overlay.clarify) {
@@ -53700,7 +55334,7 @@ function useInputHandlers(ctx) {
53700
55334
  return scrollTranscript(1);
53701
55335
  }
53702
55336
  if (key.pageUp || key.pageDown) {
53703
- const viewport = terminal.scrollRef.current?.getViewportHeight() ?? Math.max(6, (terminal.stdout?.rows ?? 24) - 8);
55337
+ const viewport = terminal2.scrollRef.current?.getViewportHeight() ?? Math.max(6, (terminal2.stdout?.rows ?? 24) - 8);
53704
55338
  const step = Math.max(4, Math.floor(viewport / 2));
53705
55339
  return scrollTranscript(key.pageUp ? -step : step);
53706
55340
  }
@@ -53710,12 +55344,9 @@ function useInputHandlers(ctx) {
53710
55344
  if (key.escape && cState.queueEditIdx !== null) {
53711
55345
  return cActions.clearIn();
53712
55346
  }
53713
- if (key.escape && terminal.hasSelection) {
55347
+ if (key.escape && terminal2.hasSelection) {
53714
55348
  return clearSelection2();
53715
55349
  }
53716
- if (key.escape && cState.attachedImage) {
53717
- return cActions.clearAttachedImage();
53718
- }
53719
55350
  if (key.escape && live.focusedPane === "transcript") {
53720
55351
  patchUiState({ focusedPane: "composer" });
53721
55352
  return;
@@ -53739,7 +55370,7 @@ function useInputHandlers(ctx) {
53739
55370
  }
53740
55371
  }
53741
55372
  if (isCopyShortcut(key, ch)) {
53742
- if (terminal.hasSelection) {
55373
+ if (terminal2.hasSelection) {
53743
55374
  return copySelection();
53744
55375
  }
53745
55376
  const inputSel = getInputSelection();
@@ -53764,7 +55395,7 @@ function useInputHandlers(ctx) {
53764
55395
  sys: actions.sys
53765
55396
  });
53766
55397
  }
53767
- if (cState.input || cState.inputBuf.length || cState.attachedImage) {
55398
+ if (cState.input || cState.inputBuf.length) {
53768
55399
  return cActions.clearIn();
53769
55400
  }
53770
55401
  return actions.die();
@@ -53774,7 +55405,7 @@ function useInputHandlers(ctx) {
53774
55405
  }
53775
55406
  if (isAction(key, ch, "l")) {
53776
55407
  clearSelection2();
53777
- forceRedraw(terminal.stdout ?? process.stdout);
55408
+ forceRedraw(terminal2.stdout ?? process.stdout);
53778
55409
  return;
53779
55410
  }
53780
55411
  if (isVoiceToggleKey(key, ch, voice.recordKey)) {
@@ -53924,7 +55555,7 @@ var init_useLongRunToolCharms = __esm({
53924
55555
  });
53925
55556
 
53926
55557
  // src/app/useSessionLifecycle.ts
53927
- import { writeFileSync as writeFileSync21 } from "node:fs";
55558
+ import { writeFileSync as writeFileSync22 } from "node:fs";
53928
55559
  import { useCallback as useCallback8 } from "react";
53929
55560
  function useSessionLifecycle(opts) {
53930
55561
  const {
@@ -53954,7 +55585,6 @@ function useSessionLifecycle(opts) {
53954
55585
  setHistoryItems([]);
53955
55586
  setLastUserMsg("");
53956
55587
  setStickyPrompt("");
53957
- composerActions.clearAttachedImage();
53958
55588
  composerActions.setPasteSnips([]);
53959
55589
  evictInkCaches("half");
53960
55590
  }, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt, setVoiceProcessing, setVoiceRecording]);
@@ -53967,7 +55597,6 @@ function useSessionLifecycle(opts) {
53967
55597
  setHistoryItems(info ? [introMsg(info)] : []);
53968
55598
  setStickyPrompt("");
53969
55599
  setLastUserMsg("");
53970
- composerActions.clearAttachedImage();
53971
55600
  composerActions.setPasteSnips([]);
53972
55601
  patchTurnState({ activity: [] });
53973
55602
  patchUiState({ info, usage: usageFrom(info) });
@@ -54109,7 +55738,7 @@ var init_useSessionLifecycle = __esm({
54109
55738
  return;
54110
55739
  }
54111
55740
  try {
54112
- writeFileSync21(file2, JSON.stringify({ session_id: sessionId }), { mode: 384 });
55741
+ writeFileSync22(file2, JSON.stringify({ session_id: sessionId }), { mode: 384 });
54113
55742
  } catch {
54114
55743
  }
54115
55744
  }, "writeActiveSessionFile");
@@ -54198,7 +55827,6 @@ function useSubmission(opts) {
54198
55827
  const expand = expandSnips(composerState.pasteSnips);
54199
55828
  const startSubmit = /* @__PURE__ */ __name((displayText, submitText, showUserMessage2 = true) => {
54200
55829
  const sid2 = getUiState().sid;
54201
- const imageAttachment = composerState.attachedImage;
54202
55830
  if (!sid2) {
54203
55831
  return sys("session not ready yet");
54204
55832
  }
@@ -54211,20 +55839,7 @@ function useSubmission(opts) {
54211
55839
  patchUiState({ busy: true, status: "running\u2026" });
54212
55840
  turnController.bufRef = "";
54213
55841
  turnController.interrupted = false;
54214
- const submitParams = { session_id: sid2, text: submitText };
54215
- if (imageAttachment?.attachment_id) {
54216
- submitParams.image_attachment_id = imageAttachment.attachment_id;
54217
- }
54218
- if (imageAttachment) {
54219
- composerActions.setAttachedImage(null);
54220
- }
54221
- gw.request("prompt.submit", submitParams).catch((e) => {
54222
- if (imageAttachment) {
54223
- composerActions.setAttachedImage(imageAttachment);
54224
- sys(`error: ${e.message}`);
54225
- patchUiState({ busy: false, status: "ready" });
54226
- return;
54227
- }
55842
+ gw.request("prompt.submit", { session_id: sid2, text: submitText }).catch((e) => {
54228
55843
  if (isSessionBusyError(e)) {
54229
55844
  composerActions.enqueue(submitText);
54230
55845
  patchUiState({ busy: true, status: "queued for next turn" });
@@ -54250,7 +55865,7 @@ function useSubmission(opts) {
54250
55865
  startSubmit(r.text || text, expand(r.text || text), showUserMessage);
54251
55866
  }).catch(() => startSubmit(text, expand(text), showUserMessage));
54252
55867
  },
54253
- [appendMessage, composerActions, composerState.attachedImage, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys]
55868
+ [appendMessage, composerActions, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys]
54254
55869
  );
54255
55870
  const shellExec = useCallback9(
54256
55871
  (cmd) => {
@@ -54313,16 +55928,6 @@ function useSubmission(opts) {
54313
55928
  }
54314
55929
  sys(note);
54315
55930
  }, "fallback");
54316
- if (composerState.attachedImage) {
54317
- if (live.sid) {
54318
- turnController.interruptTurn({ appendMessage, gw, sid: live.sid, sys });
54319
- }
54320
- if (hasInterpolation(full)) {
54321
- patchUiState({ busy: true });
54322
- return interpolate(full, send);
54323
- }
54324
- return send(full);
54325
- }
54326
55931
  if (mode === "queue") {
54327
55932
  return composerActions.enqueue(full);
54328
55933
  }
@@ -54344,7 +55949,7 @@ function useSubmission(opts) {
54344
55949
  }
54345
55950
  send(full);
54346
55951
  },
54347
- [appendMessage, composerActions, composerRefs, composerState.attachedImage, gw, interpolate, send, sys]
55952
+ [appendMessage, composerActions, composerRefs, gw, interpolate, send, sys]
54348
55953
  );
54349
55954
  const dispatchSubmission = useCallback9(
54350
55955
  (full) => {
@@ -54369,8 +55974,8 @@ function useSubmission(opts) {
54369
55974
  return;
54370
55975
  }
54371
55976
  const editIdx = composerRefs.queueEditRef.current;
55977
+ composerActions.clearIn();
54372
55978
  if (editIdx !== null) {
54373
- composerActions.clearIn();
54374
55979
  composerActions.replaceQueue(editIdx, full);
54375
55980
  const picked = composerRefs.queueRef.current.splice(editIdx, 1)[0];
54376
55981
  composerActions.syncQueue();
@@ -54387,7 +55992,6 @@ function useSubmission(opts) {
54387
55992
  }
54388
55993
  return sendQueued(picked);
54389
55994
  }
54390
- composerActions.clearIn({ keepAttachedImage: !!composerState.attachedImage });
54391
55995
  composerActions.pushHistory(full);
54392
55996
  if (getUiState().busy) {
54393
55997
  return handleBusyInput(full);
@@ -54398,7 +56002,7 @@ function useSubmission(opts) {
54398
56002
  }
54399
56003
  send(full);
54400
56004
  },
54401
- [appendMessage, composerActions, composerRefs, composerState.attachedImage, handleBusyInput, interpolate, send, sendQueued, shellExec, slashRef]
56005
+ [appendMessage, composerActions, composerRefs, handleBusyInput, interpolate, send, sendQueued, shellExec, slashRef]
54402
56006
  );
54403
56007
  const submit = useCallback9(
54404
56008
  (value) => {
@@ -54516,7 +56120,8 @@ function useMainApp(gw) {
54516
56120
  const scrollRef = useRef13(null);
54517
56121
  const onEventRef = useRef13(() => {
54518
56122
  });
54519
- const clipboardPasteRef = useRef13(() => false);
56123
+ const clipboardPasteRef = useRef13(() => {
56124
+ });
54520
56125
  const submitRef = useRef13(() => {
54521
56126
  });
54522
56127
  const terminalHintsShownRef = useRef13(/* @__PURE__ */ new Set());
@@ -54569,7 +56174,9 @@ function useMainApp(gw) {
54569
56174
  const composer = useComposerState({
54570
56175
  gw,
54571
56176
  onClipboardPaste: /* @__PURE__ */ __name((quiet) => clipboardPasteRef.current(quiet), "onClipboardPaste"),
54572
- onImageAttached: /* @__PURE__ */ __name(() => patchUiState({ status: "image attached" }), "onImageAttached"),
56177
+ onImageAttached: /* @__PURE__ */ __name((info) => {
56178
+ sys(attachedImageNotice(info));
56179
+ }, "onImageAttached"),
54573
56180
  submitRef
54574
56181
  });
54575
56182
  const { actions: composerActions, refs: composerRefs, state: composerState } = composer;
@@ -54777,25 +56384,17 @@ function useMainApp(gw) {
54777
56384
  const paste2 = useCallback10(
54778
56385
  (quiet = false) => rpc("clipboard.paste", { session_id: getUiState().sid }).then((r) => {
54779
56386
  if (!r) {
54780
- return false;
56387
+ return;
54781
56388
  }
54782
56389
  if (r.attached) {
54783
- composerActions.setAttachedImage({
54784
- attachment_id: r.attachment_id,
54785
- height: r.height,
54786
- name: r.name ?? `Image #${r.count ?? 1}`,
54787
- token_estimate: r.token_estimate,
54788
- width: r.width
54789
- });
54790
- patchUiState({ status: "image attached" });
54791
- return true;
56390
+ const meta = imageTokenMeta(r);
56391
+ return sys(`\u{1F4CE} Image #${r.count} attached from clipboard${meta ? ` \xB7 ${meta}` : ""}`);
54792
56392
  }
54793
56393
  if (!quiet) {
54794
56394
  sys(r.message || "No image found in clipboard");
54795
56395
  }
54796
- return false;
54797
56396
  }),
54798
- [composerActions, rpc, sys]
56397
+ [rpc, sys]
54799
56398
  );
54800
56399
  clipboardPasteRef.current = paste2;
54801
56400
  const { dispatchSubmission, send, sendQueued, submit } = useSubmission({
@@ -54996,8 +56595,13 @@ function useMainApp(gw) {
54996
56595
  );
54997
56596
  const onModelSelect = useCallback10((value) => {
54998
56597
  patchOverlayState({ modelPicker: false });
56598
+ if (value === "__openjaw_connect_complete__") {
56599
+ sys("provider connected \u2014 starting OpenJaw session\u2026");
56600
+ session.newSession();
56601
+ return;
56602
+ }
54999
56603
  slashRef.current(`/model ${value}`);
55000
- }, []);
56604
+ }, [session, sys]);
55001
56605
  const hasReasoning = useTurnSelector((state) => Boolean(state.reasoning.trim()));
55002
56606
  const anyPanelVisible = SECTION_NAMES.some(
55003
56607
  (s) => sectionMode(s, ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== "hidden"
@@ -55035,7 +56639,6 @@ function useMainApp(gw) {
55035
56639
  cols,
55036
56640
  compIdx: composerState.compIdx,
55037
56641
  completions: composerState.completions,
55038
- attachedImage: composerState.attachedImage,
55039
56642
  empty,
55040
56643
  handleTextPaste: composerActions.handleTextPaste,
55041
56644
  input: composerState.input,
@@ -55092,6 +56695,7 @@ var init_useMainApp = __esm({
55092
56695
  init_env();
55093
56696
  init_limits();
55094
56697
  init_details();
56698
+ init_messages();
55095
56699
  init_paths();
55096
56700
  init_useGitBranch();
55097
56701
  init_useVirtualHistory();
@@ -55169,9 +56773,9 @@ __export(perfPane_exports, {
55169
56773
  PerfPane: () => PerfPane,
55170
56774
  logFrameEvent: () => logFrameEvent
55171
56775
  });
55172
- import { appendFileSync as appendFileSync5, mkdirSync as mkdirSync19 } from "node:fs";
55173
- import { homedir as homedir30 } from "node:os";
55174
- import { dirname as dirname7, join as join46 } from "node:path";
56776
+ import { appendFileSync as appendFileSync5, mkdirSync as mkdirSync20 } from "node:fs";
56777
+ import { homedir as homedir31 } from "node:os";
56778
+ import { dirname as dirname7, join as join47 } from "node:path";
55175
56779
  import { Profiler } from "react";
55176
56780
  import { jsx as jsx17 } from "react/jsx-runtime";
55177
56781
  function PerfPane({ children, id }) {
@@ -55187,13 +56791,13 @@ var init_perfPane = __esm({
55187
56791
  init_entry_exports();
55188
56792
  ENABLED = /^(?:1|true|yes|on)$/i.test((process.env.OPENJAW_DEV_PERF ?? "").trim());
55189
56793
  THRESHOLD_MS = Number(process.env.OPENJAW_DEV_PERF_MS ?? "2") || 0;
55190
- LOG_PATH2 = process.env.OPENJAW_DEV_PERF_LOG?.trim() || join46(homedir30(), ".openjaw-agent", "perf.log");
56794
+ LOG_PATH2 = process.env.OPENJAW_DEV_PERF_LOG?.trim() || join47(homedir31(), ".openjaw-agent", "perf.log");
55191
56795
  logReady = false;
55192
56796
  writeRow = /* @__PURE__ */ __name((row) => {
55193
56797
  if (!logReady) {
55194
56798
  logReady = true;
55195
56799
  try {
55196
- mkdirSync19(dirname7(LOG_PATH2), { recursive: true });
56800
+ mkdirSync20(dirname7(LOG_PATH2), { recursive: true });
55197
56801
  } catch {
55198
56802
  }
55199
56803
  }
@@ -55446,6 +57050,10 @@ function Detail({ id, node, t }) {
55446
57050
  ] }),
55447
57051
  /* @__PURE__ */ jsxs9(Box_default, { flexDirection: "column", marginTop: 1, children: [
55448
57052
  /* @__PURE__ */ jsx18(Field, { name: "depth", t, value: `${item.depth} \xB7 ${item.status}` }),
57053
+ item.workflowId ? /* @__PURE__ */ jsx18(Field, { name: "workflow", t, value: item.workflowId }) : null,
57054
+ item.workerRole ? /* @__PURE__ */ jsx18(Field, { name: "role", t, value: item.workerRole }) : null,
57055
+ item.verificationState ? /* @__PURE__ */ jsx18(Field, { name: "verification", t, value: item.verificationState }) : null,
57056
+ item.currentStep ? /* @__PURE__ */ jsx18(Field, { name: "current", t, value: item.currentStep }) : null,
55449
57057
  item.model ? /* @__PURE__ */ jsx18(Field, { name: "model", t, value: item.model }) : null,
55450
57058
  item.toolsets?.length ? /* @__PURE__ */ jsx18(Field, { name: "toolsets", t, value: item.toolsets.join(", ") }) : null,
55451
57059
  /* @__PURE__ */ jsx18(Field, { name: "tools", t, value: `${item.toolCount ?? 0} (subtree ${agg.totalTools})` }),
@@ -55520,7 +57128,8 @@ function Detail({ id, node, t }) {
55520
57128
  " ",
55521
57129
  line
55522
57130
  ] }, i)) }) : null,
55523
- item.summary ? /* @__PURE__ */ jsx18(OverlaySection, { defaultOpen: true, t, title: "Summary", children: /* @__PURE__ */ jsx18(Text9, { color: t.color.text, wrap: "wrap", children: item.summary }) }) : null
57131
+ item.summary ? /* @__PURE__ */ jsx18(OverlaySection, { defaultOpen: true, t, title: "Summary", children: /* @__PURE__ */ jsx18(Text9, { color: t.color.text, wrap: "wrap", children: item.summary }) }) : null,
57132
+ item.details && item.details !== item.summary ? /* @__PURE__ */ jsx18(OverlaySection, { defaultOpen: true, t, title: "Details", children: /* @__PURE__ */ jsx18(Text9, { color: t.color.text, wrap: "wrap", children: item.details }) }) : null
55524
57133
  ] });
55525
57134
  }
55526
57135
  function ListRow({
@@ -55620,12 +57229,23 @@ function DiffView({
55620
57229
  ] })
55621
57230
  ] });
55622
57231
  }
55623
- function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }) {
55624
- const liveSubagents = useTurnSelector((state) => state.subagents);
57232
+ function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t, workflowId = null }) {
57233
+ const allLiveSubagents = useTurnSelector((state) => state.subagents);
55625
57234
  const delegation = useStore4($delegationState);
55626
57235
  const history = useStore4($spawnHistory);
55627
57236
  const diffPair = useStore4($spawnDiff);
57237
+ const workflowSnapshots = useStore4($workflowSnapshots);
55628
57238
  const { stdout } = useStdout();
57239
+ const liveSubagents = workflowId ? allLiveSubagents.filter((item) => item.workflowId === workflowId) : allLiveSubagents;
57240
+ const workflowSnapshot = workflowId ? workflowSnapshots[workflowId] ?? null : null;
57241
+ const workflowReplaySnapshot = workflowSnapshot && liveSubagents.length === 0 ? {
57242
+ finishedAt: workflowSnapshot.finishedAt ?? Date.now(),
57243
+ id: workflowSnapshot.id,
57244
+ label: workflowSnapshot.goal,
57245
+ sessionId: workflowSnapshot.id,
57246
+ startedAt: workflowSnapshot.startedAt,
57247
+ subagents: workflowSnapshot.workers
57248
+ } : null;
55629
57249
  const [historyIndex, setHistoryIndex] = useState14(
55630
57250
  () => Math.max(0, Math.min(history.length, Math.floor(initialHistoryIndex)))
55631
57251
  );
@@ -55637,9 +57257,9 @@ function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }) {
55637
57257
  const [mode, setMode] = useState14("list");
55638
57258
  const detailScrollRef = useRef14(null);
55639
57259
  const prevLiveCountRef = useRef14(liveSubagents.length);
55640
- const activeSnapshot = historyIndex > 0 ? history[historyIndex - 1] : null;
55641
- const justFinishedSnapshot = historyIndex === 0 && liveSubagents.length === 0 ? history[0] ?? null : null;
55642
- const effectiveSnapshot = activeSnapshot ?? justFinishedSnapshot;
57260
+ const activeSnapshot = !workflowId && historyIndex > 0 ? history[historyIndex - 1] : null;
57261
+ const justFinishedSnapshot = !workflowId && historyIndex === 0 && liveSubagents.length === 0 ? history[0] ?? null : null;
57262
+ const effectiveSnapshot = workflowReplaySnapshot ?? activeSnapshot ?? justFinishedSnapshot;
55643
57263
  const replayMode = effectiveSnapshot != null;
55644
57264
  const subagents = replayMode ? effectiveSnapshot.subagents : liveSubagents;
55645
57265
  const tree = useMemo8(() => buildSubagentTree(subagents), [subagents]);
@@ -55808,7 +57428,7 @@ function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }) {
55808
57428
  const capsLabel = delegation.maxSpawnDepth ? `caps d${delegation.maxSpawnDepth}/${delegation.maxConcurrentChildren ?? "?"}` : "";
55809
57429
  const title = replayMode && effectiveSnapshot ? `${historyIndex > 0 ? `Replay ${historyIndex}/${history.length}` : "Last turn"} \xB7 finished ${new Date(
55810
57430
  effectiveSnapshot.finishedAt
55811
- ).toLocaleTimeString()}` : `Spawn tree${delegation.paused ? " \xB7 \u23F8 paused" : ""}`;
57431
+ ).toLocaleTimeString()}` : workflowId ? `Workflow ${workflowId}${delegation.paused ? " \xB7 \u23F8 paused" : ""}` : `Spawn tree${delegation.paused ? " \xB7 \u23F8 paused" : ""}`;
55812
57432
  const metaLine = [formatSummary(totals), spark, capsLabel, mix2 ? `\xB7 ${mix2}` : ""].filter(Boolean).join(" ");
55813
57433
  const controlsHint = replayMode ? " \xB7 controls locked" : ` \xB7 x kill \xB7 X subtree \xB7 p ${delegation.paused ? "resume" : "pause"}`;
55814
57434
  if (diffPair) {
@@ -55822,7 +57442,7 @@ function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }) {
55822
57442
  metaLine
55823
57443
  ] }) : null
55824
57444
  ] }) }),
55825
- rows.length === 0 ? /* @__PURE__ */ jsx18(Box_default, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx18(Text9, { color: t.color.muted, children: "No subagents this turn. Trigger delegate_task to populate the tree." }) }) : mode === "list" ? /* @__PURE__ */ jsxs9(Box_default, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, children: [
57445
+ rows.length === 0 ? /* @__PURE__ */ jsx18(Box_default, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx18(Text9, { color: t.color.muted, children: workflowId ? "No workers for this workflow yet." : "No subagents this turn. Trigger delegate_task to populate the tree." }) }) : mode === "list" ? /* @__PURE__ */ jsxs9(Box_default, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, children: [
55826
57446
  /* @__PURE__ */ jsx18(GanttStrip, { cols, cursor, flatNodes: rows, maxRows: 6, now: now2, t }),
55827
57447
  /* @__PURE__ */ jsx18(Box_default, { flexDirection: "column", flexGrow: 0, flexShrink: 0, overflow: "hidden", children: rows.slice(listWindowStart, listWindowStart + rowsH).map((node, i) => /* @__PURE__ */ jsx18(
55828
57448
  ListRow,
@@ -55868,6 +57488,7 @@ var init_agentsOverlay = __esm({
55868
57488
  init_overlayStore();
55869
57489
  init_spawnHistoryStore();
55870
57490
  init_turnStore();
57491
+ init_workflowStore();
55871
57492
  init_rpc();
55872
57493
  init_subagentTree();
55873
57494
  init_text();
@@ -57796,7 +59417,7 @@ function ModelPicker2({ gw, mode = "switch", onCancel, onSelect, sessionId, t })
57796
59417
  refreshProviders();
57797
59418
  }
57798
59419
  if (mode === "connect") {
57799
- onCancel();
59420
+ onSelect("__openjaw_connect_complete__");
57800
59421
  return;
57801
59422
  }
57802
59423
  setStage("model");
@@ -57892,7 +59513,7 @@ function ModelPicker2({ gw, mode = "switch", onCancel, onSelect, sessionId, t })
57892
59513
  setKeyInput("");
57893
59514
  setKeySaving(false);
57894
59515
  if (mode === "connect") {
57895
- onCancel();
59516
+ onSelect("__openjaw_connect_complete__");
57896
59517
  return;
57897
59518
  }
57898
59519
  setStage("model");
@@ -57931,7 +59552,7 @@ function ModelPicker2({ gw, mode = "switch", onCancel, onSelect, sessionId, t })
57931
59552
  if (r?.disconnected) {
57932
59553
  setProviders(
57933
59554
  (prev) => prev.map(
57934
- (p) => p.slug === provider.slug ? { ...p, authenticated: false, models: [], model_options: [], total_models: 0, warning: p.key_env ? `paste ${p.key_env} to activate` : "run `hermes model` to configure" } : p
59555
+ (p) => p.slug === provider.slug ? { ...p, authenticated: false, models: [], model_options: [], total_models: 0, warning: p.key_env ? `paste ${p.key_env} to activate` : "run /connect to configure" } : p
57935
59556
  )
57936
59557
  );
57937
59558
  }
@@ -57979,7 +59600,7 @@ function ModelPicker2({ gw, mode = "switch", onCancel, onSelect, sessionId, t })
57979
59600
  return;
57980
59601
  }
57981
59602
  if (mode === "connect") {
57982
- onCancel();
59603
+ onSelect("__openjaw_connect_complete__");
57983
59604
  return;
57984
59605
  }
57985
59606
  setStage("model");
@@ -58067,7 +59688,7 @@ function ModelPicker2({ gw, mode = "switch", onCancel, onSelect, sessionId, t })
58067
59688
  "Configure ",
58068
59689
  provider.name
58069
59690
  ] }),
58070
- /* @__PURE__ */ jsx25(Text9, { color: t.color.muted, wrap: "truncate-end", children: "Paste your API key below (saved to ~/.hermes/.env)" }),
59691
+ /* @__PURE__ */ jsx25(Text9, { color: t.color.muted, wrap: "truncate-end", children: "Paste your API key below (saved to OpenJaw credentials)" }),
58071
59692
  /* @__PURE__ */ jsx25(Text9, { color: t.color.muted, wrap: "truncate-end", children: " " }),
58072
59693
  /* @__PURE__ */ jsxs13(Text9, { color: t.color.muted, wrap: "truncate-end", children: [
58073
59694
  provider.key_env,
@@ -59263,14 +60884,14 @@ __export(fpsStore_exports, {
59263
60884
  $fpsState: () => $fpsState,
59264
60885
  trackFrame: () => trackFrame
59265
60886
  });
59266
- import { atom as atom7 } from "nanostores";
60887
+ import { atom as atom8 } from "nanostores";
59267
60888
  var WINDOW_SIZE, $fpsState, timestamps, totalFrames, trackFrame;
59268
60889
  var init_fpsStore = __esm({
59269
60890
  "src/lib/fpsStore.ts"() {
59270
60891
  "use strict";
59271
60892
  init_env();
59272
60893
  WINDOW_SIZE = 30;
59273
- $fpsState = atom7({ fps: 0, lastDurationMs: 0, totalFrames: 0 });
60894
+ $fpsState = atom8({ fps: 0, lastDurationMs: 0, totalFrames: 0 });
59274
60895
  timestamps = [];
59275
60896
  totalFrames = 0;
59276
60897
  trackFrame = SHOW_FPS ? (durationMs) => {
@@ -59544,7 +61165,7 @@ var init_emoji = __esm({
59544
61165
  });
59545
61166
 
59546
61167
  // src/lib/externalLink.ts
59547
- import { isIP } from "node:net";
61168
+ import { isIP as isIP2 } from "node:net";
59548
61169
  import { useEffect as useEffect21, useMemo as useMemo13, useState as useState25 } from "react";
59549
61170
  function normalizeExternalUrl(value) {
59550
61171
  const trimmed = value.trim();
@@ -59650,13 +61271,13 @@ function isPrivateIpv6(value) {
59650
61271
  }
59651
61272
  return false;
59652
61273
  }
59653
- function normalizeHostname(value) {
61274
+ function normalizeHostname2(value) {
59654
61275
  const withoutBrackets = value.replace(/^\[/, "").replace(/\]$/, "");
59655
61276
  const withoutZoneId = withoutBrackets.split("%", 1)[0];
59656
61277
  return withoutZoneId.replace(/\.$/, "").toLowerCase();
59657
61278
  }
59658
61279
  function isPrivateOrLocalHost(hostname) {
59659
- const normalized = normalizeHostname(hostname);
61280
+ const normalized = normalizeHostname2(hostname);
59660
61281
  if (!normalized) {
59661
61282
  return true;
59662
61283
  }
@@ -59666,7 +61287,7 @@ function isPrivateOrLocalHost(hostname) {
59666
61287
  if (LOCAL_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
59667
61288
  return true;
59668
61289
  }
59669
- const ipVersion = isIP(normalized);
61290
+ const ipVersion = isIP2(normalized);
59670
61291
  if (ipVersion === 4) {
59671
61292
  return isPrivateIpv4(normalized);
59672
61293
  }
@@ -59850,7 +61471,7 @@ var init_externalLink = __esm({
59850
61471
  __name(parseIpv4Octets, "parseIpv4Octets");
59851
61472
  __name(isPrivateIpv4, "isPrivateIpv4");
59852
61473
  __name(isPrivateIpv6, "isPrivateIpv6");
59853
- __name(normalizeHostname, "normalizeHostname");
61474
+ __name(normalizeHostname2, "normalizeHostname");
59854
61475
  __name(isPrivateOrLocalHost, "isPrivateOrLocalHost");
59855
61476
  __name(isTitleFetchable, "isTitleFetchable");
59856
61477
  __name(decodeHtmlEntities, "decodeHtmlEntities");
@@ -60434,7 +62055,7 @@ var init_mathUnicode = __esm({
60434
62055
 
60435
62056
  // src/lib/syntax.ts
60436
62057
  function highlightLine(line, lang, t) {
60437
- const spec = resolve4(lang);
62058
+ const spec = resolve5(lang);
60438
62059
  if (!spec) {
60439
62060
  return [["", line]];
60440
62061
  }
@@ -60466,7 +62087,7 @@ function highlightLine(line, lang, t) {
60466
62087
  }
60467
62088
  return tokens;
60468
62089
  }
60469
- var KW, TS, PY, SH, GO, RUST, SQL, LANGS, ALIAS, resolve4, isHighlightable, TOKEN_RE;
62090
+ var KW, TS, PY, SH, GO, RUST, SQL, LANGS, ALIAS, resolve5, isHighlightable, TOKEN_RE;
60470
62091
  var init_syntax = __esm({
60471
62092
  "src/lib/syntax.ts"() {
60472
62093
  "use strict";
@@ -60520,8 +62141,8 @@ var init_syntax = __esm({
60520
62141
  yml: "yaml",
60521
62142
  zsh: "sh"
60522
62143
  };
60523
- resolve4 = /* @__PURE__ */ __name((lang) => LANGS[ALIAS[lang] ?? lang] ?? null, "resolve");
60524
- isHighlightable = /* @__PURE__ */ __name((lang) => resolve4(lang) !== null, "isHighlightable");
62144
+ resolve5 = /* @__PURE__ */ __name((lang) => LANGS[ALIAS[lang] ?? lang] ?? null, "resolve");
62145
+ isHighlightable = /* @__PURE__ */ __name((lang) => resolve5(lang) !== null, "isHighlightable");
60525
62146
  TOKEN_RE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|\b\d+(?:\.\d+)?\b|[A-Za-z_$][\w$]*/g;
60526
62147
  __name(highlightLine, "highlightLine");
60527
62148
  }
@@ -62396,7 +64017,6 @@ var init_appLayout = __esm({
62396
64017
  init_env();
62397
64018
  init_limits();
62398
64019
  init_placeholders();
62399
- init_messages();
62400
64020
  init_inputMetrics();
62401
64021
  init_perfPane();
62402
64022
  init_agentsOverlay();
@@ -62516,7 +64136,6 @@ var init_appLayout = __esm({
62516
64136
  const inputColumns = stableComposerColumns(composer.cols, promptWidth);
62517
64137
  const inputHeight = inputVisualHeight(composer.input, inputColumns);
62518
64138
  const inputMouseRef = useRef19(null);
62519
- const attachedImageMeta = composer.attachedImage ? imageTokenMeta(composer.attachedImage) : "";
62520
64139
  const captureInputDrag = /* @__PURE__ */ __name((e) => {
62521
64140
  if (e.button !== 0) {
62522
64141
  return;
@@ -62587,12 +64206,6 @@ var init_appLayout = __esm({
62587
64206
  ),
62588
64207
  composer.input === "?" && !composer.inputBuf.length && /* @__PURE__ */ jsx41(HelpHint, { t: ui.theme }),
62589
64208
  !isBlocked && /* @__PURE__ */ jsxs28(Fragment11, { children: [
62590
- composer.attachedImage && /* @__PURE__ */ jsx41(Box_default, { width: Math.max(1, composer.cols - 2), children: /* @__PURE__ */ jsxs28(Text9, { color: ui.theme.color.systemNote, wrap: "truncate-end", children: [
62591
- "\u{1F4CE} ",
62592
- composer.attachedImage.name,
62593
- attachedImageMeta ? ` \xB7 ${attachedImageMeta}` : "",
62594
- /* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.muted, children: " \xB7 Esc to remove" })
62595
- ] }) }),
62596
64209
  composer.inputBuf.map((line, i) => /* @__PURE__ */ jsxs28(Box_default, { children: [
62597
64210
  /* @__PURE__ */ jsx41(Box_default, { width: promptWidth, children: i === 0 ? /* @__PURE__ */ jsx41(PromptPrefix, { color: ui.theme.color.muted, promptText, width: promptWidth }) : /* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.muted, children: promptBlank }) }),
62598
64211
  /* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.composeText, children: line || " " })
@@ -62653,8 +64266,9 @@ var init_appLayout = __esm({
62653
64266
  {
62654
64267
  gw,
62655
64268
  initialHistoryIndex: overlay.agentsInitialHistoryIndex,
62656
- onClose: () => patchOverlayState({ agents: false, agentsInitialHistoryIndex: 0 }),
62657
- t: ui.theme
64269
+ onClose: () => patchOverlayState({ agents: false, agentsInitialHistoryIndex: 0, agentsWorkflowId: null }),
64270
+ t: ui.theme,
64271
+ workflowId: overlay.agentsWorkflowId
62658
64272
  }
62659
64273
  );
62660
64274
  }, "AgentsOverlayPane"));