@flowriki/agent 0.1.0-beta.50 → 0.1.0-beta.53
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
AndroidAppSchema,
|
|
3
4
|
ArtifactPolicySchema,
|
|
4
5
|
AuthReferenceSchema,
|
|
5
6
|
CHECKS,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
ExecutionEnvironmentSchema,
|
|
14
15
|
FeatureSchema,
|
|
15
16
|
FlowDocumentSchema,
|
|
17
|
+
IosAppSchema,
|
|
16
18
|
MobileLane,
|
|
17
19
|
PrerequisiteProblem,
|
|
18
20
|
RecordedAssertionSchema,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
nodeAt,
|
|
46
48
|
normalizeStartUrl,
|
|
47
49
|
openMobileSession,
|
|
50
|
+
openSessionOn,
|
|
48
51
|
parseFlowDocument,
|
|
49
52
|
performMobileStep,
|
|
50
53
|
platformsOf,
|
|
@@ -65,7 +68,7 @@ import {
|
|
|
65
68
|
textIndependent,
|
|
66
69
|
validateActors,
|
|
67
70
|
webCloudOf
|
|
68
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-FIR6NOLA.js";
|
|
69
72
|
|
|
70
73
|
// src/engine/machine.ts
|
|
71
74
|
import fs from "fs";
|
|
@@ -73,7 +76,7 @@ import os from "os";
|
|
|
73
76
|
import path from "path";
|
|
74
77
|
import { createRequire } from "module";
|
|
75
78
|
import { spawnSync } from "child_process";
|
|
76
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
79
|
+
var AGENT_VERSION = "0.1.0-beta.53";
|
|
77
80
|
async function prepareBrowsers() {
|
|
78
81
|
if (browsersReady()) {
|
|
79
82
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -133,7 +136,7 @@ async function checkMachine() {
|
|
|
133
136
|
}
|
|
134
137
|
} catch {
|
|
135
138
|
}
|
|
136
|
-
const { mobileDoctor: mobileDoctor2 } = await import("./dist-
|
|
139
|
+
const { mobileDoctor: mobileDoctor2 } = await import("./dist-6GKVE5CL.js");
|
|
137
140
|
return {
|
|
138
141
|
agentVersion: AGENT_VERSION,
|
|
139
142
|
node: process.versions.node,
|
|
@@ -232,7 +235,7 @@ function forgetConnection(service, env = process.env) {
|
|
|
232
235
|
}
|
|
233
236
|
var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
|
|
234
237
|
|
|
235
|
-
// ../contracts/dist/chunk-
|
|
238
|
+
// ../contracts/dist/chunk-NTBXG5EZ.js
|
|
236
239
|
import { z } from "zod";
|
|
237
240
|
var PROTOCOL_VERSION = 1;
|
|
238
241
|
var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -281,6 +284,9 @@ var CommandNameSchema = z.enum([
|
|
|
281
284
|
// Reading a live phone session: its screen as a picture, and what is under
|
|
282
285
|
// a point. Not effectful — a picture is far too big to keep as a result.
|
|
283
286
|
"mobile.screen",
|
|
287
|
+
// The screen of whatever phone session is open on a platform — a run's,
|
|
288
|
+
// a recording's — as a picture, for the live mirror. Reading.
|
|
289
|
+
"mobile.peek",
|
|
284
290
|
// What this computer has installed for web and phone testing. Reading.
|
|
285
291
|
"machine.check"
|
|
286
292
|
]);
|
|
@@ -3960,7 +3966,7 @@ async function replayCase(opts) {
|
|
|
3960
3966
|
let status2 = "passed";
|
|
3961
3967
|
let error = null;
|
|
3962
3968
|
const browserRows = occurrences.filter((o) => o.platform !== "android" && o.platform !== "ios");
|
|
3963
|
-
const
|
|
3969
|
+
const laneOf2 = new Map(sessionLanes(browserRows, opts.defaultActorId).map((plan, i) => [browserRows[i].occurrenceId, plan]));
|
|
3964
3970
|
try {
|
|
3965
3971
|
for (const [index, occurrence] of occurrences.entries()) {
|
|
3966
3972
|
if (status2 !== "passed" || opts.signal?.aborted) {
|
|
@@ -4012,7 +4018,7 @@ async function replayCase(opts) {
|
|
|
4012
4018
|
const navigates = navigatesFirst(occurrence);
|
|
4013
4019
|
const who = occurrence.actorId ?? opts.defaultActorId ?? "default";
|
|
4014
4020
|
const entry = resolveStartUrl(occurrence.baseUrl ?? baseUrl, occurrence.startPath ?? "/");
|
|
4015
|
-
const plan =
|
|
4021
|
+
const plan = laneOf2.get(occurrence.occurrenceId);
|
|
4016
4022
|
try {
|
|
4017
4023
|
if (plan.opens && startOf(occurrence) === "fresh") await actors.restart(who, entry);
|
|
4018
4024
|
else if (plan.opens) await actors.select(who, entry);
|
|
@@ -6157,7 +6163,7 @@ function toSummary(row) {
|
|
|
6157
6163
|
};
|
|
6158
6164
|
}
|
|
6159
6165
|
|
|
6160
|
-
// ../repository-store/dist/chunk-
|
|
6166
|
+
// ../repository-store/dist/chunk-QF7BTHZB.js
|
|
6161
6167
|
var FLOWRIKI_DIR = ".flowriki";
|
|
6162
6168
|
var FEATURES_DIR = `${FLOWRIKI_DIR}/features`;
|
|
6163
6169
|
var CONFIG_PATH = `${FLOWRIKI_DIR}/config.json`;
|
|
@@ -6219,12 +6225,18 @@ function readEnvironments(raw) {
|
|
|
6219
6225
|
const seen = /* @__PURE__ */ new Set();
|
|
6220
6226
|
for (const entry of raw) {
|
|
6221
6227
|
if (!entry || typeof entry !== "object") continue;
|
|
6222
|
-
const { name, baseUrl } = entry;
|
|
6228
|
+
const { name, baseUrl, android, ios } = entry;
|
|
6223
6229
|
if (typeof name !== "string" || !name.trim()) continue;
|
|
6224
6230
|
const key = name.trim().toLowerCase();
|
|
6225
6231
|
if (seen.has(key)) continue;
|
|
6226
6232
|
seen.add(key);
|
|
6227
|
-
|
|
6233
|
+
const app = { android: AndroidAppSchema.safeParse(android), ios: IosAppSchema.safeParse(ios) };
|
|
6234
|
+
out.push({
|
|
6235
|
+
name: name.trim(),
|
|
6236
|
+
baseUrl: typeof baseUrl === "string" ? baseUrl : "",
|
|
6237
|
+
...app.android.success ? { android: app.android.data } : {},
|
|
6238
|
+
...app.ios.success ? { ios: app.ios.data } : {}
|
|
6239
|
+
});
|
|
6228
6240
|
}
|
|
6229
6241
|
return out;
|
|
6230
6242
|
}
|
|
@@ -7982,6 +7994,105 @@ var BrowserBusy = class extends Error {
|
|
|
7982
7994
|
this.name = "BrowserBusy";
|
|
7983
7995
|
}
|
|
7984
7996
|
};
|
|
7997
|
+
var LANES = ["web", "android", "ios"];
|
|
7998
|
+
var OneLane = class {
|
|
7999
|
+
owner = null;
|
|
8000
|
+
phase = "idle";
|
|
8001
|
+
since = null;
|
|
8002
|
+
/** Take the lane, or report who has it. Never awaits — that is the point. */
|
|
8003
|
+
acquire(owner) {
|
|
8004
|
+
if (this.owner !== null) {
|
|
8005
|
+
return { ok: false, heldBy: this.owner, phase: this.phase };
|
|
8006
|
+
}
|
|
8007
|
+
this.owner = owner;
|
|
8008
|
+
this.phase = "starting";
|
|
8009
|
+
this.since = Date.now();
|
|
8010
|
+
return { ok: true };
|
|
8011
|
+
}
|
|
8012
|
+
/** The holder finished starting and is now doing the thing. */
|
|
8013
|
+
activate(owner) {
|
|
8014
|
+
if (this.owner === owner) this.phase = "active";
|
|
8015
|
+
}
|
|
8016
|
+
/** The holder has begun shutting down. Still not idle: a second stop must
|
|
8017
|
+
* join the first rather than starting its own. */
|
|
8018
|
+
beginRelease(owner) {
|
|
8019
|
+
if (this.owner !== owner || this.phase === "stopping") return false;
|
|
8020
|
+
this.phase = "stopping";
|
|
8021
|
+
return true;
|
|
8022
|
+
}
|
|
8023
|
+
release(owner) {
|
|
8024
|
+
if (this.owner !== owner) return;
|
|
8025
|
+
this.owner = null;
|
|
8026
|
+
this.phase = "idle";
|
|
8027
|
+
this.since = null;
|
|
8028
|
+
}
|
|
8029
|
+
forceRelease() {
|
|
8030
|
+
this.owner = null;
|
|
8031
|
+
this.phase = "idle";
|
|
8032
|
+
this.since = null;
|
|
8033
|
+
}
|
|
8034
|
+
state() {
|
|
8035
|
+
return { owner: this.owner, phase: this.phase, since: this.since };
|
|
8036
|
+
}
|
|
8037
|
+
};
|
|
8038
|
+
var BrowserLease = class {
|
|
8039
|
+
lanes = { web: new OneLane(), android: new OneLane(), ios: new OneLane() };
|
|
8040
|
+
/** The web lane's methods, as they always were: the browser coordinators
|
|
8041
|
+
* drive one platform and need not know about lanes. */
|
|
8042
|
+
acquire(owner, lane = "web") {
|
|
8043
|
+
return this.lanes[lane].acquire(owner);
|
|
8044
|
+
}
|
|
8045
|
+
activate(owner, lane = "web") {
|
|
8046
|
+
this.lanes[lane].activate(owner);
|
|
8047
|
+
}
|
|
8048
|
+
beginRelease(owner, lane = "web") {
|
|
8049
|
+
return this.lanes[lane].beginRelease(owner);
|
|
8050
|
+
}
|
|
8051
|
+
release(owner, lane = "web") {
|
|
8052
|
+
this.lanes[lane].release(owner);
|
|
8053
|
+
}
|
|
8054
|
+
state(lane = "web") {
|
|
8055
|
+
return this.lanes[lane].state();
|
|
8056
|
+
}
|
|
8057
|
+
/** One lane, bound: what a coordinator for that platform is handed. */
|
|
8058
|
+
lane(lane) {
|
|
8059
|
+
return this.lanes[lane];
|
|
8060
|
+
}
|
|
8061
|
+
/** Take several lanes at once — all or none, so a job on two platforms
|
|
8062
|
+
* never holds one while refused the other. */
|
|
8063
|
+
acquireAll(owner, lanes) {
|
|
8064
|
+
const taken = [];
|
|
8065
|
+
for (const lane of new Set(lanes)) {
|
|
8066
|
+
const claim = this.lanes[lane].acquire(owner);
|
|
8067
|
+
if (!claim.ok) {
|
|
8068
|
+
for (const had of taken) this.lanes[had].release(owner);
|
|
8069
|
+
return { ok: false, lane, heldBy: claim.heldBy, phase: claim.phase };
|
|
8070
|
+
}
|
|
8071
|
+
taken.push(lane);
|
|
8072
|
+
}
|
|
8073
|
+
return { ok: true };
|
|
8074
|
+
}
|
|
8075
|
+
activateAll(owner, lanes) {
|
|
8076
|
+
for (const lane of new Set(lanes)) this.lanes[lane].activate(owner);
|
|
8077
|
+
}
|
|
8078
|
+
releaseAll(owner, lanes) {
|
|
8079
|
+
for (const lane of new Set(lanes)) this.lanes[lane].release(owner);
|
|
8080
|
+
}
|
|
8081
|
+
/** Drop every lane whoever holds it — shutdown, and nothing else. */
|
|
8082
|
+
forceRelease() {
|
|
8083
|
+
for (const lane of LANES) this.lanes[lane].forceRelease();
|
|
8084
|
+
}
|
|
8085
|
+
/** The sentence a refused caller is shown. Names what is happening and on
|
|
8086
|
+
* what, because "busy" leaves someone hunting for a window they cannot find. */
|
|
8087
|
+
static refusal(heldBy, phase, lane = "web") {
|
|
8088
|
+
const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
|
|
8089
|
+
const where = lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone";
|
|
8090
|
+
if (phase === "starting") return `${what} is starting on ${where} \u2014 wait for it to open, or stop it first`;
|
|
8091
|
+
if (phase === "stopping") return `${what} is still closing on ${where} \u2014 try again in a moment`;
|
|
8092
|
+
return `${what} is already using ${where} \u2014 stop it before starting another`;
|
|
8093
|
+
}
|
|
8094
|
+
};
|
|
8095
|
+
var laneOf = (platform) => platform === "android" || platform === "ios" ? platform : "web";
|
|
7985
8096
|
var COMMANDS = /* @__PURE__ */ new Set([
|
|
7986
8097
|
"play",
|
|
7987
8098
|
"pause",
|
|
@@ -8158,11 +8269,12 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8158
8269
|
const steps = (opts.steps ?? flow.steps).map((s2) => DraftStepSchema.parse(s2));
|
|
8159
8270
|
if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
|
|
8160
8271
|
const baseUrl = store.baseUrlFor(flow);
|
|
8161
|
-
const
|
|
8272
|
+
const lane = laneOf(flow.platform);
|
|
8273
|
+
const claim = deps.lease?.acquire("run", lane);
|
|
8162
8274
|
if (claim && !claim.ok) {
|
|
8163
|
-
throw new BrowserBusy(
|
|
8275
|
+
throw new BrowserBusy(`${lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone"} on this machine is in use (${claim.heldBy}, ${claim.phase})`);
|
|
8164
8276
|
}
|
|
8165
|
-
deps.lease?.activate("run");
|
|
8277
|
+
deps.lease?.activate("run", lane);
|
|
8166
8278
|
try {
|
|
8167
8279
|
const secrets = store.readSecrets();
|
|
8168
8280
|
if (isMobile(flow.platform)) {
|
|
@@ -8207,7 +8319,7 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8207
8319
|
resolveSecret: (ref) => secrets[ref]
|
|
8208
8320
|
});
|
|
8209
8321
|
} finally {
|
|
8210
|
-
deps.lease?.release("run");
|
|
8322
|
+
deps.lease?.release("run", lane);
|
|
8211
8323
|
}
|
|
8212
8324
|
}
|
|
8213
8325
|
function deliveredRun(delivered) {
|
|
@@ -8227,55 +8339,6 @@ function deliveredRun(delivered) {
|
|
|
8227
8339
|
resolveFile: () => void 0
|
|
8228
8340
|
};
|
|
8229
8341
|
}
|
|
8230
|
-
var BrowserLease = class {
|
|
8231
|
-
owner = null;
|
|
8232
|
-
phase = "idle";
|
|
8233
|
-
since = null;
|
|
8234
|
-
/** Take the lease, or report who has it. Never awaits — that is the point. */
|
|
8235
|
-
acquire(owner) {
|
|
8236
|
-
if (this.owner !== null) {
|
|
8237
|
-
return { ok: false, heldBy: this.owner, phase: this.phase };
|
|
8238
|
-
}
|
|
8239
|
-
this.owner = owner;
|
|
8240
|
-
this.phase = "starting";
|
|
8241
|
-
this.since = Date.now();
|
|
8242
|
-
return { ok: true };
|
|
8243
|
-
}
|
|
8244
|
-
/** The holder finished starting and is now doing the thing. */
|
|
8245
|
-
activate(owner) {
|
|
8246
|
-
if (this.owner === owner) this.phase = "active";
|
|
8247
|
-
}
|
|
8248
|
-
/** The holder has begun shutting down. Still not idle: a second stop must
|
|
8249
|
-
* join the first rather than starting its own. */
|
|
8250
|
-
beginRelease(owner) {
|
|
8251
|
-
if (this.owner !== owner || this.phase === "stopping") return false;
|
|
8252
|
-
this.phase = "stopping";
|
|
8253
|
-
return true;
|
|
8254
|
-
}
|
|
8255
|
-
release(owner) {
|
|
8256
|
-
if (this.owner !== owner) return;
|
|
8257
|
-
this.owner = null;
|
|
8258
|
-
this.phase = "idle";
|
|
8259
|
-
this.since = null;
|
|
8260
|
-
}
|
|
8261
|
-
/** Drop the lease whoever holds it — shutdown, and nothing else. */
|
|
8262
|
-
forceRelease() {
|
|
8263
|
-
this.owner = null;
|
|
8264
|
-
this.phase = "idle";
|
|
8265
|
-
this.since = null;
|
|
8266
|
-
}
|
|
8267
|
-
state() {
|
|
8268
|
-
return { owner: this.owner, phase: this.phase, since: this.since };
|
|
8269
|
-
}
|
|
8270
|
-
/** The sentence a refused caller is shown. Names what is happening, because
|
|
8271
|
-
* "busy" leaves someone hunting for a window they cannot find. */
|
|
8272
|
-
static refusal(heldBy, phase) {
|
|
8273
|
-
const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
|
|
8274
|
-
if (phase === "starting") return `${what} is starting \u2014 wait for it to open, or stop it first`;
|
|
8275
|
-
if (phase === "stopping") return `${what} is still closing \u2014 try again in a moment`;
|
|
8276
|
-
return `${what} is already using the browser \u2014 stop it before starting another`;
|
|
8277
|
-
}
|
|
8278
|
-
};
|
|
8279
8342
|
|
|
8280
8343
|
// src/engine/checkouts.ts
|
|
8281
8344
|
function resolveCheckouts(dirs) {
|
|
@@ -9232,11 +9295,12 @@ function executionHandlers(ctx) {
|
|
|
9232
9295
|
detail: `this flow needs ${missing.length} secret value(s) that are not on this machine (${missing.map((m) => m.ref || m.field).join(", ")}) \u2014 set them on the machine that runs it`
|
|
9233
9296
|
};
|
|
9234
9297
|
}
|
|
9235
|
-
const
|
|
9298
|
+
const lanes = [laneOf(flow.platform)];
|
|
9299
|
+
const claim = lease?.acquireAll("run", lanes);
|
|
9236
9300
|
if (claim && !claim.ok) {
|
|
9237
|
-
return { ok: false, reason: "busy", detail:
|
|
9301
|
+
return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
|
|
9238
9302
|
}
|
|
9239
|
-
lease?.
|
|
9303
|
+
lease?.activateAll("run", lanes);
|
|
9240
9304
|
onActivity?.({ kind: "run", projectId: project.id, detail: flow.name });
|
|
9241
9305
|
const aborter = new AbortController();
|
|
9242
9306
|
cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
|
|
@@ -9271,6 +9335,8 @@ function executionHandlers(ctx) {
|
|
|
9271
9335
|
ok: true,
|
|
9272
9336
|
result: {
|
|
9273
9337
|
runId: outcome.run?.id ?? null,
|
|
9338
|
+
// What it was called, for a service that only knew its id.
|
|
9339
|
+
flowName: flow.name,
|
|
9274
9340
|
// The WHOLE outcome, not a summary of it. A dashboard renders the
|
|
9275
9341
|
// per-step report exactly as the local app does, and a result
|
|
9276
9342
|
// carrying only counts makes that screen crash rather than degrade.
|
|
@@ -9286,7 +9352,7 @@ function executionHandlers(ctx) {
|
|
|
9286
9352
|
if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
|
|
9287
9353
|
if (runId) cancels.delete(runId);
|
|
9288
9354
|
onActivity?.(null);
|
|
9289
|
-
lease?.
|
|
9355
|
+
lease?.releaseAll("run", lanes);
|
|
9290
9356
|
}
|
|
9291
9357
|
},
|
|
9292
9358
|
/** Run a test case: several flows, one browser, in order.
|
|
@@ -9304,11 +9370,12 @@ function executionHandlers(ctx) {
|
|
|
9304
9370
|
if (testCase.occurrences.length === 0) {
|
|
9305
9371
|
return { ok: false, reason: "failed", detail: `"${testCase.name}" has no flows in it to run` };
|
|
9306
9372
|
}
|
|
9307
|
-
const
|
|
9373
|
+
const lanes = [...new Set(testCase.occurrences.map((o) => laneOf(o.platform)))];
|
|
9374
|
+
const claim = lease?.acquireAll("run", lanes);
|
|
9308
9375
|
if (claim && !claim.ok) {
|
|
9309
|
-
return { ok: false, reason: "busy", detail:
|
|
9376
|
+
return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
|
|
9310
9377
|
}
|
|
9311
|
-
lease?.
|
|
9378
|
+
lease?.activateAll("run", lanes);
|
|
9312
9379
|
onActivity?.({ kind: "run", projectId: project.id, detail: testCase.name });
|
|
9313
9380
|
const aborter = new AbortController();
|
|
9314
9381
|
cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
|
|
@@ -9347,6 +9414,8 @@ function executionHandlers(ctx) {
|
|
|
9347
9414
|
ok: true,
|
|
9348
9415
|
result: {
|
|
9349
9416
|
runId: run?.id ?? null,
|
|
9417
|
+
// What it was called, for a service that only knew its id.
|
|
9418
|
+
caseName: testCase.name,
|
|
9350
9419
|
// The whole outcome, per-flow groups included: a dashboard renders
|
|
9351
9420
|
// the report from this, and a result carrying only counts makes
|
|
9352
9421
|
// that screen crash rather than degrade.
|
|
@@ -9358,7 +9427,7 @@ function executionHandlers(ctx) {
|
|
|
9358
9427
|
if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
|
|
9359
9428
|
if (runId) cancels.delete(runId);
|
|
9360
9429
|
onActivity?.(null);
|
|
9361
|
-
lease?.
|
|
9430
|
+
lease?.releaseAll("run", lanes);
|
|
9362
9431
|
}
|
|
9363
9432
|
},
|
|
9364
9433
|
"run.cancel": async (command, project) => {
|
|
@@ -9726,6 +9795,7 @@ function assistantHandlers({ deps }) {
|
|
|
9726
9795
|
const askId = randomUUID();
|
|
9727
9796
|
const linked = foldersFor(project.id).filter((f) => f.exists).map((f) => f.path);
|
|
9728
9797
|
const mode = command.args["mode"] === "ask" ? "ask" : command.args["mode"] === "do" ? "do" : void 0;
|
|
9798
|
+
const brief = typeof command.args["brief"] === "string" && command.args["brief"].trim() ? command.args["brief"].slice(0, 4e4) : void 0;
|
|
9729
9799
|
const started = assistant.ask({
|
|
9730
9800
|
askId,
|
|
9731
9801
|
prompt,
|
|
@@ -9733,6 +9803,7 @@ function assistantHandlers({ deps }) {
|
|
|
9733
9803
|
projectName: project.name,
|
|
9734
9804
|
...linked.length ? { folders: linked } : {},
|
|
9735
9805
|
...mode ? { mode } : {},
|
|
9806
|
+
...brief ? { brief } : {},
|
|
9736
9807
|
...typeof sessionId === "string" ? { sessionId } : {},
|
|
9737
9808
|
emit
|
|
9738
9809
|
});
|
|
@@ -9769,7 +9840,9 @@ var runFile = (file2, args, timeoutMs) => new Promise((resolve2, reject) => {
|
|
|
9769
9840
|
resolve2({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
9770
9841
|
});
|
|
9771
9842
|
});
|
|
9772
|
-
var live =
|
|
9843
|
+
var live = /* @__PURE__ */ new Map();
|
|
9844
|
+
var liveById = (sessionId) => [...live.values()].find((l) => l.id === sessionId);
|
|
9845
|
+
var ownerOf = (l) => l.draft ? "recording" : "debug";
|
|
9773
9846
|
var SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|\bpin\b|cvv|cvc/i;
|
|
9774
9847
|
async function screenOf(l, withImage = false) {
|
|
9775
9848
|
l.session.changed();
|
|
@@ -9795,8 +9868,9 @@ async function screenOf(l, withImage = false) {
|
|
|
9795
9868
|
}
|
|
9796
9869
|
function mobileHandlers(ctx) {
|
|
9797
9870
|
const require2 = (command) => {
|
|
9798
|
-
|
|
9799
|
-
|
|
9871
|
+
const l = liveById(command.args["sessionId"]);
|
|
9872
|
+
if (!l) throw new Error("that phone session has ended \u2014 open it again");
|
|
9873
|
+
return l;
|
|
9800
9874
|
};
|
|
9801
9875
|
return {
|
|
9802
9876
|
// Everything this computer has for testing — web and phones — for the Guide.
|
|
@@ -9807,6 +9881,21 @@ function mobileHandlers(ctx) {
|
|
|
9807
9881
|
const devices = await listDevices();
|
|
9808
9882
|
return { ok: true, result: { devices, ...command.args["doctor"] === true ? { doctor: await mobileDoctor() } : {} } };
|
|
9809
9883
|
},
|
|
9884
|
+
// THE PHONE A RUN IS DRIVING, as a picture: whatever session is open on
|
|
9885
|
+
// that platform — a run's, a recording's. Not effectful, never kept, and
|
|
9886
|
+
// nothing when no phone is open: the mirror then shows that.
|
|
9887
|
+
"mobile.peek": async (command) => {
|
|
9888
|
+
const platform = command.args["platform"];
|
|
9889
|
+
if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
|
|
9890
|
+
const session = live.get(platform)?.session ?? openSessionOn(platform);
|
|
9891
|
+
if (!session) return { ok: true, result: { image: null } };
|
|
9892
|
+
try {
|
|
9893
|
+
const png = await session.screenshot();
|
|
9894
|
+
return { ok: true, result: { image: png.toString("base64"), device: session.deviceName } };
|
|
9895
|
+
} catch {
|
|
9896
|
+
return { ok: true, result: { image: null } };
|
|
9897
|
+
}
|
|
9898
|
+
},
|
|
9810
9899
|
"mobile.screen": async (command) => {
|
|
9811
9900
|
try {
|
|
9812
9901
|
const l = require2(command);
|
|
@@ -9823,14 +9912,21 @@ function mobileHandlers(ctx) {
|
|
|
9823
9912
|
},
|
|
9824
9913
|
"mobile.session": async (command, project) => {
|
|
9825
9914
|
const op = String(command.args["op"] ?? "");
|
|
9915
|
+
let claimed = null;
|
|
9826
9916
|
try {
|
|
9827
9917
|
if (op === "start") {
|
|
9828
|
-
if (live) {
|
|
9829
|
-
await live.session.close().catch(() => void 0);
|
|
9830
|
-
live = null;
|
|
9831
|
-
}
|
|
9832
9918
|
const platform = command.args["platform"];
|
|
9833
9919
|
if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
|
|
9920
|
+
const prior = live.get(platform);
|
|
9921
|
+
if (prior) {
|
|
9922
|
+
live.delete(prior.platform);
|
|
9923
|
+
await prior.session.close().catch(() => void 0);
|
|
9924
|
+
ctx.lease?.release(ownerOf(prior), prior.platform);
|
|
9925
|
+
}
|
|
9926
|
+
const owner = command.args["record"] === true ? "recording" : "debug";
|
|
9927
|
+
const claim = ctx.lease?.acquire(owner, platform);
|
|
9928
|
+
if (claim && !claim.ok) return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, platform) };
|
|
9929
|
+
claimed = { owner, platform };
|
|
9834
9930
|
const config = command.args["config"] ?? {};
|
|
9835
9931
|
const secrets = command.args["secrets"] ?? {};
|
|
9836
9932
|
const device = command.args["device"] ? DeviceChoiceSchema.parse(command.args["device"]) : void 0;
|
|
@@ -9865,7 +9961,8 @@ function mobileHandlers(ctx) {
|
|
|
9865
9961
|
...auth?.profileId ? { signIn: auth } : {}
|
|
9866
9962
|
};
|
|
9867
9963
|
}
|
|
9868
|
-
live
|
|
9964
|
+
live.set(l2.platform, l2);
|
|
9965
|
+
ctx.lease?.activate(owner, l2.platform);
|
|
9869
9966
|
if (/^[a-z][\w+.-]*:\/\//i.test(startPath)) await session.openLink(startPath);
|
|
9870
9967
|
const opening = [
|
|
9871
9968
|
...l2.draft?.signIn ? [RecordedStepSchema.parse({ id: "sign-in", type: "signIn", auth: l2.draft.signIn, role: "", name: "Sign in", scope: "", value: "", secret: false, legacySelector: "", proposals: [] })] : [],
|
|
@@ -9948,7 +10045,8 @@ function mobileHandlers(ctx) {
|
|
|
9948
10045
|
return { ok: true, result: { steps: l.draft.steps } };
|
|
9949
10046
|
}
|
|
9950
10047
|
if (op === "stop" || op === "discard") {
|
|
9951
|
-
live
|
|
10048
|
+
live.delete(l.platform);
|
|
10049
|
+
ctx.lease?.release(ownerOf(l), l.platform);
|
|
9952
10050
|
await l.session.close().catch(() => void 0);
|
|
9953
10051
|
if (op === "discard" || !l.draft) return { ok: true, result: { ended: true } };
|
|
9954
10052
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9972,6 +10070,7 @@ function mobileHandlers(ctx) {
|
|
|
9972
10070
|
}
|
|
9973
10071
|
return { ok: false, reason: "failed", detail: `unknown phone session operation "${op}"` };
|
|
9974
10072
|
} catch (error) {
|
|
10073
|
+
if (claimed && !live.has(claimed.platform)) ctx.lease?.release(claimed.owner, claimed.platform);
|
|
9975
10074
|
return { ok: false, reason: "failed", detail: error.message.split("\n")[0] ?? "the phone session failed" };
|
|
9976
10075
|
}
|
|
9977
10076
|
}
|
|
@@ -10068,10 +10167,13 @@ var ASK_MODE_TOOLS = [
|
|
|
10068
10167
|
function buildAskArgs(opts) {
|
|
10069
10168
|
const ask = opts.mode === "ask";
|
|
10070
10169
|
const folders = opts.folders ?? [];
|
|
10170
|
+
const brief = opts.brief?.trim();
|
|
10071
10171
|
const guidance = [
|
|
10072
|
-
`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
|
|
10172
|
+
`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. ` + (brief ? "The project brief below is this project's memory for this job: read it first." : "Start with project_brief: it is this project's memory."),
|
|
10073
10173
|
ask ? "ASK MODE: answer the question; change nothing. Cite what you rely on as [[testCase:<its id, tc_\u2026>]], [[knowledge:features/checkout.md]] 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.",
|
|
10074
|
-
folders.length ? `The project's code is in ${folders.join(" and ")}. Read it with Read, Grep and Glob when it helps; you cannot change it.` : ""
|
|
10174
|
+
folders.length ? `The project's code is in ${folders.join(" and ")}. Read it with Read, Grep and Glob when it helps; you cannot change it.` : "",
|
|
10175
|
+
brief ? `
|
|
10176
|
+
${brief}` : ""
|
|
10075
10177
|
].filter(Boolean).join("\n");
|
|
10076
10178
|
const flowriki = ask ? ASK_MODE_TOOLS.map((t) => `mcp__${MCP_SERVER_NAME}__${t}`) : [`mcp__${MCP_SERVER_NAME}`];
|
|
10077
10179
|
const files = folders.length ? ["Read", "Grep", "Glob"] : [];
|
|
@@ -17,7 +17,7 @@ import path3 from "path";
|
|
|
17
17
|
import fs4 from "fs";
|
|
18
18
|
import path4 from "path";
|
|
19
19
|
|
|
20
|
-
// ../test-model/dist/chunk-
|
|
20
|
+
// ../test-model/dist/chunk-I5COYCQ5.js
|
|
21
21
|
import { z as z2 } from "zod";
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
var PLATFORMS = ["web", "android", "ios"];
|
|
@@ -86,8 +86,8 @@ var DeviceChoiceSchema = z.discriminatedUnion("kind", [
|
|
|
86
86
|
}).strict()
|
|
87
87
|
]);
|
|
88
88
|
var MobileConfigSchema = z.object({
|
|
89
|
-
android: AndroidAppSchema.optional(),
|
|
90
|
-
ios: IosAppSchema.optional(),
|
|
89
|
+
android: AndroidAppSchema.partial({ appPackage: true }).optional(),
|
|
90
|
+
ios: IosAppSchema.partial({ bundleId: true }).optional(),
|
|
91
91
|
/** Between runs: keep the app's data (stays signed in), or start clean. */
|
|
92
92
|
reset: z.enum(["keepData", "fresh"]).default("keepData"),
|
|
93
93
|
/** Where runs go when nobody says — absent means whatever is available. */
|
|
@@ -98,6 +98,10 @@ var mobileOf = (config) => {
|
|
|
98
98
|
const parsed = MobileConfigSchema.safeParse(config?.mobile ?? {});
|
|
99
99
|
return parsed.success ? parsed.data : MobileConfigSchema.parse({});
|
|
100
100
|
};
|
|
101
|
+
var EnvironmentAppsSchema = z.object({
|
|
102
|
+
android: AndroidAppSchema.optional(),
|
|
103
|
+
ios: IosAppSchema.optional()
|
|
104
|
+
});
|
|
101
105
|
var DeviceInfoSchema = z.object({
|
|
102
106
|
id: z.string(),
|
|
103
107
|
name: z.string(),
|
|
@@ -1684,6 +1688,14 @@ var BugSchema = z7.object({
|
|
|
1684
1688
|
updatedAt: z7.string()
|
|
1685
1689
|
}).strict();
|
|
1686
1690
|
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(" "));
|
|
1691
|
+
function exploreSummary(markdown) {
|
|
1692
|
+
const screens = /^Screens:\s*(\d+)\s*$/im.exec(markdown)?.[1];
|
|
1693
|
+
return {
|
|
1694
|
+
exploredAt: /^Explored:\s*(\S+)\s*$/im.exec(markdown)?.[1] ?? null,
|
|
1695
|
+
environment: /^Environment:\s*(.+?)\s*$/im.exec(markdown)?.[1] ?? null,
|
|
1696
|
+
screens: screens ? Number(screens) : null
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1687
1699
|
var CODEBASE_TAGS = ["all", "web", "android", "ios", "mobile", "other"];
|
|
1688
1700
|
var CODEBASE_TAG_NAME = { all: "All combined", web: "Web", android: "Android", ios: "iOS", mobile: "Mobile", other: "Other" };
|
|
1689
1701
|
var CodebaseIndexSchema = z8.object({
|
|
@@ -2518,11 +2530,11 @@ var CLOUD_HUBS = {
|
|
|
2518
2530
|
function appOf(target) {
|
|
2519
2531
|
if (target.platform === "android") {
|
|
2520
2532
|
const app2 = target.mobile.android;
|
|
2521
|
-
if (!app2) throw new Error("This
|
|
2533
|
+
if (!app2?.appPackage) throw new Error("This environment has not named its Android app \u2014 add its package in Settings \u2192 Environments.");
|
|
2522
2534
|
return { id: app2.appPackage, ...app2.appActivity ? { activity: app2.appActivity } : {}, ...app2.build ? { build: app2.build } : {} };
|
|
2523
2535
|
}
|
|
2524
2536
|
const app = target.mobile.ios;
|
|
2525
|
-
if (!app) throw new Error("This
|
|
2537
|
+
if (!app?.bundleId) throw new Error("This environment has not named its iOS app \u2014 add its bundle ID in Settings \u2192 Environments.");
|
|
2526
2538
|
return { id: app.bundleId, ...app.build ? { build: app.build } : {} };
|
|
2527
2539
|
}
|
|
2528
2540
|
async function localBuild(build) {
|
|
@@ -2556,6 +2568,10 @@ async function localCapabilities(target, udid, opts = {}) {
|
|
|
2556
2568
|
"appium:appPackage": app.id,
|
|
2557
2569
|
...app.activity ? { "appium:appActivity": app.activity } : {},
|
|
2558
2570
|
"appium:appWaitActivity": "*",
|
|
2571
|
+
// Its own ports: two phones on one Appium each need their own driver
|
|
2572
|
+
// server and picture stream, or the second session steals the first's.
|
|
2573
|
+
...opts.ports?.system ? { "appium:systemPort": opts.ports.system } : {},
|
|
2574
|
+
...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
|
|
2559
2575
|
"appium:autoGrantPermissions": false,
|
|
2560
2576
|
"appium:disableWindowAnimation": true,
|
|
2561
2577
|
"appium:uiautomator2ServerLaunchTimeout": 12e4,
|
|
@@ -2572,6 +2588,8 @@ async function localCapabilities(target, udid, opts = {}) {
|
|
|
2572
2588
|
"appium:bundleId": app.id,
|
|
2573
2589
|
"appium:wdaLaunchTimeout": 24e4,
|
|
2574
2590
|
"appium:wdaConnectionTimeout": 24e4,
|
|
2591
|
+
...opts.ports?.wda ? { "appium:wdaLocalPort": opts.ports.wda } : {},
|
|
2592
|
+
...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
|
|
2575
2593
|
...target.mobile.ios?.teamId ? { "appium:xcodeOrgId": target.mobile.ios.teamId, "appium:xcodeSigningId": "Apple Development" } : {}
|
|
2576
2594
|
};
|
|
2577
2595
|
}
|
|
@@ -3191,6 +3209,8 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
|
|
|
3191
3209
|
throw last;
|
|
3192
3210
|
}
|
|
3193
3211
|
var uploaded = /* @__PURE__ */ new Map();
|
|
3212
|
+
var OPEN = /* @__PURE__ */ new Map();
|
|
3213
|
+
var openSessionOn = (platform) => OPEN.get(platform) ?? null;
|
|
3194
3214
|
var MobileSession = class _MobileSession {
|
|
3195
3215
|
constructor(driver, platform, appId, deviceName, cloud) {
|
|
3196
3216
|
this.driver = driver;
|
|
@@ -3231,6 +3251,7 @@ var MobileSession = class _MobileSession {
|
|
|
3231
3251
|
throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
|
|
3232
3252
|
});
|
|
3233
3253
|
const cloudSession = new _MobileSession(driver2, target.platform, app.id, choice2.device, true);
|
|
3254
|
+
OPEN.set(target.platform, cloudSession);
|
|
3234
3255
|
await cloudSession.settle(2e4);
|
|
3235
3256
|
return cloudSession;
|
|
3236
3257
|
}
|
|
@@ -3244,12 +3265,14 @@ var MobileSession = class _MobileSession {
|
|
|
3244
3265
|
const endpoint = await appiumServer();
|
|
3245
3266
|
const buildFile = app.build ? await localBuild(app.build) : null;
|
|
3246
3267
|
say(`Opening the app on ${known.name}\u2026`);
|
|
3247
|
-
const
|
|
3268
|
+
const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
|
|
3269
|
+
const capabilities = await localCapabilities(target, udid, { buildFile, ports });
|
|
3248
3270
|
const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
|
|
3249
3271
|
const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
|
|
3250
3272
|
throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
|
|
3251
3273
|
});
|
|
3252
3274
|
const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
|
|
3275
|
+
OPEN.set(target.platform, session);
|
|
3253
3276
|
await session.terminateApp().catch(() => {
|
|
3254
3277
|
});
|
|
3255
3278
|
await session.launchApp().catch(() => {
|
|
@@ -3482,6 +3505,7 @@ var MobileSession = class _MobileSession {
|
|
|
3482
3505
|
return { relPath: path4.basename(file), bytes: bytes.length };
|
|
3483
3506
|
}
|
|
3484
3507
|
async close() {
|
|
3508
|
+
if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
|
|
3485
3509
|
if (this.recording) await this.stopVideo(null);
|
|
3486
3510
|
await this.driver.close();
|
|
3487
3511
|
}
|
|
@@ -4053,6 +4077,8 @@ export {
|
|
|
4053
4077
|
isMobile,
|
|
4054
4078
|
stepRefusedOn,
|
|
4055
4079
|
platformsOf,
|
|
4080
|
+
AndroidAppSchema,
|
|
4081
|
+
IosAppSchema,
|
|
4056
4082
|
CLOUD_PROVIDERS,
|
|
4057
4083
|
CLOUD_PROVIDER_NAME,
|
|
4058
4084
|
DeviceChoiceSchema,
|
|
@@ -4086,6 +4112,7 @@ export {
|
|
|
4086
4112
|
PrerequisiteProblem,
|
|
4087
4113
|
expandForRun,
|
|
4088
4114
|
DeliveredHelperSchema,
|
|
4115
|
+
exploreSummary,
|
|
4089
4116
|
CODEBASE_TAG_NAME,
|
|
4090
4117
|
codebaseIndexOf,
|
|
4091
4118
|
runWhereOf,
|
|
@@ -4137,6 +4164,7 @@ export {
|
|
|
4137
4164
|
CheckError,
|
|
4138
4165
|
textOf,
|
|
4139
4166
|
checkMobileAssertion,
|
|
4167
|
+
openSessionOn,
|
|
4140
4168
|
MobileSession,
|
|
4141
4169
|
MOBILE_TIMEOUTS,
|
|
4142
4170
|
performMobileStep,
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
runtimeValue,
|
|
24
24
|
start,
|
|
25
25
|
status
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-6AR63WTN.js";
|
|
27
27
|
import {
|
|
28
28
|
CLOUD_PROVIDERS,
|
|
29
29
|
CLOUD_PROVIDER_NAME,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
RecordedStepSchema,
|
|
33
33
|
checkMobileAssertion,
|
|
34
34
|
codebaseIndexOf,
|
|
35
|
+
exploreSummary,
|
|
35
36
|
isMobile,
|
|
36
37
|
listDevices,
|
|
37
38
|
locate,
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
screenLines,
|
|
49
50
|
stepRefusedOn,
|
|
50
51
|
targetFor
|
|
51
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-FIR6NOLA.js";
|
|
52
53
|
|
|
53
54
|
// src/cli.ts
|
|
54
55
|
import { parseArgs } from "util";
|
|
@@ -325,6 +326,10 @@ var ServiceClient = class {
|
|
|
325
326
|
return this.call(this.scoped(`/bugs/${encodeURIComponent(id)}`, projectId), { method: "PATCH", body: JSON.stringify(body) });
|
|
326
327
|
}
|
|
327
328
|
// ── Knowledge, sources, requirements ──────────────────────
|
|
329
|
+
/** The project's brief for a job: its slice of the knowledge. */
|
|
330
|
+
brief(projectId, scope) {
|
|
331
|
+
return this.call(this.scoped("/brief", projectId, { ...scope.platform ? { platform: scope.platform } : {}, ...scope.features?.length ? { features: scope.features.join(",") } : {} }));
|
|
332
|
+
}
|
|
328
333
|
knowledgePages(projectId) {
|
|
329
334
|
return this.call(this.scoped("/knowledge", projectId));
|
|
330
335
|
}
|
|
@@ -985,7 +990,7 @@ Review and publish it here: ${review}`;
|
|
|
985
990
|
async startMobile(platform, catalog) {
|
|
986
991
|
const draft = this.draft;
|
|
987
992
|
const mobile = catalog.mobile;
|
|
988
|
-
if (!mobile) throw new Error("this project has no phone apps set up \u2014 turn on Android or iOS in Settings \u2192
|
|
993
|
+
if (!mobile) throw new Error("this project has no phone apps set up \u2014 turn on Android or iOS in Settings \u2192 Platforms");
|
|
989
994
|
const resolvers = draft.signIn || draft.device?.kind === "cloud" ? await this.prepare() : null;
|
|
990
995
|
this.mobile = await openMobileSession({
|
|
991
996
|
platform,
|
|
@@ -1233,7 +1238,10 @@ codebase/<folder>.md, which must start "# <folder>", then "Commit: <full sha>"
|
|
|
1233
1238
|
and "Learned: <yyyy-mm-dd>" lines: that is how the Knowledge screen shows it
|
|
1234
1239
|
as learned. codebase.json says which platform's code each folder holds (Web,
|
|
1235
1240
|
Android, iOS, Mobile, All combined, Other): read the web folder's page for a
|
|
1236
|
-
website test and the phone folder's for an app test.
|
|
1241
|
+
website test and the phone folder's for an app test. explore/<platform>.md is
|
|
1242
|
+
what the assistant saw WALKING that platform \u2014 screens, the words on them,
|
|
1243
|
+
journeys, rules it saw refused, vocabulary: read it before writing a test for
|
|
1244
|
+
that platform, because it says what a person sees; the code does not. Keep what you learn with write_knowledge (features/<name>.md,
|
|
1237
1245
|
memory/decisions.md, a session summary), on Claude's branch for review \u2014
|
|
1238
1246
|
never a password, key or token.
|
|
1239
1247
|
|
|
@@ -1412,7 +1420,7 @@ It is now the project these tools work on. Open it: ${service.origin}/projects/$
|
|
|
1412
1420
|
const here = phones.filter((d) => d.platform === p);
|
|
1413
1421
|
const farms = (c.mobile?.cloud ?? []).map((f) => CLOUD_PROVIDER_NAME[f.provider]);
|
|
1414
1422
|
const chosen = c.mobile?.devices[p];
|
|
1415
|
-
return `${p === "android" ? "Android" : "iOS"} app: ${app ?? "(not
|
|
1423
|
+
return `${p === "android" ? "Android" : "iOS"} app: ${app ?? "(not named \u2014 Settings \u2192 Environments)"}
|
|
1416
1424
|
Phones on this computer: ${here.length ? here.map((d) => `${d.name}${d.state === "ready" ? "" : ` (${d.state})`}`).join(", ") : "none"}` + (farms.length ? `
|
|
1417
1425
|
Cloud device farms: ${farms.join(", ")}` : "") + `
|
|
1418
1426
|
Runs: ${c.runWhere?.[p] === "cloud" ? "in the cloud" : "on this computer"}` + (chosen ? ` \u2014 ${chosen.kind === "local" ? chosen.name || chosen.id : `${CLOUD_PROVIDER_NAME[chosen.provider]}: ${chosen.device}`}` : "");
|
|
@@ -2009,11 +2017,19 @@ Use it with start_flow signIn {"profileId": "\u2026", "accountId": "${made.accou
|
|
|
2009
2017
|
{
|
|
2010
2018
|
name: "project_brief",
|
|
2011
2019
|
title: "Read the project's memory",
|
|
2012
|
-
description: "The project's own knowledge, in one call: About the product (product.md, and product/<web|android|ios>.md where a platform differs), memory/decisions.md, what each linked codebase folder was learned as and which platform's code it holds, and the list of feature pages and session summaries. Call it first in every conversation.",
|
|
2013
|
-
inputSchema: { type: "object", properties: { projectId: { type: "string" } }, additionalProperties: false },
|
|
2020
|
+
description: "The project's own knowledge, in one call: About the product (product.md, and product/<web|android|ios>.md where a platform differs), memory/decisions.md, what each linked codebase folder was learned as and which platform's code it holds, what each platform looked like when explored (explore/<platform>.md), and the list of feature pages and session summaries. Call it first in every conversation. With platform and/or features, the brief is the slice for that job: that platform's part and exploration, those features' notes, the code tagged for the platform, and who can sign in.",
|
|
2021
|
+
inputSchema: { type: "object", properties: { projectId: { type: "string" }, platform: { type: "string", enum: ["web", "android", "ios"], description: "The platform the job is about." }, features: { type: "array", items: { type: "string" }, description: "The features (or case folders) the job is about, by name." } }, additionalProperties: false },
|
|
2014
2022
|
annotations: { readOnlyHint: true },
|
|
2015
2023
|
run: async (args) => {
|
|
2016
2024
|
const projectId = await projectFor(args["projectId"]);
|
|
2025
|
+
const platform = str(args["platform"]);
|
|
2026
|
+
const features = Array.isArray(args["features"]) ? args["features"].filter((f) => typeof f === "string") : [];
|
|
2027
|
+
if (platform || features.length) {
|
|
2028
|
+
try {
|
|
2029
|
+
return text((await service.brief(projectId, { ...platform ? { platform } : {}, features })).brief);
|
|
2030
|
+
} catch {
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2017
2033
|
const { pages } = await service.knowledgePages(projectId);
|
|
2018
2034
|
const read = async (path2) => pages.some((p) => p.path === path2) ? (await service.knowledgePage(projectId, path2)).content : null;
|
|
2019
2035
|
const [product, glossary, decisions, web, android, ios, tagged] = await Promise.all(["product.md", "glossary.md", "memory/decisions.md", "product/web.md", "product/android.md", "product/ios.md", "codebase.json"].map(read));
|
|
@@ -2023,7 +2039,12 @@ Use it with start_flow signIn {"profileId": "\u2026", "accountId": "${made.accou
|
|
|
2023
2039
|
const holds = tags[p.path.slice("codebase/".length, -".md".length)]?.tags ?? [];
|
|
2024
2040
|
return `- ${p.path}${holds.length ? ` (${holds.map((t) => CODEBASE_TAG_NAME[t]).join(", ")} code)` : ""}: ${(await read(p.path))?.split("\n").filter((l) => l.trim()).slice(0, 3).join(" \xB7 ") ?? ""}`;
|
|
2025
2041
|
}));
|
|
2026
|
-
const
|
|
2042
|
+
const explored = pages.filter((p) => p.path.startsWith("explore/"));
|
|
2043
|
+
const walks = await Promise.all(explored.map(async (p) => {
|
|
2044
|
+
const s = exploreSummary(await read(p.path) ?? "");
|
|
2045
|
+
return `- ${p.path}: explored ${s.exploredAt ?? "?"}${s.environment ? ` in ${s.environment}` : ""}${s.screens !== null ? `, ${s.screens} screens` : ""} \u2014 read it with knowledge_read before testing that platform`;
|
|
2046
|
+
}));
|
|
2047
|
+
const shown = /* @__PURE__ */ new Set(["product.md", "glossary.md", "memory/decisions.md", "product/web.md", "product/android.md", "product/ios.md", "codebase.json", ...codebases.map((p) => p.path), ...explored.map((p) => p.path)]);
|
|
2027
2048
|
const parts = [
|
|
2028
2049
|
product ? `## About the product (product.md)
|
|
2029
2050
|
${product}` : "No About the product yet \u2014 the person writes it on the Knowledge screen; offer to help draft it.",
|
|
@@ -2039,6 +2060,8 @@ ${glossary}` : "",
|
|
|
2039
2060
|
${decisions}` : "",
|
|
2040
2061
|
codebases.length ? `## Codebase, as learned
|
|
2041
2062
|
${headers.join("\n")}` : "No codebase learned yet.",
|
|
2063
|
+
walks.length ? `## The product, as explored
|
|
2064
|
+
${walks.join("\n")}` : "",
|
|
2042
2065
|
`## Other pages
|
|
2043
2066
|
${pages.filter((p) => !shown.has(p.path)).map((p) => `- ${p.path}`).join("\n") || "none"}`
|
|
2044
2067
|
];
|
|
@@ -2074,7 +2097,7 @@ ${pages.filter((p) => !shown.has(p.path)).map((p) => `- ${p.path}`).join("\n") |
|
|
|
2074
2097
|
{
|
|
2075
2098
|
name: "write_knowledge",
|
|
2076
2099
|
title: "Write a knowledge page",
|
|
2077
|
-
description: `Write what the project should remember, on Claude's working branch for review: product.md (About the product \u2014 the person's own description; write it only when they ask) and product/<web|android|ios>.md (what is particular to that platform's app), codebase/<folder>.md (what a linked folder holds \u2014 starting with "# <folder>", "Commit: <sha>" and "Learned: <yyyy-mm-dd>" lines, then routes, screens, forms and their validation, roles, APIs, flags; summarised, never copied code), features/<name>.md (screens, rules, quirks), memory/decisions.md (decisions and conventions), memory/sessions/<yyyy-mm-dd>-<topic>.md (what a job did). append: true adds to the end instead of replacing. Never a password, key or token \u2014 those are refused.`,
|
|
2100
|
+
description: `Write what the project should remember, on Claude's working branch for review: product.md (About the product \u2014 the person's own description; write it only when they ask) and product/<web|android|ios>.md (what is particular to that platform's app), explore/<web|android|ios>.md (what you saw walking that platform: "# Explored: <Platform>", "Explored: <yyyy-mm-dd>", "Environment: <name>", "Screens: <n>" lines, then Screens, Journeys, Rules observed, Vocabulary, Suggested features), codebase/<folder>.md (what a linked folder holds \u2014 starting with "# <folder>", "Commit: <sha>" and "Learned: <yyyy-mm-dd>" lines, then routes, screens, forms and their validation, roles, APIs, flags; summarised, never copied code), features/<name>.md (screens, rules, quirks), memory/decisions.md (decisions and conventions), memory/sessions/<yyyy-mm-dd>-<topic>.md (what a job did). append: true adds to the end instead of replacing. Never a password, key or token \u2014 those are refused.`,
|
|
2078
2101
|
inputSchema: {
|
|
2079
2102
|
type: "object",
|
|
2080
2103
|
properties: { projectId: { type: "string" }, path: { type: "string" }, content: { type: "string" }, append: { type: "boolean" }, branch: { type: "string" } },
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
mobileSetup,
|
|
32
32
|
nodeAt,
|
|
33
33
|
openMobileSession,
|
|
34
|
+
openSessionOn,
|
|
34
35
|
performMobileStep,
|
|
35
36
|
readScreen,
|
|
36
37
|
replayMobileCase,
|
|
@@ -44,7 +45,7 @@ import {
|
|
|
44
45
|
textOf,
|
|
45
46
|
toolEnv,
|
|
46
47
|
typesSecrets
|
|
47
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-FIR6NOLA.js";
|
|
48
49
|
export {
|
|
49
50
|
AGENT_HOME,
|
|
50
51
|
APPIUM_DIR,
|
|
@@ -77,6 +78,7 @@ export {
|
|
|
77
78
|
mobileSetup,
|
|
78
79
|
nodeAt,
|
|
79
80
|
openMobileSession,
|
|
81
|
+
openSessionOn,
|
|
80
82
|
performMobileStep,
|
|
81
83
|
readScreen,
|
|
82
84
|
replayMobileCase,
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED