@foldspace_npm/harness 0.1.27 → 0.1.29
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 +301 -188
- package/README.md +54 -36
- package/bin/ask.mjs +9 -7
- package/bin/attach.mjs +80 -51
- package/bin/badge.mjs +28 -8
- package/bin/build.ts +19 -6
- package/bin/cli.mjs +23 -6
- package/bin/inject.mjs +3 -4
- package/bin/observe.mjs +22 -6
- package/bin/published.mjs +1 -1
- 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 +3 -3
- 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 +14 -19
- package/src/cdp-ownership.mjs +41 -20
- package/src/cli-help.mjs +7 -5
- package/src/cli-registry.mjs +65 -46
- package/src/diagnostics.mjs +1 -1
- package/src/init.mjs +5 -3
- package/src/lint/index.mjs +10 -1
- package/src/lint/rules.mjs +371 -35
- package/src/observe-core.mjs +163 -34
- 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 +1 -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
|
|
@@ -40,14 +40,12 @@ export function readLaunchState(projectDir = resolveProjectDir()) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async function listTargets(port) {
|
|
43
|
+
async function listTargets(port, command) {
|
|
44
44
|
let response;
|
|
45
45
|
try {
|
|
46
46
|
response = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
47
47
|
} catch {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`The inject Chrome is not answering on :${port}. It was closed; run \`npm run inject\` again.`,
|
|
50
|
-
);
|
|
48
|
+
throw new Error(ownershipErrorMessage("cdp_not_ready", { port, command }));
|
|
51
49
|
}
|
|
52
50
|
return response.json();
|
|
53
51
|
}
|
|
@@ -82,15 +80,15 @@ export function choosePage(pages, hosts = []) {
|
|
|
82
80
|
throw new Error(
|
|
83
81
|
off
|
|
84
82
|
? `The test window is on ${off.host}, which this project does not cover (hosts: ${hosts.join(", ")}). ` +
|
|
85
|
-
`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
|
|
86
|
-
: `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.`,
|
|
87
85
|
);
|
|
88
86
|
}
|
|
89
87
|
return real[0];
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
export async function pickPage(port, hosts = []) {
|
|
93
|
-
return choosePage(await listTargets(port), hosts);
|
|
90
|
+
export async function pickPage(port, hosts = [], command) {
|
|
91
|
+
return choosePage(await listTargets(port, command), hosts);
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
// One check per process: it opens a browser-level session.
|
|
@@ -101,16 +99,12 @@ let ownershipVerified = false;
|
|
|
101
99
|
* same check as `attach`. A stale port, or CDP_PORT pointing elsewhere, would
|
|
102
100
|
* otherwise have these commands reload and click in someone else's browser.
|
|
103
101
|
*/
|
|
104
|
-
export async function assertTestWindow(state, verify = assertOwnedCdp) {
|
|
102
|
+
export async function assertTestWindow(state, verify = assertOwnedCdp, command) {
|
|
105
103
|
if (ownershipVerified) return;
|
|
106
104
|
const owned = await verify({ profileDir: state.profileDir, port: state.port, token: state.sentinel });
|
|
107
105
|
if (!owned.ok) {
|
|
108
106
|
throw new Error(
|
|
109
|
-
ownershipErrorMessage(owned.reason, {
|
|
110
|
-
port: state.port,
|
|
111
|
-
profileDir: state.profileDir,
|
|
112
|
-
root: state.projectDir,
|
|
113
|
-
}),
|
|
107
|
+
ownershipErrorMessage(owned.reason, { port: state.port, command }),
|
|
114
108
|
);
|
|
115
109
|
}
|
|
116
110
|
ownershipVerified = true;
|
|
@@ -128,10 +122,11 @@ export class CdpPage {
|
|
|
128
122
|
socket.addEventListener("message", (event) => this.#dispatch(JSON.parse(event.data)));
|
|
129
123
|
}
|
|
130
124
|
|
|
131
|
-
|
|
125
|
+
// `command` names the refusing command in a closed-window message.
|
|
126
|
+
static async open({ projectDir, command } = {}) {
|
|
132
127
|
const state = readLaunchState(projectDir);
|
|
133
|
-
await assertTestWindow(state);
|
|
134
|
-
const page = await pickPage(state.port, state.hosts);
|
|
128
|
+
await assertTestWindow(state, assertOwnedCdp, command);
|
|
129
|
+
const page = await pickPage(state.port, state.hosts, command);
|
|
135
130
|
const socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
136
131
|
await new Promise((resolve, reject) => {
|
|
137
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",
|
|
@@ -349,7 +364,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
349
364
|
value("--timeout", "seconds", "Wait this long for a reply", { default: "90" }),
|
|
350
365
|
],
|
|
351
366
|
prerequisites: [
|
|
352
|
-
"foldspace attach --daemon is running
|
|
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",
|
|
353
368
|
],
|
|
354
369
|
effects: [
|
|
355
370
|
"Types into the panel the way a person does; touches nothing inside the SDK",
|
|
@@ -358,8 +373,8 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
358
373
|
"Saves panel and full-page screenshots under .foldspace-dev/ask/",
|
|
359
374
|
],
|
|
360
375
|
next: [
|
|
361
|
-
"Read reply, cards and handler.execute / handler.render, and look at the screenshot; after foldspace published
|
|
362
|
-
"Only then invite the human to try it: with attach running, foldspace badge ready first; after 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",
|
|
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",
|
|
363
378
|
],
|
|
364
379
|
}),
|
|
365
380
|
Object.freeze({
|
|
@@ -377,13 +392,13 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
377
392
|
name: "state",
|
|
378
393
|
required: true,
|
|
379
394
|
description:
|
|
380
|
-
"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",
|
|
381
396
|
}),
|
|
382
397
|
],
|
|
383
398
|
options: [
|
|
384
|
-
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)"),
|
|
385
400
|
],
|
|
386
|
-
prerequisites: ["foldspace attach is running (it draws the
|
|
401
|
+
prerequisites: ["foldspace attach is running (it draws the pill and the corner tag)"],
|
|
387
402
|
effects: [
|
|
388
403
|
"Writes one key to the test window's sessionStorage; touches nothing else on the page",
|
|
389
404
|
"The pill survives navigation inside the test window",
|
|
@@ -467,7 +482,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
467
482
|
|
|
468
483
|
export const CAPABILITY_CATALOGUE = Object.freeze([
|
|
469
484
|
["project.scaffold", "Create a new configured project on local disk"],
|
|
470
|
-
["project.upgrade", "Update the consuming project
|
|
485
|
+
["project.upgrade", "Update the harness version the consuming project uses"],
|
|
471
486
|
["artifact.build", "Build the portable dist/index.js action artifact"],
|
|
472
487
|
["browser.launch", "Launch an isolated local Chrome profile"],
|
|
473
488
|
["browser.cdp", "Connect to local Chrome through CDP"],
|
|
@@ -506,6 +521,7 @@ export function commandByName(name) {
|
|
|
506
521
|
export function createCliRegistry({ packageName, packageVersion, env = process.env }) {
|
|
507
522
|
const showInternal = env.FOLDSPACE_INTERNAL === "1";
|
|
508
523
|
const visible = CLI_COMMANDS.filter((command) => showInternal || !command.internal);
|
|
524
|
+
const used = (key) => new Set(visible.flatMap((command) => [].concat(command[key] ?? [])));
|
|
509
525
|
const diagnostics = diagnosticCatalogue()
|
|
510
526
|
.filter((diagnostic) => PUBLIC_DIAGNOSTICS.has(diagnostic.name))
|
|
511
527
|
.map((diagnostic) => ({
|
|
@@ -524,27 +540,30 @@ export function createCliRegistry({ packageName, packageVersion, env = process.e
|
|
|
524
540
|
workflow: ["init", "build", "inject", "attach"],
|
|
525
541
|
verifyWorkflow: ["observe", "ask"],
|
|
526
542
|
publishWorkflow: showInternal ? ["deploy"] : [],
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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]) => ({
|
|
533
549
|
id,
|
|
534
550
|
description,
|
|
535
551
|
})),
|
|
552
|
+
environments: ENVIRONMENT_CATALOGUE.filter(([id]) => used("environment").has(id)).map(
|
|
553
|
+
([id, description]) => ({ id, description }),
|
|
554
|
+
),
|
|
536
555
|
commands: visible.map(({ entry: _entry, internal: _internal, ...command }) => command),
|
|
537
556
|
diagnostics,
|
|
538
557
|
notes: [
|
|
539
558
|
"Capability metadata describes current behavior; it is not hosted-environment enforcement.",
|
|
540
|
-
"
|
|
559
|
+
"Actions are verified only through the agent: `npx foldspace ask` asks it through its panel; nothing calls a handler directly.",
|
|
541
560
|
"The harness bin alias is equivalent to foldspace.",
|
|
542
561
|
"attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
|
|
543
|
-
"Coding agents
|
|
544
|
-
"If update.outdated is true, ask the
|
|
545
|
-
"If update.broken is true, this version was withdrawn and build and attach refuse to run. Ask the
|
|
546
|
-
"observe
|
|
547
|
-
"
|
|
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).",
|
|
548
567
|
],
|
|
549
568
|
};
|
|
550
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 = {}) {
|
package/src/init.mjs
CHANGED
|
@@ -137,7 +137,7 @@ function validateIdentifier(value, label) {
|
|
|
137
137
|
function validateProductId(value) {
|
|
138
138
|
const productId = validateIdentifier(value, "Product ID");
|
|
139
139
|
if (/^EU-/i.test(productId)) {
|
|
140
|
-
throw new Error("
|
|
140
|
+
throw new Error("Use the bare product ID, not the SDK key (the one that starts with EU-, in every region).");
|
|
141
141
|
}
|
|
142
142
|
return productId;
|
|
143
143
|
}
|
|
@@ -504,9 +504,11 @@ export async function runInit(argv, options = {}) {
|
|
|
504
504
|
if (!installed) {
|
|
505
505
|
log(" npm install --ignore-scripts");
|
|
506
506
|
}
|
|
507
|
+
log(" # before the first build, CLAUDE.md step 5: a code project's agent/ replaces the scaffold's");
|
|
507
508
|
log(" npm run build");
|
|
508
|
-
log(" npm run inject");
|
|
509
|
-
log("
|
|
509
|
+
log(" npm run inject # the human signs in in that window");
|
|
510
|
+
log(" npx foldspace observe wait-login");
|
|
511
|
+
log(" npx foldspace attach --daemon");
|
|
510
512
|
|
|
511
513
|
return { ...result, installed };
|
|
512
514
|
}
|
package/src/lint/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { listSourceFiles, parseFile, shouldSkipActionFile } from "./ast.mjs";
|
|
4
|
-
import { lintActionFile, lintViewsFile, ruleRegistryIntegrity } from "./rules.mjs";
|
|
4
|
+
import { lintActionFile, lintViewsFile, ruleNoTestModeInBundle, ruleRegistryIntegrity } from "./rules.mjs";
|
|
5
|
+
|
|
6
|
+
export { bundleTurnsOnTestMode } from "./rules.mjs";
|
|
5
7
|
|
|
6
8
|
export const LINT_SCHEMA_VERSION = 1;
|
|
7
9
|
|
|
@@ -44,6 +46,13 @@ export function lintProject(projectDir) {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// Any file under agent/ can end up in the bundle, including code that runs
|
|
50
|
+
// as soon as the bundle loads, so this rule reads all of it - not only
|
|
51
|
+
// actions and views.
|
|
52
|
+
for (const filePath of listSourceFiles(path.join(projectDir, "agent"))) {
|
|
53
|
+
ruleNoTestModeInBundle(parseFile(filePath), path.relative(projectDir, filePath), findings);
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
ruleRegistryIntegrity({
|
|
48
57
|
indexFile: entry,
|
|
49
58
|
indexSource: parseFile(entry),
|