@swmansion/argent 0.16.2-next.8 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/argent-android-devtools-0.1.0.apk +0 -0
- package/bin/darwin/ax-service +0 -0
- package/bin/darwin/tvos-ax-service +0 -0
- package/bin/darwin/tvos-hid-daemon +0 -0
- package/bin/tcp/ax-service +0 -0
- package/dist/cli-cmds.mjs +1710 -1594
- package/dist/installer.mjs +67 -63
- package/dist/mcp-server.mjs +306 -188
- package/dist/tool-server.cjs +206 -52
- package/dylibs/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/libKeyboardPatch.dylib +0 -0
- package/dylibs/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tcp/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tcp/libKeyboardPatch.dylib +0 -0
- package/dylibs/tcp/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tvos/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tvos/libKeyboardPatch.dylib +0 -0
- package/dylibs/tvos/libNativeDevtoolsIos.dylib +0 -0
- package/package.json +1 -1
- package/skills/argent-screen-recording/SKILL.md +10 -4
package/dist/tool-server.cjs
CHANGED
|
@@ -109,7 +109,9 @@ var init_artifacts = __esm({
|
|
|
109
109
|
".xml": "application/xml",
|
|
110
110
|
".txt": "text/plain",
|
|
111
111
|
".md": "text/markdown",
|
|
112
|
-
".html": "text/html"
|
|
112
|
+
".html": "text/html",
|
|
113
|
+
".mp4": "video/mp4",
|
|
114
|
+
".mov": "video/quicktime"
|
|
113
115
|
};
|
|
114
116
|
ArtifactStore = class {
|
|
115
117
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -138,6 +140,7 @@ var init_artifacts = __esm({
|
|
|
138
140
|
};
|
|
139
141
|
if (mtimeMs != null) handle.mtimeMs = mtimeMs;
|
|
140
142
|
if (isDirectory2) handle.archive = "tar.gz";
|
|
143
|
+
if (opts?.saveDir) handle.saveDir = opts.saveDir;
|
|
141
144
|
return handle;
|
|
142
145
|
}
|
|
143
146
|
get(id) {
|
|
@@ -382,6 +385,7 @@ var init_failure_codes = __esm({
|
|
|
382
385
|
SCREEN_RECORDING_NO_ACTIVE_SESSION: "SCREEN_RECORDING_NO_ACTIVE_SESSION",
|
|
383
386
|
SCREEN_RECORDING_STOP_IN_PROGRESS: "SCREEN_RECORDING_STOP_IN_PROGRESS",
|
|
384
387
|
SCREEN_RECORDING_START_EXITED: "SCREEN_RECORDING_START_EXITED",
|
|
388
|
+
SCREEN_RECORDING_START_TIMEOUT: "SCREEN_RECORDING_START_TIMEOUT",
|
|
385
389
|
SCREEN_RECORDING_PROCESS_ERROR: "SCREEN_RECORDING_PROCESS_ERROR",
|
|
386
390
|
SCREEN_RECORDING_OUTPUT_MISSING: "SCREEN_RECORDING_OUTPUT_MISSING",
|
|
387
391
|
SCREEN_RECORDING_SERVER_SHUTTING_DOWN: "SCREEN_RECORDING_SERVER_SHUTTING_DOWN",
|
|
@@ -34931,7 +34935,7 @@ var require_view = __commonJS({
|
|
|
34931
34935
|
var path38 = require("node:path");
|
|
34932
34936
|
var fs50 = require("node:fs");
|
|
34933
34937
|
var dirname14 = path38.dirname;
|
|
34934
|
-
var
|
|
34938
|
+
var basename10 = path38.basename;
|
|
34935
34939
|
var extname4 = path38.extname;
|
|
34936
34940
|
var join46 = path38.join;
|
|
34937
34941
|
var resolve7 = path38.resolve;
|
|
@@ -34970,7 +34974,7 @@ var require_view = __commonJS({
|
|
|
34970
34974
|
var root2 = roots[i];
|
|
34971
34975
|
var loc = resolve7(root2, name);
|
|
34972
34976
|
var dir = dirname14(loc);
|
|
34973
|
-
var file2 =
|
|
34977
|
+
var file2 = basename10(loc);
|
|
34974
34978
|
path39 = this.resolve(dir, file2);
|
|
34975
34979
|
}
|
|
34976
34980
|
return path39;
|
|
@@ -35000,7 +35004,7 @@ var require_view = __commonJS({
|
|
|
35000
35004
|
if (stat4 && stat4.isFile()) {
|
|
35001
35005
|
return path39;
|
|
35002
35006
|
}
|
|
35003
|
-
path39 = join46(dir,
|
|
35007
|
+
path39 = join46(dir, basename10(file2, ext), "index" + ext);
|
|
35004
35008
|
stat4 = tryStat(path39);
|
|
35005
35009
|
if (stat4 && stat4.isFile()) {
|
|
35006
35010
|
return path39;
|
|
@@ -57445,9 +57449,9 @@ var require_content_disposition = __commonJS({
|
|
|
57445
57449
|
if (typeof fallback === "string" && NON_LATIN1_REGEXP.test(fallback)) {
|
|
57446
57450
|
throw new TypeError("fallback must be ISO-8859-1 string");
|
|
57447
57451
|
}
|
|
57448
|
-
var name =
|
|
57452
|
+
var name = basename10(filename);
|
|
57449
57453
|
var isQuotedString = TEXT_REGEXP.test(name);
|
|
57450
|
-
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) :
|
|
57454
|
+
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename10(fallback);
|
|
57451
57455
|
var hasFallback = typeof fallbackName === "string" && fallbackName !== name;
|
|
57452
57456
|
if (hasFallback || !isQuotedString || hasHexEscape(name)) {
|
|
57453
57457
|
params["filename*"] = name;
|
|
@@ -57567,7 +57571,7 @@ var require_content_disposition = __commonJS({
|
|
|
57567
57571
|
this.type = type;
|
|
57568
57572
|
this.parameters = parameters;
|
|
57569
57573
|
}
|
|
57570
|
-
function
|
|
57574
|
+
function basename10(path38) {
|
|
57571
57575
|
const normalized = path38.replaceAll("\\", "/");
|
|
57572
57576
|
let end = normalized.length;
|
|
57573
57577
|
while (end > 0 && normalized[end - 1] === "/") {
|
|
@@ -104489,7 +104493,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
|
|
|
104489
104493
|
var SESSION_ID = (0, import_node_crypto3.randomUUID)();
|
|
104490
104494
|
function readCliVersion() {
|
|
104491
104495
|
if (true) {
|
|
104492
|
-
return "0.
|
|
104496
|
+
return "0.17.0";
|
|
104493
104497
|
}
|
|
104494
104498
|
return "0.0.0";
|
|
104495
104499
|
}
|
|
@@ -105877,7 +105881,7 @@ var import_node_path5 = __toESM(require("node:path"));
|
|
|
105877
105881
|
var import_semver = __toESM(require_semver2());
|
|
105878
105882
|
|
|
105879
105883
|
// ../tool-server/package.json
|
|
105880
|
-
var version3 = "0.
|
|
105884
|
+
var version3 = "0.17.0";
|
|
105881
105885
|
|
|
105882
105886
|
// ../tool-server/src/utils/update-checker.ts
|
|
105883
105887
|
var import_update_core = __toESM(require_dist4());
|
|
@@ -114038,6 +114042,27 @@ function sendCommand(api, cmd) {
|
|
|
114038
114042
|
ws.once("open", () => ws.send(payload));
|
|
114039
114043
|
}
|
|
114040
114044
|
}
|
|
114045
|
+
function setPointerVisible(api, show, signal) {
|
|
114046
|
+
return pointerPost(api, { show }, signal);
|
|
114047
|
+
}
|
|
114048
|
+
function setPointerTrail(api, trail, signal) {
|
|
114049
|
+
return pointerPost(api, { trail }, signal);
|
|
114050
|
+
}
|
|
114051
|
+
async function pointerPost(api, body, signal) {
|
|
114052
|
+
try {
|
|
114053
|
+
const res = await fetch(`${api.apiUrl}/api/pointer`, {
|
|
114054
|
+
method: "POST",
|
|
114055
|
+
headers: { "Content-Type": "application/json" },
|
|
114056
|
+
body: JSON.stringify(body),
|
|
114057
|
+
signal
|
|
114058
|
+
});
|
|
114059
|
+
if (!res.ok) return false;
|
|
114060
|
+
const parsed = await res.json().catch(() => null);
|
|
114061
|
+
return parsed?.status === "ok";
|
|
114062
|
+
} catch {
|
|
114063
|
+
return false;
|
|
114064
|
+
}
|
|
114065
|
+
}
|
|
114041
114066
|
async function simulatorPost(toolLabel, api, endpoint, reqBody, signal, fallbackHint) {
|
|
114042
114067
|
let res;
|
|
114043
114068
|
try {
|
|
@@ -142986,6 +143011,8 @@ function clearLiveState2(state3) {
|
|
|
142986
143011
|
state3.pendingChild = null;
|
|
142987
143012
|
state3.frameStream = null;
|
|
142988
143013
|
state3.lastFrameStreamError = null;
|
|
143014
|
+
state3.pointerDisable = null;
|
|
143015
|
+
state3.pointerFailed = false;
|
|
142989
143016
|
state3.recordingTimedOut = false;
|
|
142990
143017
|
state3.recordingExitedUnexpectedly = false;
|
|
142991
143018
|
state3.lastExitInfo = null;
|
|
@@ -143036,6 +143063,11 @@ var screenRecordingSessionBlueprint = {
|
|
|
143036
143063
|
frameStream: null,
|
|
143037
143064
|
lastFrameStreamError: null,
|
|
143038
143065
|
pumpTimer: null,
|
|
143066
|
+
trimStatic: true,
|
|
143067
|
+
framesWritten: 0,
|
|
143068
|
+
trimmedAnyFrames: false,
|
|
143069
|
+
pointerDisable: null,
|
|
143070
|
+
pointerFailed: false,
|
|
143039
143071
|
wallClockStartMs: null,
|
|
143040
143072
|
wallClockEndMs: null,
|
|
143041
143073
|
timeLimitSeconds: null,
|
|
@@ -143058,6 +143090,12 @@ var screenRecordingSessionBlueprint = {
|
|
|
143058
143090
|
state3.pumpTimer = null;
|
|
143059
143091
|
}
|
|
143060
143092
|
state3.frameStream?.close();
|
|
143093
|
+
if (state3.pointerDisable) {
|
|
143094
|
+
const disable = state3.pointerDisable;
|
|
143095
|
+
state3.pointerDisable = null;
|
|
143096
|
+
await disable().catch(() => {
|
|
143097
|
+
});
|
|
143098
|
+
}
|
|
143061
143099
|
if (state3.pendingChild) {
|
|
143062
143100
|
try {
|
|
143063
143101
|
state3.pendingChild.kill("SIGKILL");
|
|
@@ -143486,6 +143524,7 @@ var OUTPUT_FPS2 = 30;
|
|
|
143486
143524
|
var FRAME_INTERVAL_MS = 1e3 / OUTPUT_FPS2;
|
|
143487
143525
|
var MAX_CATCHUP_FRAMES = 5;
|
|
143488
143526
|
var MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
143527
|
+
var STATIC_GRACE_MS = 1e3;
|
|
143489
143528
|
var STREAM_CONNECT_TIMEOUT_MS = 1e4;
|
|
143490
143529
|
var FIRST_FRAME_TIMEOUT_MS = 1e4;
|
|
143491
143530
|
var START_FAILFAST_GRACE_MS = 800;
|
|
@@ -143542,24 +143581,59 @@ function ffmpegArgs(opts) {
|
|
|
143542
143581
|
function framesDue(startedAtMs, nowMs) {
|
|
143543
143582
|
return Math.floor((nowMs - startedAtMs) * OUTPUT_FPS2 / 1e3);
|
|
143544
143583
|
}
|
|
143584
|
+
function sameFrame(a, b) {
|
|
143585
|
+
if (a === b) return true;
|
|
143586
|
+
if (!a || !b) return false;
|
|
143587
|
+
return a.equals(b);
|
|
143588
|
+
}
|
|
143545
143589
|
function startPump(api, stream) {
|
|
143546
143590
|
const child = api.captureProcess;
|
|
143547
|
-
const
|
|
143548
|
-
|
|
143591
|
+
const trim = api.trimStatic;
|
|
143592
|
+
api.framesWritten = 0;
|
|
143593
|
+
api.trimmedAnyFrames = false;
|
|
143594
|
+
let paceBaseMs = api.wallClockStartMs ?? Date.now();
|
|
143595
|
+
let paceBaseFrames = 0;
|
|
143596
|
+
let lastFrame = null;
|
|
143597
|
+
let lastChangeMs = paceBaseMs;
|
|
143598
|
+
let dead = false;
|
|
143549
143599
|
api.pumpTimer = setInterval(() => {
|
|
143550
143600
|
const stdin = child?.stdin;
|
|
143551
143601
|
if (!stdin || !stdin.writable) return;
|
|
143552
143602
|
const frame = stream.latest;
|
|
143553
143603
|
if (!frame) return;
|
|
143554
143604
|
if (stdin.writableLength > MAX_BUFFERED_BYTES) return;
|
|
143555
|
-
const
|
|
143605
|
+
const now = Date.now();
|
|
143606
|
+
if (trim) {
|
|
143607
|
+
if (!sameFrame(frame, lastFrame)) {
|
|
143608
|
+
lastFrame = frame;
|
|
143609
|
+
lastChangeMs = now;
|
|
143610
|
+
}
|
|
143611
|
+
if (now - lastChangeMs > STATIC_GRACE_MS) {
|
|
143612
|
+
dead = true;
|
|
143613
|
+
api.trimmedAnyFrames = true;
|
|
143614
|
+
return;
|
|
143615
|
+
}
|
|
143616
|
+
if (dead) {
|
|
143617
|
+
dead = false;
|
|
143618
|
+
paceBaseMs = now;
|
|
143619
|
+
paceBaseFrames = api.framesWritten;
|
|
143620
|
+
}
|
|
143621
|
+
}
|
|
143622
|
+
const target = paceBaseFrames + framesDue(paceBaseMs, now);
|
|
143623
|
+
const missing = Math.min(target - api.framesWritten, MAX_CATCHUP_FRAMES);
|
|
143556
143624
|
for (let i = 0; i < missing; i++) {
|
|
143557
143625
|
if (!stdin.writable) return;
|
|
143558
143626
|
stdin.write(frame);
|
|
143559
|
-
|
|
143627
|
+
api.framesWritten++;
|
|
143560
143628
|
}
|
|
143561
143629
|
}, FRAME_INTERVAL_MS);
|
|
143562
143630
|
}
|
|
143631
|
+
async function disablePointer(api) {
|
|
143632
|
+
const disable = api.pointerDisable;
|
|
143633
|
+
api.pointerDisable = null;
|
|
143634
|
+
if (disable) await disable().catch(() => {
|
|
143635
|
+
});
|
|
143636
|
+
}
|
|
143563
143637
|
function stopPump(api) {
|
|
143564
143638
|
if (api.pumpTimer) {
|
|
143565
143639
|
clearInterval(api.pumpTimer);
|
|
@@ -143654,11 +143728,14 @@ async function startCaptureLocked(api, params) {
|
|
|
143654
143728
|
api.recordingTimedOut = false;
|
|
143655
143729
|
api.recordingExitedUnexpectedly = false;
|
|
143656
143730
|
api.pendingRetrieval = false;
|
|
143731
|
+
api.pointerFailed = false;
|
|
143657
143732
|
api.lastExitInfo = null;
|
|
143658
143733
|
api.lastFrameStreamError = null;
|
|
143659
143734
|
api.outputFile = outputFile;
|
|
143660
143735
|
api.logoFile = logoFile;
|
|
143661
143736
|
api.watermarkSkipped = watermarkSkipped;
|
|
143737
|
+
api.trimStatic = params.trimStatic;
|
|
143738
|
+
api.framesWritten = 0;
|
|
143662
143739
|
api.captureProcess = child;
|
|
143663
143740
|
api.frameStream = stream;
|
|
143664
143741
|
api.recordingActive = true;
|
|
@@ -143667,16 +143744,6 @@ async function startCaptureLocked(api, params) {
|
|
|
143667
143744
|
api.timeLimitSeconds = params.timeLimitSeconds;
|
|
143668
143745
|
registerActiveScreenRecording(api.deviceId, api.wallClockStartMs, params.timeLimitSeconds);
|
|
143669
143746
|
startPump(api, stream);
|
|
143670
|
-
api.recordingTimeout = setTimeout(() => {
|
|
143671
|
-
api.recordingTimeout = null;
|
|
143672
|
-
if (api.captureProcess !== child) return;
|
|
143673
|
-
api.recordingTimedOut = true;
|
|
143674
|
-
api.recordingActive = false;
|
|
143675
|
-
api.wallClockEndMs = Date.now();
|
|
143676
|
-
api.pendingRetrieval = true;
|
|
143677
|
-
markScreenRecordingFinalized(api.deviceId, `it hit its ${params.timeLimitSeconds}s time limit`);
|
|
143678
|
-
finalizeCapture(api);
|
|
143679
|
-
}, params.timeLimitSeconds * 1e3);
|
|
143680
143747
|
child.on("exit", (code, signal) => {
|
|
143681
143748
|
if (api.captureProcess !== child) return;
|
|
143682
143749
|
api.lastExitInfo = { code, signal };
|
|
@@ -143691,9 +143758,25 @@ async function startCaptureLocked(api, params) {
|
|
|
143691
143758
|
api.wallClockEndMs = Date.now();
|
|
143692
143759
|
api.pendingRetrieval = true;
|
|
143693
143760
|
stopPump(api);
|
|
143761
|
+
void disablePointer(api);
|
|
143694
143762
|
markScreenRecordingFinalized(api.deviceId, "the recording process exited unexpectedly");
|
|
143695
143763
|
}
|
|
143696
143764
|
});
|
|
143765
|
+
if (params.pointer) {
|
|
143766
|
+
api.pointerDisable = params.pointer.disable;
|
|
143767
|
+
api.pointerFailed = !await params.pointer.enable();
|
|
143768
|
+
}
|
|
143769
|
+
api.recordingTimeout = setTimeout(() => {
|
|
143770
|
+
api.recordingTimeout = null;
|
|
143771
|
+
if (api.captureProcess !== child) return;
|
|
143772
|
+
api.recordingTimedOut = true;
|
|
143773
|
+
api.recordingActive = false;
|
|
143774
|
+
api.wallClockEndMs = Date.now();
|
|
143775
|
+
api.pendingRetrieval = true;
|
|
143776
|
+
markScreenRecordingFinalized(api.deviceId, `it hit its ${params.timeLimitSeconds}s time limit`);
|
|
143777
|
+
finalizeCapture(api);
|
|
143778
|
+
void disablePointer(api);
|
|
143779
|
+
}, params.timeLimitSeconds * 1e3);
|
|
143697
143780
|
return {
|
|
143698
143781
|
status: "recording",
|
|
143699
143782
|
timeLimitSeconds: params.timeLimitSeconds,
|
|
@@ -143756,9 +143839,11 @@ async function stopCapture(api) {
|
|
|
143756
143839
|
const outputFile = api.outputFile;
|
|
143757
143840
|
const logoFile = api.logoFile;
|
|
143758
143841
|
const startedAtMs = api.wallClockStartMs;
|
|
143842
|
+
const trimStatic = api.trimStatic;
|
|
143759
143843
|
const endedEarly = api.recordingTimedOut || api.recordingExitedUnexpectedly;
|
|
143760
143844
|
const streamError = api.frameStream?.error ?? api.lastFrameStreamError ?? null;
|
|
143761
143845
|
const watermarkSkipped = api.watermarkSkipped;
|
|
143846
|
+
const pointerFailed = api.pointerFailed;
|
|
143762
143847
|
let warning;
|
|
143763
143848
|
try {
|
|
143764
143849
|
const child = api.captureProcess;
|
|
@@ -143792,9 +143877,23 @@ async function stopCapture(api) {
|
|
|
143792
143877
|
if (watermarkSkipped) {
|
|
143793
143878
|
warning = [warning, `The watermark was not applied (${watermarkSkipped}).`].filter(Boolean).join(" ");
|
|
143794
143879
|
}
|
|
143880
|
+
if (pointerFailed) {
|
|
143881
|
+
warning = [
|
|
143882
|
+
warning,
|
|
143883
|
+
"The touch visualizer could not be enabled on simulator-server, so touches are not shown in this video."
|
|
143884
|
+
].filter(Boolean).join(" ");
|
|
143885
|
+
}
|
|
143795
143886
|
const size = await statNonEmptyOutput(outputFile, "screen_recording_stop");
|
|
143796
|
-
const
|
|
143797
|
-
|
|
143887
|
+
const wallClockMs = startedAtMs === null ? null : (api.wallClockEndMs ?? Date.now()) - startedAtMs;
|
|
143888
|
+
const durationMs = trimStatic ? Math.round(api.framesWritten / OUTPUT_FPS2 * 1e3) : wallClockMs;
|
|
143889
|
+
const trimmedMs = trimStatic && wallClockMs !== null && api.trimmedAnyFrames ? Math.max(0, wallClockMs - durationMs) : void 0;
|
|
143890
|
+
return {
|
|
143891
|
+
outputFile,
|
|
143892
|
+
sizeBytes: size,
|
|
143893
|
+
durationMs,
|
|
143894
|
+
...trimmedMs !== void 0 ? { wallClockMs, trimmedMs } : {},
|
|
143895
|
+
...warning ? { warning } : {}
|
|
143896
|
+
};
|
|
143798
143897
|
} catch (err) {
|
|
143799
143898
|
const empty2 = await import_fs17.promises.stat(outputFile).then((s) => s.size === 0).catch(() => false);
|
|
143800
143899
|
if (empty2) await import_fs17.promises.rm(outputFile, { force: true }).catch(() => {
|
|
@@ -143802,6 +143901,7 @@ async function stopCapture(api) {
|
|
|
143802
143901
|
throw err;
|
|
143803
143902
|
} finally {
|
|
143804
143903
|
stopPump(api);
|
|
143904
|
+
await disablePointer(api);
|
|
143805
143905
|
api.recordingActive = false;
|
|
143806
143906
|
api.stopPending = false;
|
|
143807
143907
|
api.pendingRetrieval = false;
|
|
@@ -143809,6 +143909,9 @@ async function stopCapture(api) {
|
|
|
143809
143909
|
api.outputFile = null;
|
|
143810
143910
|
api.logoFile = null;
|
|
143811
143911
|
api.watermarkSkipped = null;
|
|
143912
|
+
api.pointerFailed = false;
|
|
143913
|
+
api.framesWritten = 0;
|
|
143914
|
+
api.trimmedAnyFrames = false;
|
|
143812
143915
|
api.wallClockStartMs = null;
|
|
143813
143916
|
api.wallClockEndMs = null;
|
|
143814
143917
|
api.timeLimitSeconds = null;
|
|
@@ -143825,10 +143928,18 @@ async function stopCapture(api) {
|
|
|
143825
143928
|
// ../tool-server/src/tools/screen-recording/screen-recording-start.ts
|
|
143826
143929
|
var DEFAULT_TIME_LIMIT_SECONDS = 180;
|
|
143827
143930
|
var MAX_TIME_LIMIT_SECONDS = 600;
|
|
143931
|
+
var POINTER_TRAIL_LENGTH = 8;
|
|
143932
|
+
var POINTER_REQUEST_TIMEOUT_MS = 2e3;
|
|
143828
143933
|
var zodSchema51 = external_exports.object({
|
|
143829
143934
|
udid: external_exports.string().describe("Target device id from `list-devices` (iOS Simulator UDID or Android serial)."),
|
|
143830
143935
|
timeLimitSeconds: external_exports.number().int().min(1).max(MAX_TIME_LIMIT_SECONDS).optional().describe(
|
|
143831
143936
|
`Auto-stop cap in seconds (default ${DEFAULT_TIME_LIMIT_SECONDS}, max ${MAX_TIME_LIMIT_SECONDS}). Set it to slightly more than the interaction you plan to capture.`
|
|
143937
|
+
),
|
|
143938
|
+
trimStatic: external_exports.boolean().optional().describe(
|
|
143939
|
+
"Default true. Collapse stretches where the screen does not change: the first second of each still stretch is kept, then unchanged frames are dropped until something moves again, so a long recording with brief activity comes back short instead of full of dead air. The returned durationMs is the trimmed length; wallClockMs/trimmedMs report what was removed. Set false to keep a faithful real-time recording."
|
|
143940
|
+
),
|
|
143941
|
+
showTouches: external_exports.boolean().optional().describe(
|
|
143942
|
+
"Default true. Draw simulator-server's touch visualizer into the recording: a pulse marks each tap, a comet trail follows swipes and drags, and paired markers show two-finger pinch/rotate, so the video makes clear where every interaction landed. Set false to record the raw screen with no overlay."
|
|
143832
143943
|
)
|
|
143833
143944
|
});
|
|
143834
143945
|
var capability26 = {
|
|
@@ -143839,7 +143950,9 @@ function createScreenRecordingStartTool(registry2) {
|
|
|
143839
143950
|
return {
|
|
143840
143951
|
id: "screen-recording-start",
|
|
143841
143952
|
capability: capability26,
|
|
143842
|
-
description: `Start recording the device screen to a video file (h264 mp4,
|
|
143953
|
+
description: `Start recording the device screen to a video file (h264 mp4, 30fps at the device's native resolution).
|
|
143954
|
+
By default stretches where the screen does not change are trimmed out (see trimStatic), so a long session with only brief activity comes back as a short clip instead of minutes of dead air.
|
|
143955
|
+
By default every tap, swipe, drag, pinch and rotate is drawn into the video as an on-screen touch marker (see showTouches), so the recording shows where each interaction landed.
|
|
143843
143956
|
The recording keeps running across other tool calls (every result carries a reminder) until \`screen-recording-stop\` is called or timeLimitSeconds elapses \u2014 immediately after starting, set yourself a reminder/wakeup for the expected end of the recording so it is never left running.
|
|
143844
143957
|
Use when the user wants a video of an interaction, animation, or app behavior \u2014 for a single still frame use \`screenshot\` instead.
|
|
143845
143958
|
Returns { status: "recording", timeLimitSeconds, outputFile } \u2014 the video is retrieved later by \`screen-recording-stop\`, not by reading outputFile directly.
|
|
@@ -143884,20 +143997,53 @@ Fails if a recording is already running on the device, the device is not booted,
|
|
|
143884
143997
|
}
|
|
143885
143998
|
);
|
|
143886
143999
|
}
|
|
144000
|
+
const showTouches = params.showTouches ?? true;
|
|
144001
|
+
const pointer = showTouches ? makePointerControl(simulator) : void 0;
|
|
143887
144002
|
return startCapture(api, {
|
|
143888
144003
|
streamUrl,
|
|
143889
144004
|
timeLimitSeconds,
|
|
143890
|
-
watermark: isFeatureEnabled("video-watermark")
|
|
144005
|
+
watermark: isFeatureEnabled("video-watermark"),
|
|
144006
|
+
trimStatic: params.trimStatic ?? true,
|
|
144007
|
+
pointer
|
|
144008
|
+
});
|
|
144009
|
+
}
|
|
144010
|
+
};
|
|
144011
|
+
}
|
|
144012
|
+
function makePointerControl(simulator) {
|
|
144013
|
+
let enabling = null;
|
|
144014
|
+
return {
|
|
144015
|
+
async enable() {
|
|
144016
|
+
const run2 = (async () => {
|
|
144017
|
+
await setPointerTrail(
|
|
144018
|
+
simulator,
|
|
144019
|
+
POINTER_TRAIL_LENGTH,
|
|
144020
|
+
AbortSignal.timeout(POINTER_REQUEST_TIMEOUT_MS)
|
|
144021
|
+
);
|
|
144022
|
+
return setPointerVisible(simulator, true, AbortSignal.timeout(POINTER_REQUEST_TIMEOUT_MS));
|
|
144023
|
+
})();
|
|
144024
|
+
enabling = run2;
|
|
144025
|
+
try {
|
|
144026
|
+
return await run2;
|
|
144027
|
+
} finally {
|
|
144028
|
+
if (enabling === run2) enabling = null;
|
|
144029
|
+
}
|
|
144030
|
+
},
|
|
144031
|
+
async disable() {
|
|
144032
|
+
const pending = enabling;
|
|
144033
|
+
if (pending) await pending.catch(() => {
|
|
143891
144034
|
});
|
|
144035
|
+
await setPointerVisible(simulator, false, AbortSignal.timeout(POINTER_REQUEST_TIMEOUT_MS));
|
|
143892
144036
|
}
|
|
143893
144037
|
};
|
|
143894
144038
|
}
|
|
143895
144039
|
|
|
143896
144040
|
// ../tool-server/src/tools/screen-recording/screen-recording-stop.ts
|
|
144041
|
+
var import_node_path20 = require("node:path");
|
|
143897
144042
|
init_zod();
|
|
143898
144043
|
var zodSchema52 = external_exports.object({
|
|
143899
144044
|
udid: external_exports.string().describe("Target device id from `list-devices` (iOS Simulator UDID or Android serial).")
|
|
143900
144045
|
});
|
|
144046
|
+
var RECORDINGS_DIR = ".argent/recordings";
|
|
143901
144047
|
var capability27 = {
|
|
143902
144048
|
apple: { simulator: true },
|
|
143903
144049
|
android: { emulator: true, device: true, unknown: true }
|
|
@@ -143908,7 +144054,7 @@ var screenRecordingStopTool = {
|
|
|
143908
144054
|
description: `Stop the screen recording started by \`screen-recording-start\` and retrieve the video: frame capture ends and ffmpeg finalizes the mp4.
|
|
143909
144055
|
Also retrieves the video when the recording already ended on its own (time limit reached, capture process died) \u2014 call it even after the cap fired.
|
|
143910
144056
|
Use when the interaction being captured is finished, or a tool-result note reminds you a recording is still running.
|
|
143911
|
-
Returns { video, durationMs, warning? }; video is a downloadable artifact materialized to a local path.
|
|
144057
|
+
Returns { video, durationMs, wallClockMs?, trimmedMs?, warning? }; video is a downloadable artifact materialized to a local path. When static-frame trimming removed dead air, durationMs is the trimmed video length and wallClockMs/trimmedMs report the real duration and how much was cut.
|
|
143912
144058
|
Fails if no recording (running or finished-but-unretrieved) exists for the given udid.`,
|
|
143913
144059
|
searchHint: "stop end finish screen recording video capture save retrieve",
|
|
143914
144060
|
zodSchema: zodSchema52,
|
|
@@ -143921,8 +144067,16 @@ Fails if no recording (running or finished-but-unretrieved) exists for the given
|
|
|
143921
144067
|
assertSupported("screen-recording-stop", capability27, device);
|
|
143922
144068
|
const stopped = await stopCapture(api);
|
|
143923
144069
|
const artifacts = requireArtifacts(ctx);
|
|
143924
|
-
const video = await artifacts.register(stopped.outputFile, {
|
|
144070
|
+
const video = await artifacts.register(stopped.outputFile, {
|
|
144071
|
+
mimeType: "video/mp4",
|
|
144072
|
+
// Drop the internal `argent-` temp prefix so the saved file reads cleanly
|
|
144073
|
+
// as `.argent/recordings/screen-recording-<device>-<ts>.mp4`.
|
|
144074
|
+
filename: (0, import_node_path20.basename)(stopped.outputFile).replace(/^argent-/, ""),
|
|
144075
|
+
saveDir: RECORDINGS_DIR
|
|
144076
|
+
});
|
|
143925
144077
|
const result = { video, durationMs: stopped.durationMs };
|
|
144078
|
+
if (stopped.wallClockMs !== void 0) result.wallClockMs = stopped.wallClockMs;
|
|
144079
|
+
if (stopped.trimmedMs !== void 0) result.trimmedMs = stopped.trimmedMs;
|
|
143926
144080
|
if (stopped.warning) result.warning = stopped.warning;
|
|
143927
144081
|
return result;
|
|
143928
144082
|
}
|
|
@@ -150941,7 +151095,7 @@ init_zod();
|
|
|
150941
151095
|
|
|
150942
151096
|
// ../tool-server/src/utils/workspace-reader.ts
|
|
150943
151097
|
var import_promises12 = require("node:fs/promises");
|
|
150944
|
-
var
|
|
151098
|
+
var import_node_path21 = require("node:path");
|
|
150945
151099
|
var import_node_child_process27 = require("node:child_process");
|
|
150946
151100
|
var import_dotenv = __toESM(require_main());
|
|
150947
151101
|
async function exists(path38) {
|
|
@@ -151029,7 +151183,7 @@ async function detectLockfile(workspacePath) {
|
|
|
151029
151183
|
const checks = await Promise.all(
|
|
151030
151184
|
LOCKFILES.map(async (name) => ({
|
|
151031
151185
|
name,
|
|
151032
|
-
exists: await exists((0,
|
|
151186
|
+
exists: await exists((0, import_node_path21.join)(workspacePath, name))
|
|
151033
151187
|
}))
|
|
151034
151188
|
);
|
|
151035
151189
|
return checks.find((c) => c.exists)?.name ?? null;
|
|
@@ -151048,7 +151202,7 @@ var CI_CONFIGS = [
|
|
|
151048
151202
|
];
|
|
151049
151203
|
async function detectCiConfig(workspacePath) {
|
|
151050
151204
|
for (const ci of CI_CONFIGS) {
|
|
151051
|
-
if (await exists((0,
|
|
151205
|
+
if (await exists((0, import_node_path21.join)(workspacePath, ci.path))) return ci.label;
|
|
151052
151206
|
}
|
|
151053
151207
|
return null;
|
|
151054
151208
|
}
|
|
@@ -151069,7 +151223,7 @@ async function detectLintStagedConfig(workspacePath, packageJson) {
|
|
|
151069
151223
|
return packageJson["lint-staged"];
|
|
151070
151224
|
}
|
|
151071
151225
|
for (const file2 of LINT_STAGED_FILES) {
|
|
151072
|
-
const full = (0,
|
|
151226
|
+
const full = (0, import_node_path21.join)(workspacePath, file2);
|
|
151073
151227
|
if (await exists(full)) {
|
|
151074
151228
|
if (file2.endsWith(".json") || file2 === ".lintstagedrc") {
|
|
151075
151229
|
return readJsonFile(full);
|
|
@@ -151111,7 +151265,7 @@ var CONFIG_FILES = [
|
|
|
151111
151265
|
async function detectConfigFiles(workspacePath) {
|
|
151112
151266
|
const results = await Promise.allSettled(
|
|
151113
151267
|
CONFIG_FILES.map(async (file2) => {
|
|
151114
|
-
if (await exists((0,
|
|
151268
|
+
if (await exists((0, import_node_path21.join)(workspacePath, file2))) return file2;
|
|
151115
151269
|
return null;
|
|
151116
151270
|
})
|
|
151117
151271
|
);
|
|
@@ -151150,7 +151304,7 @@ async function detectEnvFiles(workspacePath) {
|
|
|
151150
151304
|
const envFileNames = entries.filter((e) => e === ".env" || e.startsWith(".env."));
|
|
151151
151305
|
const results = await Promise.all(
|
|
151152
151306
|
envFileNames.map(async (name) => {
|
|
151153
|
-
const content = await readTextFile((0,
|
|
151307
|
+
const content = await readTextFile((0, import_node_path21.join)(workspacePath, name));
|
|
151154
151308
|
return {
|
|
151155
151309
|
name,
|
|
151156
151310
|
keys: content ? extractEnvKeys(content) : []
|
|
@@ -151177,34 +151331,34 @@ async function readWorkspaceSnapshot(workspacePath) {
|
|
|
151177
151331
|
ciConfig,
|
|
151178
151332
|
configFilesFound
|
|
151179
151333
|
] = await Promise.all([
|
|
151180
|
-
readJsonFile((0,
|
|
151181
|
-
readJsonFile((0,
|
|
151182
|
-
readJsonFile((0,
|
|
151183
|
-
readJsonFile((0,
|
|
151184
|
-
readTextFile((0,
|
|
151185
|
-
(r) => r ?? readTextFile((0,
|
|
151334
|
+
readJsonFile((0, import_node_path21.join)(workspacePath, "package.json")),
|
|
151335
|
+
readJsonFile((0, import_node_path21.join)(workspacePath, "app.json")),
|
|
151336
|
+
readJsonFile((0, import_node_path21.join)(workspacePath, "eas.json")),
|
|
151337
|
+
readJsonFile((0, import_node_path21.join)(workspacePath, "tsconfig.json")),
|
|
151338
|
+
readTextFile((0, import_node_path21.join)(workspacePath, "metro.config.js")).then(
|
|
151339
|
+
(r) => r ?? readTextFile((0, import_node_path21.join)(workspacePath, "metro.config.ts"))
|
|
151186
151340
|
),
|
|
151187
|
-
readTextFile((0,
|
|
151188
|
-
(r) => r ?? readTextFile((0,
|
|
151341
|
+
readTextFile((0, import_node_path21.join)(workspacePath, "babel.config.js")).then(
|
|
151342
|
+
(r) => r ?? readTextFile((0, import_node_path21.join)(workspacePath, "babel.config.cjs"))
|
|
151189
151343
|
),
|
|
151190
|
-
isDirectory((0,
|
|
151191
|
-
isDirectory((0,
|
|
151344
|
+
isDirectory((0, import_node_path21.join)(workspacePath, "ios")),
|
|
151345
|
+
isDirectory((0, import_node_path21.join)(workspacePath, "android")),
|
|
151192
151346
|
detectLockfile(workspacePath),
|
|
151193
151347
|
detectEnvFiles(workspacePath),
|
|
151194
151348
|
detectToolVersions(workspacePath),
|
|
151195
|
-
listDir((0,
|
|
151196
|
-
listDir((0,
|
|
151349
|
+
listDir((0, import_node_path21.join)(workspacePath, "scripts")),
|
|
151350
|
+
listDir((0, import_node_path21.join)(workspacePath, ".husky")),
|
|
151197
151351
|
detectCiConfig(workspacePath),
|
|
151198
151352
|
detectConfigFiles(workspacePath)
|
|
151199
151353
|
]);
|
|
151200
|
-
const iosDir = (0,
|
|
151201
|
-
const androidDir = (0,
|
|
151354
|
+
const iosDir = (0, import_node_path21.join)(workspacePath, "ios");
|
|
151355
|
+
const androidDir = (0, import_node_path21.join)(workspacePath, "android");
|
|
151202
151356
|
const [iosWorkspace, iosHasPodfile, makefileText, lintStagedConfig, androidHasGradle] = await Promise.all([
|
|
151203
151357
|
hasIosDir ? findIosWorkspace(iosDir) : Promise.resolve(null),
|
|
151204
|
-
exists((0,
|
|
151205
|
-
readTextFile((0,
|
|
151358
|
+
exists((0, import_node_path21.join)(workspacePath, "ios", "Podfile")),
|
|
151359
|
+
readTextFile((0, import_node_path21.join)(workspacePath, "Makefile")),
|
|
151206
151360
|
detectLintStagedConfig(workspacePath, packageJson),
|
|
151207
|
-
hasAndroidDir ? exists((0,
|
|
151361
|
+
hasAndroidDir ? exists((0, import_node_path21.join)(androidDir, "gradlew")) : Promise.resolve(false)
|
|
151208
151362
|
]);
|
|
151209
151363
|
const metroPort = metroConfigRaw ? extractMetroPort(metroConfigRaw) : null;
|
|
151210
151364
|
const makefileTargets = makefileText ? extractMakefileTargets(makefileText) : null;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ One recording per device at a time; different devices can record concurrently. R
|
|
|
14
14
|
|
|
15
15
|
## 2. Critical: never leave a recording running
|
|
16
16
|
|
|
17
|
-
A recording does not stop itself before its `timeLimitSeconds` cap,
|
|
17
|
+
A recording does not stop itself before its `timeLimitSeconds` cap, so a forgotten one keeps capturing until the cap fires — holding the recording session, wasting disk, and delaying the video you are waiting on (and with `trimStatic: false` it comes back padded with dead air). Two safety nets exist — use both:
|
|
18
18
|
|
|
19
19
|
1. **Set yourself a reminder the moment the recording starts.** You know the expected capture length (the interaction you are about to drive). Immediately after `screen-recording-start` returns, schedule a wake-up for that expected end time using whatever your harness provides — a built-in reminder/wakeup or scheduled-task tool if you have one, otherwise a background shell running `sleep <expected-seconds>` whose completion notification pulls you back. When it fires, call `screen-recording-stop`. Do not rely on remembering.
|
|
20
20
|
2. **Read the tool-result notes.** While a recording is running, every argent tool result carries a `NOTE:` reminding you it is still going and how to stop it. If the note says the recording already ended (time limit hit), still call `screen-recording-stop` — that is what hands you the file.
|
|
@@ -24,18 +24,24 @@ A recording does not stop itself before its `timeLimitSeconds` cap, and a forgot
|
|
|
24
24
|
## 3. Workflow
|
|
25
25
|
|
|
26
26
|
1. Ensure the target device is booted and the app is in the state you want the video to open on (`list-devices`, `launch-app`, `argent-device-interact`).
|
|
27
|
-
2. Call `screen-recording-start` with `udid` and a `timeLimitSeconds` slightly above the expected interaction length (default 180, max 600).
|
|
27
|
+
2. Call `screen-recording-start` with `udid` and a `timeLimitSeconds` slightly above the expected interaction length (default 180, max 600). Taps and swipes are drawn into the video by default (see the touch-visualizer note below); pass `showTouches: false` for a clean raw-screen capture.
|
|
28
28
|
3. Set the end-of-recording reminder described in §2 — this step is not optional.
|
|
29
29
|
4. Drive the interaction to capture: gestures, navigation, typing (`argent-device-interact`). Prefer `run-sequence` for tight multi-step interactions so tool-call latency does not pad the video.
|
|
30
|
-
5. Call `screen-recording-stop` with the same `udid`. It returns `{ video, durationMs, warning? }`; `video` is an artifact
|
|
30
|
+
5. Call `screen-recording-stop` with the same `udid`. It returns `{ video, durationMs, wallClockMs?, trimmedMs?, warning? }`; `video` is an artifact whose resolved path points at the durably-saved file (see below). The video is already final when stop returns (the watermark is stamped during capture, not in a second pass), so stop takes well under a second.
|
|
31
31
|
6. Check `warning`: it reports cap-triggered stops, early encoder exits, a dropped frame stream, and possibly-truncated containers. Verify the file plays (or at least has a sane size) before presenting it to the user.
|
|
32
32
|
|
|
33
|
+
**Where the file lands (durable, not scratch).** Unlike most argent artifacts (which live in a disposable temp cache the OS reclaims), a finished recording is saved durably on the **client** into `<project>/.argent/recordings/` — the project being the nearest ancestor of the client's working directory with a `.git`/`package.json`/`.argent`, or `~/.argent/recordings/` when the client isn't inside a project. This holds even for a remote `argent link` tool-server: the mp4 is written on the client host, not the server. The saved name is `screen-recording-<device>-<timestamp>.mp4`; if that name is already taken the new file lands beside it as `… (2).mp4` rather than overwriting. Because these files persist in the working tree, mention the path to the user (and note that they are untracked — add them to `.gitignore` or clean them up if they shouldn't be committed).
|
|
34
|
+
|
|
35
|
+
**Static-frame trimming (on by default).** Stretches where the screen does not change are collapsed: the first second of each still stretch is kept so pauses read naturally, then unchanged frames are dropped until something moves again (a change of even a couple of pixels counts). So you can leave a recording running across slow steps, waits, or thinking time without padding the clip with dead air — a 40-second session with 5 seconds of real activity comes back as a ~5-7 second video. When trimming removed anything, stop also returns `wallClockMs` (real elapsed time) and `trimmedMs` (how much was cut); `durationMs` is always the length of the video you actually get. Pass `trimStatic: false` to `screen-recording-start` when you want a faithful real-time recording (e.g. to measure how long something took on screen).
|
|
36
|
+
|
|
37
|
+
**Touch visualizer (on by default).** Every interaction argent drives is drawn straight into the video: a pulse marks each tap, a fading comet trail follows swipes and drags, and paired markers show the two contact points of a pinch or rotate. This makes a recording self-explanatory — a viewer can see _where_ each gesture landed rather than watching the UI react to an invisible finger. It is rendered by simulator-server into the frame stream (nothing is composited host-side), so it costs nothing extra and never appears in a `screenshot`. Pass `showTouches: false` to `screen-recording-start` to record the raw screen with no overlay (e.g. when capturing exactly what the user would see). If simulator-server cannot enable it, the recording still succeeds and stop returns a `warning` saying touches are not shown.
|
|
38
|
+
|
|
33
39
|
---
|
|
34
40
|
|
|
35
41
|
## 4. Platform notes and limits
|
|
36
42
|
|
|
37
43
|
- **What can be recorded**: anything simulator-server drives — iOS simulators, Android emulators, and physical Android devices. The only length limit is `timeLimitSeconds` (max 600).
|
|
38
|
-
- **The timeline is
|
|
44
|
+
- **The timeline is paced to a steady 30 fps**: a device only emits a frame when its screen changes, so captured frames are re-paced onto a fixed timeline rather than bunching up. With static-frame trimming off (`trimStatic: false`) that timeline is wall-clock accurate — a completely still screen still comes back as a full-length video (compressing to almost nothing) and `durationMs` matches the time you actually recorded. With trimming on (the default, see §3) still stretches past the grace window are collapsed, so `durationMs` is the trimmed video length and `wallClockMs` carries the real elapsed time.
|
|
39
45
|
- **Android**: records at the device's native resolution; secure screens (DRM, some password fields) come out black.
|
|
40
46
|
- **Unsupported**: tvOS simulators, physical iPhones, Chromium apps, Vega/Fire TV, and remote (`remote:`-prefixed) simulators — none of them expose a readable frame stream. For a single still frame use `screenshot`; for a replayable interaction script use `argent-create-flow` instead of a video.
|
|
41
47
|
- **ffmpeg is required**: it is the encoder, so `screen-recording-start` fails up front with an install hint if it is missing (`brew install ffmpeg`). It is resolved from `PATH` plus the usual Homebrew prefixes.
|