@foldspace_npm/harness 0.1.2 → 0.1.3
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 +61 -12
- package/bin/attach.mjs +779 -107
- package/bin/cli.mjs +122 -44
- package/bin/deploy.mjs +37 -33
- package/bin/inject.mjs +64 -295
- package/package.json +5 -2
- package/src/action-observer.mjs +271 -0
- package/src/attach-helpers.mjs +162 -0
- package/src/attach-preflight.mjs +332 -0
- package/src/bootstrap-script.mjs +120 -0
- package/src/cdp-request-manager.mjs +61 -0
- package/src/cli-help.mjs +143 -0
- package/src/cli-registry.mjs +309 -0
- package/src/diagnostics.mjs +482 -0
- package/src/init.mjs +2 -2
- package/src/project-config.mjs +37 -0
- package/src/protocol.mjs +181 -0
- package/src/session-summary.mjs +133 -0
- package/templates/agent-starter/CLAUDE.md +31 -8
- package/templates/agent-starter/README.md +26 -3
- package/templates/agent-starter/foldspace.dev.json +1 -3
- package/bin/buildExtension.mjs +0 -90
- package/bin/packageExtension.mjs +0 -29
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
export const ACTION_OBSERVER_GLOBAL = "__FOLDSPACE_DEV_ACTION_OBSERVER__";
|
|
2
|
+
export const ACTION_LOG_PREFIX = "[foldspace-action]";
|
|
3
|
+
|
|
4
|
+
export function buildActionObserverScript({
|
|
5
|
+
agentApiName,
|
|
6
|
+
mode = "OVERLAY",
|
|
7
|
+
namespace = "foldspace",
|
|
8
|
+
maxEvents = 200,
|
|
9
|
+
}) {
|
|
10
|
+
if (typeof agentApiName !== "string" || !agentApiName) {
|
|
11
|
+
throw new TypeError("agentApiName is required");
|
|
12
|
+
}
|
|
13
|
+
if (!Number.isInteger(maxEvents) || maxEvents < 1 || maxEvents > 1_000) {
|
|
14
|
+
throw new TypeError("maxEvents must be an integer between 1 and 1000");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return `(() => {
|
|
18
|
+
if (window.top !== window.self) return;
|
|
19
|
+
const AGENT = ${JSON.stringify(agentApiName)};
|
|
20
|
+
const MODE = ${JSON.stringify(mode.toUpperCase())};
|
|
21
|
+
const AGENT_ID = MODE.toLowerCase() + "-" + AGENT;
|
|
22
|
+
const NS = ${JSON.stringify(namespace)};
|
|
23
|
+
const MAX_EVENTS = ${maxEvents};
|
|
24
|
+
const OBSERVER_KEY = ${JSON.stringify(ACTION_OBSERVER_GLOBAL)};
|
|
25
|
+
const ACTIONS_KEY = "__FOLDSPACE_REMOTE_ACTIONS__";
|
|
26
|
+
const WRAPPED_MARKER = Symbol("foldspaceHarnessObservedActions");
|
|
27
|
+
const LOG_PREFIX = ${JSON.stringify(ACTION_LOG_PREFIX)};
|
|
28
|
+
|
|
29
|
+
if (window[OBSERVER_KEY]?.version === 1) return;
|
|
30
|
+
|
|
31
|
+
const state = {
|
|
32
|
+
version: 1,
|
|
33
|
+
targetAgentId: AGENT_ID,
|
|
34
|
+
expectedActionNames: [],
|
|
35
|
+
actionNameLimitExceeded: false,
|
|
36
|
+
captureCount: 0,
|
|
37
|
+
subscribed: false,
|
|
38
|
+
events: [],
|
|
39
|
+
droppedEvents: 0,
|
|
40
|
+
};
|
|
41
|
+
window[OBSERVER_KEY] = state;
|
|
42
|
+
|
|
43
|
+
const record = (event) => {
|
|
44
|
+
const safeEvent = { ...event, timestampMs: Date.now() };
|
|
45
|
+
if (state.events.length >= MAX_EVENTS) {
|
|
46
|
+
state.events.shift();
|
|
47
|
+
state.droppedEvents += 1;
|
|
48
|
+
}
|
|
49
|
+
state.events.push(safeEvent);
|
|
50
|
+
try {
|
|
51
|
+
window.console?.log(LOG_PREFIX + " " + JSON.stringify(safeEvent));
|
|
52
|
+
} catch {}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const parameterKeys = (value) => {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
57
|
+
return Object.keys(value)
|
|
58
|
+
.filter((key) => typeof key === "string")
|
|
59
|
+
.sort()
|
|
60
|
+
.slice(0, 50);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const now = () => {
|
|
64
|
+
try { return window.performance?.now?.() ?? Date.now(); }
|
|
65
|
+
catch { return Date.now(); }
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const wrapFunction = (actionName, phase, fn) =>
|
|
69
|
+
function observedActionPhase(...args) {
|
|
70
|
+
const startedAt = now();
|
|
71
|
+
record({
|
|
72
|
+
source: "local-handler",
|
|
73
|
+
actionName,
|
|
74
|
+
phase,
|
|
75
|
+
status: "started",
|
|
76
|
+
...(phase === "execute"
|
|
77
|
+
? { parameterKeys: parameterKeys(args[0]) }
|
|
78
|
+
: {}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const succeeded = (value) => {
|
|
82
|
+
record({
|
|
83
|
+
source: "local-handler",
|
|
84
|
+
actionName,
|
|
85
|
+
phase,
|
|
86
|
+
status: "succeeded",
|
|
87
|
+
durationMs: Math.max(0, Math.round(now() - startedAt)),
|
|
88
|
+
});
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
const failed = (error) => {
|
|
92
|
+
record({
|
|
93
|
+
source: "local-handler",
|
|
94
|
+
actionName,
|
|
95
|
+
phase,
|
|
96
|
+
status: "failed",
|
|
97
|
+
durationMs: Math.max(0, Math.round(now() - startedAt)),
|
|
98
|
+
errorName:
|
|
99
|
+
typeof error?.name === "string"
|
|
100
|
+
? error.name.slice(0, 100)
|
|
101
|
+
: "Error",
|
|
102
|
+
});
|
|
103
|
+
throw error;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const value = Reflect.apply(fn, this, args);
|
|
108
|
+
return value && typeof value.then === "function"
|
|
109
|
+
? Promise.resolve(value).then(succeeded, failed)
|
|
110
|
+
: succeeded(value);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return failed(error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const wrapHandlers = (handlers) => {
|
|
117
|
+
if (!handlers || typeof handlers !== "object") return handlers;
|
|
118
|
+
if (handlers[WRAPPED_MARKER] === true) return handlers;
|
|
119
|
+
|
|
120
|
+
const allNames = Object.keys(handlers)
|
|
121
|
+
.filter((name) => typeof name === "string")
|
|
122
|
+
.sort();
|
|
123
|
+
const names = allNames.slice(0, 200);
|
|
124
|
+
const descriptors = Object.getOwnPropertyDescriptors(handlers);
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
const descriptor = descriptors[name];
|
|
127
|
+
const definition =
|
|
128
|
+
descriptor && "value" in descriptor
|
|
129
|
+
? descriptor.value
|
|
130
|
+
: handlers[name];
|
|
131
|
+
if (!definition || typeof definition !== "object") {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const actionDescriptors = Object.getOwnPropertyDescriptors(definition);
|
|
135
|
+
for (const phase of ["execute", "render"]) {
|
|
136
|
+
const phaseDescriptor = actionDescriptors[phase];
|
|
137
|
+
if (
|
|
138
|
+
phaseDescriptor &&
|
|
139
|
+
"value" in phaseDescriptor &&
|
|
140
|
+
typeof phaseDescriptor.value === "function"
|
|
141
|
+
) {
|
|
142
|
+
actionDescriptors[phase] = {
|
|
143
|
+
...phaseDescriptor,
|
|
144
|
+
value: wrapFunction(name, phase, phaseDescriptor.value),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!descriptor || !("value" in descriptor)) continue;
|
|
149
|
+
const observedDefinition = Object.create(
|
|
150
|
+
Object.getPrototypeOf(definition),
|
|
151
|
+
actionDescriptors,
|
|
152
|
+
);
|
|
153
|
+
if (!Object.isExtensible(definition)) {
|
|
154
|
+
Object.preventExtensions(observedDefinition);
|
|
155
|
+
}
|
|
156
|
+
descriptors[name] = {
|
|
157
|
+
...descriptor,
|
|
158
|
+
value: observedDefinition,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const wrapped = Object.create(
|
|
162
|
+
Object.getPrototypeOf(handlers),
|
|
163
|
+
descriptors,
|
|
164
|
+
);
|
|
165
|
+
Object.defineProperty(wrapped, WRAPPED_MARKER, {
|
|
166
|
+
value: true,
|
|
167
|
+
enumerable: false,
|
|
168
|
+
});
|
|
169
|
+
state.expectedActionNames = names;
|
|
170
|
+
state.actionNameLimitExceeded = allNames.length > names.length;
|
|
171
|
+
state.captureCount += 1;
|
|
172
|
+
record({
|
|
173
|
+
source: "harness",
|
|
174
|
+
phase: "registration",
|
|
175
|
+
status: names.length ? "captured" : "empty",
|
|
176
|
+
actionNames: names,
|
|
177
|
+
});
|
|
178
|
+
return wrapped;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let currentActions;
|
|
182
|
+
const getCurrentActions = () => currentActions;
|
|
183
|
+
const setCurrentActions = (handlers) => {
|
|
184
|
+
currentActions = wrapHandlers(handlers);
|
|
185
|
+
};
|
|
186
|
+
const ensureActionCapture = () => {
|
|
187
|
+
const descriptor = Object.getOwnPropertyDescriptor(window, ACTIONS_KEY);
|
|
188
|
+
if (descriptor?.get === getCurrentActions) return;
|
|
189
|
+
currentActions =
|
|
190
|
+
descriptor && "value" in descriptor
|
|
191
|
+
? wrapHandlers(descriptor.value)
|
|
192
|
+
: undefined;
|
|
193
|
+
try {
|
|
194
|
+
Object.defineProperty(window, ACTIONS_KEY, {
|
|
195
|
+
configurable: true,
|
|
196
|
+
enumerable: true,
|
|
197
|
+
get: getCurrentActions,
|
|
198
|
+
set: setCurrentActions,
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
record({
|
|
202
|
+
source: "harness",
|
|
203
|
+
phase: "registration",
|
|
204
|
+
status: "capture_failed",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
ensureActionCapture();
|
|
209
|
+
|
|
210
|
+
const observedAgents = new WeakSet();
|
|
211
|
+
const subscribe = () => {
|
|
212
|
+
ensureActionCapture();
|
|
213
|
+
const sdk = window[NS];
|
|
214
|
+
if (!sdk || typeof sdk.agent !== "function") return;
|
|
215
|
+
let ids = [];
|
|
216
|
+
try { ids = typeof sdk.agentIds === "function" ? sdk.agentIds() : []; }
|
|
217
|
+
catch {}
|
|
218
|
+
if (!ids.includes(AGENT_ID)) return;
|
|
219
|
+
|
|
220
|
+
let agent;
|
|
221
|
+
try { agent = sdk.agent({ apiName: AGENT, mode: MODE }); }
|
|
222
|
+
catch { return; }
|
|
223
|
+
if (!agent || observedAgents.has(agent)) return;
|
|
224
|
+
observedAgents.add(agent);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const recordSdkAction = (event) => {
|
|
228
|
+
record({
|
|
229
|
+
source: "sdk",
|
|
230
|
+
actionName:
|
|
231
|
+
typeof event?.actionKey === "string"
|
|
232
|
+
? event.actionKey.slice(0, 200)
|
|
233
|
+
: "unknown",
|
|
234
|
+
phase: "action",
|
|
235
|
+
status:
|
|
236
|
+
typeof event?.status === "string"
|
|
237
|
+
? event.status.slice(0, 100)
|
|
238
|
+
: "unknown",
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
agent.on?.("action.callback", recordSdkAction);
|
|
242
|
+
agent.on?.("remoteActions.loaded", ensureActionCapture);
|
|
243
|
+
agent.on?.("remoteActions.failed", ensureActionCapture);
|
|
244
|
+
state.subscribed = true;
|
|
245
|
+
const widget = window.document?.querySelector?.("#eucera-agent-view");
|
|
246
|
+
widget?.setAttribute?.("data-foldspace-dev-agent-id", AGENT_ID);
|
|
247
|
+
} catch {
|
|
248
|
+
state.subscribed = false;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const arm = () => {
|
|
253
|
+
try { window[NS]?.("when", "ready", subscribe); } catch {}
|
|
254
|
+
subscribe();
|
|
255
|
+
setInterval(subscribe, 1000);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (window[NS]) arm();
|
|
259
|
+
else {
|
|
260
|
+
let attempts = 0;
|
|
261
|
+
const timer = setInterval(() => {
|
|
262
|
+
if (window[NS]) {
|
|
263
|
+
clearInterval(timer);
|
|
264
|
+
arm();
|
|
265
|
+
} else if (++attempts > 200) {
|
|
266
|
+
clearInterval(timer);
|
|
267
|
+
}
|
|
268
|
+
}, 50);
|
|
269
|
+
}
|
|
270
|
+
})();`;
|
|
271
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export const ATTACH_MODES = Object.freeze({
|
|
2
|
+
SWAP: "swap",
|
|
3
|
+
BOOTSTRAP: "bootstrap",
|
|
4
|
+
REPLACE: "replace",
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export function attachModeFromArgs(argv) {
|
|
8
|
+
const bootstrap = argv.includes("--bootstrap");
|
|
9
|
+
const replace = argv.includes("--replace");
|
|
10
|
+
if (bootstrap && replace) {
|
|
11
|
+
throw new Error("--bootstrap and --replace cannot be used together");
|
|
12
|
+
}
|
|
13
|
+
if (bootstrap) return ATTACH_MODES.BOOTSTRAP;
|
|
14
|
+
if (replace) return ATTACH_MODES.REPLACE;
|
|
15
|
+
return ATTACH_MODES.SWAP;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hostPatternsFromMatches(matches) {
|
|
19
|
+
return matches.map((match) =>
|
|
20
|
+
match.replace("*://", "").replace("/*", ""),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hostPatternsFromTarget(target = {}) {
|
|
25
|
+
if (Array.isArray(target.hosts) && target.hosts.length) {
|
|
26
|
+
return target.hosts
|
|
27
|
+
.filter((host) => typeof host === "string" && host.trim())
|
|
28
|
+
.map((host) => host.trim().replace(/^\*:\/\//, "").replace(/\/\*$/, ""));
|
|
29
|
+
}
|
|
30
|
+
if (typeof target.startUrl !== "string" || !target.startUrl) return [];
|
|
31
|
+
try {
|
|
32
|
+
const { hostname } = new URL(target.startUrl);
|
|
33
|
+
return hostname ? [hostname] : [];
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hostMatches(url, hostPatterns) {
|
|
40
|
+
let hostname;
|
|
41
|
+
try {
|
|
42
|
+
hostname = new URL(url).hostname;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return hostPatterns.some((pattern) =>
|
|
47
|
+
pattern.startsWith("*.")
|
|
48
|
+
? hostname === pattern.slice(2) || hostname.endsWith(pattern.slice(1))
|
|
49
|
+
: hostname === pattern,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseAgentId(agentId) {
|
|
54
|
+
if (typeof agentId !== "string") return null;
|
|
55
|
+
const separator = agentId.indexOf("-");
|
|
56
|
+
if (separator < 1 || separator === agentId.length - 1) return null;
|
|
57
|
+
return {
|
|
58
|
+
id: agentId,
|
|
59
|
+
mode: agentId.slice(0, separator).toUpperCase(),
|
|
60
|
+
apiName: agentId.slice(separator + 1),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function characterizePageAgents({
|
|
65
|
+
sdkPresent,
|
|
66
|
+
agentIds = [],
|
|
67
|
+
expectedApiName,
|
|
68
|
+
expectedMode = "OVERLAY",
|
|
69
|
+
}) {
|
|
70
|
+
const parsedAgents = agentIds
|
|
71
|
+
.map(parseAgentId)
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.slice(0, 50);
|
|
74
|
+
const matchingAgents = parsedAgents.filter(
|
|
75
|
+
(agent) =>
|
|
76
|
+
agent.apiName === expectedApiName &&
|
|
77
|
+
agent.mode === expectedMode.toUpperCase(),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let status;
|
|
81
|
+
if (!sdkPresent) {
|
|
82
|
+
status = "no-sdk";
|
|
83
|
+
} else if (parsedAgents.length === 0) {
|
|
84
|
+
status = "sdk-without-agent";
|
|
85
|
+
} else if (matchingAgents.length === 0) {
|
|
86
|
+
status = "different-agent";
|
|
87
|
+
} else if (parsedAgents.length === 1) {
|
|
88
|
+
status = "same-agent";
|
|
89
|
+
} else {
|
|
90
|
+
status = "multiple-agents";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
status,
|
|
95
|
+
expectedAgentId: `${expectedMode.toLowerCase()}-${expectedApiName}`,
|
|
96
|
+
agents: parsedAgents,
|
|
97
|
+
matchingAgentIds: matchingAgents.map((agent) => agent.id),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseActionRequest(url) {
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = new URL(url);
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
109
|
+
const agentIndex = segments.findIndex(
|
|
110
|
+
(segment, index) =>
|
|
111
|
+
segment === "agent" && segments[index + 1] === "actions",
|
|
112
|
+
);
|
|
113
|
+
if (agentIndex < 0 || segments.length < agentIndex + 6) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const [environment, productId, agentApiName, ...artifactSegments] =
|
|
118
|
+
segments.slice(agentIndex + 2);
|
|
119
|
+
if (!environment || !productId || !agentApiName || !artifactSegments.length) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
return {
|
|
125
|
+
environment: decodeURIComponent(environment).toUpperCase(),
|
|
126
|
+
productId: decodeURIComponent(productId),
|
|
127
|
+
agentApiName: decodeURIComponent(agentApiName),
|
|
128
|
+
artifactPath: artifactSegments.map(decodeURIComponent).join("/"),
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function actionRequestMatchesTarget(request, target) {
|
|
136
|
+
return Boolean(
|
|
137
|
+
request &&
|
|
138
|
+
request.productId === target.productId &&
|
|
139
|
+
request.agentApiName === target.agentApiName,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function summarizeConsoleMessages(messages, limit = 50) {
|
|
144
|
+
const summaries = new Map();
|
|
145
|
+
for (const message of messages) {
|
|
146
|
+
const text = String(message?.text ?? message ?? "")
|
|
147
|
+
.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "<id>")
|
|
148
|
+
.replace(/\s+/g, " ")
|
|
149
|
+
.trim()
|
|
150
|
+
.slice(0, 1_000);
|
|
151
|
+
if (!text) continue;
|
|
152
|
+
const level = String(message?.level || "log").toLowerCase();
|
|
153
|
+
const key = `${level}:${text}`;
|
|
154
|
+
const existing = summaries.get(key);
|
|
155
|
+
if (existing) {
|
|
156
|
+
existing.count += 1;
|
|
157
|
+
} else if (summaries.size < limit) {
|
|
158
|
+
summaries.set(key, { level, text, count: 1 });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return Array.from(summaries.values());
|
|
162
|
+
}
|