@runcofactor/cli 0.10.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 +125 -0
- package/dist/api-catalog-GFGCLVS4.js +401 -0
- package/dist/api-catalog-GFGCLVS4.js.map +1 -0
- package/dist/attachment-SJC7D7FY.js +276 -0
- package/dist/attachment-SJC7D7FY.js.map +1 -0
- package/dist/auth-EN6NQCJY.js +31 -0
- package/dist/auth-EN6NQCJY.js.map +1 -0
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-3CZU7XCS.js +272 -0
- package/dist/chunk-3CZU7XCS.js.map +1 -0
- package/dist/chunk-4NRDONSC.js +1048 -0
- package/dist/chunk-4NRDONSC.js.map +1 -0
- package/dist/chunk-4PNOV7OV.js +51980 -0
- package/dist/chunk-4PNOV7OV.js.map +1 -0
- package/dist/chunk-52IO7ACJ.js +2941 -0
- package/dist/chunk-52IO7ACJ.js.map +1 -0
- package/dist/chunk-6LEZCKKV.js +663 -0
- package/dist/chunk-6LEZCKKV.js.map +1 -0
- package/dist/chunk-A4OFTUVV.js +170 -0
- package/dist/chunk-A4OFTUVV.js.map +1 -0
- package/dist/chunk-B2VISN2V.js +2459 -0
- package/dist/chunk-B2VISN2V.js.map +1 -0
- package/dist/chunk-CLAWT7BJ.js +271 -0
- package/dist/chunk-CLAWT7BJ.js.map +1 -0
- package/dist/chunk-GKY2KXO5.js +281 -0
- package/dist/chunk-GKY2KXO5.js.map +1 -0
- package/dist/chunk-I4QLI3JO.js +413 -0
- package/dist/chunk-I4QLI3JO.js.map +1 -0
- package/dist/chunk-IMPZAX6L.js +180 -0
- package/dist/chunk-IMPZAX6L.js.map +1 -0
- package/dist/chunk-J6SH7GQT.js +241 -0
- package/dist/chunk-J6SH7GQT.js.map +1 -0
- package/dist/chunk-JDBCZ3Z2.js +298 -0
- package/dist/chunk-JDBCZ3Z2.js.map +1 -0
- package/dist/chunk-JICSYFRJ.js +46 -0
- package/dist/chunk-JICSYFRJ.js.map +1 -0
- package/dist/chunk-JWHYXEA2.js +32 -0
- package/dist/chunk-JWHYXEA2.js.map +1 -0
- package/dist/chunk-N7HFALX3.js +88 -0
- package/dist/chunk-N7HFALX3.js.map +1 -0
- package/dist/chunk-Q4BO4Z5V.js +178 -0
- package/dist/chunk-Q4BO4Z5V.js.map +1 -0
- package/dist/chunk-QFDOWABX.js +195 -0
- package/dist/chunk-QFDOWABX.js.map +1 -0
- package/dist/chunk-SJJW77WN.js +605 -0
- package/dist/chunk-SJJW77WN.js.map +1 -0
- package/dist/chunk-VKVL7WBN.js +50 -0
- package/dist/chunk-VKVL7WBN.js.map +1 -0
- package/dist/chunk-WOHL4FTB.js +353 -0
- package/dist/chunk-WOHL4FTB.js.map +1 -0
- package/dist/chunk-XQXVG7RA.js +426 -0
- package/dist/chunk-XQXVG7RA.js.map +1 -0
- package/dist/chunk-ZD3KHESI.js +94 -0
- package/dist/chunk-ZD3KHESI.js.map +1 -0
- package/dist/credential-COLTBV6D.js +450 -0
- package/dist/credential-COLTBV6D.js.map +1 -0
- package/dist/credential-template-EEPYFG7R.js +291 -0
- package/dist/credential-template-EEPYFG7R.js.map +1 -0
- package/dist/debug-workflow-KOVH5GOP.js +259 -0
- package/dist/debug-workflow-KOVH5GOP.js.map +1 -0
- package/dist/dist-44PZR7BQ.js +423 -0
- package/dist/dist-44PZR7BQ.js.map +1 -0
- package/dist/docs-3SSORHB5.js +366 -0
- package/dist/docs-3SSORHB5.js.map +1 -0
- package/dist/doctor-OTSIWV6G.js +132 -0
- package/dist/doctor-OTSIWV6G.js.map +1 -0
- package/dist/feature-flag-T2LRZME3.js +283 -0
- package/dist/feature-flag-T2LRZME3.js.map +1 -0
- package/dist/file-type-S3UUBVCT.js +4205 -0
- package/dist/file-type-S3UUBVCT.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +982 -0
- package/dist/index.js.map +1 -0
- package/dist/init-TGWRECT2.js +208 -0
- package/dist/init-TGWRECT2.js.map +1 -0
- package/dist/install-DXSNWZKO.js +295 -0
- package/dist/install-DXSNWZKO.js.map +1 -0
- package/dist/issues-XIZOPX6T.js +564 -0
- package/dist/issues-XIZOPX6T.js.map +1 -0
- package/dist/knobs-NBINZIUB.js +295 -0
- package/dist/knobs-NBINZIUB.js.map +1 -0
- package/dist/list-3TDHK637.js +59 -0
- package/dist/list-3TDHK637.js.map +1 -0
- package/dist/local-HICVD6PU.js +100 -0
- package/dist/local-HICVD6PU.js.map +1 -0
- package/dist/login-UOIUXVUK.js +315 -0
- package/dist/login-UOIUXVUK.js.map +1 -0
- package/dist/mcp-54IJEOUA.js +1956 -0
- package/dist/mcp-54IJEOUA.js.map +1 -0
- package/dist/pdf-extract-ABWV3L5X.js +13 -0
- package/dist/pdf-extract-ABWV3L5X.js.map +1 -0
- package/dist/pdfjs-DQ2YA5S3.js +41561 -0
- package/dist/pdfjs-DQ2YA5S3.js.map +1 -0
- package/dist/property-UXLOLPTD.js +701 -0
- package/dist/property-UXLOLPTD.js.map +1 -0
- package/dist/property-skill-guide.md +189 -0
- package/dist/psql-BYUUNLGG.js +115 -0
- package/dist/psql-BYUUNLGG.js.map +1 -0
- package/dist/record-4TR2UIIS.js +233 -0
- package/dist/record-4TR2UIIS.js.map +1 -0
- package/dist/redis-35VBUYW7.js +116 -0
- package/dist/redis-35VBUYW7.js.map +1 -0
- package/dist/release-OOLTUBUT.js +354 -0
- package/dist/release-OOLTUBUT.js.map +1 -0
- package/dist/sandbox-XGSUU6G7.js +2046 -0
- package/dist/sandbox-XGSUU6G7.js.map +1 -0
- package/dist/scenarios-GWLJEGHA.js +280 -0
- package/dist/scenarios-GWLJEGHA.js.map +1 -0
- package/dist/session-MYFNZPRP.js +2414 -0
- package/dist/session-MYFNZPRP.js.map +1 -0
- package/dist/skill-KM34LDMO.js +1839 -0
- package/dist/skill-KM34LDMO.js.map +1 -0
- package/dist/skills/debug-run/SKILL.md +262 -0
- package/dist/skills/debug-run/history/1.0.0.md +244 -0
- package/dist/skills/debug-run/history/1.0.1.md +245 -0
- package/dist/skills/debug-run/history/1.0.2.md +254 -0
- package/dist/skills/debug-run/history/1.1.0.md +262 -0
- package/dist/skills/debug-run/version.json +3 -0
- package/dist/spec-RA3CFEKG.js +951 -0
- package/dist/spec-RA3CFEKG.js.map +1 -0
- package/dist/src-LB4HBTJQ.js +1516 -0
- package/dist/src-LB4HBTJQ.js.map +1 -0
- package/dist/src-N3TPKGTY.js +244 -0
- package/dist/src-N3TPKGTY.js.map +1 -0
- package/dist/src-UVDX5UP6.js +17 -0
- package/dist/src-UVDX5UP6.js.map +1 -0
- package/dist/start-PSEQSQA3.js +151 -0
- package/dist/start-PSEQSQA3.js.map +1 -0
- package/dist/tag-YY6YAABQ.js +22 -0
- package/dist/tag-YY6YAABQ.js.map +1 -0
- package/dist/task-IQC3YR5K.js +922 -0
- package/dist/task-IQC3YR5K.js.map +1 -0
- package/dist/test-suite-DZNJFXDW.js +394 -0
- package/dist/test-suite-DZNJFXDW.js.map +1 -0
- package/dist/trace-HXIW7U2D.js +14 -0
- package/dist/trace-HXIW7U2D.js.map +1 -0
- package/dist/triage-ORG45R5W.js +16 -0
- package/dist/triage-ORG45R5W.js.map +1 -0
- package/dist/upgrade-L2VYMP2V.js +61 -0
- package/dist/upgrade-L2VYMP2V.js.map +1 -0
- package/dist/workflow-XYFH5LTP.js +3708 -0
- package/dist/workflow-XYFH5LTP.js.map +1 -0
- package/dist/workflow-cache-NQJQFWMV.js +253 -0
- package/dist/workflow-cache-NQJQFWMV.js.map +1 -0
- package/dist/workflow-debug-BGD4UIXU.js +927 -0
- package/dist/workflow-debug-BGD4UIXU.js.map +1 -0
- package/dist/workflow-proposals-MTUDX46O.js +382 -0
- package/dist/workflow-proposals-MTUDX46O.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/session/daemon-client.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var PIDFILE_DIR = path.join(os.homedir(), ".config", "canary-cli");
|
|
11
|
+
var PIDFILE_PATH = path.join(PIDFILE_DIR, "daemon.json");
|
|
12
|
+
var HEALTH_POLL_INTERVAL_MS = 100;
|
|
13
|
+
var HEALTH_POLL_TIMEOUT_MS = 15e3;
|
|
14
|
+
async function readPidfile() {
|
|
15
|
+
try {
|
|
16
|
+
const content = await fs.readFile(PIDFILE_PATH, "utf-8");
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isProcessAlive(pid) {
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
var DAEMON_REQUEST_TIMEOUT_MS = 3e4;
|
|
31
|
+
var DAEMON_FETCH_MAX_RETRIES = 2;
|
|
32
|
+
function isConnectionError(err) {
|
|
33
|
+
if (err instanceof TypeError && err.message === "fetch failed") return true;
|
|
34
|
+
const cause = err?.cause;
|
|
35
|
+
if (cause?.code === "UND_ERR_SOCKET") return true;
|
|
36
|
+
if (cause?.code === "ECONNREFUSED") return true;
|
|
37
|
+
if (cause?.code === "ECONNRESET") return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
async function daemonFetchOnce(port, method, path2, body) {
|
|
41
|
+
const url = `http://127.0.0.1:${port}${path2}`;
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers: {
|
|
45
|
+
...body ? { "Content-Type": "application/json" } : void 0,
|
|
46
|
+
Connection: "close"
|
|
47
|
+
},
|
|
48
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
49
|
+
signal: AbortSignal.timeout(DAEMON_REQUEST_TIMEOUT_MS)
|
|
50
|
+
});
|
|
51
|
+
return res.json();
|
|
52
|
+
}
|
|
53
|
+
async function daemonFetchWithRetry(method, path2, body) {
|
|
54
|
+
let port = await ensureDaemon();
|
|
55
|
+
let lastError;
|
|
56
|
+
for (let attempt = 0; attempt <= DAEMON_FETCH_MAX_RETRIES; attempt++) {
|
|
57
|
+
try {
|
|
58
|
+
return await daemonFetchOnce(port, method, path2, body);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
lastError = err;
|
|
61
|
+
if (!isConnectionError(err) || attempt === DAEMON_FETCH_MAX_RETRIES) {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await fs.unlink(PIDFILE_PATH);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
port = await ensureDaemon();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw lastError;
|
|
72
|
+
}
|
|
73
|
+
async function healthCheck(port) {
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
76
|
+
signal: AbortSignal.timeout(2e3),
|
|
77
|
+
headers: { Connection: "close" }
|
|
78
|
+
});
|
|
79
|
+
return res.ok;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function spawnDaemon() {
|
|
85
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
86
|
+
const candidates = [
|
|
87
|
+
path.resolve(dir, "..", "index.ts"),
|
|
88
|
+
// source layout
|
|
89
|
+
path.resolve(dir, "index.js"),
|
|
90
|
+
// dist layout (flat)
|
|
91
|
+
path.resolve(dir, "..", "index.js")
|
|
92
|
+
// fallback
|
|
93
|
+
];
|
|
94
|
+
const cliEntry = candidates.find((p) => existsSync(p));
|
|
95
|
+
if (!cliEntry) {
|
|
96
|
+
throw new Error(`Cannot find CLI entry point. Searched: ${candidates.join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
const child = spawn(process.execPath, [cliEntry, "session", "daemon"], {
|
|
99
|
+
detached: true,
|
|
100
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
101
|
+
env: { ...process.env },
|
|
102
|
+
windowsHide: true
|
|
103
|
+
});
|
|
104
|
+
child.unref();
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
let output = "";
|
|
107
|
+
const timeout = setTimeout(() => {
|
|
108
|
+
reject(new Error("Daemon startup timed out"));
|
|
109
|
+
}, HEALTH_POLL_TIMEOUT_MS);
|
|
110
|
+
child.stdout.on("data", (data) => {
|
|
111
|
+
output += data.toString();
|
|
112
|
+
const match = output.match(/DAEMON_READY:(\d+)/);
|
|
113
|
+
if (match) {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
child.stdout.destroy();
|
|
116
|
+
resolve(parseInt(match[1], 10));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
child.on("error", (err) => {
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
reject(err);
|
|
122
|
+
});
|
|
123
|
+
child.on("exit", (code) => {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
if (!output.includes("DAEMON_READY")) {
|
|
126
|
+
reject(new Error(`Daemon exited with code ${code} before becoming ready`));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function ensureDaemon() {
|
|
132
|
+
const state = await readPidfile();
|
|
133
|
+
if (state) {
|
|
134
|
+
if (isProcessAlive(state.pid)) {
|
|
135
|
+
if (await healthCheck(state.port)) {
|
|
136
|
+
return state.port;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
await fs.unlink(PIDFILE_PATH);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const port = await spawnDaemon();
|
|
145
|
+
const deadline = Date.now() + HEALTH_POLL_TIMEOUT_MS;
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
if (await healthCheck(port)) return port;
|
|
148
|
+
await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));
|
|
149
|
+
}
|
|
150
|
+
throw new Error("Daemon failed to become healthy after spawn");
|
|
151
|
+
}
|
|
152
|
+
async function createSession(params) {
|
|
153
|
+
return daemonFetchWithRetry("POST", "/sessions", params);
|
|
154
|
+
}
|
|
155
|
+
async function listSessions() {
|
|
156
|
+
return daemonFetchWithRetry("GET", "/sessions");
|
|
157
|
+
}
|
|
158
|
+
async function getSession(sessionId) {
|
|
159
|
+
return daemonFetchWithRetry("GET", `/sessions/${sessionId}`);
|
|
160
|
+
}
|
|
161
|
+
async function deleteSession(sessionId) {
|
|
162
|
+
return daemonFetchWithRetry("DELETE", `/sessions/${sessionId}`);
|
|
163
|
+
}
|
|
164
|
+
async function deleteAllSessions() {
|
|
165
|
+
return daemonFetchWithRetry("DELETE", "/sessions");
|
|
166
|
+
}
|
|
167
|
+
async function swapSessionContext(sessionId, params) {
|
|
168
|
+
return daemonFetchWithRetry("POST", `/sessions/${sessionId}/swap-context`, params);
|
|
169
|
+
}
|
|
170
|
+
async function getSessionStorageState(sessionId) {
|
|
171
|
+
return daemonFetchWithRetry(
|
|
172
|
+
"GET",
|
|
173
|
+
`/sessions/${sessionId}/storage-state`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
async function callTool(sessionId, toolName, args) {
|
|
177
|
+
return daemonFetchWithRetry(
|
|
178
|
+
"POST",
|
|
179
|
+
`/sessions/${sessionId}/tools/${toolName}`,
|
|
180
|
+
args
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
async function createSessionWithRelay(params) {
|
|
184
|
+
return createSession(params);
|
|
185
|
+
}
|
|
186
|
+
async function acquireCloudLock(sessionId) {
|
|
187
|
+
return daemonFetchWithRetry("POST", `/sessions/${sessionId}/acquire-cloud`);
|
|
188
|
+
}
|
|
189
|
+
async function releaseCloudLock(sessionId) {
|
|
190
|
+
return daemonFetchWithRetry("POST", `/sessions/${sessionId}/release-cloud`);
|
|
191
|
+
}
|
|
192
|
+
async function takeControl(sessionId) {
|
|
193
|
+
return daemonFetchWithRetry("POST", `/sessions/${sessionId}/take-control`);
|
|
194
|
+
}
|
|
195
|
+
async function resumeCloud(sessionId) {
|
|
196
|
+
return daemonFetchWithRetry("POST", `/sessions/${sessionId}/resume-cloud`);
|
|
197
|
+
}
|
|
198
|
+
async function resolveTargetSession(sessionIdOrName) {
|
|
199
|
+
const result = await listSessions();
|
|
200
|
+
if (!result.ok || !result.data) {
|
|
201
|
+
throw new Error("Failed to list sessions");
|
|
202
|
+
}
|
|
203
|
+
const sessions = result.data;
|
|
204
|
+
if (sessions.length === 0) {
|
|
205
|
+
throw new Error("No active sessions. Start one with: canary session start");
|
|
206
|
+
}
|
|
207
|
+
if (sessionIdOrName) {
|
|
208
|
+
const match = sessions.find((s) => s.id === sessionIdOrName || s.name === sessionIdOrName);
|
|
209
|
+
if (!match) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Session "${sessionIdOrName}" not found. Active sessions: ${sessions.map((s) => s.id).join(", ")}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return match;
|
|
215
|
+
}
|
|
216
|
+
if (sessions.length === 1) {
|
|
217
|
+
return sessions[0];
|
|
218
|
+
}
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Multiple sessions active. Specify one with --session:
|
|
221
|
+
${sessions.map((s) => ` ${s.id} (${s.name})`).join("\n")}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
createSession,
|
|
227
|
+
listSessions,
|
|
228
|
+
getSession,
|
|
229
|
+
deleteSession,
|
|
230
|
+
deleteAllSessions,
|
|
231
|
+
swapSessionContext,
|
|
232
|
+
getSessionStorageState,
|
|
233
|
+
callTool,
|
|
234
|
+
createSessionWithRelay,
|
|
235
|
+
acquireCloudLock,
|
|
236
|
+
releaseCloudLock,
|
|
237
|
+
takeControl,
|
|
238
|
+
resumeCloud,
|
|
239
|
+
resolveTargetSession
|
|
240
|
+
};
|
|
241
|
+
//# sourceMappingURL=chunk-J6SH7GQT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/session/daemon-client.ts"],"sourcesContent":["/**\n * Daemon client — HTTP client for the session daemon.\n *\n * Handles pidfile read/write, stale PID detection, auto-start,\n * and provides typed HTTP helpers for daemon communication.\n *\n * @module\n */\n\nimport { spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport { fileURLToPath } from 'node:url';\nimport type {\n DaemonState,\n DaemonResponse,\n SessionInfo,\n CloudRelayConfig,\n CreateSessionRequest,\n SwapContextRequest,\n ToolResponse,\n StorageStateResponse,\n} from './types.js';\n\nconst PIDFILE_DIR = path.join(os.homedir(), '.config', 'canary-cli');\nconst PIDFILE_PATH = path.join(PIDFILE_DIR, 'daemon.json');\nconst HEALTH_POLL_INTERVAL_MS = 100;\nconst HEALTH_POLL_TIMEOUT_MS = 15_000;\n\n/* ── Pidfile helpers ─────────────────────────────────────────────────── */\n\nasync function readPidfile(): Promise<DaemonState | null> {\n try {\n const content = await fs.readFile(PIDFILE_PATH, 'utf-8');\n return JSON.parse(content) as DaemonState;\n } catch {\n return null;\n }\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nconst DAEMON_REQUEST_TIMEOUT_MS = 30_000;\nconst DAEMON_FETCH_MAX_RETRIES = 2;\n\n/* ── HTTP helpers ────────────────────────────────────────────────────── */\n\nfunction isConnectionError(err: unknown): boolean {\n if (err instanceof TypeError && err.message === 'fetch failed') return true;\n const cause = (err as { cause?: { code?: string } })?.cause;\n if (cause?.code === 'UND_ERR_SOCKET') return true;\n if (cause?.code === 'ECONNREFUSED') return true;\n if (cause?.code === 'ECONNRESET') return true;\n return false;\n}\n\nasync function daemonFetchOnce(\n port: number,\n method: string,\n path: string,\n body?: unknown\n): Promise<unknown> {\n const url = `http://127.0.0.1:${port}${path}`;\n const res = await fetch(url, {\n method,\n headers: {\n ...(body ? { 'Content-Type': 'application/json' } : undefined),\n Connection: 'close',\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: AbortSignal.timeout(DAEMON_REQUEST_TIMEOUT_MS),\n });\n return res.json();\n}\n\n/**\n * Fetch from daemon with retry. On connection errors (daemon died or\n * idle-exited between ensureDaemon and the request), re-ensure the\n * daemon and retry up to DAEMON_FETCH_MAX_RETRIES times.\n */\nasync function daemonFetchWithRetry(\n method: string,\n path: string,\n body?: unknown\n): Promise<unknown> {\n let port = await ensureDaemon();\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= DAEMON_FETCH_MAX_RETRIES; attempt++) {\n try {\n return await daemonFetchOnce(port, method, path, body);\n } catch (err) {\n lastError = err;\n if (!isConnectionError(err) || attempt === DAEMON_FETCH_MAX_RETRIES) {\n throw err;\n }\n // Daemon likely died — clean up stale pidfile and re-ensure\n try {\n await fs.unlink(PIDFILE_PATH);\n } catch {\n // ignore\n }\n port = await ensureDaemon();\n }\n }\n\n throw lastError;\n}\n\nasync function healthCheck(port: number): Promise<boolean> {\n try {\n const res = await fetch(`http://127.0.0.1:${port}/health`, {\n signal: AbortSignal.timeout(2000),\n headers: { Connection: 'close' },\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/* ── Auto-start ──────────────────────────────────────────────────────── */\n\nasync function spawnDaemon(): Promise<number> {\n // Resolve the canary CLI entry point\n // In source: this file is src/session/daemon-client.ts → ../index.ts\n // In dist: this file is dist/chunk-*.js → ./index.js (flat)\n const dir = path.dirname(fileURLToPath(import.meta.url));\n const candidates = [\n path.resolve(dir, '..', 'index.ts'), // source layout\n path.resolve(dir, 'index.js'), // dist layout (flat)\n path.resolve(dir, '..', 'index.js'), // fallback\n ];\n const cliEntry = candidates.find((p) => existsSync(p));\n if (!cliEntry) {\n throw new Error(`Cannot find CLI entry point. Searched: ${candidates.join(', ')}`);\n }\n\n const child = spawn(process.execPath, [cliEntry, 'session', 'daemon'], {\n detached: true,\n stdio: ['ignore', 'pipe', 'ignore'],\n env: { ...process.env },\n windowsHide: true,\n });\n\n child.unref();\n\n // Wait for \"DAEMON_READY:<port>\" on stdout\n return new Promise<number>((resolve, reject) => {\n let output = '';\n const timeout = setTimeout(() => {\n reject(new Error('Daemon startup timed out'));\n }, HEALTH_POLL_TIMEOUT_MS);\n\n child.stdout!.on('data', (data: Buffer) => {\n output += data.toString();\n const match = output.match(/DAEMON_READY:(\\d+)/);\n if (match) {\n clearTimeout(timeout);\n child.stdout!.destroy(); // Release the pipe so the CLI process can exit\n resolve(parseInt(match[1], 10));\n }\n });\n\n child.on('error', (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n\n child.on('exit', (code) => {\n clearTimeout(timeout);\n if (!output.includes('DAEMON_READY')) {\n reject(new Error(`Daemon exited with code ${code} before becoming ready`));\n }\n });\n });\n}\n\n/* ── Public API ──────────────────────────────────────────────────────── */\n\n/**\n * Ensure the daemon is running and return its port.\n * Starts the daemon if needed, cleans up stale pidfiles.\n */\nasync function ensureDaemon(): Promise<number> {\n const state = await readPidfile();\n\n if (state) {\n if (isProcessAlive(state.pid)) {\n // Verify it actually responds\n if (await healthCheck(state.port)) {\n return state.port;\n }\n }\n // Stale pidfile — clean up\n try {\n await fs.unlink(PIDFILE_PATH);\n } catch {\n // ignore\n }\n }\n\n // Spawn new daemon\n const port = await spawnDaemon();\n\n // Poll until healthy\n const deadline = Date.now() + HEALTH_POLL_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if (await healthCheck(port)) return port;\n await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));\n }\n\n throw new Error('Daemon failed to become healthy after spawn');\n}\n\n/* ── Session operations ──────────────────────────────────────────────── */\n\nexport async function createSession(\n params: CreateSessionRequest\n): Promise<DaemonResponse<SessionInfo>> {\n return daemonFetchWithRetry('POST', '/sessions', params) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\nexport async function listSessions(): Promise<DaemonResponse<SessionInfo[]>> {\n return daemonFetchWithRetry('GET', '/sessions') as Promise<DaemonResponse<SessionInfo[]>>;\n}\n\nexport async function getSession(sessionId: string): Promise<DaemonResponse<SessionInfo>> {\n return daemonFetchWithRetry('GET', `/sessions/${sessionId}`) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\nexport async function deleteSession(sessionId: string): Promise<DaemonResponse> {\n return daemonFetchWithRetry('DELETE', `/sessions/${sessionId}`) as Promise<DaemonResponse>;\n}\n\nexport async function deleteAllSessions(): Promise<DaemonResponse> {\n return daemonFetchWithRetry('DELETE', '/sessions') as Promise<DaemonResponse>;\n}\n\nexport async function swapSessionContext(\n sessionId: string,\n params: SwapContextRequest\n): Promise<DaemonResponse<SessionInfo>> {\n return daemonFetchWithRetry('POST', `/sessions/${sessionId}/swap-context`, params) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\nexport async function getSessionStorageState(sessionId: string): Promise<StorageStateResponse> {\n return daemonFetchWithRetry(\n 'GET',\n `/sessions/${sessionId}/storage-state`\n ) as Promise<StorageStateResponse>;\n}\n\nexport async function callTool(\n sessionId: string,\n toolName: string,\n args: Record<string, unknown>\n): Promise<ToolResponse> {\n return daemonFetchWithRetry(\n 'POST',\n `/sessions/${sessionId}/tools/${toolName}`,\n args\n ) as Promise<ToolResponse>;\n}\n\n/* ── Cloud relay ────────────────────────────────────────────────────── */\n\nexport async function createSessionWithRelay(\n params: Omit<CreateSessionRequest, 'cloudRelay'> & { cloudRelay: CloudRelayConfig }\n): Promise<DaemonResponse<SessionInfo>> {\n return createSession(params);\n}\n\n/* ── Lock management ────────────────────────────────────────────────── */\n\nexport async function acquireCloudLock(sessionId: string): Promise<DaemonResponse<SessionInfo>> {\n return daemonFetchWithRetry('POST', `/sessions/${sessionId}/acquire-cloud`) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\nexport async function releaseCloudLock(sessionId: string): Promise<DaemonResponse<SessionInfo>> {\n return daemonFetchWithRetry('POST', `/sessions/${sessionId}/release-cloud`) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\nexport async function takeControl(sessionId: string): Promise<DaemonResponse<SessionInfo>> {\n return daemonFetchWithRetry('POST', `/sessions/${sessionId}/take-control`) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\nexport async function resumeCloud(sessionId: string): Promise<DaemonResponse<SessionInfo>> {\n return daemonFetchWithRetry('POST', `/sessions/${sessionId}/resume-cloud`) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\n/**\n * Resolve the target session for a command.\n * If there's exactly one session, auto-targets it.\n * If a sessionId or name is provided, looks it up.\n */\nexport async function resolveTargetSession(sessionIdOrName?: string): Promise<SessionInfo> {\n const result = await listSessions();\n if (!result.ok || !result.data) {\n throw new Error('Failed to list sessions');\n }\n const sessions = result.data;\n\n if (sessions.length === 0) {\n throw new Error('No active sessions. Start one with: canary session start');\n }\n\n if (sessionIdOrName) {\n const match = sessions.find((s) => s.id === sessionIdOrName || s.name === sessionIdOrName);\n if (!match) {\n throw new Error(\n `Session \"${sessionIdOrName}\" not found. Active sessions: ${sessions.map((s) => s.id).join(', ')}`\n );\n }\n return match;\n }\n\n if (sessions.length === 1) {\n return sessions[0];\n }\n\n throw new Error(\n `Multiple sessions active. Specify one with --session:\\n${sessions.map((s) => ` ${s.id} (${s.name})`).join('\\n')}`\n );\n}\n"],"mappings":"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","names":["path"]}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// ../workflow-diff/src/node-fields.ts
|
|
4
|
+
var NODE_FIELDS = {
|
|
5
|
+
login: [
|
|
6
|
+
{ key: "credentialName", label: "Credential" },
|
|
7
|
+
{ key: "performLogin", label: "Perform login" }
|
|
8
|
+
],
|
|
9
|
+
navigate: [
|
|
10
|
+
{ key: "pageUrl", label: "Destination page" },
|
|
11
|
+
{ key: "openInNewTab", label: "Open in new tab" }
|
|
12
|
+
],
|
|
13
|
+
action: [
|
|
14
|
+
{ key: "customActionTitle", label: "Title" },
|
|
15
|
+
{ key: "actionDescription", label: "Description" },
|
|
16
|
+
{ key: "alwaysUseAgent", label: "Always use agent" },
|
|
17
|
+
{ key: "inputValues", label: "Inputs", kind: "kv" },
|
|
18
|
+
{ key: "declaredOutputs", label: "Outputs", kind: "kv" },
|
|
19
|
+
{ key: "inlineAssertions", label: "Inline assertions", kind: "inline_assertions" }
|
|
20
|
+
],
|
|
21
|
+
assertion: [
|
|
22
|
+
{ key: "condition", label: "Condition" },
|
|
23
|
+
{ key: "severity", label: "Severity" },
|
|
24
|
+
{ key: "alwaysUseAgent", label: "Always use agent" }
|
|
25
|
+
],
|
|
26
|
+
wait: [
|
|
27
|
+
{ key: "delaySeconds", label: "Delay" }
|
|
28
|
+
],
|
|
29
|
+
condition: [
|
|
30
|
+
{ key: "conditionDescription", label: "Condition" },
|
|
31
|
+
{ key: "evaluationHints", label: "Hints", kind: "json" }
|
|
32
|
+
],
|
|
33
|
+
notification: [
|
|
34
|
+
{ key: "channel", label: "Channel" },
|
|
35
|
+
{ key: "to", label: "To" },
|
|
36
|
+
{ key: "subject", label: "Subject" },
|
|
37
|
+
{ key: "body", label: "Body" },
|
|
38
|
+
{ key: "attachmentIds", label: "Attachments", kind: "json" },
|
|
39
|
+
{ key: "attachmentFilenames", label: "Attachment filenames", kind: "json" }
|
|
40
|
+
],
|
|
41
|
+
audio: [
|
|
42
|
+
{ key: "label", label: "Label" },
|
|
43
|
+
{ key: "attachmentIds", label: "Audio clip", kind: "json" },
|
|
44
|
+
{ key: "loop", label: "Loop" },
|
|
45
|
+
{ key: "waitForCompletion", label: "Wait for completion" }
|
|
46
|
+
],
|
|
47
|
+
api_sequence: [
|
|
48
|
+
{ key: "description", label: "Description" },
|
|
49
|
+
{ key: "stopOnFailure", label: "Stop on failure" },
|
|
50
|
+
{ key: "steps", label: "Steps", kind: "json" },
|
|
51
|
+
{ key: "exports", label: "Exports", kind: "json" }
|
|
52
|
+
],
|
|
53
|
+
setup: [
|
|
54
|
+
{ key: "setupFlowName", label: "Setup flow" },
|
|
55
|
+
{ key: "snapshotVersion", label: "Version" },
|
|
56
|
+
{ key: "executionMode", label: "Execution mode" }
|
|
57
|
+
],
|
|
58
|
+
seed: [
|
|
59
|
+
{ key: "seedWorkflowName", label: "Seed flow" },
|
|
60
|
+
{ key: "snapshotVersion", label: "Version" },
|
|
61
|
+
{ key: "executionMode", label: "Execution mode" },
|
|
62
|
+
{ key: "onFailure", label: "On failure" },
|
|
63
|
+
{ key: "runTeardown", label: "Run teardown" }
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
function getNodeFields(nodeType) {
|
|
67
|
+
return NODE_FIELDS[nodeType] ?? [];
|
|
68
|
+
}
|
|
69
|
+
function pickCanonicalConfig(nodeType, config) {
|
|
70
|
+
if (!config) return {};
|
|
71
|
+
const fields = getNodeFields(nodeType);
|
|
72
|
+
if (fields.length === 0) return {};
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const f of fields) {
|
|
75
|
+
if (Object.prototype.hasOwnProperty.call(config, f.key)) {
|
|
76
|
+
out[f.key] = config[f.key];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ../workflow-diff/src/diff.ts
|
|
83
|
+
function fromApiSnapshot(snapshot) {
|
|
84
|
+
return {
|
|
85
|
+
nodes: (snapshot.nodes ?? []).map((n) => ({
|
|
86
|
+
id: n.id,
|
|
87
|
+
nodeType: n.nodeType,
|
|
88
|
+
orderIndex: n.orderIndex,
|
|
89
|
+
configJson: n.configJson ?? {}
|
|
90
|
+
})),
|
|
91
|
+
edges: (snapshot.edges ?? []).map((e) => ({
|
|
92
|
+
id: e.id,
|
|
93
|
+
sourceNodeId: e.sourceNodeId,
|
|
94
|
+
targetNodeId: e.targetNodeId,
|
|
95
|
+
edgeType: e.edgeType ?? null
|
|
96
|
+
}))
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function diffConfig(before, after, basePath = "") {
|
|
100
|
+
const out = [];
|
|
101
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
102
|
+
for (const key of keys) {
|
|
103
|
+
const path = basePath ? `${basePath}.${key}` : key;
|
|
104
|
+
const bv = before[key];
|
|
105
|
+
const av = after[key];
|
|
106
|
+
if (isPlainObject(bv) && isPlainObject(av)) {
|
|
107
|
+
out.push(
|
|
108
|
+
...diffConfig(
|
|
109
|
+
bv,
|
|
110
|
+
av,
|
|
111
|
+
path
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!valuesEqual(bv, av)) {
|
|
117
|
+
out.push({ path, before: bv, after: av });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function isPlainObject(v) {
|
|
123
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
124
|
+
}
|
|
125
|
+
function valuesEqual(a, b) {
|
|
126
|
+
if (a === b) return true;
|
|
127
|
+
if (a === null || a === void 0 || b === null || b === void 0) return a === b;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(sortObjectKeys(a)) === JSON.stringify(sortObjectKeys(b));
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function sortObjectKeys(value) {
|
|
135
|
+
if (Array.isArray(value)) return value.map(sortObjectKeys);
|
|
136
|
+
if (value && typeof value === "object") {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const k of Object.keys(value).sort()) {
|
|
139
|
+
out[k] = sortObjectKeys(value[k]);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
function diffSnapshots(current, proposed) {
|
|
146
|
+
const currentNodeMap = new Map(current.nodes.map((n) => [n.id, n]));
|
|
147
|
+
const proposedNodeMap = new Map(proposed.nodes.map((n) => [n.id, n]));
|
|
148
|
+
const addedNodes = [];
|
|
149
|
+
const removedNodes = [];
|
|
150
|
+
const changedNodes = [];
|
|
151
|
+
for (const node of proposed.nodes) {
|
|
152
|
+
const prev = currentNodeMap.get(node.id);
|
|
153
|
+
if (!prev) {
|
|
154
|
+
addedNodes.push(node);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const typeChanged = prev.nodeType !== node.nodeType;
|
|
158
|
+
const prevCanonical = pickCanonicalConfig(prev.nodeType, prev.configJson);
|
|
159
|
+
const nextCanonical = pickCanonicalConfig(node.nodeType, node.configJson);
|
|
160
|
+
const fieldChanges = diffConfig(prevCanonical, nextCanonical);
|
|
161
|
+
const moved = false;
|
|
162
|
+
if (typeChanged || fieldChanges.length > 0 || moved) {
|
|
163
|
+
changedNodes.push({
|
|
164
|
+
id: node.id,
|
|
165
|
+
nodeType: node.nodeType,
|
|
166
|
+
typeChanged,
|
|
167
|
+
beforeType: typeChanged ? prev.nodeType : void 0,
|
|
168
|
+
afterType: typeChanged ? node.nodeType : void 0,
|
|
169
|
+
fieldChanges,
|
|
170
|
+
moved
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const node of current.nodes) {
|
|
175
|
+
if (!proposedNodeMap.has(node.id)) removedNodes.push(node);
|
|
176
|
+
}
|
|
177
|
+
const currentEdgeMap = new Map(current.edges.map((e) => [edgeKey(e), e]));
|
|
178
|
+
const proposedEdgeMap = new Map(proposed.edges.map((e) => [edgeKey(e), e]));
|
|
179
|
+
const addedEdges = [];
|
|
180
|
+
const removedEdges = [];
|
|
181
|
+
for (const e of proposed.edges) {
|
|
182
|
+
if (!currentEdgeMap.has(edgeKey(e))) addedEdges.push(e);
|
|
183
|
+
}
|
|
184
|
+
for (const e of current.edges) {
|
|
185
|
+
if (!proposedEdgeMap.has(edgeKey(e))) removedEdges.push(e);
|
|
186
|
+
}
|
|
187
|
+
const touchedCount = addedNodes.length + removedNodes.length + changedNodes.length;
|
|
188
|
+
const isEmpty = touchedCount === 0 && addedEdges.length === 0 && removedEdges.length === 0;
|
|
189
|
+
return {
|
|
190
|
+
addedNodes,
|
|
191
|
+
removedNodes,
|
|
192
|
+
changedNodes,
|
|
193
|
+
addedEdges,
|
|
194
|
+
removedEdges,
|
|
195
|
+
touchedCount,
|
|
196
|
+
isEmpty
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function edgeKey(e) {
|
|
200
|
+
return `${e.sourceNodeId}\u2192${e.targetNodeId}:${e.edgeType ?? ""}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ../workflow-diff/src/node-label.ts
|
|
204
|
+
var FALLBACK_BY_TYPE = {
|
|
205
|
+
login: "Login",
|
|
206
|
+
navigate: "Navigate",
|
|
207
|
+
action: "Action",
|
|
208
|
+
assertion: "Assertion",
|
|
209
|
+
wait: "Wait",
|
|
210
|
+
condition: "Condition",
|
|
211
|
+
setup: "Setup",
|
|
212
|
+
api_sequence: "API Sequence",
|
|
213
|
+
end: "End",
|
|
214
|
+
seed: "Seed",
|
|
215
|
+
notification: "Notification"
|
|
216
|
+
};
|
|
217
|
+
function getNodeDisplayTitle(nodeType, config) {
|
|
218
|
+
const data = config ?? {};
|
|
219
|
+
const str = (v) => typeof v === "string" && v.trim().length > 0 ? v : void 0;
|
|
220
|
+
const explicit = str(data.label) ?? str(data.customActionTitle) ?? str(data.title);
|
|
221
|
+
if (explicit) return explicit;
|
|
222
|
+
switch (nodeType) {
|
|
223
|
+
case "login":
|
|
224
|
+
return str(data.credentialName) ?? "Login";
|
|
225
|
+
case "navigate":
|
|
226
|
+
return str(data.pageTitle) ?? str(data.url) ?? "Navigate";
|
|
227
|
+
case "action":
|
|
228
|
+
return str(data.actionDescription) ?? "Action";
|
|
229
|
+
case "assertion":
|
|
230
|
+
return str(data.condition) ?? str(data.description) ?? "Assertion";
|
|
231
|
+
case "wait":
|
|
232
|
+
return str(data.description) ?? "Wait";
|
|
233
|
+
case "condition":
|
|
234
|
+
return str(data.conditionDescription) ?? "Condition";
|
|
235
|
+
case "setup":
|
|
236
|
+
return str(data.setupFlowName) ?? "Setup";
|
|
237
|
+
case "api_sequence":
|
|
238
|
+
return str(data.sequenceName) ?? "API Sequence";
|
|
239
|
+
default:
|
|
240
|
+
return FALLBACK_BY_TYPE[nodeType ?? ""] ?? nodeType ?? "Step";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ../workflow-diff/src/align-snapshot-ids.ts
|
|
245
|
+
function alignSnapshotNodeIds(proposed, currentNodes) {
|
|
246
|
+
const currentIds = /* @__PURE__ */ new Set();
|
|
247
|
+
const slotMap = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const n of currentNodes) {
|
|
249
|
+
currentIds.add(n.id);
|
|
250
|
+
if (typeof n.orderIndex === "number") {
|
|
251
|
+
slotMap.set(`${n.nodeType}@${n.orderIndex}`, n.id);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
255
|
+
const remap = /* @__PURE__ */ new Map();
|
|
256
|
+
let remappedCount = 0;
|
|
257
|
+
const nodes = proposed.nodes.map((node) => {
|
|
258
|
+
if (currentIds.has(node.id) && !consumed.has(node.id)) {
|
|
259
|
+
consumed.add(node.id);
|
|
260
|
+
return node;
|
|
261
|
+
}
|
|
262
|
+
if (typeof node.orderIndex === "number") {
|
|
263
|
+
const slotKey = `${node.nodeType}@${node.orderIndex}`;
|
|
264
|
+
const candidate = slotMap.get(slotKey);
|
|
265
|
+
if (candidate && !consumed.has(candidate)) {
|
|
266
|
+
consumed.add(candidate);
|
|
267
|
+
if (candidate !== node.id) {
|
|
268
|
+
remap.set(node.id, candidate);
|
|
269
|
+
remappedCount += 1;
|
|
270
|
+
}
|
|
271
|
+
return { ...node, id: candidate };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return node;
|
|
275
|
+
});
|
|
276
|
+
const edges = proposed.edges.map((edge) => {
|
|
277
|
+
const newSource = remap.get(edge.sourceNodeId);
|
|
278
|
+
const newTarget = remap.get(edge.targetNodeId);
|
|
279
|
+
if (newSource === void 0 && newTarget === void 0) return edge;
|
|
280
|
+
return {
|
|
281
|
+
...edge,
|
|
282
|
+
sourceNodeId: newSource ?? edge.sourceNodeId,
|
|
283
|
+
targetNodeId: newTarget ?? edge.targetNodeId
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
snapshot: { ...proposed, nodes, edges },
|
|
288
|
+
remappedCount
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
fromApiSnapshot,
|
|
294
|
+
diffSnapshots,
|
|
295
|
+
getNodeDisplayTitle,
|
|
296
|
+
alignSnapshotNodeIds
|
|
297
|
+
};
|
|
298
|
+
//# sourceMappingURL=chunk-JDBCZ3Z2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../workflow-diff/src/node-fields.ts","../../workflow-diff/src/diff.ts","../../workflow-diff/src/node-label.ts","../../workflow-diff/src/align-snapshot-ids.ts"],"sourcesContent":["/**\n * Canonical per-node-type field whitelist used by BOTH the diff engine and\n * the expanded config view. Keeps the two surfaces in lockstep so the review\n * never marks a node as changed unless a value in this whitelist actually\n * differs — the UI only renders these fields, and the diff only inspects\n * these fields.\n *\n * Mirrors the editable fields in each `<Type>NodeConfig.tsx` under\n * `apps/web/src/ui/components/node-config/`.\n */\n\nexport interface NodeFieldSpec {\n key: string;\n label: string;\n kind?: 'scalar' | 'kv' | 'inline_assertions' | 'json';\n}\n\nexport const NODE_FIELDS: Record<string, NodeFieldSpec[]> = {\n login: [\n { key: 'credentialName', label: 'Credential' },\n { key: 'performLogin', label: 'Perform login' },\n ],\n navigate: [\n { key: 'pageUrl', label: 'Destination page' },\n { key: 'openInNewTab', label: 'Open in new tab' },\n ],\n action: [\n { key: 'customActionTitle', label: 'Title' },\n { key: 'actionDescription', label: 'Description' },\n { key: 'alwaysUseAgent', label: 'Always use agent' },\n { key: 'inputValues', label: 'Inputs', kind: 'kv' },\n { key: 'declaredOutputs', label: 'Outputs', kind: 'kv' },\n { key: 'inlineAssertions', label: 'Inline assertions', kind: 'inline_assertions' },\n ],\n assertion: [\n { key: 'condition', label: 'Condition' },\n { key: 'severity', label: 'Severity' },\n { key: 'alwaysUseAgent', label: 'Always use agent' },\n ],\n wait: [\n { key: 'delaySeconds', label: 'Delay' },\n ],\n condition: [\n { key: 'conditionDescription', label: 'Condition' },\n { key: 'evaluationHints', label: 'Hints', kind: 'json' },\n ],\n notification: [\n { key: 'channel', label: 'Channel' },\n { key: 'to', label: 'To' },\n { key: 'subject', label: 'Subject' },\n { key: 'body', label: 'Body' },\n { key: 'attachmentIds', label: 'Attachments', kind: 'json' },\n { key: 'attachmentFilenames', label: 'Attachment filenames', kind: 'json' },\n ],\n audio: [\n { key: 'label', label: 'Label' },\n { key: 'attachmentIds', label: 'Audio clip', kind: 'json' },\n { key: 'loop', label: 'Loop' },\n { key: 'waitForCompletion', label: 'Wait for completion' },\n ],\n api_sequence: [\n { key: 'description', label: 'Description' },\n { key: 'stopOnFailure', label: 'Stop on failure' },\n { key: 'steps', label: 'Steps', kind: 'json' },\n { key: 'exports', label: 'Exports', kind: 'json' },\n ],\n setup: [\n { key: 'setupFlowName', label: 'Setup flow' },\n { key: 'snapshotVersion', label: 'Version' },\n { key: 'executionMode', label: 'Execution mode' },\n ],\n seed: [\n { key: 'seedWorkflowName', label: 'Seed flow' },\n { key: 'snapshotVersion', label: 'Version' },\n { key: 'executionMode', label: 'Execution mode' },\n { key: 'onFailure', label: 'On failure' },\n { key: 'runTeardown', label: 'Run teardown' },\n ],\n};\n\nexport function getNodeFields(nodeType: string): NodeFieldSpec[] {\n return NODE_FIELDS[nodeType] ?? [];\n}\n\n/**\n * Pick just the canonical fields from a configJson, dropping hydration-added\n * handlers, preview objects, and any other derived state. Returns a new\n * object so downstream diffing/rendering stays pure.\n */\nexport function pickCanonicalConfig(\n nodeType: string,\n config: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n if (!config) return {};\n const fields = getNodeFields(nodeType);\n if (fields.length === 0) return {};\n const out: Record<string, unknown> = {};\n for (const f of fields) {\n if (Object.prototype.hasOwnProperty.call(config, f.key)) {\n out[f.key] = config[f.key];\n }\n }\n return out;\n}\n","/**\n * Pure diff engine for workflow snapshots.\n *\n * Takes a \"current\" and \"proposed\" snapshot (both in the API shape with\n * `{nodes, edges}` where each node is `{id, nodeType, configJson, orderIndex}`)\n * and returns a structured description of added/removed/changed nodes, added/\n * removed edges, and field-level changes within changed nodes.\n *\n * Pure TS — no React / DOM / Effect deps. Consumed by apps/web (Flow Designer\n * review UI) and packages/canary-cli (the `proposals diff` command).\n */\n\nimport { pickCanonicalConfig } from './node-fields.js';\n\n/** Canonical node shape used by the diff. Matches the API snapshot shape. */\nexport interface DiffNode {\n id: string;\n nodeType: string;\n orderIndex?: number;\n configJson: Record<string, unknown>;\n}\n\nexport interface DiffEdge {\n id: string;\n sourceNodeId: string;\n targetNodeId: string;\n edgeType?: string | null;\n}\n\nexport interface DiffSnapshot {\n nodes: DiffNode[];\n edges: DiffEdge[];\n}\n\nexport interface FieldChange {\n /** Dotted path within configJson (e.g. \"selector\" or \"actions.0.value\"). */\n path: string;\n before: unknown;\n after: unknown;\n}\n\nexport interface ChangedNodeDiff {\n id: string;\n nodeType: string;\n /** True if nodeType itself changed (rare). */\n typeChanged: boolean;\n beforeType?: string;\n afterType?: string;\n fieldChanges: FieldChange[];\n /** True if orderIndex changed. */\n moved: boolean;\n}\n\nexport interface SnapshotDiff {\n addedNodes: DiffNode[];\n removedNodes: DiffNode[];\n changedNodes: ChangedNodeDiff[];\n addedEdges: DiffEdge[];\n removedEdges: DiffEdge[];\n /** Count of nodes touched in any way (added + removed + changed). */\n touchedCount: number;\n /** True when both snapshots are structurally identical. */\n isEmpty: boolean;\n}\n\n/** Convert an API-shape snapshot (from a proposal) to canonical diff shape. */\nexport function fromApiSnapshot(snapshot: {\n nodes?: Array<{\n id: string;\n nodeType: string;\n orderIndex?: number;\n configJson?: Record<string, unknown>;\n }>;\n edges?: Array<{\n id: string;\n sourceNodeId: string;\n targetNodeId: string;\n edgeType?: string | null;\n }>;\n}): DiffSnapshot {\n return {\n nodes: (snapshot.nodes ?? []).map((n) => ({\n id: n.id,\n nodeType: n.nodeType,\n orderIndex: n.orderIndex,\n configJson: n.configJson ?? {},\n })),\n edges: (snapshot.edges ?? []).map((e) => ({\n id: e.id,\n sourceNodeId: e.sourceNodeId,\n targetNodeId: e.targetNodeId,\n edgeType: e.edgeType ?? null,\n })),\n };\n}\n\n/**\n * Diff two configJson objects, producing a flat list of FieldChange entries\n * keyed by dotted path. Arrays are compared element-wise; non-object values\n * are compared by JSON equality.\n */\nfunction diffConfig(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n basePath = '',\n): FieldChange[] {\n const out: FieldChange[] = [];\n const keys = new Set<string>([...Object.keys(before), ...Object.keys(after)]);\n\n for (const key of keys) {\n const path = basePath ? `${basePath}.${key}` : key;\n const bv = before[key];\n const av = after[key];\n\n if (isPlainObject(bv) && isPlainObject(av)) {\n out.push(\n ...diffConfig(\n bv as Record<string, unknown>,\n av as Record<string, unknown>,\n path,\n ),\n );\n continue;\n }\n\n if (!valuesEqual(bv, av)) {\n out.push({ path, before: bv, after: av });\n }\n }\n\n return out;\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return (\n typeof v === 'object' &&\n v !== null &&\n !Array.isArray(v) &&\n Object.getPrototypeOf(v) === Object.prototype\n );\n}\n\nfunction valuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || a === undefined || b === null || b === undefined) return a === b;\n try {\n return JSON.stringify(sortObjectKeys(a)) === JSON.stringify(sortObjectKeys(b));\n } catch {\n return false;\n }\n}\n\n/**\n * Recursively sort object keys so two semantically-identical configs in\n * different key order compare equal. Arrays keep their order (order IS\n * significant for lists).\n */\nfunction sortObjectKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortObjectKeys);\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(value as Record<string, unknown>).sort()) {\n out[k] = sortObjectKeys((value as Record<string, unknown>)[k]);\n }\n return out;\n }\n return value;\n}\n\n/** Main entry point — compute a full diff between two snapshots. */\nexport function diffSnapshots(\n current: DiffSnapshot,\n proposed: DiffSnapshot,\n): SnapshotDiff {\n const currentNodeMap = new Map(current.nodes.map((n) => [n.id, n]));\n const proposedNodeMap = new Map(proposed.nodes.map((n) => [n.id, n]));\n\n const addedNodes: DiffNode[] = [];\n const removedNodes: DiffNode[] = [];\n const changedNodes: ChangedNodeDiff[] = [];\n\n for (const node of proposed.nodes) {\n const prev = currentNodeMap.get(node.id);\n if (!prev) {\n addedNodes.push(node);\n continue;\n }\n const typeChanged = prev.nodeType !== node.nodeType;\n // Diff only the canonical (editable) fields — the client hydrates UI\n // handlers and preview objects into `data`, which would otherwise show\n // up here as phantom changes.\n const prevCanonical = pickCanonicalConfig(prev.nodeType, prev.configJson);\n const nextCanonical = pickCanonicalConfig(node.nodeType, node.configJson);\n const fieldChanges = diffConfig(prevCanonical, nextCanonical);\n const moved = false;\n if (typeChanged || fieldChanges.length > 0 || moved) {\n changedNodes.push({\n id: node.id,\n nodeType: node.nodeType,\n typeChanged,\n beforeType: typeChanged ? prev.nodeType : undefined,\n afterType: typeChanged ? node.nodeType : undefined,\n fieldChanges,\n moved,\n });\n }\n }\n\n for (const node of current.nodes) {\n if (!proposedNodeMap.has(node.id)) removedNodes.push(node);\n }\n\n const currentEdgeMap = new Map(current.edges.map((e) => [edgeKey(e), e]));\n const proposedEdgeMap = new Map(proposed.edges.map((e) => [edgeKey(e), e]));\n\n const addedEdges: DiffEdge[] = [];\n const removedEdges: DiffEdge[] = [];\n for (const e of proposed.edges) {\n if (!currentEdgeMap.has(edgeKey(e))) addedEdges.push(e);\n }\n for (const e of current.edges) {\n if (!proposedEdgeMap.has(edgeKey(e))) removedEdges.push(e);\n }\n\n const touchedCount =\n addedNodes.length + removedNodes.length + changedNodes.length;\n const isEmpty =\n touchedCount === 0 && addedEdges.length === 0 && removedEdges.length === 0;\n\n return {\n addedNodes,\n removedNodes,\n changedNodes,\n addedEdges,\n removedEdges,\n touchedCount,\n isEmpty,\n };\n}\n\nfunction edgeKey(e: DiffEdge): string {\n return `${e.sourceNodeId}→${e.targetNodeId}:${e.edgeType ?? ''}`;\n}\n","/**\n * Derive a human-readable title for a node from its config, so that diff UI\n * can show meaningful labels instead of UUIDs.\n *\n * Mirrors the logic in `improvements/ImprovementOverlay.tsx` so the two\n * review surfaces stay consistent.\n */\n\nconst FALLBACK_BY_TYPE: Record<string, string> = {\n login: 'Login',\n navigate: 'Navigate',\n action: 'Action',\n assertion: 'Assertion',\n wait: 'Wait',\n condition: 'Condition',\n setup: 'Setup',\n api_sequence: 'API Sequence',\n end: 'End',\n seed: 'Seed',\n notification: 'Notification',\n};\n\nexport function getNodeDisplayTitle(\n nodeType: string | undefined,\n config: Record<string, unknown> | undefined,\n): string {\n const data = config ?? {};\n const str = (v: unknown): string | undefined =>\n typeof v === 'string' && v.trim().length > 0 ? v : undefined;\n\n const explicit =\n str(data.label) ?? str(data.customActionTitle) ?? str(data.title);\n if (explicit) return explicit;\n\n switch (nodeType) {\n case 'login':\n return str(data.credentialName) ?? 'Login';\n case 'navigate':\n return str(data.pageTitle) ?? str(data.url) ?? 'Navigate';\n case 'action':\n return str(data.actionDescription) ?? 'Action';\n case 'assertion':\n return str(data.condition) ?? str(data.description) ?? 'Assertion';\n case 'wait':\n return str(data.description) ?? 'Wait';\n case 'condition':\n return str(data.conditionDescription) ?? 'Condition';\n case 'setup':\n return str(data.setupFlowName) ?? 'Setup';\n case 'api_sequence':\n return str(data.sequenceName) ?? 'API Sequence';\n default:\n return FALLBACK_BY_TYPE[nodeType ?? ''] ?? nodeType ?? 'Step';\n }\n}\n","/**\n * Remap proposed snapshot node IDs to current draft node IDs by\n * (nodeType, orderIndex) when callers submit a structurally identical\n * snapshot with re-minted UUIDs.\n *\n * Why this exists: the diff engine in @chatsdet/workflow-diff matches nodes by\n * `id` only. Callers like `canary workflow sync --propose` historically minted\n * fresh UUIDs for every node (because agents passed in just `{nodeType,\n * configJson}`), which made every proposal read as a full\n * remove-everything/add-everything rewrite even when the structure was\n * identical. This pass canonicalizes IDs at the write boundary so the diff\n * stays pure and downstream consumers (compose-snapshot, partial accept, the\n * review UI) all keep matching by id.\n *\n * The rule is intentionally conservative: a proposed node only inherits a\n * current node's id when both the nodeType and the orderIndex line up. That\n * matches the common \"agent rewrote configs but kept the shape\" case without\n * collapsing genuine reorders/insertions/deletions into false matches.\n */\nexport interface AlignNode {\n id: string;\n nodeType: string;\n orderIndex?: number;\n // configJson is intentionally unknown — the aligner doesn't read it. Keeping\n // it permissive lets callers pass straight through from drizzle rows\n // (`unknown`-typed jsonb) without an explicit cast.\n configJson?: unknown;\n}\n\nexport interface AlignEdge {\n id: string;\n sourceNodeId: string;\n targetNodeId: string;\n edgeType?: string | null;\n}\n\nexport interface AlignSnapshot<N extends AlignNode = AlignNode, E extends AlignEdge = AlignEdge> {\n nodes: N[];\n edges: E[];\n}\n\nexport interface AlignResult<S extends AlignSnapshot> {\n snapshot: S;\n /** Number of proposed nodes whose id was remapped to a current node id. */\n remappedCount: number;\n}\n\n/**\n * Return a new snapshot whose node IDs (and edge endpoint IDs) are remapped\n * to existing current-draft IDs whenever a proposed node clearly corresponds\n * to a current one.\n *\n * - If a proposed node's id already exists in the current draft, keep it.\n * - Else, look for an unmatched current node with the same nodeType at the\n * same orderIndex. If found, remap the proposed id to that current id.\n * - Else, leave the proposed id as-is (it's a genuinely new node).\n *\n * Edges are rewritten to reference the remapped node IDs. Edge IDs are kept\n * as-is because the diff engine matches edges by source+target+type, not by\n * edge id.\n *\n * Pure: the input snapshot is not mutated.\n */\nexport function alignSnapshotNodeIds<S extends AlignSnapshot>(\n proposed: S,\n currentNodes: ReadonlyArray<AlignNode>,\n): AlignResult<S> {\n const currentIds = new Set<string>();\n // Key: `${nodeType}@${orderIndex}` — orderIndex is required for slot matching.\n const slotMap = new Map<string, string>();\n for (const n of currentNodes) {\n currentIds.add(n.id);\n if (typeof n.orderIndex === 'number') {\n slotMap.set(`${n.nodeType}@${n.orderIndex}`, n.id);\n }\n }\n\n const consumed = new Set<string>();\n const remap = new Map<string, string>();\n let remappedCount = 0;\n\n const nodes = proposed.nodes.map((node) => {\n if (currentIds.has(node.id) && !consumed.has(node.id)) {\n consumed.add(node.id);\n return node;\n }\n if (typeof node.orderIndex === 'number') {\n const slotKey = `${node.nodeType}@${node.orderIndex}`;\n const candidate = slotMap.get(slotKey);\n if (candidate && !consumed.has(candidate)) {\n consumed.add(candidate);\n if (candidate !== node.id) {\n remap.set(node.id, candidate);\n remappedCount += 1;\n }\n return { ...node, id: candidate };\n }\n }\n return node;\n });\n\n const edges = proposed.edges.map((edge) => {\n const newSource = remap.get(edge.sourceNodeId);\n const newTarget = remap.get(edge.targetNodeId);\n if (newSource === undefined && newTarget === undefined) return edge;\n return {\n ...edge,\n sourceNodeId: newSource ?? edge.sourceNodeId,\n targetNodeId: newTarget ?? edge.targetNodeId,\n };\n });\n\n return {\n snapshot: { ...proposed, nodes, edges } as S,\n remappedCount,\n };\n}\n"],"mappings":"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","names":[]}
|