@foldspace_npm/harness 0.1.2 → 0.1.4

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.
@@ -0,0 +1,482 @@
1
+ import {
2
+ CAPABILITIES,
3
+ ERROR_CODES,
4
+ createDiagnosticRequest,
5
+ diagnosticFailure,
6
+ diagnosticSuccess,
7
+ } from "./protocol.mjs";
8
+
9
+ // Diagnostic run functions are deliberately self-contained: CDP and extension
10
+ // adapters serialize them into the page, so they cannot close over this module.
11
+ export const DIAGNOSTICS = Object.freeze({
12
+ inspectSdkState: Object.freeze({
13
+ description:
14
+ "Report Foldspace SDK presence, configured key, script sources, and existing agent instance IDs.",
15
+ risk: "observe",
16
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
17
+ run: function inspectSdkState(args = {}) {
18
+ const root = globalThis.window || globalThis;
19
+ const namespace =
20
+ typeof root.__FOLD_SPACE__ === "string" && root.__FOLD_SPACE__
21
+ ? root.__FOLD_SPACE__
22
+ : "foldspace";
23
+ const sdk = root[namespace];
24
+ let agentIds = [];
25
+ let agentIdsError = null;
26
+ try {
27
+ if (typeof sdk?.agentIds === "function") {
28
+ const value = sdk.agentIds();
29
+ if (Array.isArray(value)) {
30
+ agentIds = value
31
+ .filter((id) => typeof id === "string")
32
+ .slice(0, 50);
33
+ }
34
+ }
35
+ } catch (error) {
36
+ agentIdsError = String(error?.message || error).slice(0, 500);
37
+ }
38
+
39
+ let scripts = [];
40
+ try {
41
+ scripts = Array.from(root.document?.scripts || [])
42
+ .map((script) => script?.src)
43
+ .filter(
44
+ (source) =>
45
+ typeof source === "string" &&
46
+ /foldspace|eucera/i.test(source),
47
+ )
48
+ .slice(0, 20)
49
+ .map((source) => source.slice(0, 2_000));
50
+ } catch {
51
+ // A document is optional for SDK-only fixtures.
52
+ }
53
+ let trackerKey = null;
54
+ try {
55
+ const value = sdk?.getTracker?.()?.getOriginalKey?.();
56
+ if (typeof value === "string") trackerKey = value.slice(0, 500);
57
+ } catch {
58
+ // Older SDKs may not expose their tracker.
59
+ }
60
+
61
+ const expectedApiName =
62
+ typeof args.expectedApiName === "string" ? args.expectedApiName : null;
63
+ const expectedMode =
64
+ typeof args.expectedMode === "string"
65
+ ? args.expectedMode.toLowerCase()
66
+ : null;
67
+ const matchingAgentIds = expectedApiName
68
+ ? agentIds.filter((id) => {
69
+ const separator = id.indexOf("-");
70
+ if (separator < 1) return false;
71
+ const mode = id.slice(0, separator).toLowerCase();
72
+ const apiName = id.slice(separator + 1);
73
+ return (
74
+ apiName === expectedApiName &&
75
+ (expectedMode === null || mode === expectedMode)
76
+ );
77
+ })
78
+ : [];
79
+
80
+ return {
81
+ pageUrl:
82
+ typeof root.location?.href === "string"
83
+ ? root.location.href.slice(0, 2_000)
84
+ : null,
85
+ documentReadyState:
86
+ typeof root.document?.readyState === "string"
87
+ ? root.document.readyState
88
+ : null,
89
+ sdkPresent: sdk !== undefined && sdk !== null,
90
+ sdkReady: typeof sdk?.agent === "function",
91
+ namespace,
92
+ sdkKey:
93
+ typeof sdk?.k === "string" ? sdk.k.slice(0, 500) : null,
94
+ trackerKey,
95
+ agentIds,
96
+ agentIdsError,
97
+ scripts,
98
+ expectedApiName,
99
+ expectedMode,
100
+ matchingAgentIds,
101
+ };
102
+ },
103
+ }),
104
+
105
+ inspectRegistration: Object.freeze({
106
+ description:
107
+ "Read the exact action names registered on an existing Foldspace agent instance.",
108
+ risk: "observe",
109
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
110
+ run: async function inspectRegistration(args = {}) {
111
+ const root = globalThis.window || globalThis;
112
+ const namespace =
113
+ typeof root.__FOLD_SPACE__ === "string" && root.__FOLD_SPACE__
114
+ ? root.__FOLD_SPACE__
115
+ : "foldspace";
116
+ const sdk = root[namespace];
117
+ if (typeof sdk?.agentIds !== "function" || typeof sdk?.agent !== "function") {
118
+ return {
119
+ sdkPresent: sdk !== undefined && sdk !== null,
120
+ sdkReady: false,
121
+ agentFound: false,
122
+ actionNames: [],
123
+ };
124
+ }
125
+
126
+ const apiName =
127
+ typeof args.apiName === "string" ? args.apiName : "";
128
+ const requestedMode =
129
+ typeof args.mode === "string" ? args.mode.toLowerCase() : null;
130
+ const ownerAgentId =
131
+ typeof args.ownerAgentId === "string" ? args.ownerAgentId : null;
132
+ const ids = sdk
133
+ .agentIds()
134
+ .filter((id) => typeof id === "string")
135
+ .slice(0, 50);
136
+ const matchedId = ownerAgentId
137
+ ? ids.find((id) => id === ownerAgentId)
138
+ : ids.find((id) => {
139
+ const separator = id.indexOf("-");
140
+ if (separator < 1) return false;
141
+ const mode = id.slice(0, separator).toLowerCase();
142
+ const name = id.slice(separator + 1);
143
+ return (
144
+ name === apiName &&
145
+ (requestedMode === null || requestedMode === mode)
146
+ );
147
+ });
148
+ if (!matchedId) {
149
+ return {
150
+ sdkPresent: true,
151
+ sdkReady: true,
152
+ agentFound: false,
153
+ agentIds: ids,
154
+ actionNames: [],
155
+ };
156
+ }
157
+
158
+ const separator = matchedId.indexOf("-");
159
+ const mode = matchedId.slice(0, separator);
160
+ const agent = sdk.agent({
161
+ apiName: matchedId.slice(separator + 1),
162
+ mode: mode.toUpperCase(),
163
+ });
164
+ const available = await agent?.getAvailableActions?.();
165
+ const actionNames =
166
+ available && typeof available === "object"
167
+ ? Object.keys(available).sort().slice(0, 200)
168
+ : [];
169
+ const expectedActionNames = Array.isArray(args.expectedActionNames)
170
+ ? args.expectedActionNames
171
+ .filter((name) => typeof name === "string")
172
+ .slice(0, 200)
173
+ : [];
174
+ const actionSet = new Set(actionNames);
175
+ const expectedSet = new Set(expectedActionNames);
176
+
177
+ return {
178
+ sdkPresent: true,
179
+ sdkReady: true,
180
+ agentFound: true,
181
+ agentId: matchedId,
182
+ actionNames,
183
+ expectedActionNames,
184
+ missingActionNames: expectedActionNames.filter(
185
+ (name) => !actionSet.has(name),
186
+ ),
187
+ unexpectedActionNames: actionNames.filter(
188
+ (name) => !expectedSet.has(name),
189
+ ),
190
+ };
191
+ },
192
+ }),
193
+
194
+ verifyWidgetVisible: Object.freeze({
195
+ description:
196
+ "Report whether a Foldspace widget candidate is mounted and has a visible bounding box.",
197
+ risk: "observe",
198
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
199
+ run: function verifyWidgetVisible(args = {}) {
200
+ const root = globalThis.window || globalThis;
201
+ const selectors =
202
+ Array.isArray(args.selectors) && args.selectors.length
203
+ ? args.selectors
204
+ .filter((selector) => typeof selector === "string")
205
+ .slice(0, 10)
206
+ : [
207
+ "#eucera-agent-view",
208
+ "#foldspace-container",
209
+ "[data-foldspace-agent]",
210
+ ];
211
+
212
+ const candidates = [];
213
+ const seen = new Set();
214
+ for (const selector of selectors) {
215
+ let elements = [];
216
+ try {
217
+ if (typeof root.document?.querySelectorAll === "function") {
218
+ elements = Array.from(root.document.querySelectorAll(selector));
219
+ } else {
220
+ const element = root.document?.querySelector?.(selector);
221
+ if (element) elements = [element];
222
+ }
223
+ } catch {
224
+ continue;
225
+ }
226
+ for (const element of elements.slice(0, 30)) {
227
+ if (!element || seen.has(element)) continue;
228
+ seen.add(element);
229
+ const rect = element.getBoundingClientRect?.();
230
+ const width = Math.max(0, Math.round(rect?.width || 0));
231
+ const height = Math.max(0, Math.round(rect?.height || 0));
232
+ candidates.push({
233
+ selector,
234
+ visible: width > 0 && height > 0,
235
+ size: { width, height },
236
+ ownerAgentId:
237
+ element.getAttribute?.("data-foldspace-dev-agent-id") || null,
238
+ });
239
+ }
240
+ }
241
+
242
+ const visibleCandidates = candidates.filter(
243
+ (candidate) => candidate.visible,
244
+ );
245
+ const expectedOwner =
246
+ typeof args.ownerAgentId === "string" ? args.ownerAgentId : null;
247
+ const first = candidates[0] || null;
248
+ return {
249
+ mounted: candidates.length > 0,
250
+ visible: visibleCandidates.length > 0,
251
+ soleVisible: visibleCandidates.length === 1,
252
+ visibleCount: visibleCandidates.length,
253
+ ownerMatches:
254
+ expectedOwner === null ||
255
+ (visibleCandidates.length === 1 &&
256
+ visibleCandidates[0].ownerAgentId === expectedOwner),
257
+ expectedOwner,
258
+ selector: first?.selector || null,
259
+ size: first?.size || null,
260
+ candidates,
261
+ };
262
+ },
263
+ }),
264
+
265
+ inspectActionObservation: Object.freeze({
266
+ description:
267
+ "Report the local action registry and bounded passive action lifecycle evidence captured in the page.",
268
+ risk: "observe",
269
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
270
+ run: function inspectActionObservation(args = {}) {
271
+ const root = globalThis.window || globalThis;
272
+ const state = root.__FOLDSPACE_DEV_ACTION_OBSERVER__;
273
+ if (!state || state.version !== 1) {
274
+ return {
275
+ installed: false,
276
+ targetAgentId: null,
277
+ expectedActionNames: [],
278
+ actionNameLimitExceeded: false,
279
+ captureCount: 0,
280
+ subscribed: false,
281
+ eventCount: 0,
282
+ droppedEvents: 0,
283
+ events: [],
284
+ };
285
+ }
286
+
287
+ const expectedTarget =
288
+ typeof args.ownerAgentId === "string" ? args.ownerAgentId : null;
289
+ const events = Array.isArray(state.events)
290
+ ? state.events.slice(-100).map((event) => ({
291
+ source:
292
+ typeof event?.source === "string"
293
+ ? event.source.slice(0, 100)
294
+ : "unknown",
295
+ actionName:
296
+ typeof event?.actionName === "string"
297
+ ? event.actionName.slice(0, 200)
298
+ : null,
299
+ phase:
300
+ typeof event?.phase === "string"
301
+ ? event.phase.slice(0, 100)
302
+ : "unknown",
303
+ status:
304
+ typeof event?.status === "string"
305
+ ? event.status.slice(0, 100)
306
+ : "unknown",
307
+ durationMs: Number.isFinite(event?.durationMs)
308
+ ? Math.max(0, Math.round(event.durationMs))
309
+ : null,
310
+ parameterKeys: Array.isArray(event?.parameterKeys)
311
+ ? event.parameterKeys
312
+ .filter((key) => typeof key === "string")
313
+ .slice(0, 50)
314
+ : [],
315
+ }))
316
+ : [];
317
+
318
+ return {
319
+ installed: true,
320
+ targetAgentId:
321
+ typeof state.targetAgentId === "string"
322
+ ? state.targetAgentId.slice(0, 200)
323
+ : null,
324
+ ownerMatches:
325
+ expectedTarget === null || state.targetAgentId === expectedTarget,
326
+ expectedActionNames: Array.isArray(state.expectedActionNames)
327
+ ? state.expectedActionNames
328
+ .filter((name) => typeof name === "string")
329
+ .sort()
330
+ .slice(0, 200)
331
+ : [],
332
+ actionNameLimitExceeded:
333
+ state.actionNameLimitExceeded === true,
334
+ captureCount: Number.isFinite(state.captureCount)
335
+ ? Math.max(0, Math.round(state.captureCount))
336
+ : 0,
337
+ subscribed: state.subscribed === true,
338
+ eventCount: Array.isArray(state.events) ? state.events.length : 0,
339
+ droppedEvents: Number.isFinite(state.droppedEvents)
340
+ ? Math.max(0, Math.round(state.droppedEvents))
341
+ : 0,
342
+ events,
343
+ };
344
+ },
345
+ }),
346
+
347
+ runActionCheck: Object.freeze({
348
+ description:
349
+ "Invoke one action on an existing Foldspace instance without relying on model action selection.",
350
+ risk: "test",
351
+ capabilities: Object.freeze([
352
+ CAPABILITIES.PAGE_EVALUATE,
353
+ CAPABILITIES.PAGE_MUTATE,
354
+ ]),
355
+ run: async function runActionCheck(args = {}) {
356
+ const root = globalThis.window || globalThis;
357
+ const namespace =
358
+ typeof root.__FOLD_SPACE__ === "string" && root.__FOLD_SPACE__
359
+ ? root.__FOLD_SPACE__
360
+ : "foldspace";
361
+ const sdk = root[namespace];
362
+ if (typeof sdk?.agentIds !== "function" || typeof sdk?.agent !== "function") {
363
+ return { invoked: false, reason: "SDK_NOT_FOUND" };
364
+ }
365
+
366
+ const apiName =
367
+ typeof args.apiName === "string" ? args.apiName : "";
368
+ const actionName =
369
+ typeof args.actionName === "string" ? args.actionName : "";
370
+ const requestedMode =
371
+ typeof args.mode === "string" ? args.mode.toLowerCase() : null;
372
+ const matchedId = sdk.agentIds().find((id) => {
373
+ if (typeof id !== "string") return false;
374
+ const separator = id.indexOf("-");
375
+ if (separator < 1) return false;
376
+ const mode = id.slice(0, separator).toLowerCase();
377
+ const name = id.slice(separator + 1);
378
+ return (
379
+ name === apiName &&
380
+ (requestedMode === null || requestedMode === mode)
381
+ );
382
+ });
383
+ if (!matchedId) {
384
+ return { invoked: false, reason: "AGENT_NOT_FOUND" };
385
+ }
386
+
387
+ const separator = matchedId.indexOf("-");
388
+ const agent = sdk.agent({
389
+ apiName: matchedId.slice(separator + 1),
390
+ mode: matchedId.slice(0, separator).toUpperCase(),
391
+ });
392
+ if (typeof agent?.runAction !== "function") {
393
+ return { invoked: false, reason: "RUN_ACTION_UNAVAILABLE" };
394
+ }
395
+
396
+ const value = await agent.runAction(
397
+ actionName,
398
+ args.params && typeof args.params === "object" ? args.params : {},
399
+ );
400
+ return { invoked: true, agentId: matchedId, actionName, value };
401
+ },
402
+ }),
403
+ });
404
+
405
+ export function diagnosticCatalogue() {
406
+ return Object.entries(DIAGNOSTICS).map(([name, diagnostic]) => ({
407
+ name,
408
+ description: diagnostic.description,
409
+ risk: diagnostic.risk,
410
+ capabilities: [...diagnostic.capabilities],
411
+ }));
412
+ }
413
+
414
+ export function serializeDiagnostic(name, args = {}) {
415
+ const diagnostic = DIAGNOSTICS[name];
416
+ if (!diagnostic) {
417
+ throw new Error(`Unknown diagnostic: ${name}`);
418
+ }
419
+ return `Promise.resolve((${diagnostic.run.toString()})(${JSON.stringify(args)}))`;
420
+ }
421
+
422
+ export async function executeDiagnostic(input, options = {}) {
423
+ let request;
424
+ try {
425
+ request = createDiagnosticRequest(input);
426
+ } catch (error) {
427
+ const fallback = {
428
+ id: typeof input?.id === "string" ? input.id : "invalid-request",
429
+ name: typeof input?.name === "string" ? input.name : "invalid-diagnostic",
430
+ };
431
+ return diagnosticFailure(fallback, {
432
+ code: ERROR_CODES.INVALID_REQUEST,
433
+ message: error instanceof Error ? error.message : String(error),
434
+ });
435
+ }
436
+
437
+ const diagnostic = DIAGNOSTICS[request.name];
438
+ if (!diagnostic) {
439
+ return diagnosticFailure(request, {
440
+ code: ERROR_CODES.INVALID_REQUEST,
441
+ message: `Unknown diagnostic: ${request.name}`,
442
+ });
443
+ }
444
+
445
+ const availableCapabilities = new Set(options.capabilities || []);
446
+ const missingCapabilities = diagnostic.capabilities.filter(
447
+ (capability) => !availableCapabilities.has(capability),
448
+ );
449
+ if (missingCapabilities.length) {
450
+ return diagnosticFailure(request, {
451
+ code: ERROR_CODES.CAPABILITY_DISABLED,
452
+ message: `Diagnostic requires: ${missingCapabilities.join(", ")}`,
453
+ });
454
+ }
455
+
456
+ const startedAt = Date.now();
457
+ let timeout;
458
+ try {
459
+ const result = await Promise.race([
460
+ Promise.resolve(diagnostic.run(request.args)),
461
+ new Promise((_, reject) => {
462
+ timeout = setTimeout(
463
+ () => reject(new Error("Diagnostic timed out")),
464
+ request.timeoutMs,
465
+ );
466
+ }),
467
+ ]);
468
+ return diagnosticSuccess(request, result, {
469
+ durationMs: Date.now() - startedAt,
470
+ });
471
+ } catch (error) {
472
+ return diagnosticFailure(request, {
473
+ code:
474
+ error instanceof Error && error.message === "Diagnostic timed out"
475
+ ? ERROR_CODES.TIMEOUT
476
+ : ERROR_CODES.DIAGNOSTIC_FAILED,
477
+ message: error instanceof Error ? error.message : String(error),
478
+ });
479
+ } finally {
480
+ clearTimeout(timeout);
481
+ }
482
+ }
package/src/init.mjs CHANGED
@@ -4,15 +4,21 @@ import path from "node:path";
4
4
  import readline from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { commandByName } from "./cli-registry.mjs";
