@foldspace_npm/harness 0.1.27 → 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 +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/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 +8 -1
- package/src/lint/rules.mjs +240 -43
- 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/README.md
CHANGED
|
@@ -12,12 +12,12 @@ else about how a builder runs differs between them; this does not.
|
|
|
12
12
|
|
|
13
13
|
| | **A — local** *(today)* | **B — remote** *(designed, unbuilt)* |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
|
|
|
16
|
-
| Whose browser | an isolated Chrome the harness launches | the
|
|
15
|
+
| Where the build runs | Claude Code on the human's computer | Claude Agent SDK in GCP |
|
|
16
|
+
| Whose browser | an isolated Chrome the harness launches | the human's **own** Chrome |
|
|
17
17
|
| How code reaches the page | harness CDP preparation | builder extension loads the cloud artifact |
|
|
18
18
|
| Prerequisites | Claude Code, a client repo, Node, an isolated Chrome profile | the extension installed |
|
|
19
|
-
| Session and cookies | in a profile the harness creates | the
|
|
20
|
-
| Code and history | a git repo on the
|
|
19
|
+
| Session and cookies | in a profile the harness creates | the human's own, untouched |
|
|
20
|
+
| Code and history | a git repo on the human's computer | a workspace in the hosted environment |
|
|
21
21
|
|
|
22
22
|
"Export to repo" moves a build from B to A — **and it is only a copy, not a
|
|
23
23
|
rewrite, while `agent/` is identical in both.** Keeping it identical is the
|
|
@@ -29,7 +29,7 @@ constraint this package exists to protect.
|
|
|
29
29
|
nothing more. Actions built in one track run unmodified in the other.
|
|
30
30
|
- `agent/utils.ts` — configures and re-exports `@foldspace_npm/harness/runtime`
|
|
31
31
|
- `foldspace build` — esbuild → `dist/index.js`
|
|
32
|
-
- `foldspace deploy`
|
|
32
|
+
- `foldspace deploy` (Foldspace-internal) - uploads `dist/index.js` to `agent/actions/<env>/<productId>/<agentApiName>`
|
|
33
33
|
- fixtures and tests
|
|
34
34
|
- **the verb interface** below
|
|
35
35
|
|
|
@@ -95,7 +95,7 @@ Non-interactive / CI form:
|
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
97
|
npx --yes @foldspace_npm/harness init foldspace-agent \
|
|
98
|
-
--product-id
|
|
98
|
+
--product-id <product-id> \
|
|
99
99
|
--agent-key my-agent \
|
|
100
100
|
--app-domain app.example.com \
|
|
101
101
|
--name "My Agent"
|
|
@@ -105,7 +105,7 @@ When running directly from a harness checkout during development:
|
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
107
|
node bin/cli.mjs init ../foldspace-agent \
|
|
108
|
-
--product-id
|
|
108
|
+
--product-id <product-id> \
|
|
109
109
|
--agent-key my-agent \
|
|
110
110
|
--app-domain app.example.com
|
|
111
111
|
```
|
|
@@ -115,8 +115,9 @@ not the sidecar directory name. `--agent-api-name` remains a deprecated alias
|
|
|
115
115
|
for `--agent-key`. `--domain` remains a deprecated alias for `--app-domain`.
|
|
116
116
|
`--name` is optional and defaults to the target directory
|
|
117
117
|
name (`foldspace-agent` unless you pass a directory). The product ID must be
|
|
118
|
-
the bare ID, not the
|
|
119
|
-
or an HTTP(S) URL without a port or
|
|
118
|
+
the bare ID, not the SDK key (the one that starts with `EU-`, in every
|
|
119
|
+
region). The app domain may be a hostname or an HTTP(S) URL without a port or
|
|
120
|
+
path.
|
|
120
121
|
|
|
121
122
|
`--region us|eu` (default `us`) is the Foldspace region the account lives in.
|
|
122
123
|
It is written to `foldspace.dev.json` as `region`, and picks the SDK the
|
|
@@ -131,9 +132,11 @@ initialize Git or overwrite files. On a TTY it can offer
|
|
|
131
132
|
```bash
|
|
132
133
|
cd foldspace-agent
|
|
133
134
|
npm install --ignore-scripts # if you skipped the install prompt
|
|
135
|
+
# before the first build, CLAUDE.md step 5: a code project's agent/ replaces the scaffold's
|
|
134
136
|
npm run build
|
|
135
|
-
npm run inject
|
|
136
|
-
|
|
137
|
+
npm run inject # the human signs in in that window
|
|
138
|
+
npx foldspace observe wait-login
|
|
139
|
+
npx foldspace attach --daemon
|
|
137
140
|
```
|
|
138
141
|
|
|
139
142
|
The generated npm scripts intentionally remain the normal project interface;
|
|
@@ -146,21 +149,21 @@ to that import with `foldspace upgrade --refresh-instructions`.
|
|
|
146
149
|
### Keep the harness current
|
|
147
150
|
|
|
148
151
|
Tenant projects pin an exact `@foldspace_npm/harness` version. Publishing a
|
|
149
|
-
newer package does not move them. Check, ask the
|
|
152
|
+
newer package does not move them. Check, ask the human, then bump:
|
|
150
153
|
|
|
151
154
|
```bash
|
|
152
155
|
npx --yes @foldspace_npm/harness@latest upgrade --check
|
|
153
|
-
foldspace upgrade --yes # only after the
|
|
156
|
+
foldspace upgrade --yes # only after the human agrees
|
|
154
157
|
```
|
|
155
158
|
|
|
156
159
|
`--check` (and a non-interactive `upgrade` with no `--yes`) never installs.
|
|
157
|
-
`--yes`
|
|
158
|
-
afterward; that
|
|
160
|
+
`--yes` writes the new exact version and runs `npm install --ignore-scripts`.
|
|
161
|
+
Rebuild afterward; that publishes nothing.
|
|
159
162
|
|
|
160
163
|
`foldspace build` and `foldspace attach` run the same check before they start
|
|
161
164
|
(`attach --status` and `--stop` do not). A project that is behind gets one line
|
|
162
165
|
on stderr and carries on. A project on a **withdrawn** version (below) refuses
|
|
163
|
-
to run and prints the reason. To build on it anyway, once the
|
|
166
|
+
to run and prints the reason. To build on it anyway, once the human has declined
|
|
164
167
|
the upgrade:
|
|
165
168
|
|
|
166
169
|
```bash
|
|
@@ -200,13 +203,14 @@ To undo a withdrawal, deprecate with an empty message:
|
|
|
200
203
|
npm deprecate @foldspace_npm/harness@0.1.14 ""
|
|
201
204
|
```
|
|
202
205
|
|
|
203
|
-
**It is not instant
|
|
204
|
-
in `.foldspace-dev/harness-update.json`.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
**It is not instant for `build` and `attach`.** They use the registry's answer
|
|
207
|
+
cached for 12 hours in `.foldspace-dev/harness-update.json`. `foldspace help
|
|
208
|
+
--json` (which the manual reads first) and `foldspace upgrade` ask the registry
|
|
209
|
+
each time and refresh that cache, so a withdrawal reaches a project at its next
|
|
210
|
+
session start, and within 12 hours otherwise. It only reaches projects on a
|
|
211
|
+
harness version that has this check.
|
|
208
212
|
|
|
209
|
-
### Lint
|
|
213
|
+
### Lint agent code before it ships
|
|
210
214
|
|
|
211
215
|
`foldspace build` runs `foldspace lint` first. Errors skip bundling; warnings
|
|
212
216
|
print and still produce `dist/index.js`.
|
|
@@ -218,12 +222,16 @@ foldspace lint --json
|
|
|
218
222
|
|
|
219
223
|
v1 flags:
|
|
220
224
|
|
|
221
|
-
- **error** `no-static-agent-prompt`
|
|
225
|
+
- **error** `no-static-agent-prompt` - `execute`, a card's `callback` or its `readyState` handing the agent instructions (`directive`, “tell the user”, …)
|
|
222
226
|
- **error** `no-runtask-prompt` — `runTask({ data: { prompt } })` and the same keys; a customer API field named `prompt` is allowed
|
|
227
|
+
- **error** `card-contract` - `awaitUserInput: true` on a view card (its `render` uses no `callback`, `cancel` or `readyState`): the chat panel ignores it, but in headless mode the agent waits for a callback that never comes
|
|
228
|
+
- **error** `no-test-mode-in-bundle` - `armAllInstances(...)`, or `setTestMode(...)` with anything but a literal `false`, anywhere in `agent/`
|
|
229
|
+
- **warning** `card-contract` - any other `awaitUserInput` on a view card, `awaitUserInput` on an input card (not needed), or a `render` that declares `callback`, `cancel` or `readyState` and uses none of them (the SDK treats it as a view card)
|
|
223
230
|
- **warning** `no-emoji` — emoji in handler or `agent/views` string literals
|
|
224
231
|
- **warning** `registry-integrity` — duplicate keys, `{ execute }` not registered, registry value missing `execute`
|
|
225
232
|
|
|
226
|
-
Put action instructions in Agent Studio or
|
|
233
|
+
Put action instructions in Agent Studio or with the Foldspace tools. `foldspace
|
|
234
|
+
lint` cannot see that copy.
|
|
227
235
|
|
|
228
236
|
### Runtime helpers
|
|
229
237
|
|
|
@@ -232,7 +240,10 @@ Tenant actions import from `agent/utils.ts`, which configures and re-exports
|
|
|
232
240
|
runs in the customer's page — it is not a CLI command.
|
|
233
241
|
|
|
234
242
|
`API_BASE` and `AUTH_SOURCE` stamp empty. Fill them from a captured XHR, not a
|
|
235
|
-
guess.
|
|
243
|
+
guess. For auth that is not Bearer, follow
|
|
244
|
+
`recipes/swap-the-login-method/README.md`: for a custom header, copy its
|
|
245
|
+
`agent/utils.ts` over `agent/utils.ts`; for cookies with no token, its
|
|
246
|
+
`variants/utils.cookies.ts`.
|
|
236
247
|
|
|
237
248
|
### Choose an attach mode
|
|
238
249
|
|
|
@@ -242,21 +253,25 @@ guess. Custom auth headers stay a tenant override of one function in `utils.ts`.
|
|
|
242
253
|
has an SDK without the configured agent.
|
|
243
254
|
|
|
244
255
|
Use `foldspace help attach` for mode requirements, effects, and safety options.
|
|
245
|
-
Coding agents
|
|
256
|
+
Coding agents run `foldspace attach --daemon` so the invoking tool
|
|
246
257
|
returns after `[lifecycle] inspect_registration:…`. Foreground attach remains
|
|
247
|
-
the default for
|
|
258
|
+
the default for someone watching the terminal. `attach --status` and
|
|
248
259
|
`attach --stop` inspect and tear down a daemon. Look at the customer's app with
|
|
249
|
-
`foldspace observe`: it reads the
|
|
260
|
+
`foldspace observe`: it reads the test window, read-only, while attach runs.
|
|
250
261
|
|
|
251
262
|
### Verify actions through the agent
|
|
252
263
|
|
|
253
264
|
`foldspace attach` does not invoke action handlers directly. It loads the local
|
|
254
265
|
artifact, verifies that the captured local registry matches the configured SDK
|
|
255
|
-
agent
|
|
266
|
+
agent - including an empty registry - and then observes action calls while
|
|
256
267
|
you exercise the normal agent experience. Named actions are not required to
|
|
257
268
|
attach.
|
|
258
269
|
|
|
259
|
-
|
|
270
|
+
`npx foldspace ask "<question>" --retry 2` types the question into the agent's
|
|
271
|
+
panel and reports the reply, the card text and a screenshot, and - while attach
|
|
272
|
+
runs - whether execute and render ran.
|
|
273
|
+
|
|
274
|
+
Action output shows the SDK's action calls and local `execute`/`render` phases:
|
|
260
275
|
|
|
261
276
|
```text
|
|
262
277
|
[actions] sdk:action show_account_360 executed
|
|
@@ -311,8 +326,7 @@ npm i -D @foldspace_npm/harness
|
|
|
311
326
|
"build": "foldspace build",
|
|
312
327
|
"inject": "foldspace inject",
|
|
313
328
|
"attach": "foldspace attach",
|
|
314
|
-
"attach:daemon": "foldspace attach --daemon"
|
|
315
|
-
"deploy": "foldspace deploy"
|
|
329
|
+
"attach:daemon": "foldspace attach --daemon"
|
|
316
330
|
} }
|
|
317
331
|
```
|
|
318
332
|
|
|
@@ -320,9 +334,13 @@ Every command resolves the **consuming** repo — `process.cwd()`, or
|
|
|
320
334
|
`FOLDSPACE_PROJECT_DIR` so a hosted builder can point it at a workspace it
|
|
321
335
|
controls. Config comes from the consumer's `foldspace.dev.json`.
|
|
322
336
|
|
|
323
|
-
`foldspace deploy` is
|
|
324
|
-
|
|
325
|
-
|
|
337
|
+
`foldspace deploy` is Foldspace-internal: it writes to Foldspace's own storage,
|
|
338
|
+
which no customer can write to. A coding agent never runs or offers it. With a
|
|
339
|
+
code project, a build session publishes the agent's code with the Foldspace
|
|
340
|
+
tools instead: `repo_write` saves it, and the `deploy` tool publishes it - to
|
|
341
|
+
dev after saying so, to production only on the human's yes - as the manual's
|
|
342
|
+
"Done, and what you say at the end" says. Without a code project, the
|
|
343
|
+
Foldspace team does it with the human.
|
|
326
344
|
|
|
327
345
|
---
|
|
328
346
|
|
|
@@ -336,7 +354,7 @@ and **neither had all five**:
|
|
|
336
354
|
|---|---|
|
|
337
355
|
| SDK load guard + retry | On an app with no `document.body` at document-start the append throws; the old guard tested for the stub, so nothing retried and the agent silently never loaded |
|
|
338
356
|
| Worker release on auto-attach | Auto-attach pauses every worker until the attaching client releases it — the harness froze the app's own workers for the whole session |
|
|
339
|
-
| `agentIds` enumeration | `agent({apiName})` returns the OVERLAY handle; arming it on an embedded
|
|
357
|
+
| `agentIds` enumeration | `agent({apiName})` returns the OVERLAY handle; arming it on an embedded agent leaves real conversations untagged |
|
|
340
358
|
| `--test-mode` (off by default) | Test mode was armed unconditionally, hiding exactly what initial setup needs to see; now it is opt-in |
|
|
341
359
|
| Honest badge | It asserted `TEST MODE` whether or not test mode was on |
|
|
342
360
|
|
package/bin/ask.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// foldspace ask
|
|
2
|
+
// foldspace ask - put a question to the visible agent through its panel,
|
|
3
3
|
// exactly as a person would, and report what came back: the reply, any card,
|
|
4
4
|
// what the handlers did, and a picture.
|
|
5
5
|
//
|
|
6
|
-
// It exists so the coding agent stops asking
|
|
7
|
-
//
|
|
6
|
+
// It exists so the coding agent stops asking the human to "reload, try these
|
|
7
|
+
// two sentences and tell me what you saw".
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// npx foldspace ask "<question>" [--retry <n>] [--every <s>] [--timeout <s>]
|
|
10
10
|
//
|
|
11
11
|
// Nothing here reaches into the SDK, patches fetch or injects into the panel:
|
|
12
12
|
// it opens the panel, types into the composer, presses Enter and reads the
|
|
@@ -272,7 +272,7 @@ async function main() {
|
|
|
272
272
|
{
|
|
273
273
|
ok: false,
|
|
274
274
|
reason:
|
|
275
|
-
'No question. Usage: foldspace ask "<question>" [--retry <n>] [--every <s>] [--timeout <s>]
|
|
275
|
+
'No question. Usage: foldspace ask "<question>" [--retry <n>] [--every <s>] [--timeout <s>]',
|
|
276
276
|
},
|
|
277
277
|
1,
|
|
278
278
|
);
|
|
@@ -298,8 +298,10 @@ async function main() {
|
|
|
298
298
|
ok: false,
|
|
299
299
|
question,
|
|
300
300
|
reason: state.frame
|
|
301
|
-
? "The
|
|
302
|
-
:
|
|
301
|
+
? "The agent panel is on the page but never showed a composer. Run `npx foldspace observe screenshot` and look at the page."
|
|
302
|
+
: tracked
|
|
303
|
+
? "No agent panel on this page while attach runs. Read the lines under the last [lifecycle] line in .foldspace-dev/attach-daemon.log, then ask again."
|
|
304
|
+
: "No agent panel on this page. Run `npx foldspace attach --daemon`, follow what it says about the page, then ask again.",
|
|
303
305
|
url,
|
|
304
306
|
},
|
|
305
307
|
1,
|
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,7 +296,7 @@ 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
|
|
@@ -349,7 +353,7 @@ const testModeScript = (enable) => `(() => {
|
|
|
349
353
|
try { window.foldspace("when", "ready", armAll); } catch (e) {}
|
|
350
354
|
armAll();
|
|
351
355
|
|
|
352
|
-
// The instance serving the chat is built when the
|
|
356
|
+
// The instance serving the chat is built when the agent panel opens, long
|
|
353
357
|
// after "ready" — arming once is too early. Keep arming.
|
|
354
358
|
setInterval(armAll, 1000);
|
|
355
359
|
|
|
@@ -406,6 +410,9 @@ function logLifecycle(result) {
|
|
|
406
410
|
if (!result.ok && result.error?.message) {
|
|
407
411
|
console.log(` ${result.error.message}`);
|
|
408
412
|
}
|
|
413
|
+
if (!result.ok && Array.isArray(result.details?.findings)) {
|
|
414
|
+
for (const line of result.details.findings) console.log(` ${line}`);
|
|
415
|
+
}
|
|
409
416
|
if (!result.ok && result.details?.suggestion) {
|
|
410
417
|
console.log(` Next: ${result.details.suggestion}`);
|
|
411
418
|
}
|
|
@@ -703,6 +710,7 @@ async function prepare(ws, sessionId, url) {
|
|
|
703
710
|
mode: agentMode,
|
|
704
711
|
namespace: pageState?.namespace || "foldspace",
|
|
705
712
|
aliases,
|
|
713
|
+
testMode: !noTestMode,
|
|
706
714
|
}),
|
|
707
715
|
hostPatterns,
|
|
708
716
|
),
|
|
@@ -718,7 +726,8 @@ async function prepare(ws, sessionId, url) {
|
|
|
718
726
|
// Test mode is armed when --test-mode is set, and actively CLEARED otherwise:
|
|
719
727
|
// it is page-session state that only a reload or setTestMode(false) undoes,
|
|
720
728
|
// so a page left in test mode by an earlier attach would keep the striped
|
|
721
|
-
// banner under a LIVE
|
|
729
|
+
// test-mode banner under a LIVE corner tag until reload (a customer build,
|
|
730
|
+
// 2026-09-30).
|
|
722
731
|
scripts.push(testModeScript(!noTestMode));
|
|
723
732
|
if (bootstrap) {
|
|
724
733
|
scripts.push(
|
|
@@ -762,6 +771,45 @@ async function prepare(ws, sessionId, url) {
|
|
|
762
771
|
const verification = await verifyPreparedPage(ws, sessionId);
|
|
763
772
|
// For scoring a session: the moment the customer could first see their agent.
|
|
764
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);
|
|
765
813
|
logLifecycle(
|
|
766
814
|
createLifecycleResult({
|
|
767
815
|
operation: "inspect_registration",
|
|
@@ -774,32 +822,9 @@ async function prepare(ws, sessionId, url) {
|
|
|
774
822
|
message:
|
|
775
823
|
"The local action registry did not match the configured page agent.",
|
|
776
824
|
},
|
|
777
|
-
details:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
detectedAgentIds: verification.state?.agentIds || [],
|
|
781
|
-
expectedProductId: productId,
|
|
782
|
-
detectedProductIds: verification.products,
|
|
783
|
-
widget: verification.widget,
|
|
784
|
-
registeredActionNames:
|
|
785
|
-
verification.registration?.actionNames || [],
|
|
786
|
-
expectedActionNames:
|
|
787
|
-
verification.observation?.expectedActionNames || [],
|
|
788
|
-
expectedActionCount: Array.isArray(
|
|
789
|
-
verification.observation?.expectedActionNames,
|
|
790
|
-
)
|
|
791
|
-
? verification.observation.expectedActionNames.length
|
|
792
|
-
: 0,
|
|
793
|
-
missingActionNames:
|
|
794
|
-
verification.registration?.missingActionNames || [],
|
|
795
|
-
unexpectedActionNames:
|
|
796
|
-
verification.registration?.unexpectedActionNames || [],
|
|
797
|
-
actionObserverSubscribed:
|
|
798
|
-
verification.observation?.subscribed === true,
|
|
799
|
-
actionNameLimitExceeded:
|
|
800
|
-
verification.observation?.actionNameLimitExceeded === true,
|
|
801
|
-
diagnosticError: verification.diagnosticError,
|
|
802
|
-
},
|
|
825
|
+
details: mismatch
|
|
826
|
+
? { ...registrationDetails, findings: mismatch.lines, suggestion: mismatch.next }
|
|
827
|
+
: registrationDetails,
|
|
803
828
|
}),
|
|
804
829
|
);
|
|
805
830
|
if (!verification.ok) {
|
|
@@ -825,7 +850,7 @@ function noteUncoveredHost(url) {
|
|
|
825
850
|
if (coverage.covered || !coverage.pattern || uncoveredHostsSeen.has(coverage.host)) return;
|
|
826
851
|
uncoveredHostsSeen.add(coverage.host);
|
|
827
852
|
console.log(` [hosts] the window is on ${coverage.host}, which this project does not cover - the agent is not there.`);
|
|
828
|
-
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.`);
|
|
829
854
|
}
|
|
830
855
|
|
|
831
856
|
async function onEvent(ws, msg) {
|
|
@@ -1026,11 +1051,11 @@ const ver = await connect(owned.version);
|
|
|
1026
1051
|
console.log(`Attached to ${ver.Browser} on :${port}`);
|
|
1027
1052
|
console.log(`Mode: ${attachMode}`);
|
|
1028
1053
|
console.log(`Agent: ${agentApiName} (product ${productId}, ${agentMode})`);
|
|
1029
|
-
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"}`);
|
|
1030
1055
|
console.log(`Hosts: ${hostPatterns.join(", ")}`);
|
|
1031
1056
|
console.log(`Serving: ${path.relative(root, bundlePath)}`);
|
|
1032
1057
|
console.log(
|
|
1033
|
-
`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`,
|
|
1034
1059
|
);
|
|
1035
1060
|
|
|
1036
1061
|
function printSessionSummary() {
|
|
@@ -1136,9 +1161,9 @@ process.on("SIGTERM", requestDetach);
|
|
|
1136
1161
|
|
|
1137
1162
|
// A closed socket does not mean the browser closed. Chrome drops the CDP
|
|
1138
1163
|
// connection on target churn and transient errors too, and exiting on those
|
|
1139
|
-
// silently ends the swap while the
|
|
1140
|
-
// the
|
|
1141
|
-
// 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.
|
|
1142
1167
|
async function browserIsUp() {
|
|
1143
1168
|
try {
|
|
1144
1169
|
const r = await fetch(`http://localhost:${port}/json/version`);
|
|
@@ -1162,12 +1187,16 @@ async function onDisconnect(closedSocket) {
|
|
|
1162
1187
|
if (!(await browserIsUp())) {
|
|
1163
1188
|
clearInterval(keepAlive);
|
|
1164
1189
|
printSessionSummary();
|
|
1165
|
-
console.log("\
|
|
1190
|
+
console.log("\nThe test window was closed. Detaching.");
|
|
1166
1191
|
process.exit(0);
|
|
1167
1192
|
}
|
|
1168
1193
|
try {
|
|
1169
1194
|
console.log(` connection lost — reconnecting (${attempt}/10)…`);
|
|
1170
|
-
|
|
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);
|
|
1171
1200
|
console.log(` reconnected. ${attachMode} mode is live again.`);
|
|
1172
1201
|
return;
|
|
1173
1202
|
} catch {
|
|
@@ -1176,8 +1205,8 @@ async function onDisconnect(closedSocket) {
|
|
|
1176
1205
|
}
|
|
1177
1206
|
clearInterval(keepAlive);
|
|
1178
1207
|
printSessionSummary();
|
|
1179
|
-
console.error("\nCould not reconnect after 10 attempts. Detaching
|
|
1180
|
-
+
|
|
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.`);
|
|
1181
1210
|
process.exit(1);
|
|
1182
1211
|
} finally {
|
|
1183
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);
|