@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/src/attach-preflight.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export function guardAttachMode({
|
|
|
44
44
|
// navigates within the app the SDK on the page is OUR bootstrap, carrying
|
|
45
45
|
// the configured agent. That is the expected state, not a foreign SDK:
|
|
46
46
|
// refusing it made the agent vanish on every in-app navigation
|
|
47
|
-
// (
|
|
47
|
+
// (a customer build, 2026-09-22). Only a page whose SDK belongs to someone else
|
|
48
48
|
// is a real mismatch.
|
|
49
49
|
if (characterization.status === "same-agent") {
|
|
50
50
|
return { ok: true, mode, reattached: true };
|
|
@@ -78,8 +78,7 @@ export function guardAttachMode({
|
|
|
78
78
|
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
79
79
|
message:
|
|
80
80
|
"Swap mode requires exactly the configured agent on the page.",
|
|
81
|
-
suggestion:
|
|
82
|
-
"Use --replace when the page embeds a different agent, or close duplicate agent instances.",
|
|
81
|
+
suggestion: "Use --replace.",
|
|
83
82
|
};
|
|
84
83
|
}
|
|
85
84
|
if (pageProductIds.length === 0) {
|
|
@@ -89,8 +88,7 @@ export function guardAttachMode({
|
|
|
89
88
|
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
90
89
|
message:
|
|
91
90
|
"Swap mode could not verify which product owns the page agent.",
|
|
92
|
-
suggestion:
|
|
93
|
-
"Use --replace, or update the page SDK so its tracker key is inspectable.",
|
|
91
|
+
suggestion: "Use --replace.",
|
|
94
92
|
};
|
|
95
93
|
}
|
|
96
94
|
if (productMismatch) {
|
|
@@ -99,7 +97,7 @@ export function guardAttachMode({
|
|
|
99
97
|
mode,
|
|
100
98
|
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
101
99
|
message:
|
|
102
|
-
"The page agent
|
|
100
|
+
"The page's agent has this project's Agent Key but belongs to another product.",
|
|
103
101
|
suggestion: "Use --replace for a cross-product development session.",
|
|
104
102
|
};
|
|
105
103
|
}
|
|
@@ -184,6 +182,7 @@ export function buildReplacePrelude({
|
|
|
184
182
|
mode = "OVERLAY",
|
|
185
183
|
namespace = "foldspace",
|
|
186
184
|
aliases = [],
|
|
185
|
+
testMode = false,
|
|
187
186
|
}) {
|
|
188
187
|
const source = `(() => {
|
|
189
188
|
if (window.top !== window.self) return;
|
|
@@ -248,6 +247,20 @@ export function buildReplacePrelude({
|
|
|
248
247
|
configuration: { enableDebugLogs: true },
|
|
249
248
|
});
|
|
250
249
|
window.__FOLDSPACE_AGENT__ = target;
|
|
250
|
+
// Open the chat panel when the agent is first put on the page. The
|
|
251
|
+
// SDK closes the panel as the agent becomes ready, which undoes an
|
|
252
|
+
// open() sent before then, so open() runs once more just after
|
|
253
|
+
// agent.ready. Nothing else here opens it, so a panel the human
|
|
254
|
+
// closes stays closed until the page loads again.
|
|
255
|
+
try { target?.open?.(); } catch {}
|
|
256
|
+
let reopened = false;
|
|
257
|
+
try {
|
|
258
|
+
target?.on?.("agent.ready", () => {
|
|
259
|
+
if (reopened) return;
|
|
260
|
+
reopened = true;
|
|
261
|
+
setTimeout(() => { try { target.open?.(); } catch {} }, 0);
|
|
262
|
+
});
|
|
263
|
+
} catch {}
|
|
251
264
|
}
|
|
252
265
|
if (!target) return null;
|
|
253
266
|
|
|
@@ -265,12 +278,22 @@ export function buildReplacePrelude({
|
|
|
265
278
|
}
|
|
266
279
|
try { if (localActions && rawAddHandlers) rawAddHandlers(localActions); }
|
|
267
280
|
catch {}
|
|
268
|
-
|
|
281
|
+
// Test mode follows the attach: on only with --test-mode, off otherwise.
|
|
282
|
+
// Each pass of ensureTarget (at start, on every foldspace.agent() call
|
|
283
|
+
// that names an agent, and every second) sets that value again, so the
|
|
284
|
+
// chat panel shows test mode exactly when the corner tag says TEST MODE.
|
|
285
|
+
try { target.setTestMode?.(${testMode === true}); } catch {}
|
|
286
|
+
// show() on every pass brings the agent back if something hid it with
|
|
287
|
+
// hide().
|
|
269
288
|
try { target.show?.(); } catch {}
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
289
|
+
// In overlay mode, something on the page can set display:none on the
|
|
290
|
+
// agent's view - the SDK does it itself while its bottom bar is
|
|
291
|
+
// showing. Replace mode overrides any display:none on that view on
|
|
292
|
+
// every pass, for the life of this page. In other modes it leaves the
|
|
293
|
+
// view alone.
|
|
294
|
+
// ⚠️ That includes the bottom bar's display:none: with the bar on,
|
|
295
|
+
// closing the chat panel brings the bar back, and this may put the
|
|
296
|
+
// agent's view back beside it. Not checked live yet.
|
|
274
297
|
if (MODE === "OVERLAY") {
|
|
275
298
|
try {
|
|
276
299
|
const widget = window.document?.querySelector?.("#eucera-agent-view");
|
package/src/badge-core.mjs
CHANGED
|
@@ -11,7 +11,7 @@ export function badgePayload(state, text) {
|
|
|
11
11
|
const payload = { state: wanted };
|
|
12
12
|
if (text !== undefined) {
|
|
13
13
|
if (wanted !== "working") {
|
|
14
|
-
throw new Error(`--text only applies to working (got ${wanted});
|
|
14
|
+
throw new Error(`--text only applies to working (got ${wanted}); ready, label and off draw no pill.`);
|
|
15
15
|
}
|
|
16
16
|
const clean = String(text).replace(/\s+/g, " ").trim();
|
|
17
17
|
if (!clean) throw new Error("--text is empty.");
|
package/src/cdp-client.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// A small CDP page client for
|
|
1
|
+
// A small CDP page client for `observe`, `ask`, `badge` and `published`.
|
|
2
2
|
//
|
|
3
|
-
// It is a second client on the
|
|
3
|
+
// It is a second client on the test window and is safe to use while
|
|
4
4
|
// `foldspace attach` is running: attach's claim on the debug port is about
|
|
5
5
|
// request interception (`Fetch.enable`) and navigation, and nothing here
|
|
6
6
|
// enables Fetch. `Runtime.evaluate`, `Network.enable` and
|
|
@@ -33,20 +33,19 @@ export function readLaunchState(projectDir = resolveProjectDir()) {
|
|
|
33
33
|
projectDir,
|
|
34
34
|
port: String(port),
|
|
35
35
|
hosts,
|
|
36
|
+
targetName: state.target || null,
|
|
36
37
|
target: state.resolvedTarget || {},
|
|
37
38
|
sentinel: state.sentinel,
|
|
38
39
|
profileDir: chromeProfileDir(projectDir),
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
async function listTargets(port) {
|
|
43
|
+
async function listTargets(port, command) {
|
|
43
44
|
let response;
|
|
44
45
|
try {
|
|
45
46
|
response = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
46
47
|
} catch {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`The inject Chrome is not answering on :${port}. It was closed; run \`npm run inject\` again.`,
|
|
49
|
-
);
|
|
48
|
+
throw new Error(ownershipErrorMessage("cdp_not_ready", { port, command }));
|
|
50
49
|
}
|
|
51
50
|
return response.json();
|
|
52
51
|
}
|
|
@@ -81,15 +80,15 @@ export function choosePage(pages, hosts = []) {
|
|
|
81
80
|
throw new Error(
|
|
82
81
|
off
|
|
83
82
|
? `The test window is on ${off.host}, which this project does not cover (hosts: ${hosts.join(", ")}). ` +
|
|
84
|
-
`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
|
|
85
|
-
: `The test window has no tab on ${hosts.join(", ")}.
|
|
83
|
+
`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.`
|
|
84
|
+
: `The test window has no tab on ${hosts.join(", ")}. Ask the human to open the product there (or finish signing in), then run the command again.`,
|
|
86
85
|
);
|
|
87
86
|
}
|
|
88
87
|
return real[0];
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
export async function pickPage(port, hosts = []) {
|
|
92
|
-
return choosePage(await listTargets(port), hosts);
|
|
90
|
+
export async function pickPage(port, hosts = [], command) {
|
|
91
|
+
return choosePage(await listTargets(port, command), hosts);
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
// One check per process: it opens a browser-level session.
|
|
@@ -100,16 +99,12 @@ let ownershipVerified = false;
|
|
|
100
99
|
* same check as `attach`. A stale port, or CDP_PORT pointing elsewhere, would
|
|
101
100
|
* otherwise have these commands reload and click in someone else's browser.
|
|
102
101
|
*/
|
|
103
|
-
export async function assertTestWindow(state, verify = assertOwnedCdp) {
|
|
102
|
+
export async function assertTestWindow(state, verify = assertOwnedCdp, command) {
|
|
104
103
|
if (ownershipVerified) return;
|
|
105
104
|
const owned = await verify({ profileDir: state.profileDir, port: state.port, token: state.sentinel });
|
|
106
105
|
if (!owned.ok) {
|
|
107
106
|
throw new Error(
|
|
108
|
-
ownershipErrorMessage(owned.reason, {
|
|
109
|
-
port: state.port,
|
|
110
|
-
profileDir: state.profileDir,
|
|
111
|
-
root: state.projectDir,
|
|
112
|
-
}),
|
|
107
|
+
ownershipErrorMessage(owned.reason, { port: state.port, command }),
|
|
113
108
|
);
|
|
114
109
|
}
|
|
115
110
|
ownershipVerified = true;
|
|
@@ -127,10 +122,11 @@ export class CdpPage {
|
|
|
127
122
|
socket.addEventListener("message", (event) => this.#dispatch(JSON.parse(event.data)));
|
|
128
123
|
}
|
|
129
124
|
|
|
130
|
-
|
|
125
|
+
// `command` names the refusing command in a closed-window message.
|
|
126
|
+
static async open({ projectDir, command } = {}) {
|
|
131
127
|
const state = readLaunchState(projectDir);
|
|
132
|
-
await assertTestWindow(state);
|
|
133
|
-
const page = await pickPage(state.port, state.hosts);
|
|
128
|
+
await assertTestWindow(state, assertOwnedCdp, command);
|
|
129
|
+
const page = await pickPage(state.port, state.hosts, command);
|
|
134
130
|
const socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
135
131
|
await new Promise((resolve, reject) => {
|
|
136
132
|
socket.addEventListener("open", resolve, { once: true });
|
package/src/cdp-ownership.mjs
CHANGED
|
@@ -88,26 +88,47 @@ export function verifyCdpOwnership({ profileDir, token, reported } = {}) {
|
|
|
88
88
|
return { ok: true };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Why the Chrome on `port` cannot be used, and the one next step.
|
|
93
|
+
*
|
|
94
|
+
* `command` is the command that is refusing. Only `inject` launches Chrome and
|
|
95
|
+
* takes `--port`; attach, observe, ask, badge and published use the port inject
|
|
96
|
+
* recorded, so for them a free-port hint is wrong (observe, ask, badge and
|
|
97
|
+
* published refuse `--port`), and a closed test window is the usual cause.
|
|
98
|
+
* `published` runs with attach stopped on purpose, so its closed window ends in
|
|
99
|
+
* running it again, never in attaching.
|
|
100
|
+
*/
|
|
101
|
+
export function ownershipErrorMessage(reason, { port, command } = {}) {
|
|
102
|
+
if (command === "inject") {
|
|
103
|
+
const inject = {
|
|
104
|
+
profile_mismatch: `Another Chrome holds :${port}. Run \`npm run inject -- --port <a free port>\`.`,
|
|
105
|
+
cdp_inspect_failed:
|
|
106
|
+
`Chrome on :${port} did not say which profile it runs, so it cannot be proven to be the test window. ` +
|
|
107
|
+
`Run \`npm run inject -- --port <a free port>\`.`,
|
|
108
|
+
cdp_not_ready:
|
|
109
|
+
`Chrome did not answer on :${port} after launch. Close the test window if it is open (it may be on another port), ` +
|
|
110
|
+
`then run \`npm run inject\` again.`,
|
|
111
|
+
sentinel_mismatch:
|
|
112
|
+
"Another inject started while this one was launching. Run `npm run inject` again.",
|
|
113
|
+
};
|
|
114
|
+
return inject[reason] || `${reason} on :${port}.`;
|
|
115
|
+
}
|
|
116
|
+
if (reason === "cdp_not_ready") {
|
|
117
|
+
const then =
|
|
118
|
+
command === "published"
|
|
119
|
+
? "then run the same `npx foldspace published` command again - attach stays stopped."
|
|
120
|
+
: "then `npx foldspace attach --daemon` with the flags it ran with.";
|
|
121
|
+
return (
|
|
122
|
+
`The test window is closed: nothing answers on :${port}. ` +
|
|
123
|
+
"Run `npm run inject` to reopen it, then `npx foldspace observe wait-login` " +
|
|
124
|
+
`(it returns within seconds if the site kept the sign-in), ${then}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const again = "Run `npm run inject` again; attach, observe, ask, badge and published use the port it records.";
|
|
128
|
+
if (reason === "cdp_inspect_failed") {
|
|
129
|
+
return `Chrome on :${port} did not say which profile it runs, so it cannot be proven to be the test window. ${again}`;
|
|
130
|
+
}
|
|
131
|
+
return `Chrome on :${port} is not the test window this project opened. ${again}`;
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
export async function fetchCdpVersion(port) {
|
package/src/cli-help.mjs
CHANGED
|
@@ -20,9 +20,11 @@ export function renderGeneralHelp(registry) {
|
|
|
20
20
|
"",
|
|
21
21
|
"Typical local workflow:",
|
|
22
22
|
" foldspace init <directory>",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
23
|
+
" # before the first build, CLAUDE.md step 5: a code project's agent/ replaces the scaffold's",
|
|
24
|
+
" npm run build",
|
|
25
|
+
" npm run inject # the human signs in in that window",
|
|
26
|
+
" npx foldspace observe wait-login",
|
|
27
|
+
" npx foldspace attach --daemon",
|
|
26
28
|
];
|
|
27
29
|
|
|
28
30
|
for (const [group, title] of GROUPS) {
|
|
@@ -46,10 +48,10 @@ export function renderGeneralHelp(registry) {
|
|
|
46
48
|
" foldspace help --json Print the machine-readable CLI contract",
|
|
47
49
|
" foldspace help <command> --json Print one command contract",
|
|
48
50
|
" foldspace --version Show package and protocol versions",
|
|
49
|
-
" foldspace upgrade --check Compare the
|
|
51
|
+
" foldspace upgrade --check Compare the version this project uses to npm latest",
|
|
50
52
|
"",
|
|
51
53
|
"attach loads local actions and observes the normal agent experience.",
|
|
52
|
-
"observe, ask
|
|
54
|
+
"observe looks at the app, and ask tests the agent through its panel; both work while attach runs.",
|
|
53
55
|
...(registry.publishWorkflow.length ? ["deploy is a separate remote publication step."] : []),
|
|
54
56
|
);
|
|
55
57
|
return sections.join("\n");
|
package/src/cli-registry.mjs
CHANGED
|
@@ -64,17 +64,19 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
64
64
|
],
|
|
65
65
|
effects: ["Creates a new local project; never initializes Git"],
|
|
66
66
|
next: [
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
67
|
+
"npm install --ignore-scripts",
|
|
68
|
+
"Before the first build, CLAUDE.md step 5 (\"Every session starts the same way\"): with a code project, its agent/ replaces the scaffold's",
|
|
69
|
+
"npm run build",
|
|
70
|
+
"npm run inject - the human signs in in that window",
|
|
71
|
+
"npx foldspace observe wait-login",
|
|
72
|
+
"npx foldspace attach --daemon",
|
|
71
73
|
],
|
|
72
74
|
}),
|
|
73
75
|
Object.freeze({
|
|
74
76
|
name: "upgrade",
|
|
75
77
|
entry: null,
|
|
76
78
|
group: "start",
|
|
77
|
-
summary: "Check for a newer harness and update the
|
|
79
|
+
summary: "Check for a newer harness and, after asking, update the version this project uses",
|
|
78
80
|
usage:
|
|
79
81
|
"foldspace upgrade [--check | --yes] [--refresh-instructions]",
|
|
80
82
|
risk: "local-write",
|
|
@@ -89,7 +91,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
89
91
|
flag("--check", "Compare versions and print JSON; never install"),
|
|
90
92
|
flag(
|
|
91
93
|
"--yes",
|
|
92
|
-
"Install the latest
|
|
94
|
+
"Install the latest version after the human has agreed",
|
|
93
95
|
),
|
|
94
96
|
flag(
|
|
95
97
|
"--refresh-instructions",
|
|
@@ -102,14 +104,14 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
102
104
|
],
|
|
103
105
|
effects: [
|
|
104
106
|
"Without --yes, writes nothing except an optional update cache",
|
|
105
|
-
"With --yes,
|
|
106
|
-
"Does not rebuild dist/index.js or
|
|
107
|
+
"With --yes, writes the latest version into package.json (exact) and runs npm install --ignore-scripts",
|
|
108
|
+
"Does not rebuild dist/index.js or publish anything",
|
|
107
109
|
"--refresh-instructions copies CLAUDE.md to CLAUDE.md.bak then writes the stub",
|
|
108
110
|
],
|
|
109
111
|
next: [
|
|
110
|
-
"If outdated, ask the
|
|
111
|
-
"Run npm run build after
|
|
112
|
-
"
|
|
112
|
+
"If outdated, ask the human, then run npx foldspace upgrade --yes",
|
|
113
|
+
"Run npm run build after an upgrade",
|
|
114
|
+
"If attach is running: `npx foldspace attach --stop`, then `npx foldspace attach --daemon` with the flags it ran with, to load the rebuild",
|
|
113
115
|
],
|
|
114
116
|
}),
|
|
115
117
|
Object.freeze({
|
|
@@ -130,13 +132,16 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
130
132
|
"Runs foldspace lint first; errors skip bundling",
|
|
131
133
|
"Writes dist/index.js when lint has no errors",
|
|
132
134
|
],
|
|
133
|
-
next: [
|
|
135
|
+
next: [
|
|
136
|
+
"First build: `npm run inject` (the human signs in in that window), `npx foldspace observe wait-login`, then `npx foldspace attach --daemon`",
|
|
137
|
+
"Rebuild while attach runs: `npx foldspace attach --stop`, then `npx foldspace attach --daemon` with the flags it ran with (`--bootstrap`, `--replace`, `--test-mode`) - a running attach keeps the actions it loaded",
|
|
138
|
+
],
|
|
134
139
|
}),
|
|
135
140
|
Object.freeze({
|
|
136
141
|
name: "lint",
|
|
137
142
|
entry: "lint.mjs",
|
|
138
143
|
group: "develop",
|
|
139
|
-
summary: "Check
|
|
144
|
+
summary: "Check the code under agent/ for prompt, card, registry, copy and test-mode issues",
|
|
140
145
|
usage: "foldspace lint [--json]",
|
|
141
146
|
risk: "local-write",
|
|
142
147
|
environment: "node",
|
|
@@ -148,7 +153,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
148
153
|
],
|
|
149
154
|
prerequisites: ["agent/actions/index.ts"],
|
|
150
155
|
effects: [
|
|
151
|
-
"Reads
|
|
156
|
+
"Reads the code under agent/; writes nothing",
|
|
152
157
|
"Errors fail the process; warnings print and still exit 0",
|
|
153
158
|
],
|
|
154
159
|
next: ["Fix reported findings", "Run foldspace build"],
|
|
@@ -157,7 +162,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
157
162
|
name: "inject",
|
|
158
163
|
entry: "inject.mjs",
|
|
159
164
|
group: "verify",
|
|
160
|
-
summary: "
|
|
165
|
+
summary: "Open the test window: an isolated Chrome profile for local verification",
|
|
161
166
|
usage: "foldspace inject [options]",
|
|
162
167
|
risk: "browser-session",
|
|
163
168
|
environment: "local-chrome",
|
|
@@ -168,7 +173,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
168
173
|
value("--target", "name", "Use a target from foldspace.dev.json"),
|
|
169
174
|
value("--url", "url", "Override the target start URL"),
|
|
170
175
|
value("--product", "id", "Override the local-session product ID"),
|
|
171
|
-
value("--agent", "
|
|
176
|
+
value("--agent", "agent-key", "Use another Agent Key (as shown in Agent Studio)"),
|
|
172
177
|
value("--hosts", "a,b", "Override allowed host patterns"),
|
|
173
178
|
value(
|
|
174
179
|
"--key",
|
|
@@ -194,7 +199,10 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
194
199
|
"Otherwise creates an isolated profile and writes .foldspace-dev/state.json",
|
|
195
200
|
"Does not load the SDK or action artifact",
|
|
196
201
|
],
|
|
197
|
-
next: [
|
|
202
|
+
next: [
|
|
203
|
+
"The human signs in in that window; run `npx foldspace observe wait-login` - it returns when they are in",
|
|
204
|
+
"Run `npx foldspace attach --daemon` (L0); until an experience is chosen, observe only `screenshot` and `menu`",
|
|
205
|
+
],
|
|
198
206
|
}),
|
|
199
207
|
Object.freeze({
|
|
200
208
|
name: "attach",
|
|
@@ -203,7 +211,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
203
211
|
summary:
|
|
204
212
|
"Load local actions over CDP and observe the normal agent experience",
|
|
205
213
|
usage:
|
|
206
|
-
"foldspace attach [--bootstrap | --replace] [--daemon | --status | --stop] [--target <name>] [--port <number>] [--agent <
|
|
214
|
+
"foldspace attach [--bootstrap | --replace] [--daemon | --status | --stop] [--target <name>] [--port <number>] [--agent <agent-key>] [--test-mode] [--no-badge]",
|
|
207
215
|
risk: "browser-session",
|
|
208
216
|
environment: "local-chrome",
|
|
209
217
|
environmentVariables: [
|
|
@@ -249,13 +257,13 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
249
257
|
"number",
|
|
250
258
|
"CDP port: CLI, CDP_PORT, or the port inject recorded",
|
|
251
259
|
),
|
|
252
|
-
value("--agent", "
|
|
260
|
+
value("--agent", "agent-key", "Use another Agent Key (as shown in Agent Studio)"),
|
|
253
261
|
flag(
|
|
254
262
|
"--test-mode",
|
|
255
263
|
"Mark this session's conversations as test traffic, so they stay out of the dashboard; for an agent that already has real users",
|
|
256
264
|
),
|
|
257
|
-
flag("--no-test-mode", "
|
|
258
|
-
flag("--no-badge", "
|
|
265
|
+
flag("--no-test-mode", "Has no effect: test mode is off unless --test-mode is passed"),
|
|
266
|
+
flag("--no-badge", "Draw no corner tag and no working pill (for a recording)"),
|
|
259
267
|
flag(
|
|
260
268
|
"--daemon",
|
|
261
269
|
"Start attach in the background and return after inspect_registration",
|
|
@@ -280,12 +288,12 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
280
288
|
"--daemon returns after inspect_registration and keeps CDP in a child process",
|
|
281
289
|
],
|
|
282
290
|
next: [
|
|
283
|
-
"Coding agents
|
|
284
|
-
"Exercise the visible agent normally",
|
|
291
|
+
"Coding agents run `npx foldspace attach --daemon`, so the command returns",
|
|
285
292
|
"Confirm [lifecycle] inspect_registration:registration_ok in the terminal or daemon log",
|
|
286
|
-
"On
|
|
293
|
+
"On registration_mismatch, read the lines printed under it: what differs, and the next step",
|
|
294
|
+
"Prove a published action with `npx foldspace ask \"<question>\" --retry 2`",
|
|
287
295
|
"After a named action, read [actions] execute/render lines from the attach log",
|
|
288
|
-
"
|
|
296
|
+
"Keep attach running; `attach --stop` only to load rebuilt actions or changed hosts, or to check published code on dev (manual: \"Done, and what you say at the end\")",
|
|
289
297
|
],
|
|
290
298
|
}),
|
|
291
299
|
Object.freeze({
|
|
@@ -326,9 +334,16 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
326
334
|
"GET only. Never submits a form; clicks navigation elements only",
|
|
327
335
|
"Prints names, shapes and counts - never values, tokens or cookies",
|
|
328
336
|
"Works while foldspace attach is running",
|
|
329
|
-
"Raises the working pill on the window while it runs (not for screenshot), and drops it after",
|
|
337
|
+
"Raises the working pill on the window while it runs (not for pages, screenshot or wait-login), and drops it after",
|
|
338
|
+
],
|
|
339
|
+
next: [
|
|
340
|
+
"wait-login: run `npx foldspace attach --daemon`",
|
|
341
|
+
"screenshot: send the image to the human",
|
|
342
|
+
"menu: offer experiences from what it shows",
|
|
343
|
+
"screen, read, auth: record what you established in docs/app-profile.md, then write the handler",
|
|
344
|
+
"pages: if it returns hostsHint, follow it; otherwise run the subcommand the experience needs",
|
|
345
|
+
"styles: run it with --write, then record in docs/app-profile.md the page and date it sampled and anything it fell back on; cards import agent/views/brand.ts",
|
|
330
346
|
],
|
|
331
|
-
next: ["Record what you established in docs/app-profile.md", "Write the handler"],
|
|
332
347
|
}),
|
|
333
348
|
Object.freeze({
|
|
334
349
|
name: "ask",
|
|
@@ -344,19 +359,22 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
344
359
|
Object.freeze({ name: "question", required: true, description: "What an end user would type" }),
|
|
345
360
|
],
|
|
346
361
|
options: [
|
|
347
|
-
value("--retry", "n", "Ask again when no action fired (a newly published action can take a minute to be picked up)", { default: "0" }),
|
|
362
|
+
value("--retry", "n", "Ask again when no action fired - without attach, whenever no card came back, even if an action without a card ran (a newly published action can take a minute to be picked up)", { default: "0" }),
|
|
348
363
|
value("--every", "seconds", "Pause between retries", { default: "20" }),
|
|
349
364
|
value("--timeout", "seconds", "Wait this long for a reply", { default: "90" }),
|
|
350
365
|
],
|
|
351
|
-
prerequisites: [
|
|
366
|
+
prerequisites: [
|
|
367
|
+
"To test this project's build, `npx foldspace attach --daemon` is running: the handlers on the page are this build, and execute/render are read from its log. After `npx foldspace published`, the page runs the published code instead: execute/render read \"not tracked here (published code)\", and the reply, the card text and the screenshot are the proof",
|
|
368
|
+
],
|
|
352
369
|
effects: [
|
|
353
370
|
"Types into the panel the way a person does; touches nothing inside the SDK",
|
|
371
|
+
"handler.execute / handler.render come from the attach daemon's log; without the daemon they read \"not tracked here (published code)\" and the reply, cards and screenshot still come back",
|
|
354
372
|
"Creates a real conversation: it appears in the customer's Foldspace dashboard (test traffic only under attach --test-mode)",
|
|
355
373
|
"Saves panel and full-page screenshots under .foldspace-dev/ask/",
|
|
356
374
|
],
|
|
357
375
|
next: [
|
|
358
|
-
"Read reply, cards and handler.execute / handler.render
|
|
359
|
-
"Only then: foldspace badge ready
|
|
376
|
+
"Read reply, cards and handler.execute / handler.render, and look at the screenshot; after `npx foldspace published`, the reply, cards and screenshot are the proof",
|
|
377
|
+
"Only then invite the human to try it: with attach running, `npx foldspace badge ready` first; after `npx foldspace published` there is no pill - say in chat it is ready (a reload takes it off the published code), then `npx foldspace attach --daemon` before you build anything else",
|
|
360
378
|
],
|
|
361
379
|
}),
|
|
362
380
|
Object.freeze({
|
|
@@ -374,19 +392,52 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
374
392
|
name: "state",
|
|
375
393
|
required: true,
|
|
376
394
|
description:
|
|
377
|
-
"working:
|
|
395
|
+
"working: you are working in the window; the human leaves it alone | ready: the pill goes away, their turn | label: the small corner tag | off: draw nothing - no pill, no corner tag",
|
|
378
396
|
}),
|
|
379
397
|
],
|
|
380
398
|
options: [
|
|
381
|
-
value("--text", "text", "working only:
|
|
399
|
+
value("--text", "text", "working only: a short sentence added at the end of the pill (up to 160 characters)"),
|
|
382
400
|
],
|
|
383
|
-
prerequisites: ["foldspace attach is running (it draws the
|
|
401
|
+
prerequisites: ["foldspace attach is running (it draws the pill and the corner tag)"],
|
|
384
402
|
effects: [
|
|
385
403
|
"Writes one key to the test window's sessionStorage; touches nothing else on the page",
|
|
386
404
|
"The pill survives navigation inside the test window",
|
|
387
405
|
],
|
|
388
406
|
next: ["Say in chat whose turn it is and what to try"],
|
|
389
407
|
}),
|
|
408
|
+
Object.freeze({
|
|
409
|
+
name: "published",
|
|
410
|
+
entry: "published.mjs",
|
|
411
|
+
group: "verify",
|
|
412
|
+
summary: "Point the test window's agent at its published dev or prod code, so foldspace ask checks that code",
|
|
413
|
+
usage: "foldspace published <dev|prod>",
|
|
414
|
+
risk: "browser-session",
|
|
415
|
+
environment: "local-chrome",
|
|
416
|
+
environmentVariables: ["FOLDSPACE_PROJECT_DIR", "CDP_PORT"],
|
|
417
|
+
capabilities: ["browser.cdp", "page.evaluate", "page.mutate"],
|
|
418
|
+
positionals: [
|
|
419
|
+
Object.freeze({
|
|
420
|
+
name: "environment",
|
|
421
|
+
required: true,
|
|
422
|
+
description:
|
|
423
|
+
"dev: the code the `deploy` tool published to dev | prod: the code their users get",
|
|
424
|
+
}),
|
|
425
|
+
],
|
|
426
|
+
options: [],
|
|
427
|
+
prerequisites: [
|
|
428
|
+
"attach is not running: npx foldspace attach --stop (while it runs the page serves your local build)",
|
|
429
|
+
"The page loads this project's agent by itself: the same product and Agent Key as foldspace.dev.json",
|
|
430
|
+
],
|
|
431
|
+
effects: [
|
|
432
|
+
"Points this page's agent, in this test window, at the code published to the environment you name; the actions that code defines answer from it, the others keep the page's own handlers",
|
|
433
|
+
"A reload puts the page back on what the agent loads normally",
|
|
434
|
+
"Publishes nothing and changes nothing in Foldspace",
|
|
435
|
+
],
|
|
436
|
+
next: [
|
|
437
|
+
'Prove it: npx foldspace ask "<question>" --retry 2',
|
|
438
|
+
"To go back to the local build: npx foldspace attach --daemon",
|
|
439
|
+
],
|
|
440
|
+
}),
|
|
390
441
|
Object.freeze({
|
|
391
442
|
name: "deploy",
|
|
392
443
|
// Foldspace-internal: it uploads to Foldspace's own storage, which no
|
|
@@ -431,7 +482,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
431
482
|
|
|
432
483
|
export const CAPABILITY_CATALOGUE = Object.freeze([
|
|
433
484
|
["project.scaffold", "Create a new configured project on local disk"],
|
|
434
|
-
["project.upgrade", "Update the consuming project
|
|
485
|
+
["project.upgrade", "Update the harness version the consuming project uses"],
|
|
435
486
|
["artifact.build", "Build the portable dist/index.js action artifact"],
|
|
436
487
|
["browser.launch", "Launch an isolated local Chrome profile"],
|
|
437
488
|
["browser.cdp", "Connect to local Chrome through CDP"],
|
|
@@ -470,6 +521,7 @@ export function commandByName(name) {
|
|
|
470
521
|
export function createCliRegistry({ packageName, packageVersion, env = process.env }) {
|
|
471
522
|
const showInternal = env.FOLDSPACE_INTERNAL === "1";
|
|
472
523
|
const visible = CLI_COMMANDS.filter((command) => showInternal || !command.internal);
|
|
524
|
+
const used = (key) => new Set(visible.flatMap((command) => [].concat(command[key] ?? [])));
|
|
473
525
|
const diagnostics = diagnosticCatalogue()
|
|
474
526
|
.filter((diagnostic) => PUBLIC_DIAGNOSTICS.has(diagnostic.name))
|
|
475
527
|
.map((diagnostic) => ({
|
|
@@ -488,27 +540,30 @@ export function createCliRegistry({ packageName, packageVersion, env = process.e
|
|
|
488
540
|
workflow: ["init", "build", "inject", "attach"],
|
|
489
541
|
verifyWorkflow: ["observe", "ask"],
|
|
490
542
|
publishWorkflow: showInternal ? ["deploy"] : [],
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
543
|
+
// Only what a listed command uses: a hidden command's publish capability,
|
|
544
|
+
// risk or cloud environment would advertise a path nobody in a build takes.
|
|
545
|
+
capabilities: CAPABILITY_CATALOGUE.filter(([id]) => used("capabilities").has(id)).map(
|
|
546
|
+
([id, description]) => ({ id, description }),
|
|
547
|
+
),
|
|
548
|
+
risks: RISK_CATALOGUE.filter(([id]) => used("risk").has(id)).map(([id, description]) => ({
|
|
497
549
|
id,
|
|
498
550
|
description,
|
|
499
551
|
})),
|
|
552
|
+
environments: ENVIRONMENT_CATALOGUE.filter(([id]) => used("environment").has(id)).map(
|
|
553
|
+
([id, description]) => ({ id, description }),
|
|
554
|
+
),
|
|
500
555
|
commands: visible.map(({ entry: _entry, internal: _internal, ...command }) => command),
|
|
501
556
|
diagnostics,
|
|
502
557
|
notes: [
|
|
503
558
|
"Capability metadata describes current behavior; it is not hosted-environment enforcement.",
|
|
504
|
-
"
|
|
559
|
+
"Actions are verified only through the agent: `npx foldspace ask` asks it through its panel; nothing calls a handler directly.",
|
|
505
560
|
"The harness bin alias is equivalent to foldspace.",
|
|
506
561
|
"attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
|
|
507
|
-
"Coding agents
|
|
508
|
-
"If update.outdated is true, ask the
|
|
509
|
-
"If update.broken is true, this version was withdrawn and build and attach refuse to run. Ask the
|
|
510
|
-
"observe
|
|
511
|
-
"
|
|
562
|
+
"Coding agents run attach --daemon; foreground attach is for someone watching the terminal.",
|
|
563
|
+
"If update.outdated is true, ask the human before npx foldspace upgrade --yes.",
|
|
564
|
+
"If update.broken is true, this version was withdrawn and build and attach refuse to run. Ask the human to agree to the upgrade; set FOLDSPACE_SKIP_UPDATE_CHECK=1 only if they decline.",
|
|
565
|
+
"observe looks at the app, and ask tests the agent through its panel; both work while attach runs.",
|
|
566
|
+
"`foldspace deploy` is for Foldspace staff only: never run or offer it - it is not the `deploy` tool. With a code project, putting the agent's code in front of their users is yours, as the manual's \"Done, and what you say at the end\" says: save with `repo_write`; publish to dev with the `deploy` tool (say you are doing it, then do it - unless its dev preview comes back with no warnings, which means this agent loads dev for its users: then ask first, as for production); check dev with `npx foldspace attach --stop`, `npx foldspace published dev` and `npx foldspace ask \"<question>\" --retry 2`; and publish to production only once that check passed and the human says yes. The dev check and production need a site that loads the agent by itself: after `attach --bootstrap` or `--replace`, publish to dev only, keep attach running, and the rest is a step the Foldspace team does with the human. If `published` says this window cannot show dev, say so in one sentence, attach again, and stop after dev. Without a code project, or when a Foldspace tool refuses (except a save refused because someone saved after you read: redo it as `repo_read` command `instructions` says), the Foldspace team does it with the human (https://foldspace.ai/demo, support@foldspace.ai).",
|
|
512
567
|
],
|
|
513
568
|
};
|
|
514
569
|
}
|
package/src/diagnostics.mjs
CHANGED
|
@@ -193,7 +193,7 @@ export const DIAGNOSTICS = Object.freeze({
|
|
|
193
193
|
|
|
194
194
|
verifyWidgetVisible: Object.freeze({
|
|
195
195
|
description:
|
|
196
|
-
"Report whether
|
|
196
|
+
"Report whether the Foldspace agent's panel is mounted and has a visible bounding box.",
|
|
197
197
|
risk: "observe",
|
|
198
198
|
capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
|
|
199
199
|
run: function verifyWidgetVisible(args = {}) {
|