codeloop-mcp-server 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/critical_floors.d.ts +8 -4
- package/dist/auth/critical_floors.d.ts.map +1 -1
- package/dist/auth/critical_floors.js +17 -17
- package/dist/auth/critical_floors.js.map +1 -1
- package/dist/auth/init_hint_cache.d.ts +35 -0
- package/dist/auth/init_hint_cache.d.ts.map +1 -0
- package/dist/auth/init_hint_cache.js +143 -0
- package/dist/auth/init_hint_cache.js.map +1 -0
- package/dist/evidence/screenshot_diff.d.ts +23 -0
- package/dist/evidence/screenshot_diff.d.ts.map +1 -1
- package/dist/evidence/screenshot_diff.js +46 -13
- package/dist/evidence/screenshot_diff.js.map +1 -1
- package/dist/index.js +291 -53
- package/dist/index.js.map +1 -1
- package/dist/runners/csproj_output_path.d.ts +22 -0
- package/dist/runners/csproj_output_path.d.ts.map +1 -0
- package/dist/runners/csproj_output_path.js +108 -0
- package/dist/runners/csproj_output_path.js.map +1 -0
- package/dist/runners/png_dims.d.ts +20 -0
- package/dist/runners/png_dims.d.ts.map +1 -0
- package/dist/runners/png_dims.js +58 -0
- package/dist/runners/png_dims.js.map +1 -0
- package/dist/runners/resolve_project_dir.d.ts +67 -0
- package/dist/runners/resolve_project_dir.d.ts.map +1 -0
- package/dist/runners/resolve_project_dir.js +82 -0
- package/dist/runners/resolve_project_dir.js.map +1 -0
- package/dist/runners/screenshot.d.ts.map +1 -1
- package/dist/runners/screenshot.js +17 -2
- package/dist/runners/screenshot.js.map +1 -1
- package/dist/runners/uia_resolver.d.ts +70 -0
- package/dist/runners/uia_resolver.d.ts.map +1 -0
- package/dist/runners/uia_resolver.js +210 -0
- package/dist/runners/uia_resolver.js.map +1 -0
- package/dist/runners/window_manager.d.ts +45 -4
- package/dist/runners/window_manager.d.ts.map +1 -1
- package/dist/runners/window_manager.js +254 -26
- package/dist/runners/window_manager.js.map +1 -1
- package/dist/tools/design_compare.d.ts.map +1 -1
- package/dist/tools/design_compare.js +85 -33
- package/dist/tools/design_compare.js.map +1 -1
- package/dist/tools/desktop_app_mode.d.ts +48 -0
- package/dist/tools/desktop_app_mode.d.ts.map +1 -0
- package/dist/tools/desktop_app_mode.js +86 -0
- package/dist/tools/desktop_app_mode.js.map +1 -0
- package/dist/tools/diagnose.d.ts.map +1 -1
- package/dist/tools/diagnose.js +32 -1
- package/dist/tools/diagnose.js.map +1 -1
- package/dist/tools/discover_screens.d.ts.map +1 -1
- package/dist/tools/discover_screens.js +94 -2
- package/dist/tools/discover_screens.js.map +1 -1
- package/dist/tools/self_test.d.ts +40 -0
- package/dist/tools/self_test.d.ts.map +1 -0
- package/dist/tools/self_test.js +205 -0
- package/dist/tools/self_test.js.map +1 -0
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +4 -5
- package/dist/tools/verify.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
6
6
|
function dirHasFile(dir, predicate) {
|
|
7
7
|
try {
|
|
8
8
|
if (!existsSync(dir))
|
|
@@ -20,11 +20,13 @@ import { loadConfig } from "./config.js";
|
|
|
20
20
|
import { validateApiKey, isActivationRequired } from "./auth/api_key.js";
|
|
21
21
|
import { identifyKeySource, buildRevokedKeyDiagnostic } from "./auth/key_source.js";
|
|
22
22
|
import { warmCliCache } from "./auth/cli_cache_warmer.js";
|
|
23
|
+
import { recordInitialisedDir, wasInitialisedAtPath, } from "./auth/init_hint_cache.js";
|
|
23
24
|
import { startUpdateCheck, getUpdateInfo, formatUpdateNotice, getRunningVersion, } from "./auth/update_check.js";
|
|
24
25
|
import { applyUpdate, applyUpdateInputSchema, } from "./tools/apply_update.js";
|
|
25
26
|
import { trackUsage } from "./auth/usage_tracker.js";
|
|
26
27
|
import { isLocalMode } from "./auth/local_mode.js";
|
|
27
28
|
import { discoverProjectDir } from "./project-discovery.js";
|
|
29
|
+
import { resolveProjectDirPath } from "./runners/resolve_project_dir.js";
|
|
28
30
|
function readImageAsBase64(path) {
|
|
29
31
|
if (!existsSync(path))
|
|
30
32
|
return null;
|
|
@@ -60,6 +62,18 @@ function mimeForPath(path) {
|
|
|
60
62
|
// when the server's auto-discovered fallback is uninitialized.
|
|
61
63
|
const discovery = discoverProjectDir();
|
|
62
64
|
const projectDir = discovery.projectDir;
|
|
65
|
+
// 0.1.50 H4 — single helper that applies the project_dir precedence
|
|
66
|
+
// ladder (explicit > workspace_root > active recording > env > walked_up
|
|
67
|
+
// > default). Used by every capture / interact / record / replay / etc
|
|
68
|
+
// handler so we can't drift back to the Photometry-DB regression where
|
|
69
|
+
// missing project_dir wrote artifacts to the user's HOME folder.
|
|
70
|
+
function resolveCwd(params) {
|
|
71
|
+
return resolveProjectDirPath({
|
|
72
|
+
project_dir: params.project_dir,
|
|
73
|
+
workspace_root: params.workspace_root,
|
|
74
|
+
default_dir: projectDir,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
63
77
|
if (discovery.source !== "cwd" && discovery.source !== "env") {
|
|
64
78
|
console.error(`[CodeLoop] Auto-discovered project at: ${projectDir} (via ${discovery.source} search)`);
|
|
65
79
|
}
|
|
@@ -87,6 +101,49 @@ if (!process.env.CODELOOP_PROJECT_DIR &&
|
|
|
87
101
|
`or set CODELOOP_PROJECT_DIR in your MCP config so future calls auto-resolve. ` +
|
|
88
102
|
`codeloop_init_project will REFUSE to scaffold here.`);
|
|
89
103
|
}
|
|
104
|
+
// 0.1.49 — stale CODELOOP_PROJECT_DIR detection.
|
|
105
|
+
//
|
|
106
|
+
// When init writes a workspace pin into .cursor/mcp.json, it bakes
|
|
107
|
+
// the absolute path of the workspace at the time. If the user later
|
|
108
|
+
// renames or moves the workspace folder (common on Windows when a
|
|
109
|
+
// project graduates from D:\Work\<name> to D:\Repos\<name>), the pin
|
|
110
|
+
// keeps pointing at the old path that no longer exists, and every
|
|
111
|
+
// MCP boot resolves projectDir to a non-existent directory — which
|
|
112
|
+
// silently turns init/verify/gate into no-ops because every "does
|
|
113
|
+
// the .codeloop/ folder exist?" check returns false.
|
|
114
|
+
//
|
|
115
|
+
// We log a single, loud, agent-readable line on stderr so the agent
|
|
116
|
+
// knows to re-run `npx codeloop init` (which rewrites the pin to
|
|
117
|
+
// the workspace's current absolute path — see G8 in the CLI).
|
|
118
|
+
{
|
|
119
|
+
const pinned = process.env.CODELOOP_PROJECT_DIR;
|
|
120
|
+
if (pinned) {
|
|
121
|
+
let stale = false;
|
|
122
|
+
let reason = "";
|
|
123
|
+
try {
|
|
124
|
+
if (!existsSync(pinned)) {
|
|
125
|
+
stale = true;
|
|
126
|
+
reason = "path does not exist";
|
|
127
|
+
}
|
|
128
|
+
else if (!statSync(pinned).isDirectory()) {
|
|
129
|
+
stale = true;
|
|
130
|
+
reason = "path is not a directory";
|
|
131
|
+
}
|
|
132
|
+
else if (!existsSync(join(pinned, ".codeloop", "config.json"))) {
|
|
133
|
+
stale = true;
|
|
134
|
+
reason = "no .codeloop/config.json under the pinned path";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
stale = true;
|
|
139
|
+
reason = e.message;
|
|
140
|
+
}
|
|
141
|
+
if (stale) {
|
|
142
|
+
console.error(`[CodeLoop] ⚠ CODELOOP_PROJECT_DIR=${pinned} is stale (${reason}) — falling back to discovery. ` +
|
|
143
|
+
`Re-run \`npx codeloop init\` from the workspace's current location to rewrite the pin.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
90
147
|
const config = loadConfig(projectDir);
|
|
91
148
|
const apiKey = process.env.CODELOOP_API_KEY || config.api_key;
|
|
92
149
|
// Pre-warm the npx cache for the `codeloop` CLI in the background so
|
|
@@ -310,6 +367,13 @@ function rememberInitializedDir(dir) {
|
|
|
310
367
|
return;
|
|
311
368
|
if (isProjectInitialized(dir)) {
|
|
312
369
|
lastInitializedDir = dir;
|
|
370
|
+
// 0.1.49 — also persist to ~/.codeloop/init-hint-cache.json so
|
|
371
|
+
// the next MCP server boot (every IDE restart) doesn't false-
|
|
372
|
+
// positive the "project not initialised" hint until the agent
|
|
373
|
+
// has happened to forward `dir` to a handler that calls back
|
|
374
|
+
// into this function. Best-effort; failures swallowed inside
|
|
375
|
+
// recordInitialisedDir.
|
|
376
|
+
recordInitialisedDir(dir);
|
|
313
377
|
}
|
|
314
378
|
}
|
|
315
379
|
function withInitHint(content, dir) {
|
|
@@ -336,7 +400,12 @@ function withInitHint(content, dir) {
|
|
|
336
400
|
// home folder on Windows / Cursor — see CODELOOP_PROJECT_DIR
|
|
337
401
|
// auto-injection notes in setup-project.ts).
|
|
338
402
|
const candidates = [dir, lastInitializedDir, projectDir].filter((d) => typeof d === "string" && d.length > 0);
|
|
339
|
-
|
|
403
|
+
// 0.1.49 — also consult the persistent cache so the very first
|
|
404
|
+
// tool call after an IDE restart doesn't false-positive the hint
|
|
405
|
+
// when `dir` wasn't passed and `lastInitializedDir` is empty (the
|
|
406
|
+
// session's not warmed up yet).
|
|
407
|
+
const anyInitialized = candidates.some((d) => isProjectInitialized(d)) ||
|
|
408
|
+
candidates.some((d) => wasInitialisedAtPath(d));
|
|
340
409
|
if (!anyInitialized) {
|
|
341
410
|
head.push({ type: "text", text: INIT_HINT });
|
|
342
411
|
}
|
|
@@ -400,7 +469,7 @@ Returns: structured report with pass/fail counts, artifact paths, and next-step
|
|
|
400
469
|
project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory. MUST be an actual project folder — passing the user's home directory is rejected. If your IDE launches the MCP server from the wrong cwd (common on Windows where Cursor uses C:\\Users\\<name> as cwd), set CODELOOP_PROJECT_DIR or pass this param explicitly."),
|
|
401
470
|
workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
|
|
402
471
|
}, async (params) => {
|
|
403
|
-
const cwd = (params
|
|
472
|
+
const cwd = resolveCwd(params);
|
|
404
473
|
const explicitDir = params.project_dir || params.workspace_root;
|
|
405
474
|
const cfg = explicitDir ? loadConfig(explicitDir) : config;
|
|
406
475
|
const result = await withAuth(async () => {
|
|
@@ -488,11 +557,11 @@ Returns: categorized issues with severity, evidence, root cause, and actionable
|
|
|
488
557
|
run_id: params.run_id,
|
|
489
558
|
focus_files: params.focus_files,
|
|
490
559
|
};
|
|
491
|
-
const cwd = (params
|
|
560
|
+
const cwd = resolveCwd(params);
|
|
492
561
|
const output = await runDiagnose(input, config, cwd);
|
|
493
562
|
await trackUsage(apiKey, "verification_run");
|
|
494
563
|
return output;
|
|
495
|
-
}, { tool: "codeloop_diagnose", cwd: (params
|
|
564
|
+
}, { tool: "codeloop_diagnose", cwd: resolveCwd(params), input: params });
|
|
496
565
|
// Auto-fix-loop directive. Diagnose is only useful when it leads
|
|
497
566
|
// to a fix + re-verify, not when it leads to a long deliberation
|
|
498
567
|
// over which repair to do first. The repair_tasks array in the
|
|
@@ -556,7 +625,7 @@ Returns: pass/fail for each gate, overall confidence score, and recommendation.`
|
|
|
556
625
|
spec_path: params.spec_path,
|
|
557
626
|
acceptance_path: params.acceptance_path,
|
|
558
627
|
};
|
|
559
|
-
const cwd = (params
|
|
628
|
+
const cwd = resolveCwd(params);
|
|
560
629
|
const output = await runGateCheck(input, config, cwd);
|
|
561
630
|
// Persist gate_result and confidence to meta.json
|
|
562
631
|
try {
|
|
@@ -576,7 +645,7 @@ Returns: pass/fail for each gate, overall confidence score, and recommendation.`
|
|
|
576
645
|
catch { /* best-effort persistence */ }
|
|
577
646
|
await trackUsage(apiKey, "verification_run");
|
|
578
647
|
return output;
|
|
579
|
-
}, { tool: "codeloop_gate_check", cwd: (params
|
|
648
|
+
}, { tool: "codeloop_gate_check", cwd: resolveCwd(params), input: params });
|
|
580
649
|
const resultJson = JSON.stringify(result, null, 2);
|
|
581
650
|
const gateResult = result;
|
|
582
651
|
if (gateResult.recommendation === "continue_fixing") {
|
|
@@ -634,11 +703,11 @@ Returns: pass/fail for each gate, overall confidence score, and recommendation.`
|
|
|
634
703
|
"INCOMPLETE CRUD ARC is NEVER a reason to stop — call codeloop_plan_user_journey, follow the returned per-entity script, re-record, THEN re-gate.",
|
|
635
704
|
].join("\n");
|
|
636
705
|
return {
|
|
637
|
-
content: withInitHint([{ type: "text", text: resultJson + loopDirective }], (params
|
|
706
|
+
content: withInitHint([{ type: "text", text: resultJson + loopDirective }], resolveCwd(params)),
|
|
638
707
|
};
|
|
639
708
|
}
|
|
640
709
|
return {
|
|
641
|
-
content: withInitHint([{ type: "text", text: resultJson }], (params
|
|
710
|
+
content: withInitHint([{ type: "text", text: resultJson }], resolveCwd(params)),
|
|
642
711
|
};
|
|
643
712
|
});
|
|
644
713
|
// ── Vision Tools (agent-delegated: returns images for AI agent analysis) ──
|
|
@@ -665,11 +734,11 @@ Returns: deterministic diff results + screenshot images for visual analysis.`, {
|
|
|
665
734
|
ux_checklist_path: params.ux_checklist_path,
|
|
666
735
|
viewport_sizes: params.viewport_sizes,
|
|
667
736
|
};
|
|
668
|
-
const cwd = (params
|
|
737
|
+
const cwd = resolveCwd(params);
|
|
669
738
|
const result = await runVisualReview(input, config, cwd);
|
|
670
739
|
await trackUsage(apiKey, "visual_review");
|
|
671
740
|
return result;
|
|
672
|
-
}, { tool: "codeloop_visual_review", cwd: (params
|
|
741
|
+
}, { tool: "codeloop_visual_review", cwd: resolveCwd(params), input: params });
|
|
673
742
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
674
743
|
return { content: [{ type: "text", text: JSON.stringify(authResult, null, 2) }] };
|
|
675
744
|
}
|
|
@@ -750,11 +819,11 @@ Returns: per-screen pixel diff scores + worst-failing reference, actual, and dif
|
|
|
750
819
|
designs_dir: params.designs_dir,
|
|
751
820
|
run_id: params.run_id,
|
|
752
821
|
};
|
|
753
|
-
const cwd = (params
|
|
822
|
+
const cwd = resolveCwd(params);
|
|
754
823
|
const result = await runDesignCompare(input, config, cwd);
|
|
755
824
|
await trackUsage(apiKey, "visual_review");
|
|
756
825
|
return result;
|
|
757
|
-
}, { tool: "codeloop_design_compare", cwd: (params
|
|
826
|
+
}, { tool: "codeloop_design_compare", cwd: resolveCwd(params), input: params });
|
|
758
827
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
759
828
|
return { content: [{ type: "text", text: JSON.stringify(authResult, null, 2) }] };
|
|
760
829
|
}
|
|
@@ -1069,7 +1138,7 @@ Returns: extracted key frames as images + expected flow description + app logs f
|
|
|
1069
1138
|
}, async (params) => {
|
|
1070
1139
|
const authResult = await withAuth(async () => {
|
|
1071
1140
|
const { runInteractionReplay } = await import("./tools/interaction_replay.js");
|
|
1072
|
-
const cwd = (params
|
|
1141
|
+
const cwd = resolveCwd(params);
|
|
1073
1142
|
const output = await runInteractionReplay({
|
|
1074
1143
|
video_path: params.video_path,
|
|
1075
1144
|
run_id: params.run_id,
|
|
@@ -1077,7 +1146,7 @@ Returns: extracted key frames as images + expected flow description + app logs f
|
|
|
1077
1146
|
}, config, cwd);
|
|
1078
1147
|
await trackUsage(apiKey, "visual_review");
|
|
1079
1148
|
return output;
|
|
1080
|
-
}, { tool: "codeloop_interaction_replay", cwd: (params
|
|
1149
|
+
}, { tool: "codeloop_interaction_replay", cwd: resolveCwd(params), input: params });
|
|
1081
1150
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
1082
1151
|
return { content: [{ type: "text", text: JSON.stringify(authResult, null, 2) }] };
|
|
1083
1152
|
}
|
|
@@ -1186,7 +1255,7 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
|
|
|
1186
1255
|
const authResult = await withAuth(async () => {
|
|
1187
1256
|
const { captureScreenshot } = await import("./runners/screenshot.js");
|
|
1188
1257
|
const { createRunDir, getRunDir, getArtifactsBaseDir } = await import("./evidence/artifacts.js");
|
|
1189
|
-
const cwd = (params
|
|
1258
|
+
const cwd = resolveCwd(params);
|
|
1190
1259
|
let screenshotsDir;
|
|
1191
1260
|
if (params.run_id) {
|
|
1192
1261
|
const base = getArtifactsBaseDir(cwd);
|
|
@@ -1206,13 +1275,12 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
|
|
|
1206
1275
|
// agent forgot app_name — and the auto-fix loop would then
|
|
1207
1276
|
// burn cycles trying to "fix design diffs" against a
|
|
1208
1277
|
// screenshot of the editor.
|
|
1209
|
-
const { detectPlatform } = await import("./tools/verify.js");
|
|
1210
1278
|
const { loadConfig } = await import("./config.js");
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1279
|
+
const { isDesktopAppProject } = await import("./tools/desktop_app_mode.js");
|
|
1280
|
+
const desktopApp = isDesktopAppProject(cwd);
|
|
1213
1281
|
const cfg = loadConfig(cwd);
|
|
1214
1282
|
const targetApp = params.app_name ?? cfg.evidence?.target_app;
|
|
1215
|
-
const result = await captureScreenshot(screenshotsDir, params.screen_name, targetApp, undefined, { desktopAppMode:
|
|
1283
|
+
const result = await captureScreenshot(screenshotsDir, params.screen_name, targetApp, undefined, { desktopAppMode: desktopApp });
|
|
1216
1284
|
// Photometry-DB E2E 8 follow-on: when we capture a desktop app
|
|
1217
1285
|
// window, also resolve its on-screen bounds so the agent can
|
|
1218
1286
|
// (a) compute window-relative coords from the returned image
|
|
@@ -1223,7 +1291,7 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
|
|
|
1223
1291
|
// of the image and clicked tens or hundreds of pixels off the
|
|
1224
1292
|
// intended target.
|
|
1225
1293
|
let windowBounds = null;
|
|
1226
|
-
if (
|
|
1294
|
+
if (desktopApp && targetApp && result.captured) {
|
|
1227
1295
|
try {
|
|
1228
1296
|
const wm = await import("./runners/window_manager.js");
|
|
1229
1297
|
const b = await wm.getWindowBounds(targetApp);
|
|
@@ -1235,7 +1303,7 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
|
|
|
1235
1303
|
}
|
|
1236
1304
|
await trackUsage(apiKey, "visual_review");
|
|
1237
1305
|
return { ...result, windowBounds };
|
|
1238
|
-
}, { tool: "codeloop_capture_screenshot", cwd: (params
|
|
1306
|
+
}, { tool: "codeloop_capture_screenshot", cwd: resolveCwd(params), input: params });
|
|
1239
1307
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
1240
1308
|
return { content: [{ type: "text", text: JSON.stringify(authResult, null, 2) }] };
|
|
1241
1309
|
}
|
|
@@ -1281,8 +1349,8 @@ Returns: list of discovered screens with routes, navigation triggers, confidence
|
|
|
1281
1349
|
}, async (params) => {
|
|
1282
1350
|
const result = await withAuth(async () => {
|
|
1283
1351
|
const { discoverScreens } = await import("./tools/discover_screens.js");
|
|
1284
|
-
return discoverScreens((params
|
|
1285
|
-
}, { tool: "codeloop_discover_screens", cwd: (params
|
|
1352
|
+
return discoverScreens(resolveCwd(params), params.platform);
|
|
1353
|
+
}, { tool: "codeloop_discover_screens", cwd: resolveCwd(params), input: params });
|
|
1286
1354
|
return {
|
|
1287
1355
|
content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }]),
|
|
1288
1356
|
};
|
|
@@ -1316,8 +1384,8 @@ selects, datagrids, upload_areas, ai_features, forms }, ai_features_detected, sc
|
|
|
1316
1384
|
}, async (params) => {
|
|
1317
1385
|
const result = await withAuth(async () => {
|
|
1318
1386
|
const { discoverInteractions } = await import("./tools/discover_interactions.js");
|
|
1319
|
-
return discoverInteractions((params
|
|
1320
|
-
}, { tool: "codeloop_discover_interactions", cwd: (params
|
|
1387
|
+
return discoverInteractions(resolveCwd(params), params.platform);
|
|
1388
|
+
}, { tool: "codeloop_discover_interactions", cwd: resolveCwd(params), input: params });
|
|
1321
1389
|
return {
|
|
1322
1390
|
content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }]),
|
|
1323
1391
|
};
|
|
@@ -1359,8 +1427,8 @@ ai_substantive_prompts, upload_actions, datagrid_edits }, advice, discovered_int
|
|
|
1359
1427
|
}, async (params) => {
|
|
1360
1428
|
const result = await withAuth(async () => {
|
|
1361
1429
|
const { planUserJourney } = await import("./tools/plan_user_journey.js");
|
|
1362
|
-
return planUserJourney((params
|
|
1363
|
-
}, { tool: "codeloop_plan_user_journey", cwd: (params
|
|
1430
|
+
return planUserJourney(resolveCwd(params), params.platform, params.top_n);
|
|
1431
|
+
}, { tool: "codeloop_plan_user_journey", cwd: resolveCwd(params), input: params });
|
|
1364
1432
|
// Auto-fix loop directive. The plan is ONLY useful if the agent
|
|
1365
1433
|
// now drives it via a recording session — otherwise it's a
|
|
1366
1434
|
// detailed document that gets read and then deliberated over.
|
|
@@ -1402,7 +1470,7 @@ After recording, call codeloop_interaction_replay to extract frames and analyze
|
|
|
1402
1470
|
const authResult = await withAuth(async () => {
|
|
1403
1471
|
const { recordVideo } = await import("./runners/video_recorder.js");
|
|
1404
1472
|
const { createRunDir, getRunDir, getArtifactsBaseDir } = await import("./evidence/artifacts.js");
|
|
1405
|
-
const cwd = (params
|
|
1473
|
+
const cwd = resolveCwd(params);
|
|
1406
1474
|
let videosDir;
|
|
1407
1475
|
if (params.run_id) {
|
|
1408
1476
|
const base = getArtifactsBaseDir(cwd);
|
|
@@ -1415,7 +1483,7 @@ After recording, call codeloop_interaction_replay to extract frames and analyze
|
|
|
1415
1483
|
const result = await recordVideo(videosDir, params.duration_seconds, params.app_name);
|
|
1416
1484
|
await trackUsage(apiKey, "visual_review");
|
|
1417
1485
|
return result;
|
|
1418
|
-
}, { tool: "codeloop_record_interaction", cwd: (params
|
|
1486
|
+
}, { tool: "codeloop_record_interaction", cwd: resolveCwd(params), input: params });
|
|
1419
1487
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
1420
1488
|
return { content: [{ type: "text", text: JSON.stringify(authResult, null, 2) }] };
|
|
1421
1489
|
}
|
|
@@ -1444,7 +1512,7 @@ init for .NET/Xcode/Android projects via detect-target-app).`, {
|
|
|
1444
1512
|
const authResult = await withAuth(async () => {
|
|
1445
1513
|
const wm = await import("./runners/window_manager.js");
|
|
1446
1514
|
const { loadConfig } = await import("./config.js");
|
|
1447
|
-
const cwd = (params
|
|
1515
|
+
const cwd = resolveCwd(params);
|
|
1448
1516
|
const cfg = loadConfig(cwd);
|
|
1449
1517
|
const appName = params.app_name || cfg.evidence?.target_app;
|
|
1450
1518
|
if (!appName) {
|
|
@@ -1455,7 +1523,7 @@ init for .NET/Xcode/Android projects via detect-target-app).`, {
|
|
|
1455
1523
|
}
|
|
1456
1524
|
const r = await wm.launchDesktopApp(appName, cwd);
|
|
1457
1525
|
return { app_name: appName, ...r };
|
|
1458
|
-
}, { tool: "codeloop_launch_app", cwd: (params
|
|
1526
|
+
}, { tool: "codeloop_launch_app", cwd: resolveCwd(params), input: params });
|
|
1459
1527
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
1460
1528
|
return { content: [{ type: "text", text: JSON.stringify(authResult, null, 2) }] };
|
|
1461
1529
|
}
|
|
@@ -1503,7 +1571,7 @@ App logs (stdout, logcat, simctl log) are automatically captured alongside the v
|
|
|
1503
1571
|
const { createRunDir, getRunDir, getArtifactsBaseDir } = await import("./evidence/artifacts.js");
|
|
1504
1572
|
const { detectTargetType } = await import("./runners/platform_detect.js");
|
|
1505
1573
|
const { loadConfig } = await import("./config.js");
|
|
1506
|
-
const cwd = (params
|
|
1574
|
+
const cwd = resolveCwd(params);
|
|
1507
1575
|
let videosDir;
|
|
1508
1576
|
if (params.run_id) {
|
|
1509
1577
|
const base = getArtifactsBaseDir(cwd);
|
|
@@ -1565,7 +1633,7 @@ App logs (stdout, logcat, simctl log) are automatically captured alongside the v
|
|
|
1565
1633
|
}
|
|
1566
1634
|
await trackUsage(apiKey, "visual_review");
|
|
1567
1635
|
return result;
|
|
1568
|
-
}, { tool: "codeloop_start_recording", cwd: (params
|
|
1636
|
+
}, { tool: "codeloop_start_recording", cwd: resolveCwd(params), input: params });
|
|
1569
1637
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
1570
1638
|
return { content: [{ type: "text", text: JSON.stringify(authResult, null, 2) }] };
|
|
1571
1639
|
}
|
|
@@ -1648,7 +1716,7 @@ The agent MUST then write the report to docs/DEVELOPMENT_LOG.md and present it t
|
|
|
1648
1716
|
const result = await withAuth(async () => {
|
|
1649
1717
|
const { listRuns, loadRunMeta, getArtifactsBaseDir, getRunDir } = await import("./evidence/artifacts.js");
|
|
1650
1718
|
const { readdirSync, existsSync } = await import("fs");
|
|
1651
|
-
const cwd = (params
|
|
1719
|
+
const cwd = resolveCwd(params);
|
|
1652
1720
|
const baseDir = getArtifactsBaseDir(cwd);
|
|
1653
1721
|
const runs = listRuns(baseDir);
|
|
1654
1722
|
const runSummaries = [];
|
|
@@ -1793,7 +1861,7 @@ The agent MUST then write the report to docs/DEVELOPMENT_LOG.md and present it t
|
|
|
1793
1861
|
};
|
|
1794
1862
|
await trackUsage(apiKey, "verification_run");
|
|
1795
1863
|
return report;
|
|
1796
|
-
}, { tool: "codeloop_generate_dev_report", cwd: (params
|
|
1864
|
+
}, { tool: "codeloop_generate_dev_report", cwd: resolveCwd(params), input: params });
|
|
1797
1865
|
if (typeof result === "object" && result !== null && "error" in result) {
|
|
1798
1866
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1799
1867
|
}
|
|
@@ -1915,7 +1983,7 @@ Returns: checklist of completed and pending verification steps.`, {
|
|
|
1915
1983
|
const { listRuns, loadRunMeta, getArtifactsBaseDir, getRunDir } = await import("./evidence/artifacts.js");
|
|
1916
1984
|
const { detectPlatform } = await import("./tools/verify.js");
|
|
1917
1985
|
const { detectDesktopUI } = await import("./tools/desktop_detection.js");
|
|
1918
|
-
const cwd = (params
|
|
1986
|
+
const cwd = resolveCwd(params);
|
|
1919
1987
|
const platform = detectPlatform(cwd);
|
|
1920
1988
|
// UI detection includes desktop .NET / native: WPF, WinForms, MAUI,
|
|
1921
1989
|
// Avalonia, WinUI, UWP. Without this, every WPF/.NET 8 / MAUI / Avalonia
|
|
@@ -2012,10 +2080,25 @@ Returns: checklist of completed and pending verification steps.`, {
|
|
|
2012
2080
|
const verdict = evaluateDepth(coverage, minimums, discoverySnapshot);
|
|
2013
2081
|
const b = coverage.buckets;
|
|
2014
2082
|
const breakdown = `click=${b.click}, navigation=${b.navigation}, input=${b.input}, commit=${b.commit}, toggle=${b.toggle}, gesture=${b.gesture}, upload=${b.upload}, keystroke=${b.keystroke}, inspect=${b.inspect}`;
|
|
2083
|
+
// 0.1.49: coordinate_clicks_without_intent is now a HARD
|
|
2084
|
+
// step-7 PENDING blocker so the agent sees the gap BEFORE
|
|
2085
|
+
// gate_check, not after. Pre-0.1.49 this only surfaced as
|
|
2086
|
+
// a verify postscript note, which agents commonly ignored
|
|
2087
|
+
// until the user_journey_evidence gate failed at the
|
|
2088
|
+
// bottom of a long verify→capture→video→gate cycle —
|
|
2089
|
+
// wasting the entire UI-evidence loop.
|
|
2090
|
+
const coordsWithoutIntent = coverage.coordinate_clicks_without_intent;
|
|
2015
2091
|
if (!minimums.enabled) {
|
|
2016
2092
|
depthStatus = "n/a";
|
|
2017
2093
|
depthDetail = `Depth gate disabled in .codeloop/config.json. Observed buckets: ${breakdown}.`;
|
|
2018
2094
|
}
|
|
2095
|
+
else if (coordsWithoutIntent > 0) {
|
|
2096
|
+
depthStatus = "PENDING";
|
|
2097
|
+
depthDetail =
|
|
2098
|
+
`${coverage.successful} successful interactions across ${runs.length} run(s) (${breakdown}). ` +
|
|
2099
|
+
`BLOCKER: ${coordsWithoutIntent} coordinate-only click(s) dispatched without intent / description / purpose / step fields — the CRUD classifier in user_journey_evidence cannot credit them as edit/delete/create. ` +
|
|
2100
|
+
`Re-run codeloop_interact for those clicks WITH \`intent\` (e.g. intent="confirm delete dialog", intent="save form"). Otherwise gate_check will return continue_fixing on user_journey_evidence even after the rest of the workflow is green.`;
|
|
2101
|
+
}
|
|
2019
2102
|
else if (verdict.passed) {
|
|
2020
2103
|
depthStatus = "done";
|
|
2021
2104
|
depthDetail = `${coverage.successful} successful interactions across ${runs.length} run(s) (${breakdown}). Depth minimums met.`;
|
|
@@ -2104,7 +2187,7 @@ Returns: checklist of completed and pending verification steps.`, {
|
|
|
2104
2187
|
? "All CodeLoop verification steps are complete. You may proceed."
|
|
2105
2188
|
: `WARNING: ${pendingSteps.length} step(s) still pending. DO NOT declare this task complete. DO NOT ask the user what to do next. Complete the pending steps below, then call codeloop_gate_check. If gate returns continue_fixing, loop back and fix without asking.\n${pendingSteps.map(s => ` - ${s.step}: ${s.detail}`).join("\n")}`,
|
|
2106
2189
|
};
|
|
2107
|
-
}, { tool: "codeloop_check_workflow", cwd: (params
|
|
2190
|
+
}, { tool: "codeloop_check_workflow", cwd: resolveCwd(params), input: params });
|
|
2108
2191
|
return {
|
|
2109
2192
|
content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }]),
|
|
2110
2193
|
};
|
|
@@ -2144,11 +2227,13 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2144
2227
|
y: z.number().optional().describe("Y coordinate for click/scroll/drag/swipe"),
|
|
2145
2228
|
x2: z.number().optional().describe("End X for drag_drop/swipe"),
|
|
2146
2229
|
y2: z.number().optional().describe("End Y for drag_drop/swipe"),
|
|
2147
|
-
text: z.string().optional().describe("Text for type/type_and_submit/type_and_tab/fill"),
|
|
2230
|
+
text: z.string().optional().describe("Text for type/type_and_submit/type_and_tab/fill. 0.1.50+: ALSO accepted on click/double_click/right_click/hover with no x/y on Windows desktop targets — walks the UIA tree to find the first element whose Name property matches (exact, then substring) and clicks its centre. Closes the Photometry-DB E2E 8 regression where `{ action: \"click\", text: \"Luminaire Photometric Data\" }` produced `click at (undefined, undefined)`."),
|
|
2148
2231
|
key: z.string().optional().describe("Key name for keystroke: enter, tab, escape, backspace, delete, etc."),
|
|
2149
2232
|
keys: z.string().optional().describe("Key combo for hotkey: cmd+s, ctrl+enter, cmd+shift+z, etc."),
|
|
2150
2233
|
selector: z.string().optional().describe("CSS selector (browser) or automation ID (Windows)"),
|
|
2151
2234
|
selector2: z.string().optional().describe("Second selector for drag target"),
|
|
2235
|
+
automation_id: z.string().optional().describe("[Windows desktop] UIA AutomationId of the target element. 0.1.50+: when supplied for click/double_click/right_click/hover with no x/y, CodeLoop walks the UIA tree and resolves the element's screen coords automatically (DPI-aware, window-origin-aware), then clicks at the centre. Most stable selector for WPF/WinUI/UWP — prefer this over `text` whenever the control exposes one."),
|
|
2236
|
+
role: z.string().optional().describe("[Windows desktop] UIA ControlType programmatic name (e.g. `ControlType.Button`, `ControlType.TabItem`). 0.1.50+: when supplied for click/double_click/right_click/hover with no x/y, walks the UIA tree and clicks the FIRST element of that ControlType. Use as a last resort when neither AutomationId nor Name is specific enough."),
|
|
2152
2237
|
url: z.string().optional().describe("URL for navigate_url or deep_link"),
|
|
2153
2238
|
direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll/swipe direction"),
|
|
2154
2239
|
amount: z.number().optional().describe("Scroll amount or other numeric value"),
|
|
@@ -2180,7 +2265,8 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2180
2265
|
description: z.string().optional().describe("[Alias for intent] Same semantics."),
|
|
2181
2266
|
purpose: z.string().optional().describe("[Alias for intent] Same semantics."),
|
|
2182
2267
|
step: z.string().optional().describe("Plan-step name when this interaction is driving a codeloop_plan_user_journey arc (e.g. 'edit', 'delete', 'create', 'save', 'verify'). Logged alongside `intent` and read by the CRUD classifier."),
|
|
2183
|
-
coords: z.enum(["auto", "window", "screen"]).optional().describe("How to interpret x/y for desktop click/double_click/right_click/hover/scroll/drag/long_press. `auto` (default): if `app_name` resolves to a visible window AND (x, y) fits inside the window's client area, treat as window-relative and auto-offset by the window origin; otherwise leave as raw screen-absolute coords. `window`: ALWAYS add the window origin offset (errors if the window isn't found). `screen`: ALWAYS pass through (legacy behaviour, matches CGEvent / user32.dll / xdotool semantics). Fixes the Photometry-DB E2E 8 failure mode where the agent captured a 1600×900 window screenshot, computed click coords against the image, and missed the sidebar because the window's actual top-left was (286, 286) on a 5120×1440 screen."),
|
|
2268
|
+
coords: z.enum(["auto", "window", "screen", "screenshot"]).optional().describe("How to interpret x/y for desktop click/double_click/right_click/hover/scroll/drag/long_press. `auto` (default): if `app_name` resolves to a visible window AND (x, y) fits inside the window's client area, treat as window-relative and auto-offset by the window origin; otherwise leave as raw screen-absolute coords. `window`: ALWAYS add the window origin offset (errors if the window isn't found). `screen`: ALWAYS pass through (legacy behaviour, matches CGEvent / user32.dll / xdotool semantics). `screenshot` (most accurate for vision-driven agents): treat (x, y) as coordinates against a captured screenshot — provide `screenshot_path` so the runner can read the image's actual width/height, scale (x, y) to the window's true pixel dimensions, then add the window origin and apply DPI. Use this whenever you computed coords from the image returned by codeloop_capture_screenshot, especially when the MCP transport may have downscaled the PNG. Fixes the Photometry-DB E2E 8 failure mode where the agent captured a 1600×900 window screenshot, computed click coords against the image, and missed the sidebar because the window's actual top-left was (286, 286) on a 5120×1440 screen."),
|
|
2269
|
+
screenshot_path: z.string().optional().describe("Absolute path to the screenshot PNG that x/y were computed against. Used with `coords: \"screenshot\"` to scale agent-supplied coords from the captured image dimensions to the window's actual pixel dimensions before applying the window origin and DPI factor. Pass the `path` field returned by codeloop_capture_screenshot."),
|
|
2184
2270
|
project_dir: z.string().optional().describe("Absolute path to project root."),
|
|
2185
2271
|
workspace_root: z.string().optional().describe("[Alias for project_dir] Pass either; they're equivalent."),
|
|
2186
2272
|
}, async (params) => {
|
|
@@ -2193,7 +2279,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2193
2279
|
const bi = await import("./runners/browser_interaction.js");
|
|
2194
2280
|
const vr = await import("./runners/video_recorder.js");
|
|
2195
2281
|
// Auto-detect target_type when omitted
|
|
2196
|
-
const cwd = (params
|
|
2282
|
+
const cwd = resolveCwd(params);
|
|
2197
2283
|
let tt = params.target_type;
|
|
2198
2284
|
if (!tt) {
|
|
2199
2285
|
const recordingTarget = vr.getActiveRecordingTargetType();
|
|
@@ -2211,6 +2297,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2211
2297
|
}
|
|
2212
2298
|
// Bring the app to front before desktop interactions (non-browser, non-mobile).
|
|
2213
2299
|
let windowOriginOffset = null;
|
|
2300
|
+
let screenshotDims = null;
|
|
2214
2301
|
if (tt === "desktop") {
|
|
2215
2302
|
const appName = params.app_name || vr.getActiveRecordingAppName();
|
|
2216
2303
|
if (appName && action !== "wait") {
|
|
@@ -2233,14 +2320,68 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2233
2320
|
try {
|
|
2234
2321
|
const b = await wm.getWindowBounds(appName);
|
|
2235
2322
|
if (b && b.width > 0 && b.height > 0) {
|
|
2236
|
-
windowOriginOffset = {
|
|
2323
|
+
windowOriginOffset = {
|
|
2324
|
+
dx: b.x,
|
|
2325
|
+
dy: b.y,
|
|
2326
|
+
width: b.width,
|
|
2327
|
+
height: b.height,
|
|
2328
|
+
dpiX: b.dpi_x,
|
|
2329
|
+
dpiY: b.dpi_y,
|
|
2330
|
+
};
|
|
2237
2331
|
}
|
|
2238
2332
|
}
|
|
2239
2333
|
catch { /* best-effort */ }
|
|
2240
2334
|
}
|
|
2335
|
+
// For coords:"screenshot", load the actual PNG dims so we
|
|
2336
|
+
// can scale agent-supplied (x, y) up from the (possibly
|
|
2337
|
+
// MCP-downscaled) image to the window's true pixel size.
|
|
2338
|
+
if (coordsMode === "screenshot" && params.screenshot_path) {
|
|
2339
|
+
try {
|
|
2340
|
+
const { readPngDims } = await import("./runners/png_dims.js");
|
|
2341
|
+
screenshotDims = readPngDims(params.screenshot_path);
|
|
2342
|
+
}
|
|
2343
|
+
catch { /* best-effort */ }
|
|
2344
|
+
}
|
|
2241
2345
|
}
|
|
2242
2346
|
}
|
|
2347
|
+
// 0.1.50 H1 — when an agent passes `text` / `role` /
|
|
2348
|
+
// `automation_id` (no x/y) to a desktop click-family action,
|
|
2349
|
+
// walk the UIA tree to resolve the centre of the matching
|
|
2350
|
+
// element. The resolved (x, y) is screen-absolute so it
|
|
2351
|
+
// bypasses translateXY (which is for agent-supplied coords).
|
|
2352
|
+
const resolveDesktopSelector = async () => {
|
|
2353
|
+
if (tt !== "desktop" || process.platform !== "win32")
|
|
2354
|
+
return null;
|
|
2355
|
+
if (params.x != null && params.y != null)
|
|
2356
|
+
return null;
|
|
2357
|
+
const appName = params.app_name || vr.getActiveRecordingAppName();
|
|
2358
|
+
if (!appName)
|
|
2359
|
+
return null;
|
|
2360
|
+
const hasSelector = (params.automation_id && params.automation_id.length > 0) ||
|
|
2361
|
+
(params.text && params.text.length > 0) ||
|
|
2362
|
+
(params.role && params.role.length > 0);
|
|
2363
|
+
if (!hasSelector)
|
|
2364
|
+
return null;
|
|
2365
|
+
try {
|
|
2366
|
+
const { resolveSelectorToXY } = await import("./runners/uia_resolver.js");
|
|
2367
|
+
const r = await resolveSelectorToXY({
|
|
2368
|
+
appName,
|
|
2369
|
+
automationId: params.automation_id,
|
|
2370
|
+
text: params.text,
|
|
2371
|
+
role: params.role,
|
|
2372
|
+
});
|
|
2373
|
+
if (r.found && r.x != null && r.y != null) {
|
|
2374
|
+
return { x: r.x, y: r.y, foundBy: r.foundBy ?? "unknown" };
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
catch { /* best-effort */ }
|
|
2378
|
+
return null;
|
|
2379
|
+
};
|
|
2243
2380
|
// Helper used by every coordinate-driven desktop action below.
|
|
2381
|
+
// Photometry-DB E2E 8 + 0.1.49 hardening: handles four modes
|
|
2382
|
+
// (auto / window / screen / screenshot) plus an optional DPI
|
|
2383
|
+
// factor on the window bounds so high-DPI Windows / Retina
|
|
2384
|
+
// displays don't drop clicks 100s of pixels off-target.
|
|
2244
2385
|
const translateXY = (x, y) => {
|
|
2245
2386
|
if (tt !== "desktop" || x == null || y == null || !windowOriginOffset) {
|
|
2246
2387
|
return { x, y };
|
|
@@ -2248,8 +2389,35 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2248
2389
|
const mode = params.coords ?? "auto";
|
|
2249
2390
|
if (mode === "screen")
|
|
2250
2391
|
return { x, y };
|
|
2392
|
+
const applyDpi = (px, py) => {
|
|
2393
|
+
// window_manager records DPI in physical-pixel-per-logical
|
|
2394
|
+
// form (1.0 = 96 DPI baseline; 2.0 = 200% / Retina). When
|
|
2395
|
+
// the screenshot was captured in logical pixels but the
|
|
2396
|
+
// OS click API expects physical pixels (Win32 user32.dll
|
|
2397
|
+
// and modern macOS CGEvent both expect physical), scale up.
|
|
2398
|
+
const dpiX = windowOriginOffset.dpiX ?? 1;
|
|
2399
|
+
const dpiY = windowOriginOffset.dpiY ?? 1;
|
|
2400
|
+
if (dpiX === 1 && dpiY === 1)
|
|
2401
|
+
return { x: px, y: py };
|
|
2402
|
+
return { x: px * dpiX, y: py * dpiY };
|
|
2403
|
+
};
|
|
2404
|
+
if (mode === "screenshot") {
|
|
2405
|
+
// Scale (x, y) from screenshot dims → window dims,
|
|
2406
|
+
// then add the window origin, then DPI.
|
|
2407
|
+
let sx = x;
|
|
2408
|
+
let sy = y;
|
|
2409
|
+
if (screenshotDims && screenshotDims.width > 0 && screenshotDims.height > 0) {
|
|
2410
|
+
const ratioX = windowOriginOffset.width / screenshotDims.width;
|
|
2411
|
+
const ratioY = windowOriginOffset.height / screenshotDims.height;
|
|
2412
|
+
sx = x * ratioX;
|
|
2413
|
+
sy = y * ratioY;
|
|
2414
|
+
}
|
|
2415
|
+
const dpi = applyDpi(sx, sy);
|
|
2416
|
+
return { x: dpi.x + windowOriginOffset.dx, y: dpi.y + windowOriginOffset.dy };
|
|
2417
|
+
}
|
|
2251
2418
|
if (mode === "window") {
|
|
2252
|
-
|
|
2419
|
+
const dpi = applyDpi(x, y);
|
|
2420
|
+
return { x: dpi.x + windowOriginOffset.dx, y: dpi.y + windowOriginOffset.dy };
|
|
2253
2421
|
}
|
|
2254
2422
|
// auto: if (x, y) fits inside the window's client area,
|
|
2255
2423
|
// assume the agent computed against a window-cropped
|
|
@@ -2258,7 +2426,8 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2258
2426
|
const inside = x >= 0 && x <= windowOriginOffset.width &&
|
|
2259
2427
|
y >= 0 && y <= windowOriginOffset.height;
|
|
2260
2428
|
if (inside) {
|
|
2261
|
-
|
|
2429
|
+
const dpi = applyDpi(x, y);
|
|
2430
|
+
return { x: dpi.x + windowOriginOffset.dx, y: dpi.y + windowOriginOffset.dy };
|
|
2262
2431
|
}
|
|
2263
2432
|
return { x, y };
|
|
2264
2433
|
};
|
|
@@ -2286,7 +2455,16 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2286
2455
|
const t = translateXY(params.x, params.y);
|
|
2287
2456
|
success = await wm.clickAtPosition(t.x, t.y);
|
|
2288
2457
|
}
|
|
2289
|
-
|
|
2458
|
+
else {
|
|
2459
|
+
// 0.1.50 H1 — UIA selector fallback for click without coords.
|
|
2460
|
+
const resolved = await resolveDesktopSelector();
|
|
2461
|
+
if (resolved) {
|
|
2462
|
+
success = await wm.clickAtPosition(resolved.x, resolved.y);
|
|
2463
|
+
detail = `click at ${resolved.foundBy}=${params.automation_id || params.text || params.role} → (${resolved.x},${resolved.y})`;
|
|
2464
|
+
break;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
detail = `click at ${params.selector || params.automation_id || params.text || params.role || `(${params.x},${params.y})`}`;
|
|
2290
2468
|
break;
|
|
2291
2469
|
case "double_click":
|
|
2292
2470
|
if (tt === "browser" && params.selector) {
|
|
@@ -2296,7 +2474,15 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2296
2474
|
const t = translateXY(params.x, params.y);
|
|
2297
2475
|
success = await wm.doubleClickAtPosition(t.x, t.y);
|
|
2298
2476
|
}
|
|
2299
|
-
|
|
2477
|
+
else {
|
|
2478
|
+
const resolved = await resolveDesktopSelector();
|
|
2479
|
+
if (resolved) {
|
|
2480
|
+
success = await wm.doubleClickAtPosition(resolved.x, resolved.y);
|
|
2481
|
+
detail = `double_click at ${resolved.foundBy}=${params.automation_id || params.text || params.role} → (${resolved.x},${resolved.y})`;
|
|
2482
|
+
break;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
detail = `double_click at ${params.selector || params.automation_id || params.text || params.role || `(${params.x},${params.y})`}`;
|
|
2300
2486
|
break;
|
|
2301
2487
|
case "right_click":
|
|
2302
2488
|
if (tt === "browser" && params.selector) {
|
|
@@ -2306,7 +2492,15 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2306
2492
|
const t = translateXY(params.x, params.y);
|
|
2307
2493
|
success = await wm.rightClickAtPosition(t.x, t.y);
|
|
2308
2494
|
}
|
|
2309
|
-
|
|
2495
|
+
else {
|
|
2496
|
+
const resolved = await resolveDesktopSelector();
|
|
2497
|
+
if (resolved) {
|
|
2498
|
+
success = await wm.rightClickAtPosition(resolved.x, resolved.y);
|
|
2499
|
+
detail = `right_click at ${resolved.foundBy}=${params.automation_id || params.text || params.role} → (${resolved.x},${resolved.y})`;
|
|
2500
|
+
break;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
detail = `right_click at ${params.selector || params.automation_id || params.text || params.role || `(${params.x},${params.y})`}`;
|
|
2310
2504
|
break;
|
|
2311
2505
|
case "hover":
|
|
2312
2506
|
if (tt === "browser" && params.selector) {
|
|
@@ -2316,7 +2510,15 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2316
2510
|
const t = translateXY(params.x, params.y);
|
|
2317
2511
|
success = await wm.hoverAtPosition(t.x, t.y);
|
|
2318
2512
|
}
|
|
2319
|
-
|
|
2513
|
+
else {
|
|
2514
|
+
const resolved = await resolveDesktopSelector();
|
|
2515
|
+
if (resolved) {
|
|
2516
|
+
success = await wm.hoverAtPosition(resolved.x, resolved.y);
|
|
2517
|
+
detail = `hover at ${resolved.foundBy}=${params.automation_id || params.text || params.role} → (${resolved.x},${resolved.y})`;
|
|
2518
|
+
break;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
detail = `hover at ${params.selector || params.automation_id || params.text || params.role || `(${params.x},${params.y})`}`;
|
|
2320
2522
|
break;
|
|
2321
2523
|
case "type":
|
|
2322
2524
|
if (tt === "browser" && params.selector && params.text) {
|
|
@@ -2661,7 +2863,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2661
2863
|
case "maestro_flow":
|
|
2662
2864
|
if (params.maestro_steps) {
|
|
2663
2865
|
const mg = await import("./runners/maestro_generator.js");
|
|
2664
|
-
const cwd = (params
|
|
2866
|
+
const cwd = resolveCwd(params);
|
|
2665
2867
|
const genResult = await mg.generateMaestroFlow(params.maestro_steps, cwd);
|
|
2666
2868
|
if ("error" in genResult) {
|
|
2667
2869
|
return { success: false, action, detail: genResult.error };
|
|
@@ -2977,7 +3179,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2977
3179
|
}
|
|
2978
3180
|
catch { /* best-effort logging */ }
|
|
2979
3181
|
return { success, action, detail };
|
|
2980
|
-
}, { tool: "codeloop_interact", cwd: (params
|
|
3182
|
+
}, { tool: "codeloop_interact", cwd: resolveCwd(params), input: params });
|
|
2981
3183
|
return {
|
|
2982
3184
|
content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }]),
|
|
2983
3185
|
};
|
|
@@ -2999,7 +3201,7 @@ project. After it completes, proceed directly with \`codeloop_verify\`.`, {
|
|
|
2999
3201
|
workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
|
|
3000
3202
|
project_type: z.enum(["flutter", "web", "mobile", "xcode", "android", "dotnet", "node", "auto"]).default("auto").describe("Project type. Use 'auto' to detect automatically."),
|
|
3001
3203
|
}, async (params) => {
|
|
3002
|
-
const cwd = (params
|
|
3204
|
+
const cwd = resolveCwd(params);
|
|
3003
3205
|
const result = await (async () => {
|
|
3004
3206
|
const { runInitProject } = await import("./tools/init-project.js");
|
|
3005
3207
|
const output = await runInitProject({
|
|
@@ -3025,7 +3227,7 @@ Returns: counts for attempted / succeeded / requeued events and the queue locati
|
|
|
3025
3227
|
project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory. MUST be an actual project folder — passing the user's home directory is rejected. If your IDE launches the MCP server from the wrong cwd (common on Windows where Cursor uses C:\\Users\\<name> as cwd), set CODELOOP_PROJECT_DIR or pass this param explicitly."),
|
|
3026
3228
|
workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
|
|
3027
3229
|
}, async (params) => {
|
|
3028
|
-
const cwd = (params
|
|
3230
|
+
const cwd = resolveCwd(params);
|
|
3029
3231
|
const { flushPersistedUsage } = await import("./auth/usage_tracker.js");
|
|
3030
3232
|
const result = await flushPersistedUsage(cwd);
|
|
3031
3233
|
return {
|
|
@@ -3113,6 +3315,42 @@ No project_dir / workspace_root required — this tool is workspace-independent.
|
|
|
3113
3315
|
]),
|
|
3114
3316
|
};
|
|
3115
3317
|
});
|
|
3318
|
+
server.tool("codeloop_self_test", TOOL_BOOTSTRAP + `Pre-flight smoke test for CodeLoop on the current workspace. Run this on any NEW project BEFORE your first verify cycle, or whenever something looks off (silent IDE captures, "no .exe found", design_compare returning 0%, etc.).
|
|
3319
|
+
|
|
3320
|
+
It validates every critical pre-condition synthetically (no live build, no live screenshot, no network past the platform sniff):
|
|
3321
|
+
- Workspace exists on disk and is a directory
|
|
3322
|
+
- codeloop_init_project has been run (.codeloop/config.json present)
|
|
3323
|
+
- Platform detection produced a known platform
|
|
3324
|
+
- isDesktopAppProject correctly identifies the project type (so captureScreenshot won't silently fall back to fullscreen)
|
|
3325
|
+
- evidence.target_app is set when desktop-app mode is ON (so launchDesktopApp + captureScreenshot can resolve a window)
|
|
3326
|
+
- PNG decoder skip path is wired (corrupt PNGs become skip warnings, not 0% match)
|
|
3327
|
+
- Coordinate translation round-trips on a synthetic high-DPI fixture (so clicks land where the agent expects on Retina / 200%-DPI displays)
|
|
3328
|
+
|
|
3329
|
+
Returns a structured pass/fail report with per-check fix suggestions and a single \`next_step\` directive.
|
|
3330
|
+
|
|
3331
|
+
Use this tool FIRST when:
|
|
3332
|
+
- The user reports CodeLoop "isn't working" or evidence is missing
|
|
3333
|
+
- Switching to a project / repo you've never run CodeLoop against
|
|
3334
|
+
- Debugging unexplained gate failures that don't match the agent's mental model
|
|
3335
|
+
|
|
3336
|
+
Idempotent and free — safe to call as the first step of every new chat.`, {
|
|
3337
|
+
project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory."),
|
|
3338
|
+
workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics."),
|
|
3339
|
+
}, async (params) => {
|
|
3340
|
+
const result = await withAuth(async () => {
|
|
3341
|
+
const cwd = resolveCwd(params);
|
|
3342
|
+
const { runSelfTest } = await import("./tools/self_test.js");
|
|
3343
|
+
return runSelfTest(cwd);
|
|
3344
|
+
}, { tool: "codeloop_self_test", cwd: resolveCwd(params), input: params });
|
|
3345
|
+
if (typeof result === "object" && result !== null && "error" in result) {
|
|
3346
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
3347
|
+
}
|
|
3348
|
+
return {
|
|
3349
|
+
content: withInitHint([
|
|
3350
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
3351
|
+
]),
|
|
3352
|
+
};
|
|
3353
|
+
});
|
|
3116
3354
|
server.tool("codeloop_apply_update", TOOL_BOOTSTRAP + `Apply a pending CodeLoop MCP server update to the current chat session — without asking the user to restart their IDE.
|
|
3117
3355
|
|
|
3118
3356
|
Use this tool when:
|
|
@@ -3138,7 +3376,7 @@ Returns: status, current/latest versions, critical reasons, commands_to_run, aut
|
|
|
3138
3376
|
return applyUpdate({ auto_respawn: params.auto_respawn });
|
|
3139
3377
|
}, {
|
|
3140
3378
|
tool: "codeloop_apply_update",
|
|
3141
|
-
cwd: params
|
|
3379
|
+
cwd: resolveCwd(params),
|
|
3142
3380
|
input: params,
|
|
3143
3381
|
});
|
|
3144
3382
|
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|