@foldspace_npm/harness 0.1.26 → 0.1.28
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/CLAUDE.md +402 -204
- package/README.md +84 -36
- package/bin/ask.mjs +19 -9
- package/bin/attach.mjs +91 -56
- package/bin/badge.mjs +28 -8
- package/bin/cli.mjs +23 -6
- package/bin/inject.mjs +3 -4
- package/bin/observe.mjs +22 -6
- package/bin/published.mjs +141 -0
- package/package.json +6 -2
- package/recipes/INDEX.md +13 -8
- package/recipes/README.md +30 -7
- package/recipes/account-overview/README.md +6 -6
- package/recipes/account-overview/agent/actions/show_account_overview.ts +11 -16
- package/recipes/account-overview/agent/views/brand.ts +23 -14
- package/recipes/account-overview/agent/views/overview.ts +13 -17
- package/recipes/bottom-bar/README.md +12 -9
- package/recipes/bottom-bar/agent/bottomBar.ts +14 -12
- package/recipes/create-a-record/README.md +97 -0
- package/recipes/create-a-record/agent/actions/create_project.ts +26 -0
- package/recipes/create-a-record/agent/api/projects.ts +62 -0
- package/recipes/create-a-record/agent/projects.ts +145 -0
- package/recipes/create-a-record/agent/views/approval.ts +181 -0
- package/recipes/create-a-record/agent/views/brand.ts +35 -0
- package/recipes/create-a-record/agent/views/fields.ts +252 -0
- package/recipes/create-a-record/agent/views/project-card.ts +253 -0
- package/recipes/create-a-record/agent/views/steps.ts +92 -0
- package/recipes/create-a-record/fixtures/owners.ok.json +9 -0
- package/recipes/create-a-record/fixtures/project.created.json +1 -0
- package/recipes/create-a-record/fixtures/project.rejected.json +1 -0
- package/recipes/create-a-record/recipe.json +13 -0
- package/recipes/create-a-record/variants/actions/create_project.steps.ts +160 -0
- package/recipes/find-by-name/README.md +3 -1
- package/recipes/find-by-name/agent/actions/find_project_id.ts +1 -1
- package/recipes/lookup-then-view/README.md +73 -0
- package/recipes/lookup-then-view/agent/accounts.ts +174 -0
- package/recipes/lookup-then-view/agent/actions/find_account.ts +108 -0
- package/recipes/lookup-then-view/agent/actions/show_account_overview.ts +42 -0
- package/recipes/lookup-then-view/agent/api/accounts.ts +68 -0
- package/recipes/lookup-then-view/agent/views/account.ts +183 -0
- package/recipes/lookup-then-view/agent/views/brand.ts +35 -0
- package/recipes/lookup-then-view/agent/views/format.ts +73 -0
- package/recipes/lookup-then-view/agent/views/pick-one.ts +240 -0
- package/recipes/lookup-then-view/fixtures/accounts.json +319 -0
- package/recipes/lookup-then-view/fixtures/overview.ok.json +122 -0
- package/recipes/lookup-then-view/fixtures/overview.unsigned.json +14 -0
- package/recipes/lookup-then-view/recipe.json +11 -0
- package/recipes/lookup-then-view/studio/find_account.json +10 -0
- package/recipes/lookup-then-view/studio/show_account_overview.json +15 -0
- package/recipes/opportunities-at-risk/README.md +5 -5
- package/recipes/opportunities-at-risk/agent/actions/show_opportunities_at_risk.ts +9 -7
- package/recipes/opportunities-at-risk/agent/opportunities.ts +11 -10
- package/recipes/opportunities-at-risk/agent/views/at-risk.ts +1 -1
- package/recipes/opportunities-at-risk/agent/views/brand.ts +26 -15
- package/recipes/pick-from-a-list/README.md +7 -6
- package/recipes/pick-from-a-list/agent/actions/choose_project.ts +27 -12
- package/recipes/pick-from-a-list/agent/views/brand.ts +30 -9
- package/recipes/pick-from-a-list/agent/views/picker.ts +3 -3
- package/recipes/prepare-for-a-meeting/README.md +4 -4
- package/recipes/prepare-for-a-meeting/agent/actions/prepare_for_meeting.ts +3 -3
- package/recipes/records-list/README.md +6 -6
- package/recipes/records-list/agent/accounts.ts +3 -1
- package/recipes/records-list/agent/actions/list_accounts.ts +7 -7
- package/recipes/records-list/agent/views/brand.ts +26 -14
- package/recipes/server-rendered-forms/README.md +35 -0
- package/recipes/server-rendered-forms/agent/actions/create_record_via_form.ts +62 -0
- package/recipes/server-rendered-forms/agent/api/pages.ts +57 -0
- package/recipes/server-rendered-forms/agent/forms.ts +192 -0
- package/recipes/server-rendered-forms/fixtures/form.errors.html +9 -0
- package/recipes/server-rendered-forms/fixtures/form.html +17 -0
- package/recipes/server-rendered-forms/fixtures/list.html +9 -0
- package/recipes/server-rendered-forms/fixtures/record.html +4 -0
- package/recipes/server-rendered-forms/recipe.json +10 -0
- package/recipes/several-hosts/README.md +10 -13
- package/recipes/several-hosts/agent/hosts.ts +19 -136
- package/recipes/several-hosts/fixtures/wrong-page.json +6 -0
- package/recipes/several-hosts/recipe.json +3 -3
- package/recipes/swap-the-login-method/README.md +16 -5
- package/recipes/swap-the-login-method/agent/utils.ts +52 -3
- package/recipes/swap-the-login-method/variants/utils.cookies.ts +59 -6
- package/recipes/update-meeting-notes/README.md +10 -4
- package/recipes/update-meeting-notes/agent/actions/update_meeting_notes.ts +16 -5
- package/recipes/upload-contacts/README.md +5 -5
- package/recipes/upload-contacts/agent/actions/upload_contacts.ts +7 -7
- package/recipes/upload-contacts/agent/views/brand.ts +24 -14
- package/recipes/upload-contacts/agent/views/uploader.ts +28 -17
- package/recipes/who-is-the-user/README.md +7 -3
- package/src/ask-core.mjs +16 -4
- package/src/attach-daemon.mjs +9 -2
- package/src/attach-helpers.mjs +105 -0
- package/src/attach-preflight.mjs +34 -11
- package/src/badge-core.mjs +1 -1
- package/src/cdp-client.mjs +15 -19
- package/src/cdp-ownership.mjs +41 -20
- package/src/cli-help.mjs +7 -5
- package/src/cli-registry.mjs +102 -47
- package/src/diagnostics.mjs +1 -1
- package/src/init.mjs +5 -3
- package/src/keep-focus.mjs +17 -13
- package/src/lint/index.mjs +8 -1
- package/src/lint/rules.mjs +240 -43
- package/src/observe-core.mjs +163 -34
- package/src/published-core.mjs +392 -0
- package/src/region.mjs +1 -1
- package/src/runtime/agent.ts +16 -12
- package/src/runtime/http.ts +25 -15
- package/src/runtime/match.ts +1 -1
- package/src/runtime/render.ts +4 -4
- package/src/transports/README.md +2 -2
- package/src/upgrade.mjs +33 -10
- package/templates/agent-starter/CLAUDE.md +8 -5
- package/templates/agent-starter/README.md +32 -33
- package/templates/agent-starter/agent/actions/_example.ts +4 -4
- package/templates/agent-starter/agent/actions/index.ts +2 -2
- package/templates/agent-starter/agent/constants.ts +4 -3
- package/templates/agent-starter/agent/utils.ts +15 -18
- package/templates/agent-starter/claude-settings.json +2 -1
- package/templates/agent-starter/docs/app-profile.md +19 -14
- package/templates/agent-starter/foldspace.dev.json +1 -1
- package/recipes/several-hosts/fixtures/nav-record.json +0 -5
package/bin/attach.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* --replace The app embeds a different Foldspace product or agent.
|
|
17
17
|
* Rebind the page bootstrap to the configured product and
|
|
18
|
-
* make the configured agent the
|
|
18
|
+
* make the configured agent the only visible one.
|
|
19
19
|
*
|
|
20
20
|
* Chrome 136+ removed --load-extension and does not run content scripts for
|
|
21
21
|
* CDP-loaded extensions, so neither path uses the extension at runtime.
|
|
@@ -25,11 +25,13 @@ import path from "path";
|
|
|
25
25
|
import { fileURLToPath } from "url";
|
|
26
26
|
import {
|
|
27
27
|
ATTACH_MODES,
|
|
28
|
+
attachAgainFlags,
|
|
28
29
|
attachModeFromArgs,
|
|
29
30
|
characterizePageAgents,
|
|
30
31
|
hostMatches,
|
|
31
32
|
hostPatternsFromTarget,
|
|
32
33
|
parseAgentId,
|
|
34
|
+
registrationMismatchReport,
|
|
33
35
|
registrationVerified,
|
|
34
36
|
resolveAttachLaunch,
|
|
35
37
|
} from "../src/attach-helpers.mjs";
|
|
@@ -101,8 +103,8 @@ const { useSavedLaunch, port } = resolveAttachLaunch({
|
|
|
101
103
|
});
|
|
102
104
|
if (!port) {
|
|
103
105
|
console.error(
|
|
104
|
-
`attach: no CDP port. Run
|
|
105
|
-
` Refusing to guess 9222
|
|
106
|
+
`attach: no CDP port recorded here. Run \`npm run inject\` first; it opens the test window and records its port.\n` +
|
|
107
|
+
` Refusing to guess 9222 - that port is often someone else's Chrome.`,
|
|
106
108
|
);
|
|
107
109
|
process.exit(1);
|
|
108
110
|
}
|
|
@@ -113,7 +115,7 @@ const claimed = verifyLaunchSentinel({
|
|
|
113
115
|
});
|
|
114
116
|
if (!claimed.ok) {
|
|
115
117
|
console.error(
|
|
116
|
-
`attach: ${ownershipErrorMessage(claimed.reason, { port,
|
|
118
|
+
`attach: ${ownershipErrorMessage(claimed.reason, { port, command: "attach" })}`,
|
|
117
119
|
);
|
|
118
120
|
process.exit(1);
|
|
119
121
|
}
|
|
@@ -124,7 +126,7 @@ const owned = await assertOwnedCdp({
|
|
|
124
126
|
});
|
|
125
127
|
if (!owned.ok) {
|
|
126
128
|
console.error(
|
|
127
|
-
`attach: ${ownershipErrorMessage(owned.reason, { port,
|
|
129
|
+
`attach: ${ownershipErrorMessage(owned.reason, { port, command: "attach" })}`,
|
|
128
130
|
);
|
|
129
131
|
process.exit(1);
|
|
130
132
|
}
|
|
@@ -148,6 +150,8 @@ const noTestMode = !process.argv.includes("--test-mode");
|
|
|
148
150
|
// string that asserts you are safe while you are not. It now states what is
|
|
149
151
|
// actually true. --no-badge drops it entirely, for recording a demo.
|
|
150
152
|
const noBadge = process.argv.includes("--no-badge");
|
|
153
|
+
// What a restart step names, so it keeps this attach's mode, test mode and tag.
|
|
154
|
+
const againFlags = attachAgainFlags({ mode: attachMode, testMode: !noTestMode, noBadge });
|
|
151
155
|
// Agent api name for setTestMode — from the same config inject.mjs used.
|
|
152
156
|
const cfg = JSON.parse(fs.readFileSync(path.join(root, "foldspace.dev.json"), "utf8"));
|
|
153
157
|
const targetName =
|
|
@@ -189,7 +193,7 @@ const unresolved = PLACEHOLDERS.filter((ph) =>
|
|
|
189
193
|
if (unresolved.length) {
|
|
190
194
|
console.error(
|
|
191
195
|
`attach: foldspace.dev.json still contains placeholders: ${unresolved.join(", ")}\n` +
|
|
192
|
-
` Run "foldspace init" first, or pass --agent <
|
|
196
|
+
` Run "foldspace init" first, or pass --agent <agent-key>.\n` +
|
|
193
197
|
` Refusing to run — injecting a placeholder breaks the app's agent.`
|
|
194
198
|
);
|
|
195
199
|
process.exit(1);
|
|
@@ -203,7 +207,7 @@ if (bootstrap && (!sdkUrl || typeof sdkUrl !== "string")) {
|
|
|
203
207
|
process.exit(1);
|
|
204
208
|
}
|
|
205
209
|
if (!fs.existsSync(bundlePath)) {
|
|
206
|
-
console.error(`attach: ${path.relative(root, bundlePath)} is missing
|
|
210
|
+
console.error(`attach: ${path.relative(root, bundlePath)} is missing - run \`npm run build\` first.`);
|
|
207
211
|
process.exit(1);
|
|
208
212
|
}
|
|
209
213
|
const hostPatterns = hostPatternsFromTarget(cfgTarget);
|
|
@@ -224,10 +228,10 @@ const SDK_PATTERNS = [
|
|
|
224
228
|
|
|
225
229
|
// The badge has three states, set from outside with \`foldspace badge\`:
|
|
226
230
|
// label - the small corner tag (default): who is attached, which mode
|
|
227
|
-
// working - a pill at the foot of the window: the
|
|
228
|
-
// window, please leave it alone
|
|
231
|
+
// working - a pill at the foot of the window: the build session is working
|
|
232
|
+
// in this window, please leave it alone
|
|
229
233
|
// ready - the pill goes away and the small tag comes back: their turn.
|
|
230
|
-
// No
|
|
234
|
+
// No pill for that - they may type whatever, whenever
|
|
231
235
|
// The pill sits bottom-left, never across the top: a bar there covered the
|
|
232
236
|
// app's own header and menu (seen on a CRM, 2026-09-22). The agent's panel
|
|
233
237
|
// is on the right, so bottom-left is the corner nothing else uses.
|
|
@@ -260,11 +264,11 @@ const badgeSrc = `(() => {
|
|
|
260
264
|
'#foldspace-dev-badge .fs-dot{display:inline-block;width:10px;height:10px;border-radius:50%;background:#FFD314;margin-right:10px;vertical-align:middle;animation:fsPulse 1s ease-in-out infinite}' +
|
|
261
265
|
'#foldspace-dev-badge .fs-line{position:absolute;left:0;right:0;bottom:0;height:3px;background:rgba(255,255,255,.15);overflow:hidden}' +
|
|
262
266
|
'#foldspace-dev-badge .fs-line::after{content:"";position:absolute;top:0;width:30%;height:100%;background:#FFD314;animation:fsSweep 1.4s linear infinite}</style>' +
|
|
263
|
-
'<span class="fs-dot"></span>Foldspace is building your agent on this page. <b style="color:#FFD314">Please leave this window alone</b> until the
|
|
267
|
+
'<span class="fs-dot"></span>Foldspace is building your agent on this page. <b style="color:#FFD314">Please leave this window alone</b> until you are told in the chat that it is ready.' +
|
|
264
268
|
(s.text ? " " + s.text.replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c]) : "") +
|
|
265
269
|
'<span class="fs-line"></span>';
|
|
266
270
|
} else {
|
|
267
|
-
// "ready" and "label" both draw the small tag: their turn needs no
|
|
271
|
+
// "ready" and "label" both draw the small tag: their turn needs no pill -
|
|
268
272
|
// they may type whatever, whenever. Only the wait ever gets a pill.
|
|
269
273
|
b.style.cssText = base + "background:#3247F2;padding:5px 10px;border-bottom-right-radius:4px;";
|
|
270
274
|
b.textContent = LABEL;
|
|
@@ -292,14 +296,16 @@ const badgeSrc = `(() => {
|
|
|
292
296
|
// https://docs.foldspace.ai/reference/test-mode.md
|
|
293
297
|
//
|
|
294
298
|
// A handle is scoped by mode: window.foldspace.agent({apiName}) alone returns
|
|
295
|
-
// the OVERLAY instance. On an app that embeds the
|
|
299
|
+
// the OVERLAY instance. On an app that embeds the agent in-page that is NOT the
|
|
296
300
|
// instance serving the chat, so arming it succeeds, logs success, and leaves
|
|
297
301
|
// the real conversations untagged — the failure that actually leaks. Enumerate
|
|
298
302
|
// foldspace.agentIds() — each id is shaped "<mode>-<apiName>" — and arm every
|
|
299
303
|
// one. See the playbook's test-mode rule.
|
|
300
|
-
const
|
|
304
|
+
const testModeScript = (enable) => `(() => {
|
|
301
305
|
if (window.top !== window.self) return;
|
|
302
306
|
const AGENT = ${JSON.stringify(agentApiName)};
|
|
307
|
+
const WANT = ${enable};
|
|
308
|
+
const WORD = ${JSON.stringify(enable ? "ON" : "OFF")};
|
|
303
309
|
// Log once per instance; setTestMode itself is re-applied on every pass so an
|
|
304
310
|
// instance that is torn down and rebuilt gets re-armed.
|
|
305
311
|
const logged = new Set();
|
|
@@ -316,10 +322,10 @@ const testModeSrc = `(() => {
|
|
|
316
322
|
// Older SDK with no agentIds(): the unscoped handle is all there is.
|
|
317
323
|
if (!ids) {
|
|
318
324
|
try {
|
|
319
|
-
fs.agent({ apiName: AGENT }).setTestMode(
|
|
325
|
+
fs.agent({ apiName: AGENT }).setTestMode(WANT);
|
|
320
326
|
if (!logged.has(AGENT)) {
|
|
321
327
|
logged.add(AGENT);
|
|
322
|
-
console.log("[foldspace-dev] test mode
|
|
328
|
+
console.log("[foldspace-dev] test mode " + WORD + " (" + AGENT + " — SDK has no agentIds(), overlay handle only)");
|
|
323
329
|
}
|
|
324
330
|
} catch (e) {
|
|
325
331
|
console.warn("[foldspace-dev] setTestMode failed:", e && e.message);
|
|
@@ -332,10 +338,10 @@ const testModeSrc = `(() => {
|
|
|
332
338
|
if (cut < 1) continue;
|
|
333
339
|
try {
|
|
334
340
|
fs.agent({ apiName: id.slice(cut + 1), mode: id.slice(0, cut).toUpperCase() })
|
|
335
|
-
.setTestMode(
|
|
341
|
+
.setTestMode(WANT);
|
|
336
342
|
if (!logged.has(id)) {
|
|
337
343
|
logged.add(id);
|
|
338
|
-
console.log("[foldspace-dev] test mode
|
|
344
|
+
console.log("[foldspace-dev] test mode " + WORD + " (" + id + ")");
|
|
339
345
|
}
|
|
340
346
|
} catch (e) {
|
|
341
347
|
console.warn("[foldspace-dev] setTestMode failed for " + id + ":", e && e.message);
|
|
@@ -347,7 +353,7 @@ const testModeSrc = `(() => {
|
|
|
347
353
|
try { window.foldspace("when", "ready", armAll); } catch (e) {}
|
|
348
354
|
armAll();
|
|
349
355
|
|
|
350
|
-
// The instance serving the chat is built when the
|
|
356
|
+
// The instance serving the chat is built when the agent panel opens, long
|
|
351
357
|
// after "ready" — arming once is too early. Keep arming.
|
|
352
358
|
setInterval(armAll, 1000);
|
|
353
359
|
|
|
@@ -404,6 +410,9 @@ function logLifecycle(result) {
|
|
|
404
410
|
if (!result.ok && result.error?.message) {
|
|
405
411
|
console.log(` ${result.error.message}`);
|
|
406
412
|
}
|
|
413
|
+
if (!result.ok && Array.isArray(result.details?.findings)) {
|
|
414
|
+
for (const line of result.details.findings) console.log(` ${line}`);
|
|
415
|
+
}
|
|
407
416
|
if (!result.ok && result.details?.suggestion) {
|
|
408
417
|
console.log(` Next: ${result.details.suggestion}`);
|
|
409
418
|
}
|
|
@@ -701,6 +710,7 @@ async function prepare(ws, sessionId, url) {
|
|
|
701
710
|
mode: agentMode,
|
|
702
711
|
namespace: pageState?.namespace || "foldspace",
|
|
703
712
|
aliases,
|
|
713
|
+
testMode: !noTestMode,
|
|
704
714
|
}),
|
|
705
715
|
hostPatterns,
|
|
706
716
|
),
|
|
@@ -713,7 +723,12 @@ async function prepare(ws, sessionId, url) {
|
|
|
713
723
|
const scripts = [];
|
|
714
724
|
scripts.push(fs.readFileSync(bundlePath, "utf8"));
|
|
715
725
|
if (!noBadge) scripts.push(badgeSrc);
|
|
716
|
-
|
|
726
|
+
// Test mode is armed when --test-mode is set, and actively CLEARED otherwise:
|
|
727
|
+
// it is page-session state that only a reload or setTestMode(false) undoes,
|
|
728
|
+
// so a page left in test mode by an earlier attach would keep the striped
|
|
729
|
+
// test-mode banner under a LIVE corner tag until reload (a customer build,
|
|
730
|
+
// 2026-09-30).
|
|
731
|
+
scripts.push(testModeScript(!noTestMode));
|
|
717
732
|
if (bootstrap) {
|
|
718
733
|
scripts.push(
|
|
719
734
|
buildBootstrapScript({
|
|
@@ -756,6 +771,45 @@ async function prepare(ws, sessionId, url) {
|
|
|
756
771
|
const verification = await verifyPreparedPage(ws, sessionId);
|
|
757
772
|
// For scoring a session: the moment the customer could first see their agent.
|
|
758
773
|
if (verification.ok) recordEvent(root, "agent_visible", { mode: attachMode });
|
|
774
|
+
const registrationDetails = {
|
|
775
|
+
mode: attachMode,
|
|
776
|
+
testMode: !noTestMode,
|
|
777
|
+
noBadge,
|
|
778
|
+
expectedAgentId: verification.characterization.expectedAgentId,
|
|
779
|
+
detectedAgentIds: verification.state?.agentIds || [],
|
|
780
|
+
expectedProductId: productId,
|
|
781
|
+
detectedProductIds: verification.products,
|
|
782
|
+
widget: verification.widget,
|
|
783
|
+
registeredActionNames:
|
|
784
|
+
verification.registration?.actionNames || [],
|
|
785
|
+
expectedActionNames:
|
|
786
|
+
verification.observation?.expectedActionNames || [],
|
|
787
|
+
expectedActionCount: Array.isArray(
|
|
788
|
+
verification.observation?.expectedActionNames,
|
|
789
|
+
)
|
|
790
|
+
? verification.observation.expectedActionNames.length
|
|
791
|
+
: 0,
|
|
792
|
+
missingActionNames:
|
|
793
|
+
verification.registration?.missingActionNames || [],
|
|
794
|
+
unexpectedActionNames:
|
|
795
|
+
verification.registration?.unexpectedActionNames || [],
|
|
796
|
+
actionObserverSubscribed:
|
|
797
|
+
verification.observation?.subscribed === true,
|
|
798
|
+
// null when attach never got as far as the action checks.
|
|
799
|
+
actionObserverInstalled: verification.observation
|
|
800
|
+
? verification.observation.installed === true
|
|
801
|
+
: null,
|
|
802
|
+
actionCaptureCount: verification.observation
|
|
803
|
+
? Number(verification.observation.captureCount) || 0
|
|
804
|
+
: null,
|
|
805
|
+
agentFound: verification.registration
|
|
806
|
+
? verification.registration.agentFound === true
|
|
807
|
+
: null,
|
|
808
|
+
actionNameLimitExceeded:
|
|
809
|
+
verification.observation?.actionNameLimitExceeded === true,
|
|
810
|
+
diagnosticError: verification.diagnosticError,
|
|
811
|
+
};
|
|
812
|
+
const mismatch = verification.ok ? null : registrationMismatchReport(registrationDetails);
|
|
759
813
|
logLifecycle(
|
|
760
814
|
createLifecycleResult({
|
|
761
815
|
operation: "inspect_registration",
|
|
@@ -768,32 +822,9 @@ async function prepare(ws, sessionId, url) {
|
|
|
768
822
|
message:
|
|
769
823
|
"The local action registry did not match the configured page agent.",
|
|
770
824
|
},
|
|
771
|
-
details:
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
detectedAgentIds: verification.state?.agentIds || [],
|
|
775
|
-
expectedProductId: productId,
|
|
776
|
-
detectedProductIds: verification.products,
|
|
777
|
-
widget: verification.widget,
|
|
778
|
-
registeredActionNames:
|
|
779
|
-
verification.registration?.actionNames || [],
|
|
780
|
-
expectedActionNames:
|
|
781
|
-
verification.observation?.expectedActionNames || [],
|
|
782
|
-
expectedActionCount: Array.isArray(
|
|
783
|
-
verification.observation?.expectedActionNames,
|
|
784
|
-
)
|
|
785
|
-
? verification.observation.expectedActionNames.length
|
|
786
|
-
: 0,
|
|
787
|
-
missingActionNames:
|
|
788
|
-
verification.registration?.missingActionNames || [],
|
|
789
|
-
unexpectedActionNames:
|
|
790
|
-
verification.registration?.unexpectedActionNames || [],
|
|
791
|
-
actionObserverSubscribed:
|
|
792
|
-
verification.observation?.subscribed === true,
|
|
793
|
-
actionNameLimitExceeded:
|
|
794
|
-
verification.observation?.actionNameLimitExceeded === true,
|
|
795
|
-
diagnosticError: verification.diagnosticError,
|
|
796
|
-
},
|
|
825
|
+
details: mismatch
|
|
826
|
+
? { ...registrationDetails, findings: mismatch.lines, suggestion: mismatch.next }
|
|
827
|
+
: registrationDetails,
|
|
797
828
|
}),
|
|
798
829
|
);
|
|
799
830
|
if (!verification.ok) {
|
|
@@ -819,7 +850,7 @@ function noteUncoveredHost(url) {
|
|
|
819
850
|
if (coverage.covered || !coverage.pattern || uncoveredHostsSeen.has(coverage.host)) return;
|
|
820
851
|
uncoveredHostsSeen.add(coverage.host);
|
|
821
852
|
console.log(` [hosts] the window is on ${coverage.host}, which this project does not cover - the agent is not there.`);
|
|
822
|
-
console.log(` Next: add "${coverage.pattern}" to "hosts" in foldspace.dev.json (with the human's yes), then inject and attach
|
|
853
|
+
console.log(` Next: if it is part of the product, add "${coverage.pattern}" to "hosts" in foldspace.dev.json (with the human's yes), then \`npx foldspace attach --stop\`, \`npm run inject\` and \`npx foldspace attach${againFlags} --daemon\`. If it is not, ask the human to go back to the product in the test window.`);
|
|
823
854
|
}
|
|
824
855
|
|
|
825
856
|
async function onEvent(ws, msg) {
|
|
@@ -1020,11 +1051,11 @@ const ver = await connect(owned.version);
|
|
|
1020
1051
|
console.log(`Attached to ${ver.Browser} on :${port}`);
|
|
1021
1052
|
console.log(`Mode: ${attachMode}`);
|
|
1022
1053
|
console.log(`Agent: ${agentApiName} (product ${productId}, ${agentMode})`);
|
|
1023
|
-
console.log(`Test: ${noTestMode ? "off
|
|
1054
|
+
console.log(`Test: ${noTestMode ? "off - conversations appear in their Foldspace dashboard (--test-mode is only for an agent that already has real users)" : "on"}`);
|
|
1024
1055
|
console.log(`Hosts: ${hostPatterns.join(", ")}`);
|
|
1025
1056
|
console.log(`Serving: ${path.relative(root, bundlePath)}`);
|
|
1026
1057
|
console.log(
|
|
1027
|
-
`CDP: attached. \`foldspace observe\`
|
|
1058
|
+
`CDP: attached. \`npx foldspace observe\` and \`npx foldspace ask\` use this same window while attach runs; stop attach only to load rebuilt actions or changed hosts, or to check published code on dev.\n`,
|
|
1028
1059
|
);
|
|
1029
1060
|
|
|
1030
1061
|
function printSessionSummary() {
|
|
@@ -1130,9 +1161,9 @@ process.on("SIGTERM", requestDetach);
|
|
|
1130
1161
|
|
|
1131
1162
|
// A closed socket does not mean the browser closed. Chrome drops the CDP
|
|
1132
1163
|
// connection on target churn and transient errors too, and exiting on those
|
|
1133
|
-
// silently ends the swap while the
|
|
1134
|
-
// the
|
|
1135
|
-
// only give up once the debugging port is genuinely gone.
|
|
1164
|
+
// silently ends the swap while the human keeps browsing - the page then runs
|
|
1165
|
+
// only what the site loads by itself, with no signal that anything changed.
|
|
1166
|
+
// Reconnect, and only give up once the debugging port is genuinely gone.
|
|
1136
1167
|
async function browserIsUp() {
|
|
1137
1168
|
try {
|
|
1138
1169
|
const r = await fetch(`http://localhost:${port}/json/version`);
|
|
@@ -1156,12 +1187,16 @@ async function onDisconnect(closedSocket) {
|
|
|
1156
1187
|
if (!(await browserIsUp())) {
|
|
1157
1188
|
clearInterval(keepAlive);
|
|
1158
1189
|
printSessionSummary();
|
|
1159
|
-
console.log("\
|
|
1190
|
+
console.log("\nThe test window was closed. Detaching.");
|
|
1160
1191
|
process.exit(0);
|
|
1161
1192
|
}
|
|
1162
1193
|
try {
|
|
1163
1194
|
console.log(` connection lost — reconnecting (${attempt}/10)…`);
|
|
1164
|
-
|
|
1195
|
+
// connect() needs the browser's version endpoint; re-check ownership
|
|
1196
|
+
// so attach never adopts another Chrome that took the port meanwhile.
|
|
1197
|
+
const again = await assertOwnedCdp({ profileDir, port, token: devState.sentinel });
|
|
1198
|
+
if (!again.ok) throw new Error(again.reason);
|
|
1199
|
+
await connect(again.version);
|
|
1165
1200
|
console.log(` reconnected. ${attachMode} mode is live again.`);
|
|
1166
1201
|
return;
|
|
1167
1202
|
} catch {
|
|
@@ -1170,8 +1205,8 @@ async function onDisconnect(closedSocket) {
|
|
|
1170
1205
|
}
|
|
1171
1206
|
clearInterval(keepAlive);
|
|
1172
1207
|
printSessionSummary();
|
|
1173
|
-
console.error("\nCould not reconnect after 10 attempts. Detaching
|
|
1174
|
-
+
|
|
1208
|
+
console.error("\nCould not reconnect after 10 attempts. Detaching - after a reload the page runs only what "
|
|
1209
|
+
+ `the site loads by itself, not this build. Run \`npx foldspace attach${againFlags} --daemon\` again.`);
|
|
1175
1210
|
process.exit(1);
|
|
1176
1211
|
} finally {
|
|
1177
1212
|
reconnecting = false;
|
package/bin/badge.mjs
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* foldspace badge <working|ready|label|off> [--text "<what to show>"]
|
|
4
4
|
*
|
|
5
|
-
* Sets
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Sets what attach draws in the test window: the pill or the corner tag.
|
|
6
|
+
* You and the human share that one Chrome window, so the pill says when to
|
|
7
|
+
* wait:
|
|
8
8
|
* working - "Foldspace is building your agent on this page. Please leave
|
|
9
|
-
* this window alone until the
|
|
9
|
+
* this window alone until you are told in the chat that it is
|
|
10
|
+
* ready."
|
|
10
11
|
* ready - the pill goes away, the small corner tag comes back: their
|
|
11
|
-
* turn. No
|
|
12
|
+
* turn. No pill for that - they may type whatever, whenever.
|
|
12
13
|
* label - the small corner tag
|
|
13
|
-
* off -
|
|
14
|
+
* off - draw nothing: no pill, no corner tag
|
|
14
15
|
* The state is kept in the page's sessionStorage, so it survives navigation;
|
|
15
16
|
* attach's badge script redraws within a second. Nothing else on the page is
|
|
16
17
|
* touched.
|
|
@@ -36,12 +37,31 @@ try {
|
|
|
36
37
|
let page;
|
|
37
38
|
try {
|
|
38
39
|
page = await CdpPage.open({ projectDir });
|
|
40
|
+
// attach's badge script redraws within a second (after `badge off`, or
|
|
41
|
+
// before the body exists, there is no element yet): give it that second.
|
|
39
42
|
const shown = await page.evaluate(
|
|
40
43
|
`sessionStorage.setItem("__foldspace_badge__", ${JSON.stringify(JSON.stringify(payload))});
|
|
41
|
-
|
|
44
|
+
let b = document.getElementById("foldspace-dev-badge");
|
|
45
|
+
for (let i = 0; !b && ${payload.state !== "off"} && i < 8; i++) {
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
47
|
+
b = document.getElementById("foldspace-dev-badge");
|
|
48
|
+
}
|
|
42
49
|
return { url: location.href.split(/[?#]/)[0], hadBadge: Boolean(b) };`,
|
|
43
50
|
);
|
|
44
|
-
|
|
51
|
+
// Still no element: the state was stored but nothing will show it.
|
|
52
|
+
const note =
|
|
53
|
+
!shown.hadBadge && payload.state !== "off"
|
|
54
|
+
? {
|
|
55
|
+
note:
|
|
56
|
+
"Nothing is drawn on this page. After `npx foldspace published` that is expected - attach is " +
|
|
57
|
+
"stopped - so say in chat whose turn it is instead. Otherwise run `npx foldspace attach --status`. " +
|
|
58
|
+
"With no live daemon, run `npx foldspace attach --daemon` with the flags it ran with. With one, " +
|
|
59
|
+
"read its log (`logPath`): a `registration_mismatch` there names what differs and the next step; " +
|
|
60
|
+
"without one, attach runs with --no-badge - `npx foldspace attach --stop`, then " +
|
|
61
|
+
"`npx foldspace attach --daemon` with the flags it ran with, minus --no-badge.",
|
|
62
|
+
}
|
|
63
|
+
: {};
|
|
64
|
+
printJson({ ok: true, state: payload.state, ...(payload.text ? { text: payload.text } : {}), ...shown, ...note });
|
|
45
65
|
} catch (error) {
|
|
46
66
|
printJson({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
47
67
|
process.exit(1);
|
package/bin/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { runInit } from "../src/init.mjs";
|
|
8
|
+
import { isWriteFlag } from "../src/observe-core.mjs";
|
|
8
9
|
import {
|
|
9
10
|
checkForUpdate,
|
|
10
11
|
gateForCommand,
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
runAttachControl,
|
|
18
19
|
} from "../src/attach-daemon.mjs";
|
|
19
20
|
import {
|
|
20
|
-
CLI_COMMANDS,
|
|
21
21
|
createCliRegistry,
|
|
22
22
|
commandByName,
|
|
23
23
|
} from "../src/cli-registry.mjs";
|
|
@@ -75,13 +75,17 @@ function printHelp(topic, json) {
|
|
|
75
75
|
);
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
const update = checkForUpdate(resolveProjectDir());
|
|
79
78
|
if (json) {
|
|
80
|
-
|
|
79
|
+
// help --json is the first thing the manual reads, so it asks the registry
|
|
80
|
+
// now (as upgrade does) rather than trusting a 12-hour-old answer; offline,
|
|
81
|
+
// it falls back to that answer. One command's contract carries no update.
|
|
82
|
+
const payload = topic
|
|
83
|
+
? document
|
|
84
|
+
: { ...document, update: checkForUpdate(resolveProjectDir(), { preferFresh: true }) };
|
|
81
85
|
console.log(JSON.stringify(payload, null, 2));
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
84
|
-
nagIfOutdated(
|
|
88
|
+
nagIfOutdated(checkForUpdate(resolveProjectDir()));
|
|
85
89
|
console.log(
|
|
86
90
|
topic ? renderCommandHelp(registry, topic) : renderGeneralHelp(registry),
|
|
87
91
|
);
|
|
@@ -112,7 +116,18 @@ function normalizeCommandArgs(command, args) {
|
|
|
112
116
|
const separator = token.indexOf("=");
|
|
113
117
|
const name = separator > -1 ? token.slice(0, separator) : token;
|
|
114
118
|
const match = options.get(name);
|
|
115
|
-
if (!match)
|
|
119
|
+
if (!match) {
|
|
120
|
+
// The reason matters more than the refusal: observe never writes, and
|
|
121
|
+
// always prints JSON. The write reason fits only `observe read`.
|
|
122
|
+
const verb = args.find((item) => !item.startsWith("--"));
|
|
123
|
+
throw new Error(
|
|
124
|
+
command.name === "observe" && name === "--json"
|
|
125
|
+
? "unknown option '--json': observe always prints JSON."
|
|
126
|
+
: command.name === "observe" && verb === "read" && isWriteFlag(name)
|
|
127
|
+
? `unknown option '${name}': observe read is GET only - it never sets a method, a header or a body.`
|
|
128
|
+
: `unknown option '${name}'`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
116
131
|
const { option, canonicalName } = match;
|
|
117
132
|
if (seen.has(canonicalName)) {
|
|
118
133
|
throw new Error(`option '${canonicalName}' was provided twice`);
|
|
@@ -233,7 +248,9 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
233
248
|
}
|
|
234
249
|
}
|
|
235
250
|
} else {
|
|
236
|
-
|
|
251
|
+
// The visible commands only: listing deploy here would offer a coding agent
|
|
252
|
+
// the Foldspace-internal command help hides.
|
|
253
|
+
const names = registry.commands.map((entry) => entry.name).join(", ");
|
|
237
254
|
fail(
|
|
238
255
|
`unknown command '${command}'. Available commands: ${names}. ` +
|
|
239
256
|
`Run 'foldspace help'.`,
|
package/bin/inject.mjs
CHANGED
|
@@ -235,8 +235,8 @@ const chromeArgs = [
|
|
|
235
235
|
startUrl,
|
|
236
236
|
];
|
|
237
237
|
|
|
238
|
-
console.log(`\
|
|
239
|
-
console.log("
|
|
238
|
+
console.log(`\nOpening the test window - profile ${path.relative(root, profileDir)}, debugging port :${debugPort}.`);
|
|
239
|
+
console.log("The human signs in there; the profile keeps what the site remembers between runs.\n");
|
|
240
240
|
|
|
241
241
|
const child = spawn(chrome, chromeArgs, { detached: true, stdio: "ignore" });
|
|
242
242
|
child.unref();
|
|
@@ -254,8 +254,7 @@ if (!owned.ok) {
|
|
|
254
254
|
}
|
|
255
255
|
console.error(`inject: ${ownershipErrorMessage(owned.reason, {
|
|
256
256
|
port: debugPort,
|
|
257
|
-
|
|
258
|
-
root,
|
|
257
|
+
command: "inject",
|
|
259
258
|
})}`);
|
|
260
259
|
process.exit(1);
|
|
261
260
|
}
|
package/bin/observe.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import fs from "node:fs";
|
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
|
|
24
24
|
import { CdpPage, parseArgs, printJson, readLaunchState, sleep } from "../src/cdp-client.mjs";
|
|
25
|
+
import { ownershipErrorMessage } from "../src/cdp-ownership.mjs";
|
|
25
26
|
import { withoutStealingFocus } from "../src/keep-focus.mjs";
|
|
26
27
|
import { withWorkingBadge } from "../src/badge-auto.mjs";
|
|
27
28
|
import {
|
|
@@ -294,9 +295,7 @@ async function runPages() {
|
|
|
294
295
|
try {
|
|
295
296
|
targets = await (await fetch(`http://127.0.0.1:${state.port}/json/list`)).json();
|
|
296
297
|
} catch {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`The inject Chrome is not answering on :${state.port}. It was closed; run \`npm run inject\` again.`,
|
|
299
|
-
);
|
|
298
|
+
throw new Error(ownershipErrorMessage("cdp_not_ready", { port: state.port }));
|
|
300
299
|
}
|
|
301
300
|
const real = targets.filter(
|
|
302
301
|
(target) => target.type === "page" && !String(target.url).startsWith("devtools://"),
|
|
@@ -321,7 +320,7 @@ async function runPages() {
|
|
|
321
320
|
? {
|
|
322
321
|
hostsHint:
|
|
323
322
|
`The window is on ${off.host}, which this project does not cover (hosts: ${state.hosts.join(", ")}). ` +
|
|
324
|
-
`If it is part of the product, add "${off.pattern}" to "hosts" in foldspace.dev.json - with the human's yes - then run inject and attach
|
|
323
|
+
`If it is part of the product, add "${off.pattern}" to "hosts" in foldspace.dev.json - with the human's yes - then \`npx foldspace attach --stop\`, \`npm run inject\` and \`npx foldspace attach --daemon\` with the flags it ran with. If it is not, ask the human to go back to the product in the test window.`,
|
|
325
324
|
}
|
|
326
325
|
: {}),
|
|
327
326
|
};
|
|
@@ -338,7 +337,7 @@ async function runMenu(page) {
|
|
|
338
337
|
items.push(publicItem(entry));
|
|
339
338
|
}
|
|
340
339
|
const url = await page.evaluate("return location.href;");
|
|
341
|
-
// Links that leave this host but stay in the product (
|
|
340
|
+
// Links that leave this host but stay in the product (reports., admin. ...).
|
|
342
341
|
// Looked for across the whole page: product tiles are often not navigation.
|
|
343
342
|
const leaving = await page
|
|
344
343
|
.evaluate(
|
|
@@ -768,7 +767,24 @@ async function runWaitLogin(flags) {
|
|
|
768
767
|
}
|
|
769
768
|
await sleep(1500);
|
|
770
769
|
}
|
|
771
|
-
|
|
770
|
+
// A last check that failed (window closed, or on a host this project does
|
|
771
|
+
// not cover) says what to do itself; asking the human would only loop.
|
|
772
|
+
const secs = Math.round(timeoutMs / 1000);
|
|
773
|
+
throw new ObserveExit(
|
|
774
|
+
{
|
|
775
|
+
ok: false,
|
|
776
|
+
loggedIn: false,
|
|
777
|
+
error: lastError
|
|
778
|
+
? `No sign-in seen in ${secs} seconds. The last check said: ${lastError}`
|
|
779
|
+
: `No sign-in seen in ${secs} seconds: the test window still shows a sign-in page (${url}). ` +
|
|
780
|
+
"Ask the human whether they managed to sign in there. If they have not, run " +
|
|
781
|
+
"`npx foldspace observe wait-login` again while they do; if they say they are in, run " +
|
|
782
|
+
"`npx foldspace observe screenshot` and look at the page.",
|
|
783
|
+
url,
|
|
784
|
+
...(lastError ? { lastError } : {}),
|
|
785
|
+
},
|
|
786
|
+
2,
|
|
787
|
+
);
|
|
772
788
|
}
|
|
773
789
|
|
|
774
790
|
// -------------------------------------------------------------------- main
|