7
8
 
8
9
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
10
  const defaultTemplateRoot = path.join(packageRoot, "templates", "agent-starter");
10
- const allowedFlags = new Set(["name", "product-id", "agent-api-name", "domain"]);
11
+ const allowedFlags = new Set([
12
+ "name",
13
+ "product-id",
14
+ "agent-key",
15
+ "agent-api-name",
16
+ "domain",
17
+ ]);
11
18
  const tokenPattern = /\{\{([A-Z0-9_]+)\}\}/g;
12
19
  const defaultDirectory = "my-agent";
13
20
 
14
- export const initUsage =
15
- "foldspace init [<directory>] [--product-id <id>] [--agent-api-name <name>] [--domain <host>] [--name <display-name>]";
21
+ export const initUsage = commandByName("init").usage;
16
22
 
17
23
  function assertSupportedNode() {
18
24
  const major = Number.parseInt(process.versions.node.split(".", 1)[0], 10);
@@ -75,12 +81,17 @@ export function parseInitArgs(argv) {
75
81
  if (positional.length > 1) {
76
82
  throw new Error(`Usage: ${initUsage}`);
77
83
  }
84
+ if (flags["agent-key"] && flags["agent-api-name"]) {
85
+ throw new Error(
86
+ "Use --agent-key only; --agent-api-name is its deprecated alias.",
87
+ );
88
+ }
78
89
 
79
90
  return {
80
91
  directory: positional[0] || null,
81
92
  displayName: flags.name || null,
82
93
  productId: flags["product-id"] || null,
83
- agentApiName: flags["agent-api-name"] || null,
94
+ agentApiName: flags["agent-key"] || flags["agent-api-name"] || null,
84
95
  domain: flags.domain || null,
85
96
  };
86
97
  }
@@ -89,7 +100,7 @@ function missingInitFields(parsed) {
89
100
  const missing = [];
90
101
  if (!parsed.directory) missing.push("directory");
91
102
  if (!parsed.productId) missing.push("product-id");
92
- if (!parsed.agentApiName) missing.push("agent-api-name");
103
+ if (!parsed.agentApiName) missing.push("agent-key");
93
104
  if (!parsed.domain) missing.push("domain");
94
105
  return missing;
95
106
  }
@@ -208,8 +219,8 @@ async function promptForMissingFields(parsed, options = {}) {
208
219
 
209
220
  if (!next.agentApiName) {
210
221
  next.agentApiName = await askUntilValid(
211
- "Agent API name",
212
- (value) => validateIdentifier(value, "Agent API name"),
222
+ "Agent Key",
223
+ (value) => validateIdentifier(value, "Agent Key"),
213
224
  options,
214
225
  );
215
226
  }
@@ -243,7 +254,7 @@ function finalizeInitConfig(parsed) {
243
254
 
244
255
  for (const [key, label] of [
245
256
  ["productId", "product-id"],
246
- ["agentApiName", "agent-api-name"],
257
+ ["agentApiName", "agent-key"],
247
258
  ["domain", "domain"],
248
259
  ]) {
249
260
  if (!parsed[key]) {
@@ -282,7 +293,7 @@ function createTemplateValues(config, harnessVersion) {
282
293
  }
283
294
 
284
295
  const productId = validateProductId(config.productId);
285
- const agentApiName = validateIdentifier(config.agentApiName, "Agent API name");
296
+ const agentApiName = validateIdentifier(config.agentApiName, "Agent Key");
286
297
  const target = normalizeTarget(config.domain);
287
298
  const hosts =
288
299
  target.domain === "localhost" || /^\d+(?:\.\d+){3}$/.test(target.domain)
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function readProjectConfig(projectDir) {
5
+ const configPath = path.join(projectDir, "foldspace.dev.json");
6
+ if (!fs.existsSync(configPath)) {
7
+ throw new Error("foldspace.dev.json not found");
8
+ }
9
+ let config;
10
+ try {
11
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
12
+ } catch (error) {
13
+ throw new Error(
14
+ `foldspace.dev.json is invalid: ${
15
+ error instanceof Error ? error.message : String(error)
16
+ }`,
17
+ );
18
+ }
19
+ return config;
20
+ }
21
+
22
+ export function resolveConfiguredTarget(config, requestedTarget) {
23
+ const targetName = requestedTarget || config?.defaultTarget;
24
+ if (typeof targetName !== "string" || !targetName) {
25
+ throw new Error("foldspace.dev.json must define defaultTarget");
26
+ }
27
+ const target = config?.targets?.[targetName];
28
+ if (!target || typeof target !== "object") {
29
+ throw new Error(`target "${targetName}" is not defined`);
30
+ }
31
+ for (const field of ["productId", "agentApiName"]) {
32
+ if (typeof target[field] !== "string" || !target[field]) {
33
+ throw new Error(`target "${targetName}" must define ${field}`);
34
+ }
35
+ }
36
+ return { targetName, target };
37
+ }