@flowriki/agent 0.1.0-beta.36 → 0.1.0-beta.39

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.
@@ -6,7 +6,7 @@ import os from "os";
6
6
  import path from "path";
7
7
  import { createRequire } from "module";
8
8
  import { spawnSync } from "child_process";
9
- var AGENT_VERSION = "0.1.0-beta.36";
9
+ var AGENT_VERSION = "0.1.0-beta.39";
10
10
  async function prepareBrowsers() {
11
11
  if (browsersReady()) {
12
12
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -113,29 +113,29 @@ function allConnections(env = process.env) {
113
113
  }
114
114
  function saveConnection(connection, env = process.env) {
115
115
  ensureDataDir(env);
116
- const file = credentialsPath(env);
116
+ const file2 = credentialsPath(env);
117
117
  const next = {
118
118
  version: 3,
119
119
  connections: [...readFile(env).connections.filter((c) => c.service !== connection.service), connection]
120
120
  };
121
- const temporary = `${file}.${process.pid}.tmp`;
121
+ const temporary = `${file2}.${process.pid}.tmp`;
122
122
  fs3.writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
123
- fs3.renameSync(temporary, file);
123
+ fs3.renameSync(temporary, file2);
124
124
  }
125
125
  function forgetConnection(service, env = process.env) {
126
126
  const before = readFile(env).connections;
127
127
  const after = before.filter((c) => c.service !== service);
128
128
  if (after.length === before.length) return false;
129
129
  ensureDataDir(env);
130
- const file = credentialsPath(env);
131
- const temporary = `${file}.${process.pid}.tmp`;
130
+ const file2 = credentialsPath(env);
131
+ const temporary = `${file2}.${process.pid}.tmp`;
132
132
  fs3.writeFileSync(temporary, JSON.stringify({ version: 3, connections: after }, null, 2) + "\n", { mode: 384 });
133
- fs3.renameSync(temporary, file);
133
+ fs3.renameSync(temporary, file2);
134
134
  return true;
135
135
  }
136
136
  var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
137
137
 
138
- // ../contracts/dist/chunk-UFMRFPRU.js
138
+ // ../contracts/dist/chunk-4VVBTBPF.js
139
139
  import { z } from "zod";
140
140
  var PROTOCOL_VERSION = 1;
141
141
  var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
@@ -168,7 +168,15 @@ var CommandNameSchema = z.enum([
168
168
  "runs.list",
169
169
  "runs.stats",
170
170
  "run.get",
171
- "artifact.fetch"
171
+ "artifact.fetch",
172
+ // The AI assistant on this machine, driven from the dashboard. NOT baseline: an
173
+ // agent from before these existed must be told so up front rather than be
174
+ // sent a command it has no handler for.
175
+ "assistant.status",
176
+ "assistant.connect",
177
+ "assistant.ask",
178
+ "assistant.cancel",
179
+ "assistant.folder"
172
180
  ]);
173
181
  var EFFECTFUL = /* @__PURE__ */ new Set([
174
182
  "record.start",
@@ -200,6 +208,13 @@ var EFFECTFUL = /* @__PURE__ */ new Set([
200
208
  // dropped socket would find it already there and report a failure for
201
209
  // something that had in fact succeeded.
202
210
  "feature.create",
211
+ // Signing in opens a browser on somebody's machine; asking starts a program
212
+ // that spends their assistant usage and can record tests. Neither is a read,
213
+ // and a blind retry after a dropped socket would do either twice.
214
+ // (`assistant.status` stays out: reading is safe to repeat.)
215
+ "assistant.connect",
216
+ "assistant.ask",
217
+ "assistant.cancel",
203
218
  // Claiming writes a file, makes a commit and pushes. Blind retry after a
204
219
  // dropped socket would commit twice.
205
220
  "project.claim",
@@ -990,6 +1005,9 @@ import { z as z4 } from "zod";
990
1005
  import { z as z22 } from "zod";
991
1006
  import { z as z32 } from "zod";
992
1007
  import { z as z42 } from "zod";
1008
+ import { z as z5 } from "zod";
1009
+ import { z as z6 } from "zod";
1010
+ import { z as z7 } from "zod";
993
1011
  var EngineSchema = z4.enum(["chromium", "firefox", "webkit"]);
994
1012
  var CapabilitySchema = z4.object({
995
1013
  id: z4.string(),
@@ -1756,6 +1774,285 @@ var DeliveredHelperSchema = z42.object({
1756
1774
  name: z42.string(),
1757
1775
  config: HelperConfigSchema
1758
1776
  }).strict();
1777
+ var LIBRARY_CASE_SCHEMA_VERSION = 1;
1778
+ var TEST_TYPES = ["functional", "negative", "edge", "regression", "smoke", "accessibility", "other"];
1779
+ var PRIORITIES = ["P0", "P1", "P2", "P3"];
1780
+ var CASE_STATUSES = ["draft", "ready", "deprecated"];
1781
+ var AUTOMATION_STATES = ["none", "planned", "automated", "not-automatable"];
1782
+ var LibraryStepSchema = z5.object({
1783
+ id: z5.string().min(1),
1784
+ action: z5.string().min(1),
1785
+ /** What to type or choose, when the action needs it. */
1786
+ data: z5.string().optional(),
1787
+ /** What should happen. Absent for a step that only gets somewhere. */
1788
+ expected: z5.string().optional()
1789
+ }).strict();
1790
+ var AutomationLinkSchema = z5.object({
1791
+ automationId: z5.string().min(1),
1792
+ caseRevision: z5.number().int().nonnegative(),
1793
+ /** The automation's revision when the link was made or last confirmed —
1794
+ * an automation edited since may no longer check what the case says. */
1795
+ automationRevision: z5.number().int().nonnegative().optional(),
1796
+ at: z5.string(),
1797
+ by: z5.string().default("")
1798
+ }).strict();
1799
+ var CaseOriginSchema = z5.discriminatedUnion("kind", [
1800
+ z5.object({ kind: z5.literal("manual") }).strict(),
1801
+ z5.object({
1802
+ kind: z5.literal("import"),
1803
+ /** "csv", "xlsx", "testrail", "zephyr", "xray", "text"… */
1804
+ format: z5.string().min(1),
1805
+ file: z5.string().min(1),
1806
+ /** Row or item in that file, to find it again. */
1807
+ row: z5.string().optional()
1808
+ }).strict(),
1809
+ z5.object({
1810
+ kind: z5.literal("generated"),
1811
+ sourceId: z5.string().min(1),
1812
+ sourceVersion: z5.number().int().positive(),
1813
+ requirementIds: z5.array(z5.string().min(1)).default([])
1814
+ }).strict()
1815
+ ]);
1816
+ var TestDataSchema = z5.object({
1817
+ name: z5.string().min(1),
1818
+ value: z5.string().optional(),
1819
+ secretRef: z5.string().regex(/^[a-z][a-z0-9_]*$/i).optional()
1820
+ }).strict();
1821
+ var LibraryCaseSchema = z5.object({
1822
+ schemaVersion: z5.literal(LIBRARY_CASE_SCHEMA_VERSION),
1823
+ id: z5.string().regex(/^tcs_[A-Za-z0-9]+$/),
1824
+ /** "TC-142": what people say out loud. The id is what links point at. */
1825
+ key: z5.string().regex(/^TC-[1-9][0-9]*$/),
1826
+ /** The same features flows are filed under (`f_<slug>`), so one feature
1827
+ * holds both what to test and what tests it. */
1828
+ featureId: z5.string().min(1),
1829
+ slug: z5.string().regex(/^[a-z0-9][a-z0-9-]*$/),
1830
+ title: z5.string().min(1).max(300),
1831
+ description: z5.string().default(""),
1832
+ type: z5.enum(TEST_TYPES).default("functional"),
1833
+ priority: z5.enum(PRIORITIES).default("P2"),
1834
+ status: z5.enum(CASE_STATUSES).default("draft"),
1835
+ preconditions: z5.array(z5.string().min(1)).default([]),
1836
+ steps: z5.array(LibraryStepSchema).default([]),
1837
+ testData: z5.array(TestDataSchema).default([]),
1838
+ tags: z5.array(z5.string().min(1)).default([]),
1839
+ /** Requirement ids (Flowriki storage). The text is not copied here. */
1840
+ covers: z5.array(z5.string().min(1)).default([]),
1841
+ automation: z5.object({
1842
+ state: z5.enum(AUTOMATION_STATES).default("none"),
1843
+ /** Why it cannot be automated, when it cannot. */
1844
+ reason: z5.string().optional(),
1845
+ /** The plan for automating it: flows to reuse, flows to record, checks to
1846
+ * switch on, accounts and helpers it needs, and what cannot be automated.
1847
+ * Written (usually by the assistant) and approved before anything is
1848
+ * recorded. */
1849
+ plan: z5.string().max(2e4).optional(),
1850
+ planApproved: z5.boolean().optional(),
1851
+ links: z5.array(AutomationLinkSchema).default([])
1852
+ }).strict().default({}),
1853
+ origin: CaseOriginSchema.default({ kind: "manual" }),
1854
+ createdAt: z5.string(),
1855
+ createdBy: z5.string().default(""),
1856
+ updatedAt: z5.string(),
1857
+ updatedBy: z5.string().default(""),
1858
+ revision: z5.number().int().nonnegative().default(0)
1859
+ }).strict().superRefine((doc, ctx) => {
1860
+ const seen = /* @__PURE__ */ new Set();
1861
+ doc.steps.forEach((step, i) => {
1862
+ if (seen.has(step.id)) ctx.addIssue({ code: "custom", path: ["steps", i, "id"], message: "Step IDs must be unique" });
1863
+ seen.add(step.id);
1864
+ });
1865
+ const linked = /* @__PURE__ */ new Set();
1866
+ doc.automation.links.forEach((link, i) => {
1867
+ if (linked.has(link.automationId)) {
1868
+ ctx.addIssue({ code: "custom", path: ["automation", "links", i], message: "An automation is linked once" });
1869
+ }
1870
+ linked.add(link.automationId);
1871
+ });
1872
+ if (doc.automation.state === "not-automatable" && !doc.automation.reason?.trim()) {
1873
+ ctx.addIssue({ code: "custom", path: ["automation", "reason"], message: "Say why it cannot be automated" });
1874
+ }
1875
+ });
1876
+ var normaliseText = (text) => text.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
1877
+ var SOURCE_KINDS = ["upload", "jira", "figma", "confluence", "linear", "github-issue", "notion", "url"];
1878
+ var SourceVersionSchema = z5.object({
1879
+ v: z5.number().int().positive(),
1880
+ hash: z5.string().min(1),
1881
+ extractedText: z5.string(),
1882
+ /** Where the original file is kept, for an upload. */
1883
+ originalKey: z5.string().optional(),
1884
+ mediaType: z5.string().optional(),
1885
+ at: z5.string(),
1886
+ by: z5.string().default("")
1887
+ }).strict();
1888
+ var SourceSchema = z5.object({
1889
+ id: z5.string().regex(/^src_[A-Za-z0-9]+$/),
1890
+ projectId: z5.string().min(1),
1891
+ title: z5.string().min(1).max(300),
1892
+ kind: z5.enum(SOURCE_KINDS),
1893
+ versions: z5.array(SourceVersionSchema).min(1),
1894
+ integrationId: z5.string().optional(),
1895
+ externalId: z5.string().optional(),
1896
+ externalUrl: z5.string().url().optional(),
1897
+ createdAt: z5.string(),
1898
+ createdBy: z5.string().default("")
1899
+ }).strict();
1900
+ var RequirementSchema = z5.object({
1901
+ id: z5.string().regex(/^req_[A-Za-z0-9]+$/),
1902
+ projectId: z5.string().min(1),
1903
+ sourceId: z5.string().min(1),
1904
+ sourceVersion: z5.number().int().positive(),
1905
+ /** Where in the source: a heading path, a Jira key, a Figma node. */
1906
+ anchor: z5.string().min(1),
1907
+ text: z5.string().min(1),
1908
+ featureId: z5.string().optional(),
1909
+ /** As of the source's latest version: new in it, changed by it, unchanged
1910
+ * ("active"), or gone from it. */
1911
+ state: z5.enum(["new", "active", "changed", "removed"]).default("new"),
1912
+ history: z5.array(z5.object({
1913
+ sourceVersion: z5.number().int().positive(),
1914
+ text: z5.string(),
1915
+ state: z5.enum(["new", "active", "changed", "removed"])
1916
+ }).strict()).default([])
1917
+ }).strict();
1918
+ var MANUAL_OUTCOMES = ["untested", "passed", "failed", "blocked", "skipped"];
1919
+ var EvidenceSchema = z6.object({
1920
+ id: z6.string().regex(/^ev_[A-Za-z0-9]+$/),
1921
+ /** A file kept in Flowriki's evidence storage, or a link to one elsewhere. */
1922
+ kind: z6.enum(["file", "link"]),
1923
+ name: z6.string().min(1).max(200),
1924
+ mediaType: z6.string().optional(),
1925
+ size: z6.number().int().nonnegative().optional(),
1926
+ url: z6.string().url().optional(),
1927
+ /** The step it shows, when it belongs to one. */
1928
+ stepId: z6.string().optional(),
1929
+ at: z6.string(),
1930
+ by: z6.string().default("")
1931
+ }).strict();
1932
+ var StepOutcomeSchema = z6.object({
1933
+ stepId: z6.string().min(1),
1934
+ outcome: z6.enum(MANUAL_OUTCOMES),
1935
+ /** What actually happened, when it was not what was expected. */
1936
+ actual: z6.string().max(4e3).optional()
1937
+ }).strict();
1938
+ var ManualResultSchema = z6.object({
1939
+ outcome: z6.enum(MANUAL_OUTCOMES),
1940
+ steps: z6.array(StepOutcomeSchema).default([]),
1941
+ note: z6.string().max(8e3).default(""),
1942
+ evidence: z6.array(EvidenceSchema).default([]),
1943
+ bugIds: z6.array(z6.string().min(1)).default([]),
1944
+ tester: z6.string().default(""),
1945
+ at: z6.string()
1946
+ }).strict();
1947
+ var CycleEntrySchema = z6.object({
1948
+ testCaseId: z6.string().min(1),
1949
+ key: z6.string().min(1),
1950
+ title: z6.string().min(1),
1951
+ featureId: z6.string().min(1),
1952
+ priority: z6.enum(PRIORITIES),
1953
+ /** The case revision snapshot, so "the case changed since" is answerable. */
1954
+ caseRevision: z6.number().int().nonnegative(),
1955
+ preconditions: z6.array(z6.string()).default([]),
1956
+ steps: z6.array(z6.object({ id: z6.string(), action: z6.string(), data: z6.string().optional(), expected: z6.string().optional() }).strict()).default([]),
1957
+ assignee: z6.string().optional(),
1958
+ result: ManualResultSchema.nullable().default(null),
1959
+ /** Earlier results for this case in this cycle, newest last — a retest
1960
+ * keeps what the first attempt found. */
1961
+ history: z6.array(ManualResultSchema).default([])
1962
+ }).strict();
1963
+ var TestCycleSchema = z6.object({
1964
+ id: z6.string().regex(/^cyc_[A-Za-z0-9]+$/),
1965
+ projectId: z6.string().min(1),
1966
+ name: z6.string().min(1).max(200),
1967
+ description: z6.string().max(4e3).default(""),
1968
+ status: z6.enum(["open", "closed"]).default("open"),
1969
+ /** Which branch the cases were read from; null for the published tests. */
1970
+ changeId: z6.string().nullable().default(null),
1971
+ entries: z6.array(CycleEntrySchema).default([]),
1972
+ createdAt: z6.string(),
1973
+ createdBy: z6.string().default(""),
1974
+ updatedAt: z6.string(),
1975
+ closedAt: z6.string().nullable().default(null)
1976
+ }).strict();
1977
+ var BUG_STATUSES = ["open", "in-progress", "fixed", "verified", "closed", "wont-fix"];
1978
+ var SEVERITIES = ["critical", "major", "minor", "trivial"];
1979
+ var BugSchema = z6.object({
1980
+ id: z6.string().regex(/^bug_[A-Za-z0-9]+$/),
1981
+ key: z6.string().regex(/^BUG-[1-9][0-9]*$/),
1982
+ projectId: z6.string().min(1),
1983
+ title: z6.string().min(1).max(300),
1984
+ description: z6.string().max(2e4).default(""),
1985
+ severity: z6.enum(SEVERITIES).default("major"),
1986
+ status: z6.enum(BUG_STATUSES).default("open"),
1987
+ tracker: z6.enum(["flowriki", "jira", "linear", "github"]).default("flowriki"),
1988
+ externalKey: z6.string().optional(),
1989
+ url: z6.string().url().optional(),
1990
+ /** Where it was found: a case, the step in it, the cycle that ran it. */
1991
+ testCaseIds: z6.array(z6.string()).default([]),
1992
+ cycleIds: z6.array(z6.string()).default([]),
1993
+ runIds: z6.array(z6.string()).default([]),
1994
+ automationIds: z6.array(z6.string()).default([]),
1995
+ foundAt: z6.object({ cycleId: z6.string(), testCaseId: z6.string(), stepId: z6.string().optional() }).strict().optional(),
1996
+ createdAt: z6.string(),
1997
+ createdBy: z6.string().default(""),
1998
+ updatedAt: z6.string()
1999
+ }).strict();
2000
+ var STOP = new Set("a an the and or of to in on for with by is are be as at it this that from user users can should must shall will when then given".split(" "));
2001
+ function similarity(a, b2) {
2002
+ const words = (s2) => new Set(normaliseText(s2).split(" ").filter((w) => w.length > 1 && !STOP.has(w)));
2003
+ const x = words(a);
2004
+ const y = words(b2);
2005
+ if (x.size === 0 || y.size === 0) return 0;
2006
+ let shared = 0;
2007
+ for (const w of x) if (y.has(w)) shared += 1;
2008
+ return shared / (x.size + y.size - shared);
2009
+ }
2010
+ var ConversationSchema = z7.object({
2011
+ id: z7.string().regex(/^conv_[A-Za-z0-9]+$/),
2012
+ projectId: z7.string().min(1),
2013
+ /** Whose: conversations are personal. */
2014
+ userId: z7.string().min(1),
2015
+ title: z7.string().min(1).max(200),
2016
+ mode: z7.enum(["ask", "do"]).default("ask"),
2017
+ /** The assistant's own session, to continue where it left off. */
2018
+ sessionId: z7.string().optional(),
2019
+ /** The conversation as the dashboard draws it. Kept as given: the shape
2020
+ * belongs to the screen, and the service only stores and returns it. */
2021
+ lines: z7.array(z7.unknown()).max(2e3).default([]),
2022
+ createdAt: z7.string(),
2023
+ updatedAt: z7.string()
2024
+ }).strict();
2025
+ var INTEGRATION_PLATFORMS = ["jira", "linear", "github", "figma", "confluence"];
2026
+ var IntegrationSchema = z7.object({
2027
+ id: z7.string().regex(/^int_[A-Za-z0-9]+$/),
2028
+ projectId: z7.string().min(1),
2029
+ platform: z7.enum(INTEGRATION_PLATFORMS),
2030
+ name: z7.string().min(1).max(120),
2031
+ /** Where it is: a Jira or Confluence site, a GitHub repository, a Figma
2032
+ * file, a Linear team. Never a credential. */
2033
+ config: z7.object({
2034
+ baseUrl: z7.string().url().optional(),
2035
+ email: z7.string().optional(),
2036
+ jql: z7.string().max(2e3).optional(),
2037
+ projectKey: z7.string().max(40).optional(),
2038
+ repository: z7.string().regex(/^[\w.-]+\/[\w.-]+$/).optional(),
2039
+ fileKey: z7.string().max(120).optional(),
2040
+ teamKey: z7.string().max(40).optional(),
2041
+ spaceKey: z7.string().max(80).optional(),
2042
+ pageIds: z7.array(z7.string().max(40)).max(200).optional(),
2043
+ /** Raise bugs here when asked. */
2044
+ bugs: z7.boolean().optional(),
2045
+ /** Read items here as sources. */
2046
+ sources: z7.boolean().optional()
2047
+ }).strict(),
2048
+ /** The vault reference for its token. */
2049
+ tokenRef: z7.string().regex(/^[a-z][a-z0-9_]*$/),
2050
+ status: z7.enum(["connected", "failing"]).default("connected"),
2051
+ lastSyncAt: z7.string().nullable().default(null),
2052
+ lastError: z7.string().nullable().default(null),
2053
+ createdAt: z7.string(),
2054
+ createdBy: z7.string().default("")
2055
+ }).strict();
1759
2056
 
1760
2057
  // ../engine-playwright/dist/index.js
1761
2058
  import { createHmac, randomBytes as randomBytes2 } from "crypto";
@@ -3990,10 +4287,10 @@ async function captureFailure(opts) {
3990
4287
  }
3991
4288
  let screenshot = null;
3992
4289
  try {
3993
- const file = `step-${String(ordinal).padStart(2, "0")}.png`;
4290
+ const file2 = `step-${String(ordinal).padStart(2, "0")}.png`;
3994
4291
  fs4.mkdirSync(dir, { recursive: true });
3995
4292
  await page.screenshot({
3996
- path: path22.join(dir, file),
4293
+ path: path22.join(dir, file2),
3997
4294
  // The viewport, not the full page: a failure is about what was on screen,
3998
4295
  // and a full-page capture of an infinite-scroll app is a 20MB image.
3999
4296
  fullPage: false,
@@ -4004,7 +4301,7 @@ async function captureFailure(opts) {
4004
4301
  maskColor: "#475569",
4005
4302
  timeout: 5e3
4006
4303
  });
4007
- screenshot = file;
4304
+ screenshot = file2;
4008
4305
  } catch {
4009
4306
  screenshot = null;
4010
4307
  }
@@ -4042,20 +4339,20 @@ async function finaliseArtifacts(opts) {
4042
4339
  const { context, dir, passed, artifacts, consoleLines } = opts;
4043
4340
  const keep = (mode) => mode === "always" || mode === "on-failure" && !passed;
4044
4341
  const out = [];
4045
- const size = (file) => {
4342
+ const size = (file2) => {
4046
4343
  try {
4047
- return fs4.statSync(path22.join(dir, file)).size;
4344
+ return fs4.statSync(path22.join(dir, file2)).size;
4048
4345
  } catch {
4049
4346
  return null;
4050
4347
  }
4051
4348
  };
4052
4349
  if (artifacts.trace !== "off") {
4053
- const file = "trace.zip";
4350
+ const file2 = "trace.zip";
4054
4351
  try {
4055
4352
  if (keep(artifacts.trace)) {
4056
4353
  fs4.mkdirSync(dir, { recursive: true });
4057
- await context.tracing.stop({ path: path22.join(dir, file) });
4058
- out.push({ kind: "trace", relPath: file, bytes: size(file) });
4354
+ await context.tracing.stop({ path: path22.join(dir, file2) });
4355
+ out.push({ kind: "trace", relPath: file2, bytes: size(file2) });
4059
4356
  } else {
4060
4357
  await context.tracing.stop();
4061
4358
  }
@@ -4063,11 +4360,11 @@ async function finaliseArtifacts(opts) {
4063
4360
  }
4064
4361
  }
4065
4362
  if (artifacts.console && consoleLines.length) {
4066
- const file = "console.log";
4363
+ const file2 = "console.log";
4067
4364
  try {
4068
4365
  fs4.mkdirSync(dir, { recursive: true });
4069
- fs4.writeFileSync(path22.join(dir, file), consoleLines.join("\n") + "\n");
4070
- out.push({ kind: "console", relPath: file, bytes: size(file) });
4366
+ fs4.writeFileSync(path22.join(dir, file2), consoleLines.join("\n") + "\n");
4367
+ out.push({ kind: "console", relPath: file2, bytes: size(file2) });
4071
4368
  } catch {
4072
4369
  }
4073
4370
  }
@@ -4638,8 +4935,8 @@ async function applyAction(loc, step, value, ctx = {}) {
4638
4935
  const name = String(value ?? "");
4639
4936
  const names = step.values ?? (name ? [name] : []);
4640
4937
  const files = names.map((n2) => ctx.resolveFile?.(n2));
4641
- const file = files.every(Boolean);
4642
- if (!file) {
4938
+ const file2 = files.every(Boolean);
4939
+ if (!file2) {
4643
4940
  throw Object.assign(
4644
4941
  new Error(
4645
4942
  `this step uploads "${name || "(unnamed)"}", which is not on this machine \u2014 put a copy in .flowriki/files/`
@@ -6185,8 +6482,8 @@ async function verifySteps(opts) {
6185
6482
  var cachedAccName = null;
6186
6483
  function accNameScript2() {
6187
6484
  if (cachedAccName === null) {
6188
- const path42 = accNameScriptPath();
6189
- cachedAccName = fs32.readFileSync(path42, "utf8");
6485
+ const path43 = accNameScriptPath();
6486
+ cachedAccName = fs32.readFileSync(path43, "utf8");
6190
6487
  }
6191
6488
  return cachedAccName;
6192
6489
  }
@@ -6680,8 +6977,8 @@ async function fieldNames(page) {
6680
6977
  }
6681
6978
 
6682
6979
  // ../results/dist/chunk-LYCTVHPR.js
6683
- import { z as z5 } from "zod";
6684
- var RunStatusSchema = z5.enum([
6980
+ import { z as z8 } from "zod";
6981
+ var RunStatusSchema = z8.enum([
6685
6982
  "queued",
6686
6983
  "running",
6687
6984
  "passed",
@@ -6689,7 +6986,7 @@ var RunStatusSchema = z5.enum([
6689
6986
  "cancelled",
6690
6987
  "errored"
6691
6988
  ]);
6692
- var TaskStatusSchema = z5.enum([
6989
+ var TaskStatusSchema = z8.enum([
6693
6990
  "queued",
6694
6991
  "running",
6695
6992
  "passed",
@@ -6698,7 +6995,7 @@ var TaskStatusSchema = z5.enum([
6698
6995
  "cancelled",
6699
6996
  "errored"
6700
6997
  ]);
6701
- var FailureKindSchema = z5.enum([
6998
+ var FailureKindSchema = z8.enum([
6702
6999
  "not_found",
6703
7000
  "ambiguous",
6704
7001
  "timeout",
@@ -6707,30 +7004,30 @@ var FailureKindSchema = z5.enum([
6707
7004
  "crash",
6708
7005
  "unknown"
6709
7006
  ]);
6710
- var RunCheckSchema = z5.object({
6711
- id: z5.string(),
6712
- assertionId: z5.string().nullable(),
6713
- kind: z5.string(),
6714
- expected: z5.string().nullable(),
6715
- actual: z5.string().nullable(),
6716
- ok: z5.boolean(),
7007
+ var RunCheckSchema = z8.object({
7008
+ id: z8.string(),
7009
+ assertionId: z8.string().nullable(),
7010
+ kind: z8.string(),
7011
+ expected: z8.string().nullable(),
7012
+ actual: z8.string().nullable(),
7013
+ ok: z8.boolean(),
6717
7014
  /** A failed soft check is reported but does not fail its step, so a run can
6718
7015
  * be green and still carry findings. Stored per check rather than derived,
6719
7016
  * because the flow it came from can be edited afterwards and history has to
6720
7017
  * keep saying what actually happened. */
6721
- soft: z5.boolean().default(false),
6722
- detail: z5.string().nullable()
7018
+ soft: z8.boolean().default(false),
7019
+ detail: z8.string().nullable()
6723
7020
  });
6724
- var RunStepSchema = z5.object({
6725
- id: z5.string(),
6726
- ordinal: z5.number().int().nonnegative(),
7021
+ var RunStepSchema = z8.object({
7022
+ id: z8.string(),
7023
+ ordinal: z8.number().int().nonnegative(),
6727
7024
  /** The STABLE document step id — this is what joins runs across time and
6728
7025
  * makes flake-rate a view over existing columns rather than a migration. */
6729
- stepId: z5.string(),
6730
- action: z5.string(),
6731
- targetRole: z5.string().nullable(),
6732
- targetName: z5.string().nullable(),
6733
- targetScope: z5.string().nullable(),
7026
+ stepId: z8.string(),
7027
+ action: z8.string(),
7028
+ targetRole: z8.string().nullable(),
7029
+ targetName: z8.string().nullable(),
7030
+ targetScope: z8.string().nullable(),
6734
7031
  /** What the step typed, chose or pressed.
6735
7032
  *
6736
7033
  * A report that says `fill "Password"` and nothing else cannot tell you
@@ -6739,100 +7036,100 @@ var RunStepSchema = z5.object({
6739
7036
  *
6740
7037
  * NEVER set for a secret — see `secret` below. A run report is the thing
6741
7038
  * people paste into a ticket. */
6742
- value: z5.string().nullable().default(null),
7039
+ value: z8.string().nullable().default(null),
6743
7040
  /** The step typed a secret, so `value` is deliberately absent rather than
6744
7041
  * merely unknown. Stored, because "no value" and "a value withheld" render
6745
7042
  * differently and history has to keep saying which it was. */
6746
- secret: z5.boolean().default(false),
6747
- status: z5.enum(["passed", "failed", "skipped"]),
7043
+ secret: z8.boolean().default(false),
7044
+ status: z8.enum(["passed", "failed", "skipped"]),
6748
7045
  failureKind: FailureKindSchema.nullable(),
6749
- detail: z5.string().nullable(),
7046
+ detail: z8.string().nullable(),
6750
7047
  /** WHICH WAY FOUND IT, when the first way did not.
6751
7048
  *
6752
7049
  * A step carries several routes to its element and takes the first that
6753
7050
  * matches. Passing on a later one is not a failure, but it does mean the
6754
7051
  * page has moved under the step — and a suite that repairs itself quietly
6755
7052
  * drifts away from the application it tests. Null is the ordinary case. */
6756
- foundBy: z5.string().nullable().default(null),
6757
- durationMs: z5.number().int().nonnegative().nullable(),
7053
+ foundBy: z8.string().nullable().default(null),
7054
+ durationMs: z8.number().int().nonnegative().nullable(),
6758
7055
  /** Where the browser was when this step ran. Recorded on failure, and
6759
7056
  * frequently the whole answer — the element is missing because a step
6760
7057
  * before it navigated somewhere unexpected, not because it was renamed. */
6761
- pageUrl: z5.string().nullable().default(null),
6762
- pageTitle: z5.string().nullable().default(null),
6763
- checks: z5.array(RunCheckSchema).default([])
7058
+ pageUrl: z8.string().nullable().default(null),
7059
+ pageTitle: z8.string().nullable().default(null),
7060
+ checks: z8.array(RunCheckSchema).default([])
6764
7061
  });
6765
- var RunTaskSchema = z5.object({
6766
- id: z5.string(),
6767
- ordinal: z5.number().int().nonnegative(),
6768
- caseName: z5.string(),
6769
- rowId: z5.string().nullable(),
7062
+ var RunTaskSchema = z8.object({
7063
+ id: z8.string(),
7064
+ ordinal: z8.number().int().nonnegative(),
7065
+ caseName: z8.string(),
7066
+ rowId: z8.string().nullable(),
6770
7067
  capability: CapabilitySchema,
6771
- provider: z5.string(),
6772
- attempt: z5.number().int().positive().default(1),
6773
- maxAttempts: z5.number().int().positive().default(1),
7068
+ provider: z8.string(),
7069
+ attempt: z8.number().int().positive().default(1),
7070
+ maxAttempts: z8.number().int().positive().default(1),
6774
7071
  status: TaskStatusSchema,
6775
- workerId: z5.string().nullable(),
7072
+ workerId: z8.string().nullable(),
6776
7073
  /** Cloud provider's own session, so a failure is one click from their video. */
6777
- sessionId: z5.string().nullable(),
6778
- sessionUrl: z5.string().nullable(),
6779
- detail: z5.string().nullable(),
6780
- stepsRun: z5.number().int().nonnegative().default(0),
6781
- stepsTotal: z5.number().int().nonnegative().default(0),
6782
- durationMs: z5.number().int().nonnegative().nullable(),
6783
- steps: z5.array(RunStepSchema).default([])
7074
+ sessionId: z8.string().nullable(),
7075
+ sessionUrl: z8.string().nullable(),
7076
+ detail: z8.string().nullable(),
7077
+ stepsRun: z8.number().int().nonnegative().default(0),
7078
+ stepsTotal: z8.number().int().nonnegative().default(0),
7079
+ durationMs: z8.number().int().nonnegative().nullable(),
7080
+ steps: z8.array(RunStepSchema).default([])
6784
7081
  });
6785
- var ArtifactKindSchema = z5.enum(["screenshot", "video", "trace", "console", "har"]);
6786
- var ArtifactSchema = z5.object({
6787
- id: z5.string(),
6788
- runId: z5.string(),
6789
- runStepId: z5.string().nullable(),
7082
+ var ArtifactKindSchema = z8.enum(["screenshot", "video", "trace", "console", "har"]);
7083
+ var ArtifactSchema = z8.object({
7084
+ id: z8.string(),
7085
+ runId: z8.string(),
7086
+ runStepId: z8.string().nullable(),
6790
7087
  kind: ArtifactKindSchema,
6791
- relPath: z5.string(),
6792
- bytes: z5.number().int().nonnegative().nullable(),
6793
- masked: z5.boolean().default(false),
6794
- createdAt: z5.string()
7088
+ relPath: z8.string(),
7089
+ bytes: z8.number().int().nonnegative().nullable(),
7090
+ masked: z8.boolean().default(false),
7091
+ createdAt: z8.string()
6795
7092
  });
6796
- var RunSummarySchema = z5.object({
6797
- id: z5.string(),
6798
- projectId: z5.string(),
7093
+ var RunSummarySchema = z8.object({
7094
+ id: z8.string(),
7095
+ projectId: z8.string(),
6799
7096
  /** What the run was FOR.
6800
7097
  *
6801
7098
  * `flake` is a deliberate repetition of one flow to measure how often it
6802
7099
  * passes. It is kept out of pass-rate statistics: a 98/100 flakiness check is
6803
7100
  * a useful finding, not a failing suite, and counting it as one would make
6804
7101
  * measuring reliability look like losing it. */
6805
- kind: z5.enum(["smoke", "test", "adhoc", "flake"]),
6806
- trigger: z5.enum(["ui", "cli", "ci"]),
7102
+ kind: z8.enum(["smoke", "test", "adhoc", "flake"]),
7103
+ trigger: z8.enum(["ui", "cli", "ci"]),
6807
7104
  status: RunStatusSchema,
6808
- flowId: z5.string().nullable(),
6809
- flowName: z5.string().nullable(),
6810
- gitSha: z5.string().nullable(),
6811
- gitBranch: z5.string().nullable(),
6812
- gitDirty: z5.boolean().default(false),
6813
- env: z5.string().nullable(),
6814
- concurrency: z5.number().int().positive().nullable(),
6815
- tasksTotal: z5.number().int().nonnegative().default(0),
6816
- tasksPassed: z5.number().int().nonnegative().default(0),
6817
- tasksFailed: z5.number().int().nonnegative().default(0),
7105
+ flowId: z8.string().nullable(),
7106
+ flowName: z8.string().nullable(),
7107
+ gitSha: z8.string().nullable(),
7108
+ gitBranch: z8.string().nullable(),
7109
+ gitDirty: z8.boolean().default(false),
7110
+ env: z8.string().nullable(),
7111
+ concurrency: z8.number().int().positive().nullable(),
7112
+ tasksTotal: z8.number().int().nonnegative().default(0),
7113
+ tasksPassed: z8.number().int().nonnegative().default(0),
7114
+ tasksFailed: z8.number().int().nonnegative().default(0),
6818
7115
  /** Rolled up from the run's tasks rather than stored on the run, so they
6819
7116
  * cannot drift from the step rows they describe. "9 of 12" in a list is
6820
7117
  * what tells a reader where a run died; "1 of 1 tasks" tells them nothing
6821
7118
  * until there is more than one browser in play. */
6822
- stepsRun: z5.number().int().nonnegative().default(0),
6823
- stepsTotal: z5.number().int().nonnegative().default(0),
6824
- queuedAt: z5.string(),
6825
- startedAt: z5.string().nullable(),
6826
- finishedAt: z5.string().nullable(),
6827
- durationMs: z5.number().int().nonnegative().nullable()
7119
+ stepsRun: z8.number().int().nonnegative().default(0),
7120
+ stepsTotal: z8.number().int().nonnegative().default(0),
7121
+ queuedAt: z8.string(),
7122
+ startedAt: z8.string().nullable(),
7123
+ finishedAt: z8.string().nullable(),
7124
+ durationMs: z8.number().int().nonnegative().nullable()
6828
7125
  });
6829
7126
  var RunDetailSchema = RunSummarySchema.extend({
6830
- errorMessage: z5.string().nullable(),
6831
- artifactsDir: z5.string().nullable(),
6832
- tasks: z5.array(RunTaskSchema).default([]),
7127
+ errorMessage: z8.string().nullable(),
7128
+ artifactsDir: z8.string().nullable(),
7129
+ tasks: z8.array(RunTaskSchema).default([]),
6833
7130
  /** Screenshots and the like, addressed by `runStepId`. Kept flat rather than
6834
7131
  * nested inside each step: an artifact can belong to the run as a whole. */
6835
- artifacts: z5.array(ArtifactSchema).default([]),
7132
+ artifacts: z8.array(ArtifactSchema).default([]),
6836
7133
  /** The document as it was executed — a flow, or a test case with the copies
6837
7134
  * it held that day.
6838
7135
  *
@@ -6841,7 +7138,7 @@ var RunDetailSchema = RunSummarySchema.extend({
6841
7138
  * edited, and without surfacing it the promise was only half kept: the bytes
6842
7139
  * were there and nothing could reach them. Null for runs recorded before a
6843
7140
  * snapshot was taken. */
6844
- flowSnapshot: z5.unknown().nullable().default(null)
7141
+ flowSnapshot: z8.unknown().nullable().default(null)
6845
7142
  });
6846
7143
 
6847
7144
  // ../results/dist/chunk-IW3PJWP6.js
@@ -7068,8 +7365,8 @@ var RUN_COLUMNS = `r.*,
7068
7365
  (SELECT COALESCE(SUM(steps_run), 0) FROM run_task t WHERE t.run_id = r.id) AS steps_run,
7069
7366
  (SELECT COALESCE(SUM(steps_total), 0) FROM run_task t WHERE t.run_id = r.id) AS steps_total`;
7070
7367
  var SqliteStorage = class {
7071
- constructor(file = defaultDbPath()) {
7072
- this.file = file;
7368
+ constructor(file2 = defaultDbPath()) {
7369
+ this.file = file2;
7073
7370
  }
7074
7371
  file;
7075
7372
  db = null;
@@ -7540,7 +7837,7 @@ function toSummary(row) {
7540
7837
  };
7541
7838
  }
7542
7839
 
7543
- // ../repository-store/dist/chunk-HTFXAD4R.js
7840
+ // ../repository-store/dist/chunk-NTWCKFD3.js
7544
7841
  var FLOWRIKI_DIR = ".flowriki";
7545
7842
  var FEATURES_DIR = `${FLOWRIKI_DIR}/features`;
7546
7843
  var CONFIG_PATH = `${FLOWRIKI_DIR}/config.json`;
@@ -7552,6 +7849,7 @@ var SUITES_DIR = `${FLOWRIKI_DIR}/suites`;
7552
7849
  var AUTH_DIR = `${FLOWRIKI_DIR}/auth`;
7553
7850
  var ACCOUNTS_DIR = `${AUTH_DIR}/accounts`;
7554
7851
  var HELPERS_DIR = `${FLOWRIKI_DIR}/helpers`;
7852
+ var LIBRARY_DIR = `${FLOWRIKI_DIR}/library`;
7555
7853
  function assertSlug(slug) {
7556
7854
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid slug: ${slug}`);
7557
7855
  return slug;
@@ -7807,8 +8105,8 @@ ${line}
7807
8105
  );
7808
8106
  }
7809
8107
  };
7810
- function writeFileAtomic(file, contents) {
7811
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
8108
+ function writeFileAtomic(file2, contents) {
8109
+ const tmp = `${file2}.tmp-${process.pid}-${Date.now()}`;
7812
8110
  const handle = fs23.openSync(tmp, "w");
7813
8111
  try {
7814
8112
  fs23.writeFileSync(handle, contents);
@@ -7817,7 +8115,7 @@ function writeFileAtomic(file, contents) {
7817
8115
  fs23.closeSync(handle);
7818
8116
  }
7819
8117
  try {
7820
- fs23.renameSync(tmp, file);
8118
+ fs23.renameSync(tmp, file2);
7821
8119
  } catch (err) {
7822
8120
  fs23.rmSync(tmp, { force: true });
7823
8121
  throw err;
@@ -7844,13 +8142,13 @@ var Store = class {
7844
8142
  const out = [];
7845
8143
  for (const entry of fs23.readdirSync(this.featuresDir, { withFileTypes: true })) {
7846
8144
  if (!entry.isDirectory()) continue;
7847
- const file = path23.join(this.featuresDir, entry.name, FEATURE_FILE);
7848
- if (!fs23.existsSync(file)) continue;
8145
+ const file2 = path23.join(this.featuresDir, entry.name, FEATURE_FILE);
8146
+ if (!fs23.existsSync(file2)) continue;
7849
8147
  try {
7850
- out.push(FeatureSchema.parse(JSON.parse(fs23.readFileSync(file, "utf8"))));
8148
+ out.push(FeatureSchema.parse(JSON.parse(fs23.readFileSync(file2, "utf8"))));
7851
8149
  } catch (err) {
7852
8150
  this.featureProblems.push({
7853
- path: path23.relative(this.root, file),
8151
+ path: path23.relative(this.root, file2),
7854
8152
  error: String(err instanceof Error ? err.message : err).split("\n")[0].trim(),
7855
8153
  recoverable: false
7856
8154
  });
@@ -7892,9 +8190,9 @@ var Store = class {
7892
8190
  for (const slug of features) {
7893
8191
  const dir = path23.join(this.featuresDir, slug);
7894
8192
  if (!fs23.existsSync(dir)) continue;
7895
- for (const file of fs23.readdirSync(dir)) {
7896
- if (!isFlowFile(file)) continue;
7897
- const full = path23.join(dir, file);
8193
+ for (const file2 of fs23.readdirSync(dir)) {
8194
+ if (!isFlowFile(file2)) continue;
8195
+ const full = path23.join(dir, file2);
7898
8196
  try {
7899
8197
  out.push(parseFlowDocument(JSON.parse(fs23.readFileSync(full, "utf8"))));
7900
8198
  } catch (err) {
@@ -7923,16 +8221,16 @@ var Store = class {
7923
8221
  *
7924
8222
  * Outside `features/`, because that directory is the product and is
7925
8223
  * committed; a recovery copy is machine-local noise. */
7926
- recoveryPathFor(file) {
7927
- return path23.join(this.root, ".flowriki", ".recovery", path23.basename(file) + ".bak");
8224
+ recoveryPathFor(file2) {
8225
+ return path23.join(this.root, ".flowriki", ".recovery", path23.basename(file2) + ".bak");
7928
8226
  }
7929
8227
  /** Restore a flow file from its recovery copy. Returns false when there is
7930
8228
  * none, which is the honest answer rather than an invented document. */
7931
8229
  recoverFlowFile(relativePath) {
7932
- const file = path23.join(this.root, relativePath);
7933
- const backup = this.recoveryPathFor(file);
8230
+ const file2 = path23.join(this.root, relativePath);
8231
+ const backup = this.recoveryPathFor(file2);
7934
8232
  if (!fs23.existsSync(backup)) return false;
7935
- fs23.copyFileSync(backup, file);
8233
+ fs23.copyFileSync(backup, file2);
7936
8234
  return true;
7937
8235
  }
7938
8236
  /** Write a flow, atomically, keeping the previous contents recoverable.
@@ -7951,9 +8249,9 @@ var Store = class {
7951
8249
  const featureSlug = this.assertSlug(doc.featureId.replace(/^f_/, ""));
7952
8250
  const dir = path23.join(this.featuresDir, featureSlug);
7953
8251
  fs23.mkdirSync(dir, { recursive: true });
7954
- const file = path23.join(dir, `${this.assertSlug(doc.slug)}${FLOW_SUFFIX}`);
7955
- const destination = this.readFlowFile(file);
7956
- if (fs23.existsSync(file) && (!destination || destination.id !== doc.id)) {
8252
+ const file2 = path23.join(dir, `${this.assertSlug(doc.slug)}${FLOW_SUFFIX}`);
8253
+ const destination = this.readFlowFile(file2);
8254
+ if (fs23.existsSync(file2) && (!destination || destination.id !== doc.id)) {
7957
8255
  throw new FlowConflict("the destination contains another flow or an unreadable document \u2014 choose another name or recover it first", destination?.revision ?? 0);
7958
8256
  }
7959
8257
  const current = this.getFlow(doc.id) ?? destination;
@@ -7970,21 +8268,21 @@ var Store = class {
7970
8268
  }
7971
8269
  }
7972
8270
  doc.revision = (current?.revision ?? doc.revision ?? 0) + 1;
7973
- const previousFile = current ? path23.join(this.root, this.relPath(current)) : file;
8271
+ const previousFile = current ? path23.join(this.root, this.relPath(current)) : file2;
7974
8272
  if (fs23.existsSync(previousFile)) {
7975
8273
  const backup = this.recoveryPathFor(previousFile);
7976
8274
  fs23.mkdirSync(path23.dirname(backup), { recursive: true });
7977
8275
  fs23.copyFileSync(previousFile, backup);
7978
8276
  this.ensureRecoveryIgnored();
7979
8277
  }
7980
- writeFileAtomic(file, JSON.stringify(doc, null, 2) + "\n");
7981
- if (previousFile !== file) fs23.rmSync(previousFile);
7982
- return path23.relative(this.root, file);
8278
+ writeFileAtomic(file2, JSON.stringify(doc, null, 2) + "\n");
8279
+ if (previousFile !== file2) fs23.rmSync(previousFile);
8280
+ return path23.relative(this.root, file2);
7983
8281
  }
7984
8282
  /** Read one flow file, or null when it is missing or unreadable. */
7985
- readFlowFile(file) {
8283
+ readFlowFile(file2) {
7986
8284
  try {
7987
- return parseFlowDocument(JSON.parse(fs23.readFileSync(file, "utf8")));
8285
+ return parseFlowDocument(JSON.parse(fs23.readFileSync(file2, "utf8")));
7988
8286
  } catch {
7989
8287
  return null;
7990
8288
  }
@@ -8111,8 +8409,8 @@ var Store = class {
8111
8409
  resolveFile(name) {
8112
8410
  const first = name;
8113
8411
  if (!first || first.includes("/") || first.includes("\\") || first.includes("..")) return void 0;
8114
- const file = path23.join(this.filesDir, first);
8115
- return fs23.existsSync(file) ? file : void 0;
8412
+ const file2 = path23.join(this.filesDir, first);
8413
+ return fs23.existsSync(file2) ? file2 : void 0;
8116
8414
  }
8117
8415
  // ── secret values, held by `LocalSecrets` ───────────────────────────────
8118
8416
  //
@@ -8239,9 +8537,9 @@ function writeMarker(root, marker, opts = {}) {
8239
8537
  if (!facts.clean) {
8240
8538
  throw new ClaimRefused("this repository has uncommitted changes \u2014 commit or stash them first");
8241
8539
  }
8242
- const file = path33.join(root, MARKER_PATH);
8243
- fs33.mkdirSync(path33.dirname(file), { recursive: true });
8244
- fs33.writeFileSync(file, JSON.stringify(marker, null, 2) + "\n");
8540
+ const file2 = path33.join(root, MARKER_PATH);
8541
+ fs33.mkdirSync(path33.dirname(file2), { recursive: true });
8542
+ fs33.writeFileSync(file2, JSON.stringify(marker, null, 2) + "\n");
8245
8543
  git2(root, ["add", "--", MARKER_PATH]);
8246
8544
  git2(root, ["commit", "-m", `Bind this repository to Flowriki project ${marker.projectId}`]);
8247
8545
  const commit = git2(root, ["rev-parse", "HEAD"]);
@@ -8588,7 +8886,7 @@ async function executeFlow(opts) {
8588
8886
  for (let attempt = 1; attempt <= repeat; attempt++) {
8589
8887
  if (opts.signal?.aborted) break;
8590
8888
  const evidenceDir = runDir && repeat > 1 ? path7.join(runDir, `attempt-${String(attempt).padStart(3, "0")}`) : runDir;
8591
- const relativeTo = (file) => repeat > 1 ? `attempt-${String(attempt).padStart(3, "0")}/${file}` : file;
8889
+ const relativeTo = (file2) => repeat > 1 ? `attempt-${String(attempt).padStart(3, "0")}/${file2}` : file2;
8592
8890
  const taskId = storage && run ? await storage.createTask(run.id, {
8593
8891
  caseName: repeat > 1 ? `${flow.name} \xB7 attempt ${attempt}` : flow.name,
8594
8892
  capability: LOCAL_CHROMIUM,
@@ -8771,10 +9069,10 @@ function report(attempts, perStep) {
8771
9069
  steps
8772
9070
  };
8773
9071
  }
8774
- function sizeOf(dir, file) {
9072
+ function sizeOf(dir, file2) {
8775
9073
  if (!dir) return null;
8776
9074
  try {
8777
- return fs7.statSync(path7.join(dir, file)).size;
9075
+ return fs7.statSync(path7.join(dir, file2)).size;
8778
9076
  } catch {
8779
9077
  return null;
8780
9078
  }
@@ -9227,10 +9525,10 @@ var RecordingCoordinator = class {
9227
9525
  // saveFlow bumps this to 1; a fresh document starts unrevised.
9228
9526
  revision: 0
9229
9527
  };
9230
- const file = this.deps.store.saveFlow(doc);
9528
+ const file2 = this.deps.store.saveFlow(doc);
9231
9529
  this.draft = null;
9232
- const saved = { ...this.deps.store.getFlow(doc.id) ?? doc, path: file };
9233
- this.deps.emit({ type: "recording.stopped", flow: saved, file });
9530
+ const saved = { ...this.deps.store.getFlow(doc.id) ?? doc, path: file2 };
9531
+ this.deps.emit({ type: "recording.stopped", flow: saved, file: file2 });
9234
9532
  return this.lastResult = { ok: true, flow: saved, secrets: captured };
9235
9533
  }
9236
9534
  /** Throw the recording away: close the browser, drop the draft, write
@@ -9575,6 +9873,9 @@ function resolveCheckouts(dirs) {
9575
9873
  return { projects, skipped };
9576
9874
  }
9577
9875
 
9876
+ // src/engine/session.ts
9877
+ import fs12 from "fs";
9878
+
9578
9879
  // ../agent-core/dist/chunk-LR7LBGCT.js
9579
9880
  import { WebSocket, WebSocketServer } from "ws";
9580
9881
  var HEARTBEAT_MS = 3e4;
@@ -9753,15 +10054,15 @@ var EventJournal = class {
9753
10054
  };
9754
10055
  var { DatabaseSync } = createRequire4(import.meta.url)("node:sqlite");
9755
10056
  var AgentState = class {
9756
- constructor(file, deviceId, commandLimit = 1e5) {
10057
+ constructor(file2, deviceId, commandLimit = 1e5) {
9757
10058
  this.commandLimit = commandLimit;
9758
- if (!file || file === ":memory:") throw new Error("a persistent agent state file is required");
10059
+ if (!file2 || file2 === ":memory:") throw new Error("a persistent agent state file is required");
9759
10060
  if (!Number.isInteger(commandLimit) || commandLimit < 1) throw new Error("invalid command capacity");
9760
- fs9.mkdirSync(path9.dirname(file), { recursive: true, mode: 448 });
9761
- const fd = fs9.openSync(file, "a", 384);
10061
+ fs9.mkdirSync(path9.dirname(file2), { recursive: true, mode: 448 });
10062
+ const fd = fs9.openSync(file2, "a", 384);
9762
10063
  fs9.closeSync(fd);
9763
- fs9.chmodSync(file, 384);
9764
- this.db = new DatabaseSync(file);
10064
+ fs9.chmodSync(file2, 384);
10065
+ this.db = new DatabaseSync(file2);
9765
10066
  try {
9766
10067
  this.db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;");
9767
10068
  const version = Number(this.db.prepare("PRAGMA user_version").get()?.user_version);
@@ -10179,6 +10480,13 @@ import os5 from "os";
10179
10480
  import path10 from "path";
10180
10481
  import fs25 from "fs";
10181
10482
  import path24 from "path";
10483
+ import { randomUUID as randomUUID2 } from "crypto";
10484
+ import fs34 from "fs";
10485
+ import path34 from "path";
10486
+ import { spawn as nodeSpawn } from "child_process";
10487
+ import fs43 from "fs";
10488
+ import os22 from "os";
10489
+ import path42 from "path";
10182
10490
  var NO_CHECKOUT = {
10183
10491
  ok: false,
10184
10492
  reason: "failed",
@@ -10248,12 +10556,12 @@ function repositoryHandlers({ storeFor }) {
10248
10556
  return { ok: false, reason: "failed", detail: "no flow document was sent" };
10249
10557
  }
10250
10558
  try {
10251
- const path34 = store.saveFlow(flow, {
10559
+ const path52 = store.saveFlow(flow, {
10252
10560
  create: command.args["create"] === true,
10253
10561
  ...command.flowRevision !== void 0 ? { expectedRevision: command.flowRevision } : {}
10254
10562
  });
10255
10563
  const saved = store.getFlow(flow.id);
10256
- return { ok: true, result: { path: path34, revision: saved?.revision ?? null } };
10564
+ return { ok: true, result: { path: path52, revision: saved?.revision ?? null } };
10257
10565
  } catch (err) {
10258
10566
  if (err instanceof FlowConflict) {
10259
10567
  return { ok: false, reason: "stale_revision", detail: err.message };
@@ -10422,11 +10730,11 @@ function historyHandlers({ storage, artifactLimit }) {
10422
10730
  const artifact = run.artifacts.find((a) => a.id === artifactId);
10423
10731
  if (!artifact) return { ok: false, reason: "failed", detail: "no such artifact on that run" };
10424
10732
  const dir = run.artifactsDir ?? runArtifactsDir(runId, run.projectId);
10425
- const file = path24.join(dir, artifact.relPath);
10426
- if (!isInside(dir, file) || !fs25.existsSync(file) || !isInside(fs25.realpathSync(dir), fs25.realpathSync(file))) {
10733
+ const file2 = path24.join(dir, artifact.relPath);
10734
+ if (!isInside(dir, file2) || !fs25.existsSync(file2) || !isInside(fs25.realpathSync(dir), fs25.realpathSync(file2))) {
10427
10735
  return { ok: false, reason: "failed", detail: "the artifact file is not on this machine" };
10428
10736
  }
10429
- const { size } = fs25.statSync(file);
10737
+ const { size } = fs25.statSync(file2);
10430
10738
  if (size > artifactLimit) {
10431
10739
  return { ok: false, reason: "failed", detail: `this artifact is ${size} bytes, over the ${artifactLimit}-byte transfer limit` };
10432
10740
  }
@@ -10437,7 +10745,7 @@ function historyHandlers({ storage, artifactLimit }) {
10437
10745
  kind: artifact.kind,
10438
10746
  masked: artifact.masked,
10439
10747
  bytes: size,
10440
- base64: fs25.readFileSync(file).toString("base64")
10748
+ base64: fs25.readFileSync(file2).toString("base64")
10441
10749
  }
10442
10750
  };
10443
10751
  }
@@ -10838,6 +11146,118 @@ function debuggingHandlers(ctx) {
10838
11146
  }
10839
11147
  };
10840
11148
  }
11149
+ var file = () => path34.join(flowrikiHome(), "assistant-folders.json");
11150
+ function readAll() {
11151
+ try {
11152
+ const parsed = JSON.parse(fs34.readFileSync(file(), "utf8"));
11153
+ return parsed && typeof parsed === "object" ? parsed : {};
11154
+ } catch {
11155
+ return {};
11156
+ }
11157
+ }
11158
+ function gitHead(folder) {
11159
+ try {
11160
+ const head = fs34.readFileSync(path34.join(folder, ".git", "HEAD"), "utf8").trim();
11161
+ const ref = /^ref: (.+)$/.exec(head)?.[1];
11162
+ if (!ref) return { commit: /^[0-9a-f]{40}$/.test(head) ? head : null, branch: null };
11163
+ const branch = ref.replace(/^refs\/heads\//, "");
11164
+ const loose = path34.join(folder, ".git", ref);
11165
+ if (fs34.existsSync(loose)) return { commit: fs34.readFileSync(loose, "utf8").trim(), branch };
11166
+ const packed = fs34.readFileSync(path34.join(folder, ".git", "packed-refs"), "utf8");
11167
+ const line = packed.split("\n").find((l) => l.endsWith(` ${ref}`));
11168
+ return { commit: line?.split(" ")[0] ?? null, branch };
11169
+ } catch {
11170
+ return { commit: null, branch: null };
11171
+ }
11172
+ }
11173
+ function describeFolder(folder) {
11174
+ const exists = (() => {
11175
+ try {
11176
+ return fs34.statSync(folder).isDirectory();
11177
+ } catch {
11178
+ return false;
11179
+ }
11180
+ })();
11181
+ return { path: folder, exists, ...exists ? gitHead(folder) : { commit: null, branch: null } };
11182
+ }
11183
+ function folderFor(projectId) {
11184
+ const folder = readAll()[projectId];
11185
+ return folder ? describeFolder(folder) : null;
11186
+ }
11187
+ function setFolder(projectId, folder) {
11188
+ const all = readAll();
11189
+ if (folder === null) {
11190
+ delete all[projectId];
11191
+ } else {
11192
+ const resolved = path34.resolve(folder.replace(/^~(?=$|\/)/, process.env["HOME"] ?? "~"));
11193
+ if (!path34.isAbsolute(resolved)) throw new Error("give the folder's full path");
11194
+ if (!describeFolder(resolved).exists) throw new Error(`there is no folder at ${resolved} on this computer`);
11195
+ if (resolved === path34.parse(resolved).root || resolved === process.env["HOME"]) throw new Error("link the project's own folder, not the whole disk or home folder");
11196
+ all[projectId] = resolved;
11197
+ }
11198
+ fs34.mkdirSync(path34.dirname(file()), { recursive: true, mode: 448 });
11199
+ fs34.writeFileSync(file(), JSON.stringify(all, null, 2), { mode: 384 });
11200
+ return folder === null ? null : folderFor(projectId);
11201
+ }
11202
+ var MAX_PROMPT = 4e3;
11203
+ function assistantHandlers({ deps }) {
11204
+ const assistant = deps.assistant;
11205
+ if (!assistant) return {};
11206
+ return {
11207
+ "assistant.status": async () => ({
11208
+ ok: true,
11209
+ result: { ...await assistant.status(), busy: assistant.busy() }
11210
+ }),
11211
+ "assistant.connect": async (_command, _project, emit) => {
11212
+ const started = assistant.connect(emit);
11213
+ return started.started ? { ok: true, result: { started: true } } : { ok: false, reason: "busy", detail: started.detail ?? "could not start the sign-in" };
11214
+ },
11215
+ "assistant.ask": async (command, project, emit) => {
11216
+ const prompt = typeof command.args["prompt"] === "string" ? command.args["prompt"].trim() : "";
11217
+ if (!prompt) return { ok: false, reason: "failed", detail: "there is nothing to ask" };
11218
+ if (prompt.length > MAX_PROMPT) {
11219
+ return { ok: false, reason: "failed", detail: `keep it under ${MAX_PROMPT} characters` };
11220
+ }
11221
+ const sessionId = command.args["sessionId"];
11222
+ if (sessionId !== void 0 && !(typeof sessionId === "string" && /^[\w-]{8,100}$/.test(sessionId))) {
11223
+ return { ok: false, reason: "failed", detail: "that conversation id is not valid" };
11224
+ }
11225
+ const status2 = await assistant.status();
11226
+ if (!status2.installed) return { ok: false, reason: "failed", detail: status2.detail ?? "the assistant is not installed on this machine" };
11227
+ if (!status2.signedIn) return { ok: false, reason: "failed", detail: "the assistant is not signed in on this machine" };
11228
+ const askId = randomUUID2();
11229
+ const linked = folderFor(project.id);
11230
+ const mode = command.args["mode"] === "ask" ? "ask" : command.args["mode"] === "do" ? "do" : void 0;
11231
+ const started = assistant.ask({
11232
+ askId,
11233
+ prompt,
11234
+ projectId: project.id,
11235
+ projectName: project.name,
11236
+ ...linked?.exists ? { folder: linked.path } : {},
11237
+ ...mode ? { mode } : {},
11238
+ ...typeof sessionId === "string" ? { sessionId } : {},
11239
+ emit
11240
+ });
11241
+ return started.started ? { ok: true, result: { askId } } : { ok: false, reason: "busy", detail: started.detail ?? "the assistant is busy" };
11242
+ },
11243
+ /** Which folder on this computer holds the project's code — read with no
11244
+ * `path`, linked with a path, unlinked with null. */
11245
+ "assistant.folder": async (command, project) => {
11246
+ try {
11247
+ if (command.args["path"] === void 0) return { ok: true, result: { folder: folderFor(project.id) } };
11248
+ const asked = command.args["path"];
11249
+ const folder = setFolder(project.id, asked === null ? null : String(asked));
11250
+ return { ok: true, result: { folder } };
11251
+ } catch (error) {
11252
+ return { ok: false, reason: "failed", detail: error.message };
11253
+ }
11254
+ },
11255
+ "assistant.cancel": async (command) => {
11256
+ const askId = String(command.args["askId"] ?? "");
11257
+ return assistant.cancel(askId) ? { ok: true, result: { cancelled: true } } : { ok: false, reason: "failed", detail: "that question is not running" };
11258
+ }
11259
+ };
11260
+ }
10841
11261
  function buildHandlers(deps) {
10842
11262
  const context = {
10843
11263
  deps,
@@ -10864,9 +11284,322 @@ function buildHandlers(deps) {
10864
11284
  ...historyHandlers(context),
10865
11285
  ...executionHandlers(context),
10866
11286
  ...recordingHandlers(context),
11287
+ ...assistantHandlers(context),
10867
11288
  ...debug
10868
11289
  };
10869
11290
  }
11291
+ var MCP_SERVER_NAME = "flowriki";
11292
+ var TOOL_PREFIX = `mcp__${MCP_SERVER_NAME}__`;
11293
+ var SHOWN_KEYS = ["action", "role", "name", "kind", "title", "flow", "feature", "folder", "case", "op"];
11294
+ function describeToolInput(input) {
11295
+ if (!input || typeof input !== "object") return "";
11296
+ const record = input;
11297
+ const parts = [];
11298
+ for (const key of SHOWN_KEYS) {
11299
+ const value = record[key];
11300
+ if (typeof value === "string" && value) parts.push(value.length > 80 ? `${value.slice(0, 77)}\u2026` : value);
11301
+ }
11302
+ return parts.join(" \xB7 ");
11303
+ }
11304
+ var clip = (value, max = 100) => value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
11305
+ function toolArgs(tool, input) {
11306
+ if (!input || typeof input !== "object") return {};
11307
+ const record = input;
11308
+ const out = {};
11309
+ for (const key of ["action", "role", "name", "kind", "op", "within", "title", "flow", "feature", "folder", "startPath", "helperId", "saveAs", "signIn"]) {
11310
+ const value = record[key];
11311
+ if (typeof value === "string" && value) out[key] = clip(value);
11312
+ }
11313
+ for (const key of ["nth", "number"]) {
11314
+ const value = record[key];
11315
+ if (typeof value === "number" && Number.isFinite(value)) out[key] = value;
11316
+ }
11317
+ if (record["newBranch"] === true) out["newBranch"] = true;
11318
+ if (tool === "add_check" && typeof record["value"] === "string") out["value"] = clip(record["value"], 120);
11319
+ if (Array.isArray(record["after"])) {
11320
+ out["after"] = record["after"].filter((v) => typeof v === "string").slice(0, 5).map((v) => clip(v));
11321
+ }
11322
+ if (Array.isArray(record["rows"])) {
11323
+ out["flows"] = record["rows"].map((row) => row && typeof row === "object" ? row["flow"] : void 0).filter((v) => typeof v === "string").slice(0, 12).map((v) => clip(v));
11324
+ }
11325
+ return out;
11326
+ }
11327
+ function parseAuthStatus(raw) {
11328
+ try {
11329
+ const parsed = JSON.parse(raw);
11330
+ return {
11331
+ signedIn: parsed.loggedIn === true,
11332
+ method: typeof parsed.authMethod === "string" ? parsed.authMethod : null,
11333
+ plan: typeof parsed.subscriptionType === "string" ? parsed.subscriptionType : null
11334
+ };
11335
+ } catch {
11336
+ return { signedIn: false, method: null, plan: null };
11337
+ }
11338
+ }
11339
+ var ASK_MODE_TOOLS = [
11340
+ "list_projects",
11341
+ "project_catalog",
11342
+ "project_brief",
11343
+ "knowledge_search",
11344
+ "knowledge_read",
11345
+ "sources",
11346
+ "requirements",
11347
+ "find_similar_cases",
11348
+ "library_cases",
11349
+ "test_cycles",
11350
+ "bugs",
11351
+ "list_my_branches",
11352
+ "integrations"
11353
+ ];
11354
+ function buildAskArgs(opts) {
11355
+ const ask = opts.mode === "ask";
11356
+ const guidance = [
11357
+ `You are the Flowriki assistant for the project "${opts.projectName}" (id ${opts.projectId}). Use the Flowriki tools, and pass projectId "${opts.projectId}" wherever a tool takes one. Start with project_brief: it is this project's memory.`,
11358
+ ask ? "ASK MODE: answer the question; change nothing. Cite what you rely on as [[testCase:TC-12]], [[source:src_x@v2]], [[requirement:req_x]], [[knowledge:features/checkout.md]], [[automation:tc_x]], [[cycle:cyc_x]], [[bug:BUG-3]] or [[file:path/in/repo.ts]]." : "DO MODE: do what is asked, on Claude's working branch, for a person to review. Ask in the chat and wait before publishing, deleting, or sending anything to another system. End by writing a short summary of what you did with write_knowledge (memory/sessions/<date>-<topic>.md, append) so the next session knows.",
11359
+ opts.folder ? `The project's code is in ${opts.folder}. Read it with Read, Grep and Glob when it helps; you cannot change it.` : ""
11360
+ ].filter(Boolean).join("\n");
11361
+ const flowriki = ask ? ASK_MODE_TOOLS.map((t) => `mcp__${MCP_SERVER_NAME}__${t}`) : [`mcp__${MCP_SERVER_NAME}`];
11362
+ const files = opts.folder ? ["Read", "Grep", "Glob"] : [];
11363
+ return [
11364
+ "-p",
11365
+ opts.prompt,
11366
+ "--output-format",
11367
+ "stream-json",
11368
+ "--verbose",
11369
+ "--mcp-config",
11370
+ opts.mcpConfigPath,
11371
+ "--strict-mcp-config",
11372
+ // No built-in tools but reading files in the linked folder — no shell, no
11373
+ // edits, no web.
11374
+ "--tools",
11375
+ files.join(","),
11376
+ ...opts.folder ? ["--add-dir", opts.folder] : [],
11377
+ // Only these, and anything else is denied rather than asked about: there
11378
+ // is nobody at a terminal to answer.
11379
+ "--allowedTools",
11380
+ [...flowriki, ...files].join(","),
11381
+ "--permission-mode",
11382
+ "dontAsk",
11383
+ "--append-system-prompt",
11384
+ guidance,
11385
+ ...opts.sessionId ? ["--resume", opts.sessionId] : []
11386
+ ];
11387
+ }
11388
+ function eventsFromLine(askId, line) {
11389
+ let parsed;
11390
+ try {
11391
+ parsed = JSON.parse(line);
11392
+ } catch {
11393
+ return [];
11394
+ }
11395
+ const out = [];
11396
+ if (parsed.type === "system" && parsed.subtype === "init" && parsed.session_id) {
11397
+ out.push({ name: "assistant.session", data: { askId, sessionId: parsed.session_id } });
11398
+ }
11399
+ if (parsed.type === "assistant") {
11400
+ for (const block of parsed.message?.content ?? []) {
11401
+ if (block.type === "text" && block.text?.trim()) {
11402
+ out.push({ name: "assistant.text", data: { askId, text: block.text } });
11403
+ } else if (block.type === "tool_use" && block.name) {
11404
+ const tool = block.name.replace(TOOL_PREFIX, "");
11405
+ out.push({
11406
+ name: "assistant.tool",
11407
+ data: {
11408
+ askId,
11409
+ tool,
11410
+ target: describeToolInput(block.input),
11411
+ args: toolArgs(tool, block.input),
11412
+ ...block.id ? { id: block.id } : {}
11413
+ }
11414
+ });
11415
+ }
11416
+ }
11417
+ }
11418
+ if (parsed.type === "user") {
11419
+ for (const block of parsed.message?.content ?? []) {
11420
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
11421
+ const raw = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((part) => part && typeof part === "object" ? String(part.text ?? "") : "").join("\n") : "";
11422
+ const first = raw.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
11423
+ const changeId = /\/changes\/(chg_[A-Za-z0-9]+)/.exec(raw)?.[1];
11424
+ out.push({
11425
+ name: "assistant.toolResult",
11426
+ data: { askId, id: block.tool_use_id, ok: block.is_error !== true, summary: clip(first, 240), ...changeId ? { changeId } : {} }
11427
+ });
11428
+ }
11429
+ }
11430
+ if (parsed.type === "result") {
11431
+ out.push({
11432
+ name: "assistant.done",
11433
+ data: {
11434
+ askId,
11435
+ ok: parsed.is_error !== true,
11436
+ result: typeof parsed.result === "string" ? parsed.result : "",
11437
+ ...parsed.session_id ? { sessionId: parsed.session_id } : {}
11438
+ }
11439
+ });
11440
+ }
11441
+ return out;
11442
+ }
11443
+ function createClaudeCli(options) {
11444
+ const spawn2 = options.spawn ?? nodeSpawn;
11445
+ const binary = options.binary ?? process.env["FLOWRIKI_CLAUDE_BIN"] ?? "claude";
11446
+ const maxAskMs = options.maxAskMs ?? 30 * 6e4;
11447
+ const scratchRoot2 = options.scratchRoot ?? path42.join(os22.tmpdir(), "flowriki-claude");
11448
+ let current = null;
11449
+ let cancelled = null;
11450
+ function capture(args, timeoutMs = 15e3) {
11451
+ return new Promise((resolve) => {
11452
+ let out = "", err = "", settled = false;
11453
+ const finish = (code, failed) => {
11454
+ if (settled) return;
11455
+ settled = true;
11456
+ clearTimeout(timer);
11457
+ resolve({ code, out, err, failed });
11458
+ };
11459
+ let child;
11460
+ try {
11461
+ child = spawn2(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
11462
+ } catch {
11463
+ return resolve({ code: null, out: "", err: "", failed: true });
11464
+ }
11465
+ const timer = setTimeout(() => {
11466
+ child.kill("SIGKILL");
11467
+ finish(null, true);
11468
+ }, timeoutMs);
11469
+ child.stdout?.on("data", (chunk) => {
11470
+ out += chunk.toString();
11471
+ });
11472
+ child.stderr?.on("data", (chunk) => {
11473
+ err += chunk.toString();
11474
+ });
11475
+ child.on("error", () => finish(null, true));
11476
+ child.on("close", (code) => finish(code, false));
11477
+ });
11478
+ }
11479
+ return {
11480
+ async status() {
11481
+ const version = await capture(["--version"]);
11482
+ if (version.failed || version.code !== 0) {
11483
+ return { installed: false, version: null, signedIn: false, method: null, plan: null, detail: "The assistant\u2019s command-line tool was not found on this machine." };
11484
+ }
11485
+ const auth = await capture(["auth", "status", "--json"]);
11486
+ const parsed = parseAuthStatus(auth.out);
11487
+ return { installed: true, version: version.out.trim().split(/\s+/)[0] ?? null, ...parsed };
11488
+ },
11489
+ connect(emit) {
11490
+ if (current) return { started: false, detail: "The assistant is busy answering a question." };
11491
+ let child;
11492
+ try {
11493
+ child = spawn2(binary, ["auth", "login"], { stdio: ["ignore", "pipe", "pipe"] });
11494
+ } catch {
11495
+ return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
11496
+ }
11497
+ let finished = false;
11498
+ const done = async (detail) => {
11499
+ if (finished) return;
11500
+ finished = true;
11501
+ const status2 = await this.status();
11502
+ emit("assistant.connect.finished", { signedIn: status2.signedIn, ...detail ? { detail } : {} });
11503
+ };
11504
+ const timer = setTimeout(() => {
11505
+ child.kill("SIGTERM");
11506
+ void done("The sign-in was not completed in time.");
11507
+ }, 5 * 6e4);
11508
+ timer.unref?.();
11509
+ child.on("error", () => {
11510
+ clearTimeout(timer);
11511
+ void done("The assistant\u2019s command-line tool could not be started.");
11512
+ });
11513
+ child.on("close", () => {
11514
+ clearTimeout(timer);
11515
+ void done();
11516
+ });
11517
+ return { started: true };
11518
+ },
11519
+ ask(ask) {
11520
+ if (current) return { started: false, detail: "The assistant is already answering a question on this machine." };
11521
+ fs43.mkdirSync(scratchRoot2, { recursive: true, mode: 448 });
11522
+ const dir = fs43.mkdtempSync(path42.join(scratchRoot2, "ask-"));
11523
+ const mcpConfigPath = path42.join(dir, "mcp.json");
11524
+ fs43.writeFileSync(
11525
+ mcpConfigPath,
11526
+ JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args } } }),
11527
+ { mode: 384 }
11528
+ );
11529
+ let child;
11530
+ try {
11531
+ child = spawn2(
11532
+ binary,
11533
+ buildAskArgs({
11534
+ prompt: ask.prompt,
11535
+ mcpConfigPath,
11536
+ projectId: ask.projectId,
11537
+ projectName: ask.projectName,
11538
+ ...ask.sessionId ? { sessionId: ask.sessionId } : {},
11539
+ ...ask.folder ? { folder: ask.folder } : {},
11540
+ ...ask.mode ? { mode: ask.mode } : {}
11541
+ }),
11542
+ // THE SCRATCH FOLDER, not the project: nothing here to read or write.
11543
+ { cwd: dir, stdio: ["ignore", "pipe", "pipe"] }
11544
+ );
11545
+ } catch {
11546
+ fs43.rmSync(dir, { recursive: true, force: true });
11547
+ return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
11548
+ }
11549
+ current = { askId: ask.askId, child };
11550
+ let buffer = "", stderr = "", sawDone = false, ended = false;
11551
+ const handleLine = (line) => {
11552
+ for (const event of eventsFromLine(ask.askId, line)) {
11553
+ if (event.name === "assistant.done") sawDone = true;
11554
+ ask.emit(event.name, event.data);
11555
+ }
11556
+ };
11557
+ const end = (ok, detail) => {
11558
+ if (ended) return;
11559
+ ended = true;
11560
+ clearTimeout(timer);
11561
+ if (buffer.trim()) handleLine(buffer);
11562
+ if (!sawDone) ask.emit("assistant.done", { askId: ask.askId, ok, result: detail });
11563
+ current = null;
11564
+ fs43.rmSync(dir, { recursive: true, force: true });
11565
+ };
11566
+ const timer = setTimeout(() => {
11567
+ child.kill("SIGTERM");
11568
+ end(false, `Stopped after ${Math.max(1, Math.round(maxAskMs / 6e4))} minutes. Whatever it saved so far is on the Branches page \u2014 send \u201Ccontinue\u201D to carry on.`);
11569
+ }, maxAskMs);
11570
+ timer.unref?.();
11571
+ child.stdout?.on("data", (chunk) => {
11572
+ buffer += chunk.toString();
11573
+ let at;
11574
+ while ((at = buffer.indexOf("\n")) >= 0) {
11575
+ handleLine(buffer.slice(0, at));
11576
+ buffer = buffer.slice(at + 1);
11577
+ }
11578
+ });
11579
+ child.stderr?.on("data", (chunk) => {
11580
+ stderr = (stderr + chunk.toString()).slice(-2e3);
11581
+ });
11582
+ child.on("error", () => end(false, "The assistant\u2019s command-line tool could not be started."));
11583
+ child.on(
11584
+ "close",
11585
+ (code) => end(
11586
+ code === 0,
11587
+ cancelled === ask.askId ? "Stopped." : code === 0 ? "" : stderr.trim().split("\n").slice(-3).join(" ") || `The assistant stopped (exit ${code ?? "?"}).`
11588
+ )
11589
+ );
11590
+ return { started: true };
11591
+ },
11592
+ cancel(askId) {
11593
+ if (!current || current.askId !== askId) return false;
11594
+ cancelled = askId;
11595
+ current.child.kill("SIGTERM");
11596
+ const child = current.child;
11597
+ setTimeout(() => child.kill("SIGKILL"), 3e3).unref?.();
11598
+ return true;
11599
+ },
11600
+ busy: () => current !== null
11601
+ };
11602
+ }
10870
11603
  function createAgentRuntime(options) {
10871
11604
  const lease = new BrowserLease();
10872
11605
  let journal = null;
@@ -10995,6 +11728,7 @@ function createAgentRuntime(options) {
10995
11728
  const handlers = buildHandlers({
10996
11729
  lease,
10997
11730
  authenticationReuse: keptSignIns,
11731
+ ...options.mcpServer ? { assistant: createClaudeCli({ mcp: options.mcpServer }) } : {},
10998
11732
  storeFor: (r) => new Store(r),
10999
11733
  ...options.storage ? { storage: options.storage } : {},
11000
11734
  recorder,
@@ -11053,24 +11787,24 @@ import fs11 from "fs";
11053
11787
  import os6 from "os";
11054
11788
  function takeLock(env = process.env) {
11055
11789
  ensureDataDir(env);
11056
- const file = lockPath(env);
11790
+ const file2 = lockPath(env);
11057
11791
  const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os6.hostname() };
11058
11792
  for (let attempt = 0; attempt < 2; attempt += 1) {
11059
11793
  try {
11060
- fs11.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
11061
- return { ok: true, lock: { release: () => releaseLock(file, mine.pid) } };
11794
+ fs11.writeFileSync(file2, JSON.stringify(mine), { flag: "wx", mode: 384 });
11795
+ return { ok: true, lock: { release: () => releaseLock(file2, mine.pid) } };
11062
11796
  } catch {
11063
- const held = readLock(file);
11797
+ const held = readLock(file2);
11064
11798
  if (!held) {
11065
11799
  try {
11066
- fs11.rmSync(file, { force: true });
11800
+ fs11.rmSync(file2, { force: true });
11067
11801
  } catch {
11068
11802
  }
11069
11803
  continue;
11070
11804
  }
11071
11805
  if (held.host === mine.host && !alive(held.pid)) {
11072
11806
  try {
11073
- fs11.rmSync(file, { force: true });
11807
+ fs11.rmSync(file2, { force: true });
11074
11808
  } catch {
11075
11809
  }
11076
11810
  continue;
@@ -11078,11 +11812,11 @@ function takeLock(env = process.env) {
11078
11812
  return { ok: false, held };
11079
11813
  }
11080
11814
  }
11081
- return { ok: false, held: readLock(file) ?? { pid: -1, since: "", host: "unknown" } };
11815
+ return { ok: false, held: readLock(file2) ?? { pid: -1, since: "", host: "unknown" } };
11082
11816
  }
11083
- function readLock(file) {
11817
+ function readLock(file2) {
11084
11818
  try {
11085
- const held = JSON.parse(fs11.readFileSync(file, "utf8"));
11819
+ const held = JSON.parse(fs11.readFileSync(file2, "utf8"));
11086
11820
  return typeof held.pid === "number" ? { pid: held.pid, since: held.since ?? "", host: held.host ?? "" } : null;
11087
11821
  } catch {
11088
11822
  return null;
@@ -11097,17 +11831,26 @@ function alive(pid) {
11097
11831
  return error.code === "EPERM";
11098
11832
  }
11099
11833
  }
11100
- function releaseLock(file, pid) {
11101
- const held = readLock(file);
11834
+ function releaseLock(file2, pid) {
11835
+ const held = readLock(file2);
11102
11836
  if (held?.pid === pid) {
11103
11837
  try {
11104
- fs11.rmSync(file, { force: true });
11838
+ fs11.rmSync(file2, { force: true });
11105
11839
  } catch {
11106
11840
  }
11107
11841
  }
11108
11842
  }
11109
11843
 
11110
11844
  // src/engine/session.ts
11845
+ function mcpServer() {
11846
+ const script2 = process.argv[1];
11847
+ if (!script2) return null;
11848
+ try {
11849
+ return { command: process.execPath, args: [fs12.realpathSync(script2), "mcp"] };
11850
+ } catch {
11851
+ return null;
11852
+ }
11853
+ }
11111
11854
  async function start(options = {}) {
11112
11855
  let service;
11113
11856
  try {
@@ -11162,6 +11905,10 @@ async function start(options = {}) {
11162
11905
  stateFile: ledgerPath(connection.deviceId),
11163
11906
  projects,
11164
11907
  storage,
11908
+ // THE SAME PROGRAM, started again as `mcp`, so Claude's tools are the ones
11909
+ // this very install provides — not whatever `flowriki-agent` happens to be
11910
+ // first on somebody's PATH.
11911
+ ...mcpServer() ? { mcpServer: mcpServer() } : {},
11165
11912
  onEvent: (event) => options.onProgress?.({ kind: event.name, message: "", detail: event.data })
11166
11913
  });
11167
11914
  const link = new AgentLink({
@@ -11271,6 +12018,7 @@ export {
11271
12018
  connect,
11272
12019
  RecordedStepSchema,
11273
12020
  mirrorCheckTargets,
12021
+ similarity,
11274
12022
  runtimeValue,
11275
12023
  authenticationResolver,
11276
12024
  checkAssertion,