@loadmill/droid-cua 2.38.0 → 2.40.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/README.md +19 -7
- package/build/index.js +11 -0
- package/build/src/cli/headless-execution-config.js +6 -5
- package/build/src/cli/headless-runner.js +34 -4
- package/build/src/cli/loadmill-report-metadata.js +7 -0
- package/build/src/cli/skill-command.js +20 -3
- package/build/src/commands/help.js +1 -0
- package/build/src/core/cua-models.js +17 -15
- package/build/src/core/droid-cua-run-usage.js +22 -19
- package/build/src/core/report/render-html-report.js +27 -1
- package/build/src/desktop/computer-use.js +128 -43
- package/build/src/desktop/windows-visible-bounds.js +55 -0
- package/build/src/device/android/connection.js +13 -4
- package/build/src/device/interface.js +1 -1
- package/build/src/integrations/loadmill/report-upload-manager.js +45 -10
- package/build/src/integrations/loadmill/run-reporter.js +5 -1
- package/package.json +1 -1
- package/skill/droid-cua/SKILL.md +5 -3
- package/skill/droid-cua/references/authoring-tests.md +77 -18
- package/skill/droid-cua/references/iterative-improvement.md +10 -6
- package/skill/droid-cua/references/onboarding-and-troubleshooting.md +6 -0
- package/skill/droid-cua/references/project-and-auth.md +1 -1
- package/skill/droid-cua/references/running-and-healing.md +11 -4
package/README.md
CHANGED
|
@@ -158,8 +158,13 @@ droid-cua skill install codex
|
|
|
158
158
|
droid-cua skill install codex --local
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
Use `droid-cua skill status <agent>` to
|
|
162
|
-
|
|
161
|
+
Use `droid-cua skill status <agent>` to see the installed and bundled skill
|
|
162
|
+
versions and installation path; add `--local` to inspect a project installation.
|
|
163
|
+
The skill version is stored in `SKILL.md` under `metadata.version`, independently
|
|
164
|
+
of the CLI package version. Older copies without version metadata show `unknown`.
|
|
165
|
+
The `modified` status means the files differ from the bundled copy, which can
|
|
166
|
+
indicate an older release or local edits even when version labels match.
|
|
167
|
+
Use `droid-cua skill uninstall <agent>` to remove an installation. Installed skill
|
|
163
168
|
files are never overwritten or removed after local modification unless `--force`
|
|
164
169
|
is supplied. After upgrading the npm package, update an installed skill explicitly:
|
|
165
170
|
|
|
@@ -239,7 +244,7 @@ backward compatibility.
|
|
|
239
244
|
Example headless config:
|
|
240
245
|
```json
|
|
241
246
|
{
|
|
242
|
-
"cuaModel": "gpt-
|
|
247
|
+
"cuaModel": "gpt-6-sol",
|
|
243
248
|
"promptCustomizations": {
|
|
244
249
|
"basePromptInstructions": "",
|
|
245
250
|
"designModeInstructions": "",
|
|
@@ -290,14 +295,18 @@ Supported CLI options include:
|
|
|
290
295
|
- `--secrets`
|
|
291
296
|
- `--record`
|
|
292
297
|
- `--report`
|
|
298
|
+
- `--report-metadata <path>`
|
|
293
299
|
- `--no-cloud-report`
|
|
294
300
|
- `--debug`
|
|
295
301
|
|
|
296
302
|
`--cua-model` accepts `loadmill-smart`, `loadmill-pulse`, `loadmill-beacon`,
|
|
297
|
-
`gpt-
|
|
298
|
-
|
|
303
|
+
`gpt-6-sol`, and `gpt-6-luna`. OpenAI models use direct OpenAI mode and require
|
|
304
|
+
`OPENAI_API_KEY`. Saved direct OpenAI selections of GPT-5.6 Terra, GPT-5.6 Luna,
|
|
305
|
+
or GPT-5.4 move to GPT-6 Sol, GPT-6 Luna, or GPT-6 Sol, respectively.
|
|
299
306
|
|
|
300
|
-
Desktop control supports macOS and Windows. In the desktop app, enable **Experimental desktop control** at the bottom of Settings to show Desktop in the platform picker. Desktop runs use visual input by default:
|
|
307
|
+
Desktop control supports macOS and Windows. In the desktop app, enable **Experimental desktop control** at the bottom of Settings to show Desktop in the platform picker. Desktop runs use visual input by default: Droid shows a system notice for three seconds before taking control, brings the selected window to the foreground, then sends physical pointer and keyboard input at the model's coordinates. The three-second delay is fixed; once the test starts, use Stop in Droid to end it. Avoid using the mouse or keyboard during the run. When the run ends, Droid leaves the target window in front and shows another system notice that control has finished, including when the run fails or is stopped. CLI runs also leave the target in front. Use `--desktop-input accessibility` for cursor-free CLI input instead. macOS requires Screen & System Audio Recording and Accessibility permissions for Droid or the terminal running the CLI. Windows targets must run at the same integrity level as Droid; a non-elevated Droid process cannot inject input into an elevated application. If Windows returns an all-black window capture, Droid brings the target back to the foreground and crops the visible desktop to the window's visible frame, excluding its invisible resize border. The crop is resized only after extraction to preserve window detail. The fallback logs a warning and requires the target to remain unobstructed on the primary display; off-screen portions appear black. With accessibility input, bring the target to the foreground yourself when requested.
|
|
308
|
+
|
|
309
|
+
The system notices are shown only for desktop control runs started in the desktop app. On macOS, they may not appear in the unsigned Electron dev app; verify them in a signed packaged build.
|
|
301
310
|
|
|
302
311
|
Config and precedence rules:
|
|
303
312
|
- Use `--config <file>` to supply prompt-affecting settings for headless runs.
|
|
@@ -325,8 +334,11 @@ HTML report (`--report`):
|
|
|
325
334
|
- With no value it saves `<test>--<date>--<time>.html` (e.g. `login--2026-06-03--16-09-20.html`) in the current directory; pass `--report <file>` to pick a filename or `--report <dir>` to pick a folder.
|
|
326
335
|
- A multi-path selection writes the individual test reports plus one aggregate `Selected tests` report with a child result and timeline for every selected test. When `--report <file.html>` is used, that exact path is used for the aggregate report.
|
|
327
336
|
- In the desktop app the same report is available from a run's **Share → Download HTML report** button.
|
|
328
|
-
-
|
|
337
|
+
- Generated HTML reports opt out of Cloudflare email obfuscation so ordinary email values remain visible in hosted reports. Secret-typing values are omitted from typing logs, but may still appear in screenshots.
|
|
338
|
+
- Loadmill flow entries link **Test started** to the flow's test-suite run results, including in reports generated by CI.
|
|
339
|
+
- Runs with captured screenshots also expose **Share → Generate video**. The desktop app renders portrait mobile runs as square replays and landscape mobile, web, and desktop runs as 16:9 replays. Landscape Android and iOS screenshots keep a device frame sized to the captured screen instead of browser chrome. Rendering happens locally, then the app prompts for a save location using `<test>--<date>--<time>--test-replay.mp4` as the suggested filename. While rendering, a compact **Background tasks** card shows progress for **Generating test replay**; it can be minimized to a task-count card or canceled without opening the save dialog.
|
|
329
340
|
- Authenticated headless runs upload their HTML report and screenshots to Loadmill by default. The Loadmill run record also includes the resolved Droid version, model, provider, Agent Pace, Aim Assist, Context Optimization, and target environment. Use `--no-cloud-report` to disable artifact upload; the Droid run record is still reported.
|
|
341
|
+
- Use `--report-metadata <path>` when automation needs the exact published Loadmill run ID and last uploaded screenshot object name. Droid clears that destination when the invocation starts and writes it only after report publication succeeds, including for published failure reports.
|
|
330
342
|
|
|
331
343
|
```sh
|
|
332
344
|
droid-cua run tests/login.dcua --avd adb:emulator-5554 --report ./reports
|
package/build/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { resolveHeadlessExecutionConfig } from "./src/cli/headless-execution-con
|
|
|
22
22
|
import { artifactKindsForPool, resolveHeadlessTarget, scopeArtifactKindsToPool, shouldWaitForLoadmillCloudCompletion, } from "./src/cli/headless-target.js";
|
|
23
23
|
import { createHeadlessDebugArtifacts } from "./src/cli/headless-debug.js";
|
|
24
24
|
import { loadHeadlessInstructions } from "./src/cli/headless-instructions.js";
|
|
25
|
+
import { clearLoadmillReportMetadata } from "./src/cli/loadmill-report-metadata.js";
|
|
25
26
|
import { assertRunCommandPosition, getHeadlessInstructionSelectors, resolveHeadlessInstructionSelection, } from "./src/cli/headless-selection.js";
|
|
26
27
|
import { runHeadlessInstructionRuns } from "./src/cli/headless-runner.js";
|
|
27
28
|
import { buildLoadmillCloudDeviceConnectSummaryLines } from "./src/cli/loadmill-cloud-device-connect-summary.js";
|
|
@@ -72,6 +73,11 @@ if (args._[0] === "skill") {
|
|
|
72
73
|
process.exit(1);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
const reportMetadataPath = typeof args["report-metadata"] === "string"
|
|
77
|
+
&& args["report-metadata"].trim().length > 0
|
|
78
|
+
? args["report-metadata"].trim()
|
|
79
|
+
: null;
|
|
80
|
+
await clearLoadmillReportMetadata(reportMetadataPath);
|
|
75
81
|
const targetConfig = resolveHeadlessTarget(args);
|
|
76
82
|
let avdName = targetConfig.avdName;
|
|
77
83
|
let platform = targetConfig.platform || null; // 'ios' or 'android'
|
|
@@ -98,6 +104,10 @@ const reportConfig = {
|
|
|
98
104
|
enabled: args.report === true || (typeof args.report === "string" && args.report.trim().length > 0),
|
|
99
105
|
path: typeof args.report === "string" && args.report.trim().length > 0 ? args.report.trim() : null,
|
|
100
106
|
cloudEnabled: args["no-cloud-report"] !== true,
|
|
107
|
+
metadataPath: reportMetadataPath,
|
|
108
|
+
lloydJobId: typeof process.env.LLOYD_JOB_ID === "string" && process.env.LLOYD_JOB_ID.trim().length > 0
|
|
109
|
+
? process.env.LLOYD_JOB_ID.trim()
|
|
110
|
+
: null,
|
|
101
111
|
};
|
|
102
112
|
const screenshotDir = path.join("droid-cua-recording-" + Date.now());
|
|
103
113
|
if (recordScreenshots)
|
|
@@ -527,6 +537,7 @@ async function main() {
|
|
|
527
537
|
app: targetConfig.desktopApp,
|
|
528
538
|
windowId: targetConfig.desktopWindow,
|
|
529
539
|
inputMode: targetConfig.desktopInput,
|
|
540
|
+
focusOnConnect: true,
|
|
530
541
|
excludePids: [process.pid],
|
|
531
542
|
});
|
|
532
543
|
targetId = desktopController.deviceId;
|
|
@@ -2,7 +2,7 @@ import path from "path";
|
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
import { DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, MAX_CONTEXT_OPTIMIZATION_THRESHOLD, MIN_CONTEXT_OPTIMIZATION_THRESHOLD, } from "../core/context-optimization.js";
|
|
4
4
|
import { AIM_ASSIST_MODES, DEFAULT_AIM_ASSIST_MODE } from "../core/aim-assist.js";
|
|
5
|
-
import { CUA_MODELS, DEFAULT_OPENAI_CUA_MODEL, isOpenAiCuaModel, isValidCuaModel, resolveCuaModel } from "../core/cua-models.js";
|
|
5
|
+
import { CUA_MODELS, DEFAULT_OPENAI_CUA_MODEL, isOpenAiCuaModel, isValidCuaModel, resolveCuaModel, upgradeLegacyCuaModel } from "../core/cua-models.js";
|
|
6
6
|
import { LLM_PROVIDER_MODES, isValidLlmProviderMode, resolveLlmProviderMode } from "../core/runtime-config.js";
|
|
7
7
|
import { parseSecretsOverrideString } from "../core/secrets.js";
|
|
8
8
|
function createEmptyPromptCustomizations() {
|
|
@@ -21,10 +21,11 @@ function assertNonEmptyString(value, label) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function validateCuaModel(value, label) {
|
|
24
|
-
|
|
24
|
+
const upgraded = typeof value === "string" ? upgradeLegacyCuaModel(value) : value;
|
|
25
|
+
if (!isValidCuaModel(upgraded)) {
|
|
25
26
|
throw new Error(`${label} must be one of: ${CUA_MODELS.join(", ")}.`);
|
|
26
27
|
}
|
|
27
|
-
return
|
|
28
|
+
return upgraded;
|
|
28
29
|
}
|
|
29
30
|
function validateAimAssistMode(value, label) {
|
|
30
31
|
if (typeof value !== "string" || !AIM_ASSIST_MODES.includes(value)) {
|
|
@@ -44,8 +45,8 @@ function readEnvDefaultCuaModel() {
|
|
|
44
45
|
// OpenAI model, stay on the OpenAI default rather than crossing over to the
|
|
45
46
|
// global (Loadmill) default.
|
|
46
47
|
if (typeof envValue === "string" && envValue.trim()) {
|
|
47
|
-
const
|
|
48
|
-
return isOpenAiCuaModel(
|
|
48
|
+
const upgraded = upgradeLegacyCuaModel(envValue.trim());
|
|
49
|
+
return isOpenAiCuaModel(upgraded) ? upgraded : DEFAULT_OPENAI_CUA_MODEL;
|
|
49
50
|
}
|
|
50
51
|
return resolveCuaModel(envValue);
|
|
51
52
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { mkdir } from "fs/promises";
|
|
2
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
3
3
|
import { getCurrentPlatform as getBrowserCurrentPlatform, getScreenshotAsBase64 as getBrowserScreenshotAsBase64, } from "../browser/connection.js";
|
|
4
4
|
import { handleModelAction as handleBrowserModelAction } from "../browser/actions.js";
|
|
5
5
|
import { ExecutionEngine } from "../core/execution-engine.js";
|
|
@@ -13,6 +13,7 @@ import { reportDroidCuaCiTestRunCompleted } from "../integrations/segment/droid-
|
|
|
13
13
|
import { printCliOutput } from "../utils/console-output.js";
|
|
14
14
|
import { emitDesktopDebug } from "../utils/desktop-debug.js";
|
|
15
15
|
import { loadHeadlessInstructions } from "./headless-instructions.js";
|
|
16
|
+
import { clearLoadmillReportMetadata } from "./loadmill-report-metadata.js";
|
|
16
17
|
import { formatHeadlessTestSourceLabel } from "./headless-test-source-label.js";
|
|
17
18
|
import { buildCliRunReportModel, createCapturingAddOutput, createCliAggregateRunReport, createCliRunReport, renderCliCloudReport, writeCliRunReportArtifact, } from "./headless-report.js";
|
|
18
19
|
import { createRefuseReconnectConnectToBrowser } from "../desktop-shared/connection-predicates.js";
|
|
@@ -36,6 +37,24 @@ function logPendingScreenshotUploads(uploadManager, uploadSession, subject) {
|
|
|
36
37
|
console.log(`${subject} finished. Waiting for ${pendingCount} screenshot ${uploads} to finish...`);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
function loadmillReportMetadata(savedRunId, publication) {
|
|
41
|
+
if (!savedRunId || !publication?.published) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
runId: savedRunId,
|
|
46
|
+
...(publication.lastScreenshotObjectName
|
|
47
|
+
? { lastScreenshotObjectName: publication.lastScreenshotObjectName }
|
|
48
|
+
: {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function writeLoadmillReportMetadata(filePath, metadata) {
|
|
52
|
+
if (!filePath || !metadata) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await mkdir(path.dirname(path.resolve(filePath)), { recursive: true });
|
|
56
|
+
await writeFile(filePath, `${JSON.stringify(metadata, null, 2)}\n`, { mode: 0o600 });
|
|
57
|
+
}
|
|
39
58
|
function getRecordScreenshotDirForTest(baseScreenshotDir, testName, isBatchRun) {
|
|
40
59
|
if (!baseScreenshotDir || !isBatchRun) {
|
|
41
60
|
return baseScreenshotDir;
|
|
@@ -320,9 +339,10 @@ async function runHeadlessTest({ runId, testPath, testName, instructions, sessio
|
|
|
320
339
|
const cloudModel = cloudReportEnabled
|
|
321
340
|
? buildCliRunReportModel({ ...reportInput, events: cloudEvents })
|
|
322
341
|
: null;
|
|
342
|
+
let reportPublication = null;
|
|
323
343
|
if (cloudModel && reportToLoadmill && reportedRun?.savedRunId) {
|
|
324
344
|
logPendingScreenshotUploads(uploadManager, cloudUploadSession, "Test");
|
|
325
|
-
await uploadManager.finish(cloudUploadSession, {
|
|
345
|
+
reportPublication = await uploadManager.finish(cloudUploadSession, {
|
|
326
346
|
savedRunId: reportedRun.savedRunId,
|
|
327
347
|
html: renderCliCloudReport(cloudModel, reportedRun.savedRunId),
|
|
328
348
|
});
|
|
@@ -339,6 +359,7 @@ async function runHeadlessTest({ runId, testPath, testName, instructions, sessio
|
|
|
339
359
|
retries: stats.retryCount ?? 0,
|
|
340
360
|
localReportModel: localReport?.model ?? null,
|
|
341
361
|
reportModel: cloudModel,
|
|
362
|
+
loadmillReport: loadmillReportMetadata(reportedRun?.savedRunId, reportPublication),
|
|
342
363
|
};
|
|
343
364
|
}
|
|
344
365
|
catch (error) {
|
|
@@ -418,9 +439,10 @@ async function runHeadlessTest({ runId, testPath, testName, instructions, sessio
|
|
|
418
439
|
const cloudModel = cloudReportEnabled
|
|
419
440
|
? buildCliRunReportModel({ ...failedReportInput, events: cloudEvents })
|
|
420
441
|
: null;
|
|
442
|
+
let reportPublication = null;
|
|
421
443
|
if (cloudModel && reportToLoadmill && reportedRun?.savedRunId) {
|
|
422
444
|
logPendingScreenshotUploads(uploadManager, cloudUploadSession, "Test");
|
|
423
|
-
await uploadManager.finish(cloudUploadSession, {
|
|
445
|
+
reportPublication = await uploadManager.finish(cloudUploadSession, {
|
|
424
446
|
savedRunId: reportedRun.savedRunId,
|
|
425
447
|
html: renderCliCloudReport(cloudModel, reportedRun.savedRunId),
|
|
426
448
|
});
|
|
@@ -439,17 +461,21 @@ async function runHeadlessTest({ runId, testPath, testName, instructions, sessio
|
|
|
439
461
|
retries: stats.retryCount ?? 0,
|
|
440
462
|
localReportModel: localReport?.model ?? null,
|
|
441
463
|
reportModel: cloudModel,
|
|
464
|
+
loadmillReport: loadmillReportMetadata(reportedRun?.savedRunId, reportPublication),
|
|
442
465
|
};
|
|
443
466
|
}
|
|
467
|
+
await writeLoadmillReportMetadata(report?.metadataPath, loadmillReportMetadata(reportedRun?.savedRunId, reportPublication));
|
|
444
468
|
throw error;
|
|
445
469
|
}
|
|
446
470
|
}
|
|
447
471
|
export async function runHeadlessInstructionRuns({ runs, instructionsTarget, session, deviceInfo, headlessConfig, appContext, runtimeContext, headlessDebug, secrets = null, recordScreenshots, screenshotDir, isBrowserTarget, isDesktopTarget = false, desktopController = null, isCloudTarget, runTarget = null, report = null, droidVersion = getDroidVersion(), runHeadlessTestImpl = runHeadlessTest, uploadManager = droidReportUploadManager, }) {
|
|
472
|
+
await clearLoadmillReportMetadata(report?.metadataPath);
|
|
448
473
|
const resolvedRunTarget = runTarget ?? { isBrowserTarget, isDesktopTarget, isCloudTarget };
|
|
449
474
|
const runConfiguration = buildDroidRunConfiguration({
|
|
450
475
|
droidVersion,
|
|
451
476
|
runtimeConfig: runtimeContext.config,
|
|
452
477
|
aimAssistMode: headlessConfig.aimAssistMode,
|
|
478
|
+
lloydJobId: report?.lloydJobId,
|
|
453
479
|
});
|
|
454
480
|
const isBatchRun = ["directory", "selection", "scenario"].includes(instructionsTarget?.type);
|
|
455
481
|
const cloudReportEnabled = report?.cloudEnabled !== false &&
|
|
@@ -470,6 +496,7 @@ export async function runHeadlessInstructionRuns({ runs, instructionsTarget, ses
|
|
|
470
496
|
console.log(`\nRunning ${runs.length} tests from: ${instructionsTarget.path}\n`);
|
|
471
497
|
}
|
|
472
498
|
let failures = 0;
|
|
499
|
+
let reportMetadata = null;
|
|
473
500
|
const batchStartedAt = Date.now();
|
|
474
501
|
const aggregateStats = {
|
|
475
502
|
durationMs: 0,
|
|
@@ -530,6 +557,7 @@ export async function runHeadlessInstructionRuns({ runs, instructionsTarget, ses
|
|
|
530
557
|
if (result.reportModel) {
|
|
531
558
|
aggregateReportModels.push(result.reportModel);
|
|
532
559
|
}
|
|
560
|
+
reportMetadata = result.loadmillReport ?? reportMetadata;
|
|
533
561
|
if (result.localReportModel) {
|
|
534
562
|
aggregateLocalReportModels.push(result.localReportModel);
|
|
535
563
|
}
|
|
@@ -642,12 +670,14 @@ export async function runHeadlessInstructionRuns({ runs, instructionsTarget, ses
|
|
|
642
670
|
childModels: aggregateReportModels,
|
|
643
671
|
});
|
|
644
672
|
logPendingScreenshotUploads(uploadManager, cloudUploadSession, "Run");
|
|
645
|
-
await uploadManager.finish(cloudUploadSession, {
|
|
673
|
+
const publication = await uploadManager.finish(cloudUploadSession, {
|
|
646
674
|
savedRunId: reportedRun.savedRunId,
|
|
647
675
|
html: renderCliCloudReport(aggregateReport.model, reportedRun.savedRunId),
|
|
648
676
|
});
|
|
677
|
+
reportMetadata = loadmillReportMetadata(reportedRun.savedRunId, publication);
|
|
649
678
|
}
|
|
650
679
|
}
|
|
680
|
+
await writeLoadmillReportMetadata(report?.metadataPath, reportMetadata);
|
|
651
681
|
return {
|
|
652
682
|
exitCode: failures === 0 ? 0 : 1,
|
|
653
683
|
failures
|
|
@@ -12,7 +12,7 @@ const SUBCOMMANDS = new Set(["install", "status", "uninstall"]);
|
|
|
12
12
|
const ALLOWED_OPTIONS = new Set(["_", "local", "force", "h", "help"]);
|
|
13
13
|
const COMMAND_DESCRIPTIONS = Object.freeze({
|
|
14
14
|
install: "Install Droid CUA skill for a coding agent",
|
|
15
|
-
status: "Show
|
|
15
|
+
status: "Show installation status and installed/bundled skill versions",
|
|
16
16
|
uninstall: "Uninstall Droid CUA skill for a coding agent",
|
|
17
17
|
});
|
|
18
18
|
function skillCommandHelp(subcommand) {
|
|
@@ -53,7 +53,7 @@ function skillRootHelp() {
|
|
|
53
53
|
"",
|
|
54
54
|
"Commands:",
|
|
55
55
|
" install Install the skill",
|
|
56
|
-
" status Show installation status",
|
|
56
|
+
" status Show installation status and skill versions",
|
|
57
57
|
" uninstall Uninstall the skill",
|
|
58
58
|
].join("\n");
|
|
59
59
|
}
|
|
@@ -103,6 +103,18 @@ async function directoryMatches(sourcePath, destinationPath) {
|
|
|
103
103
|
]);
|
|
104
104
|
return JSON.stringify(sourceSnapshot) === JSON.stringify(destinationSnapshot);
|
|
105
105
|
}
|
|
106
|
+
// Read the bundled skill's block-style metadata.version scalar without a YAML dependency.
|
|
107
|
+
async function readSkillVersion(skillPath) {
|
|
108
|
+
if (await pathType(skillPath) !== "directory")
|
|
109
|
+
return null;
|
|
110
|
+
if (await pathType(path.join(skillPath, "SKILL.md")) !== "file")
|
|
111
|
+
return null;
|
|
112
|
+
const markdown = await readFile(path.join(skillPath, "SKILL.md"), "utf8");
|
|
113
|
+
const frontmatter = markdown.replace(/\r\n/g, "\n").match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
|
|
114
|
+
const metadata = frontmatter?.match(/^metadata:[ \t]*\n((?:[ \t]+[^\n]*(?:\n|$))*)/m)?.[1];
|
|
115
|
+
const value = metadata?.match(/^ version:[ \t]*(?:"([^"\n]+)"|'([^'\n]+)'|([^\s#]+))[ \t]*(?:#.*)?$/m);
|
|
116
|
+
return value ? (value[1] ?? value[2] ?? value[3]) : null;
|
|
117
|
+
}
|
|
106
118
|
async function getInstallationState(sourcePath, destinationPath) {
|
|
107
119
|
if (await pathType(destinationPath) === "missing")
|
|
108
120
|
return "not-installed";
|
|
@@ -214,7 +226,12 @@ export async function runSkillCommand({ args, cwd = process.cwd(), home = os.hom
|
|
|
214
226
|
}
|
|
215
227
|
for (const destination of states) {
|
|
216
228
|
if (command.subcommand === "status") {
|
|
217
|
-
|
|
229
|
+
const [installedVersion, bundledVersion] = await Promise.all([
|
|
230
|
+
readSkillVersion(destination.path),
|
|
231
|
+
readSkillVersion(bundledSkillPath),
|
|
232
|
+
]);
|
|
233
|
+
const installedLabel = destination.state === "not-installed" ? "not installed" : installedVersion ?? "unknown";
|
|
234
|
+
output.log(`${destination.displayName}: ${stateLabel(destination.state)} at ${destination.path}\nInstalled skill version: ${installedLabel}\nBundled skill version: ${bundledVersion ?? "unknown"}`);
|
|
218
235
|
}
|
|
219
236
|
else if (command.subcommand === "install") {
|
|
220
237
|
if (destination.state === "installed") {
|
|
@@ -54,6 +54,7 @@ export async function handleHelp(args, session, context) {
|
|
|
54
54
|
addOutput({ type: 'info', text: ' --explain Explain non-obvious decisions and completion evidence' });
|
|
55
55
|
addOutput({ type: 'info', text: ' --report [path] Save a self-contained HTML report locally' });
|
|
56
56
|
addOutput({ type: 'info', text: ' --no-cloud-report Skip uploading the HTML report and screenshots to Loadmill' });
|
|
57
|
+
addOutput({ type: 'info', text: ' --report-metadata <path> Write uploaded Loadmill report metadata as JSON' });
|
|
57
58
|
addOutput({ type: 'info', text: ' --debug Enable structured JSONL debug artifacts' });
|
|
58
59
|
addOutput({ type: 'info', text: '' });
|
|
59
60
|
addOutput({ type: 'info', text: 'Interactive commands:' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const DEFAULT_OPENAI_CUA_MODEL = "gpt-
|
|
1
|
+
export const DEFAULT_OPENAI_CUA_MODEL = "gpt-6-sol";
|
|
2
2
|
export const DEFAULT_LOADMILL_CUA_MODEL = "loadmill-smart";
|
|
3
3
|
export const DEFAULT_CUA_MODEL = DEFAULT_LOADMILL_CUA_MODEL;
|
|
4
4
|
export const CUA_CONVERSATION_POLICIES = Object.freeze({
|
|
@@ -28,27 +28,28 @@ export const CUA_MODEL_OPTIONS = Object.freeze([
|
|
|
28
28
|
label: "Loadmill - Beacon",
|
|
29
29
|
}),
|
|
30
30
|
Object.freeze({
|
|
31
|
-
id: "gpt-
|
|
31
|
+
id: "gpt-6-sol",
|
|
32
32
|
providerMode: "openai",
|
|
33
33
|
providerLabel: "OpenAI",
|
|
34
|
-
name: "GPT-
|
|
35
|
-
label: "OpenAI GPT-
|
|
34
|
+
name: "GPT-6 Sol",
|
|
35
|
+
label: "OpenAI GPT-6 Sol",
|
|
36
36
|
}),
|
|
37
37
|
Object.freeze({
|
|
38
|
-
id: "gpt-
|
|
38
|
+
id: "gpt-6-luna",
|
|
39
39
|
providerMode: "openai",
|
|
40
40
|
providerLabel: "OpenAI",
|
|
41
|
-
name: "GPT-
|
|
42
|
-
label: "OpenAI GPT-
|
|
43
|
-
}),
|
|
44
|
-
Object.freeze({
|
|
45
|
-
id: "gpt-5.4",
|
|
46
|
-
providerMode: "openai",
|
|
47
|
-
providerLabel: "OpenAI",
|
|
48
|
-
name: "GPT-5.4",
|
|
49
|
-
label: "OpenAI GPT-5.4",
|
|
41
|
+
name: "GPT-6 Luna",
|
|
42
|
+
label: "OpenAI GPT-6 Luna",
|
|
50
43
|
}),
|
|
51
44
|
]);
|
|
45
|
+
const LEGACY_OPENAI_CUA_MODELS = Object.freeze({
|
|
46
|
+
"gpt-5.6-terra": "gpt-6-sol",
|
|
47
|
+
"gpt-5.6-luna": "gpt-6-luna",
|
|
48
|
+
"gpt-5.4": "gpt-6-sol",
|
|
49
|
+
});
|
|
50
|
+
export function upgradeLegacyCuaModel(value) {
|
|
51
|
+
return LEGACY_OPENAI_CUA_MODELS[value] ?? value;
|
|
52
|
+
}
|
|
52
53
|
export const CUA_MODELS = Object.freeze(CUA_MODEL_OPTIONS.map((option) => option.id));
|
|
53
54
|
export const OPENAI_CUA_MODELS = Object.freeze(CUA_MODEL_OPTIONS
|
|
54
55
|
.filter((option) => option.providerMode === "openai")
|
|
@@ -70,7 +71,8 @@ export function isLoadmillCuaModel(value) {
|
|
|
70
71
|
return LOADMILL_CUA_MODEL_SET.has(value);
|
|
71
72
|
}
|
|
72
73
|
export function resolveCuaModel(value) {
|
|
73
|
-
|
|
74
|
+
const upgraded = upgradeLegacyCuaModel(value);
|
|
75
|
+
return isValidCuaModel(upgraded) ? upgraded : DEFAULT_CUA_MODEL;
|
|
74
76
|
}
|
|
75
77
|
export function resolveCuaModelProviderMode(value) {
|
|
76
78
|
const normalized = resolveCuaModel(value);
|
|
@@ -3,29 +3,26 @@ const OPENAI_GPT_5_4_PRICING = Object.freeze({
|
|
|
3
3
|
inputPerMillionUsd: 2.5,
|
|
4
4
|
});
|
|
5
5
|
const OPENAI_GPT_5_6_TERRA_PRICING = Object.freeze({
|
|
6
|
-
inputPerMillionUsd: 2
|
|
6
|
+
inputPerMillionUsd: 2,
|
|
7
7
|
});
|
|
8
8
|
const OPENAI_GPT_5_6_LUNA_PRICING = Object.freeze({
|
|
9
|
-
inputPerMillionUsd:
|
|
9
|
+
inputPerMillionUsd: 0.2,
|
|
10
|
+
});
|
|
11
|
+
const OPENAI_GPT_6_SOL_PRICING = Object.freeze({
|
|
12
|
+
inputPerMillionUsd: 2,
|
|
13
|
+
});
|
|
14
|
+
const OPENAI_GPT_6_LUNA_PRICING = Object.freeze({
|
|
15
|
+
inputPerMillionUsd: 0.1,
|
|
10
16
|
});
|
|
11
17
|
const MODEL_PRICING = Object.freeze({
|
|
12
|
-
"gpt-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"gpt-5.6-luna":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
}),
|
|
21
|
-
// Loadmill Smart currently routes to GPT-5.6 Terra behind the Loadmill LLM gateway.
|
|
22
|
-
// Keep this explicit so pricing can be updated here if the backing model changes.
|
|
23
|
-
"loadmill-smart": Object.freeze({
|
|
24
|
-
...OPENAI_GPT_5_6_TERRA_PRICING,
|
|
25
|
-
}),
|
|
26
|
-
"loadmill-pulse": Object.freeze({
|
|
27
|
-
...OPENAI_GPT_5_6_LUNA_PRICING,
|
|
28
|
-
}),
|
|
18
|
+
"gpt-6-sol": OPENAI_GPT_6_SOL_PRICING,
|
|
19
|
+
"gpt-6-luna": OPENAI_GPT_6_LUNA_PRICING,
|
|
20
|
+
"gpt-5.6-terra": OPENAI_GPT_5_6_TERRA_PRICING,
|
|
21
|
+
"gpt-5.6-luna": OPENAI_GPT_5_6_LUNA_PRICING,
|
|
22
|
+
"gpt-5.4": OPENAI_GPT_5_4_PRICING,
|
|
23
|
+
// Loadmill Smart and Pulse route to GPT-6 Sol and Luna, respectively.
|
|
24
|
+
"loadmill-smart": OPENAI_GPT_6_SOL_PRICING,
|
|
25
|
+
"loadmill-pulse": OPENAI_GPT_6_LUNA_PRICING,
|
|
29
26
|
});
|
|
30
27
|
function finiteNumber(value) {
|
|
31
28
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
@@ -41,6 +38,12 @@ export function normalizeDroidCuaCostModel(model) {
|
|
|
41
38
|
if (typeof model !== "string") {
|
|
42
39
|
return null;
|
|
43
40
|
}
|
|
41
|
+
if (model === "gpt-6-sol" || model.startsWith("gpt-6-sol-")) {
|
|
42
|
+
return "gpt-6-sol";
|
|
43
|
+
}
|
|
44
|
+
if (model === "gpt-6-luna" || model.startsWith("gpt-6-luna-")) {
|
|
45
|
+
return "gpt-6-luna";
|
|
46
|
+
}
|
|
44
47
|
if (model === "gpt-5.6-terra" || model.startsWith("gpt-5.6-terra-")) {
|
|
45
48
|
return "gpt-5.6-terra";
|
|
46
49
|
}
|
|
@@ -255,12 +255,36 @@ function renderCloudCard(events) {
|
|
|
255
255
|
}
|
|
256
256
|
function renderLoadmillCard(events) {
|
|
257
257
|
const lines = events.map((event) => event.text.replace(/^\[Loadmill\]\s*/, "").trim());
|
|
258
|
+
const runId = events.map((event) => event.payload?.loadmillSuiteRunId)
|
|
259
|
+
.find((value) => typeof value === "string" && value.trim()) ??
|
|
260
|
+
lines.map((line) => line.match(/Test started \(ID:\s*([^)]+)\)/i)?.[1]?.trim() ||
|
|
261
|
+
line.match(/Run ID:\s*(.+)$/i)?.[1]?.trim()).find(Boolean);
|
|
262
|
+
const baseUrl = events.map((event) => event.payload?.loadmillBaseUrl)
|
|
263
|
+
.find((value) => typeof value === "string" && value.trim()) ?? "https://app.loadmill.com";
|
|
264
|
+
let runUrl = null;
|
|
265
|
+
if (runId) {
|
|
266
|
+
try {
|
|
267
|
+
const url = new URL(`${baseUrl.trim().replace(/\/$/, "")}/app/api-tests/test-suite-runs/${encodeURIComponent(runId.trim())}`);
|
|
268
|
+
if (url.protocol === "https:" || url.protocol === "http:")
|
|
269
|
+
runUrl = url.href;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Keep the original log text when the recorded base URL is invalid.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const renderedLines = lines.map((line) => {
|
|
276
|
+
const started = line.match(/^Test started \(ID:\s*([^)]+)\)\.(.*)$/i);
|
|
277
|
+
const text = started && runUrl
|
|
278
|
+
? `<a href="${escapeHtml(runUrl)}" target="_blank" rel="noreferrer">Test started</a>${renderLinkedText(started[2])}`
|
|
279
|
+
: renderLinkedText(line);
|
|
280
|
+
return `<div>${text}</div>`;
|
|
281
|
+
});
|
|
258
282
|
const failed = events.some((event) => /\[Loadmill\]\s*FAILED|^Error:/i.test(event.text));
|
|
259
283
|
const passed = !failed && events.some((event) => /\bpassed\b/i.test(event.text));
|
|
260
284
|
const status = failed ? "failed" : passed ? "passed" : "running";
|
|
261
285
|
return `<details class="dc-special dc-loadmill"${passed ? "" : " open"}>
|
|
262
286
|
<summary><span class="dc-special-title">${ICONS.loadmill}Loadmill Flow</span><span class="dc-pill dc-pill-${status}">${status}</span></summary>
|
|
263
|
-
<div class="dc-special-body">${
|
|
287
|
+
<div class="dc-special-body">${renderedLines.join("")}</div>
|
|
264
288
|
</details>`;
|
|
265
289
|
}
|
|
266
290
|
function renderTimeline(events, shots, warnings = []) {
|
|
@@ -464,10 +488,12 @@ ${STYLES}
|
|
|
464
488
|
</style>
|
|
465
489
|
</head>
|
|
466
490
|
<body>
|
|
491
|
+
<!--email_off-->
|
|
467
492
|
<div class="dc-wrap" id="dc-top">
|
|
468
493
|
<div class="dc-header"><div class="dc-title">Run Details: ${escapeHtml(heading)}</div><span class="dc-pill dc-pill-${escapeHtml(model.status)}">${escapeHtml(runStatusLabel(model.status))}</span></div>
|
|
469
494
|
${body}
|
|
470
495
|
</div>${lightboxes ? `\n${lightboxes}` : ""}${script ? `\n${script}` : ""}
|
|
496
|
+
<!--/email_off-->
|
|
471
497
|
</body>
|
|
472
498
|
</html>`;
|
|
473
499
|
}
|