@foldspace_npm/harness 0.1.33 → 0.1.34
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 +169 -49
- package/README.md +30 -2
- package/bin/inject.mjs +2 -18
- package/bin/observe.mjs +18 -0
- package/bin/record.mjs +2 -0
- package/bin/sync.mjs +48 -0
- package/package.json +1 -1
- package/recipes/INDEX.md +1 -1
- package/recipes/bottom-bar/README.md +24 -11
- package/recipes/bottom-bar/agent/bottomBar.ts +76 -38
- package/recipes/bottom-bar/fixtures/configuration.sent.json +13 -12
- package/recipes/swap-the-login-method/README.md +1 -1
- package/recipes/swap-the-login-method/agent/utils.ts +8 -7
- package/recipes/swap-the-login-method/variants/utils.cookies.ts +8 -7
- package/src/chrome-binary.mjs +33 -0
- package/src/cli-registry.mjs +52 -5
- package/src/customer-subdomain.mjs +149 -0
- package/src/lint/index.mjs +23 -3
- package/src/lint/rules.mjs +46 -2
- package/src/lint/safety.mjs +785 -0
- package/src/observe-core.mjs +1 -1
- package/src/runtime/VERSION +1 -1
- package/src/runtime/config.ts +15 -2
- package/src/runtime/http.ts +40 -3
- package/src/runtime/index.ts +1 -0
- package/src/sync-core.mjs +374 -0
- package/src/upgrade.mjs +36 -7
- package/src/video/compose.mjs +12 -3
- package/src/video/recorder.mjs +15 -7
- package/templates/agent-starter/agent/constants.ts +15 -1
- package/templates/agent-starter/agent/utils.ts +3 -0
- package/templates/agent-starter/claude-settings.json +3 -1
- package/templates/agent-starter/skills/review-experience/SKILL.md +135 -0
- package/templates/agent-starter/skills/share-a-video/SKILL.md +9 -2
package/CLAUDE.md
CHANGED
|
@@ -11,15 +11,23 @@ session ends at something they can **see on their own page** - not at
|
|
|
11
11
|
## Every session starts the same way
|
|
12
12
|
|
|
13
13
|
1. **Harness version.** Run `npx foldspace help --json` (from an older install,
|
|
14
|
-
`npx --yes @foldspace_npm/harness@latest upgrade --check`).
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
`npx --yes @foldspace_npm/harness@latest upgrade --check`). Several people
|
|
15
|
+
can build one agent, so every session runs the newest harness:
|
|
16
|
+
- **`update.autoUpgrade` is true** - a newer version in the same line, such
|
|
17
|
+
as 0.1.30 to 0.1.34. Run `npx foldspace upgrade --yes` and
|
|
18
|
+
`npm run build` without asking, and tell the human in one line: *"I've
|
|
19
|
+
updated the Foldspace tools to the newest version."*
|
|
20
|
+
- **`update.outdated` is true, `autoUpgrade` is not** - the newest is a new
|
|
21
|
+
line (0.1 to 0.2). Say which version the project is on and which is
|
|
22
|
+
newest, and **ask before upgrading**; then the same two commands.
|
|
23
|
+
|
|
24
|
+
`build` and `attach` never upgrade by themselves. They check too: on an
|
|
25
|
+
older version they print one line and carry on - upgrade then, the same
|
|
26
|
+
way. On a version Foldspace **withdrew** they refuse to run and say why:
|
|
27
|
+
within the line, upgrade without asking; across a line, tell the human the
|
|
28
|
+
reason and ask. Only if they decide not to, rerun with
|
|
29
|
+
`FOLDSPACE_SKIP_UPDATE_CHECK=1` - never set it on your own. If the project
|
|
30
|
+
`CLAUDE.md` does not contain
|
|
23
31
|
`@node_modules/@foldspace_npm/harness/CLAUDE.md`, ask before
|
|
24
32
|
`npx foldspace upgrade --refresh-instructions`.
|
|
25
33
|
2. **Is Foldspace connected? Check — never assume.** `region` in
|
|
@@ -49,22 +57,18 @@ session ends at something they can **see on their own page** - not at
|
|
|
49
57
|
shows them.
|
|
50
58
|
- **A code project.** Call `repo_read` with command `instructions` and
|
|
51
59
|
follow what it returns. The project's `agent/` is the copy Foldspace
|
|
52
|
-
builds and publishes, and it may hold work someone else saved. Before
|
|
53
|
-
build,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
This folder's `agent/` may hold work a session never saved, so copy it to
|
|
58
|
-
`.foldspace-dev/agent-kept/` first. Put back the action files the project
|
|
59
|
-
lacks; where a file differs, the project's wins. If any differed, tell
|
|
60
|
+
builds and publishes, and it may hold work someone else saved. Before
|
|
61
|
+
you build, pull it into this folder (**Several builders, one agent**):
|
|
62
|
+
`npx foldspace sync` replaces the template with their agent, keeps a copy of this folder's
|
|
63
|
+
`agent/` in `.foldspace-dev/agent-kept/`, and puts back unsaved action
|
|
64
|
+
files the project lacks. Where a file differs, the project's wins: tell
|
|
60
65
|
the human in one line which actions had changes on this computer that
|
|
61
66
|
were never saved.
|
|
62
67
|
- **`NO_REPO`.** Nothing is saved yet. Follow the next step `status` gives:
|
|
63
68
|
it creates the code project in about a minute and changes nothing anyone
|
|
64
69
|
sees - say you are setting up their project, no need to ask. Get the agent
|
|
65
70
|
live in this folder meanwhile (L0 needs no code). Once `status` shows the
|
|
66
|
-
project,
|
|
67
|
-
you write the first action.
|
|
71
|
+
project, pull its `agent/` as above, before you write the first action.
|
|
68
72
|
- **A refusal that names `foldspace-ai-access`.** This agent has saved
|
|
69
73
|
code that Foldspace has not opened to these tools; that is final. Tell
|
|
70
74
|
the human that Foldspace manages their agent's saved code, so this build
|
|
@@ -145,19 +149,29 @@ marks their turn: they may type whatever, whenever; the pill only ever asks them
|
|
|
145
149
|
to wait. `npx foldspace badge label` puts the small corner tag back;
|
|
146
150
|
`npx foldspace badge off` removes it, for a screenshot they will share.
|
|
147
151
|
|
|
148
|
-
**After they have tried it, the second wave:
|
|
149
|
-
(item 3 - no question, one line in chat
|
|
150
|
-
|
|
151
|
-
asked the agent the question and -
|
|
152
|
-
steps in **Done, and what you say at
|
|
152
|
+
**After they have tried it, the second wave: write the page starters (item 2)
|
|
153
|
+
and tell Foldspace who the user is (item 3) - no question, one line in chat
|
|
154
|
+
each - and offer the rest as one choice, recommended first.** Once you have
|
|
155
|
+
handed over (`badge ready`), the human has asked the agent the question and -
|
|
156
|
+
with a code project - you have taken the steps in **Done, and what you say at
|
|
157
|
+
the end**:
|
|
153
158
|
|
|
154
159
|
1. **Starters.** The sign-up starters point at actions the agent does not have
|
|
155
160
|
(clicking one answers "that action isn't available"): replace them with the
|
|
156
161
|
question this experience answers. `set_conversation_starters` replaces the
|
|
157
|
-
whole set - send the full list
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
whole set - send the full list, **at most 3**, each in the right group
|
|
163
|
+
(**How the agent looks and opens is configuration**). Their account, so
|
|
164
|
+
their yes.
|
|
165
|
+
2. **Page starters** - `recipes/bottom-bar`. Foldspace already shows the bar
|
|
166
|
+
at the foot of the page; this picks what it offers on each screen. It
|
|
167
|
+
ships in this project's start-up code; nothing in their source changes.
|
|
168
|
+
1. `npx foldspace observe menu` lists the main-menu pages.
|
|
169
|
+
2. For each page, write at most 3 starters the agent can answer today.
|
|
170
|
+
Early on most pages have none: leave them out, and they keep Agent
|
|
171
|
+
Studio's starters.
|
|
172
|
+
3. `ACTION` when clicking it runs an action, `KNOWLEDGE` when the answer
|
|
173
|
+
comes from the knowledge base.
|
|
174
|
+
4. `npm run build`, attach, and look at one page that has starters.
|
|
161
175
|
3. **Tell Foldspace who the user is** - `recipes/who-is-the-user`: from then on
|
|
162
176
|
their conversations and analytics stop being anonymous, and the agent has
|
|
163
177
|
real-time context about who it is talking to.
|
|
@@ -184,7 +198,7 @@ Every question to the human is an **enumerated choice with the recommended
|
|
|
184
198
|
option first** (in Claude Code, `AskUserQuestion`; elsewhere, the editor's own question tool), asked once - not repeated in your message. A first session asks only
|
|
185
199
|
this: which agent (only if several), the app's hostname, which experience
|
|
186
200
|
first - and a yes wherever this manual asks for one, such as agreeing the
|
|
187
|
-
experience (below), upgrading, publishing an action, task agent or
|
|
201
|
+
experience (below), upgrading to a new line (0.1 to 0.2), publishing an action, task agent or
|
|
188
202
|
navigation, publishing their code for their users, replacing their starters or
|
|
189
203
|
adding hosts. The hostname is the only free-text question. Everything else -
|
|
190
204
|
attach mode, folder, name, level - you **state and move on**.
|
|
@@ -229,6 +243,42 @@ command refuses the window.
|
|
|
229
243
|
with both, the user sees an extra message and the agent takes a second
|
|
230
244
|
turn.
|
|
231
245
|
|
|
246
|
+
## Each customer on their own subdomain
|
|
247
|
+
|
|
248
|
+
Some products give each of their customers their own address -
|
|
249
|
+
`acme.example.com`, `globex.example.com` - and often a backend that carries
|
|
250
|
+
it too (`acme.api.example.com`). You build signed in to **one** of those
|
|
251
|
+
customers, and the code goes to **all** of them: an address you copy as you
|
|
252
|
+
saw it sends every user's agent to that one customer's data.
|
|
253
|
+
|
|
254
|
+
- **Find out before you write `API_BASE`.** `npx foldspace observe menu` and
|
|
255
|
+
`observe screen` report `customerSubdomain` when the page's address starts
|
|
256
|
+
with what looks like a customer's name, and `screen` names the backend
|
|
257
|
+
calls that carry it too. Ask the human once, as a choice: *"Does each of
|
|
258
|
+
your customers get their own address, like acme.example.com?"* Never decide
|
|
259
|
+
it from the address alone - `reports.` or `eu1.` is a part of the product,
|
|
260
|
+
and only they know.
|
|
261
|
+
- **Write their answer** in `agent/constants.ts`:
|
|
262
|
+
`PER_CUSTOMER_SUBDOMAIN = true` or `false`. Until it is set, observe keeps
|
|
263
|
+
asking and the build refuses an address that may be one customer's.
|
|
264
|
+
- **With `true`, `API_BASE` is built from the page's address**, at run time,
|
|
265
|
+
in the user's own browser:
|
|
266
|
+
- the backend is on the page's own host - `API_BASE = "/api"`;
|
|
267
|
+
- the backend's address carries the customer - write it with
|
|
268
|
+
`{subdomain}` where their name was:
|
|
269
|
+
`API_BASE = "https://{subdomain}.api.example.com"`;
|
|
270
|
+
- one backend for every customer (`api.example.com`, the customer known
|
|
271
|
+
from the sign-in) - the address as it is.
|
|
272
|
+
|
|
273
|
+
The runtime puts each user's own subdomain in (`apiBaseUrl()`), and
|
|
274
|
+
`apiFetch` uses it. A page whose address has no subdomain gets `reason:
|
|
275
|
+
"config"`, never another customer's backend.
|
|
276
|
+
- **Nothing else names one customer either:** a link is a path (`"/invoices/42"`),
|
|
277
|
+
never `https://acme.example.com/invoices/42`. The build refuses one
|
|
278
|
+
customer's address anywhere in `agent/` (`no-fixed-customer-address`).
|
|
279
|
+
- **Record it** in `docs/app-profile.md`: what the human said, and the
|
|
280
|
+
address you observed, with the customer's name marked as theirs.
|
|
281
|
+
|
|
232
282
|
## Find the call yourself
|
|
233
283
|
|
|
234
284
|
Never guess `API_BASE`, `AUTH_SOURCE`, an endpoint or a schema. But guessing
|
|
@@ -280,7 +330,8 @@ GET only, never submit a form, never click anything that creates, sends or
|
|
|
280
330
|
deletes.
|
|
281
331
|
|
|
282
332
|
Capture at least one real HTTP 200 for the data the action needs. If there is
|
|
283
|
-
none, pivot; do not create Foldspace resources.
|
|
333
|
+
none, pivot; do not create Foldspace resources. Before that address goes into
|
|
334
|
+
`API_BASE`, settle **Each customer on their own subdomain**. Write what you establish into
|
|
284
335
|
`docs/app-profile.md` **with how you know it**, at that moment. Do not promote
|
|
285
336
|
an assumption by quoting that file later.
|
|
286
337
|
|
|
@@ -428,6 +479,33 @@ of the human's, read the attach log for the `[actions]` SDK callback and local
|
|
|
428
479
|
execute/render lines. Those record names, statuses, durations and parameter
|
|
429
480
|
keys only - never results or error bodies.
|
|
430
481
|
|
|
482
|
+
## Several builders, one agent
|
|
483
|
+
|
|
484
|
+
With a code project, other people - or other machines - may save to the same
|
|
485
|
+
agent while you work. So this folder pulls the saved code again whenever
|
|
486
|
+
someone saved since it last did. Saving and publishing stay exactly as
|
|
487
|
+
`repo_read` command `instructions` says; this only keeps `agent/` current.
|
|
488
|
+
|
|
489
|
+
**Pull at session start (step 5), before each new experience, and before each
|
|
490
|
+
save or publish:**
|
|
491
|
+
|
|
492
|
+
1. `repo_read` `status` gives `develop_head`. Then `repo_read` `tree` with
|
|
493
|
+
`ref` `foldspace/<Agent Key>` - the line of work the instructions save to -
|
|
494
|
+
gives its head (the sha after the ref). If that line does not exist yet,
|
|
495
|
+
nothing has been saved to it: use `develop` only.
|
|
496
|
+
2. `npx foldspace sync check --develop <develop_head>`, plus
|
|
497
|
+
`--line <its head>` when the line exists. Its `baseSha` is the `base_sha`
|
|
498
|
+
for your next save, as the instructions say.
|
|
499
|
+
3. If `pull` lists anything: read `agent/` at each `ref` (`tree` and `file`)
|
|
500
|
+
into its `into` folder, same paths (`<into>/agent/actions/x.ts`). Then
|
|
501
|
+
`npx foldspace sync apply`, and do what its `next` says. Files under
|
|
502
|
+
`conflicts` changed both here and in the saved code: they now hold the
|
|
503
|
+
saved version - put your change back on top from the kept copy. Then build.
|
|
504
|
+
|
|
505
|
+
**After each save**, `npx foldspace sync saved --head <commit_sha> --paths
|
|
506
|
+
<committed_paths, comma-separated>`, so your own save is not taken for
|
|
507
|
+
someone else's.
|
|
508
|
+
|
|
431
509
|
## Done, and what you say at the end
|
|
432
510
|
|
|
433
511
|
A first session is done when the customer has seen one experience answer from
|
|
@@ -438,9 +516,16 @@ gates pass, and not when there is nothing left to improve.
|
|
|
438
516
|
their users is yours to do. Once they have tried it in the test window, before
|
|
439
517
|
you offer the second wave:
|
|
440
518
|
|
|
441
|
-
1.
|
|
442
|
-
|
|
443
|
-
|
|
519
|
+
1. Before **every** save, review the experience with the
|
|
520
|
+
`review-experience` skill (`.claude/skills/review-experience/SKILL.md`;
|
|
521
|
+
any other editor: read
|
|
522
|
+
`node_modules/@foldspace_npm/harness/templates/agent-starter/skills/review-experience/SKILL.md`):
|
|
523
|
+
only the data the answer needs, a yes from the user before every write,
|
|
524
|
+
nothing the user cannot reach in the product. A serious finding stops the
|
|
525
|
+
save until it is fixed. Then say *"I'm saving your changes and publishing
|
|
526
|
+
them to dev"* - no need to
|
|
527
|
+
ask - then pull (**Several builders, one agent**), and save and publish
|
|
528
|
+
the way `repo_read` command `instructions` says. One exception: a dev preview
|
|
444
529
|
that comes back with no warnings means this agent loads dev for its users -
|
|
445
530
|
then ask first, as for production. In chat it is "saving your changes",
|
|
446
531
|
"publishing to dev" and "publishing for your users" - never GitHub,
|
|
@@ -466,8 +551,8 @@ with `--bootstrap` or `--replace`, it does not yet: do step 1, keep attach
|
|
|
466
551
|
running, and give the answer below for the rest - their site loading the agent
|
|
467
552
|
is the step the Foldspace team does with them.
|
|
468
553
|
|
|
469
|
-
A save refused because someone saved after you read is not final:
|
|
470
|
-
|
|
554
|
+
A save refused because someone saved after you read is not final: pull again
|
|
555
|
+
(**Several builders, one agent**), put your change on top and save. Any other refusal from the Foldspace tools
|
|
471
556
|
here is final: then the answer below.
|
|
472
557
|
|
|
473
558
|
**Without a code project**, the answer to *"how do I put this in front of my
|
|
@@ -489,7 +574,9 @@ Do not report success without all six:
|
|
|
489
574
|
|
|
490
575
|
1. `npm run typecheck` compiles (`tsc --noEmit -p tsconfig.json`). Do not run
|
|
491
576
|
`npx tsc` — it can install the wrong package.
|
|
492
|
-
2. `foldspace lint` reports no errors (`foldspace build` runs it first).
|
|
577
|
+
2. `foldspace lint` reports no errors (`foldspace build` runs it first). It
|
|
578
|
+
also refuses what is unsafe for the product's users - **What the build
|
|
579
|
+
refuses**, below.
|
|
493
580
|
3. The handler appears in `dist/index.js`.
|
|
494
581
|
4. The browser reports `inspect_registration:registration_ok`, and for a named
|
|
495
582
|
action the captured registry includes it.
|
|
@@ -508,12 +595,27 @@ Do not report success without all six:
|
|
|
508
595
|
(**Agentic UI components**).
|
|
509
596
|
6. Browser evidence came from the live target, not hand-authored examples.
|
|
510
597
|
|
|
598
|
+
### What the build refuses
|
|
599
|
+
|
|
600
|
+
The first three are errors: the build stops, and the message says what to do
|
|
601
|
+
instead. Fix the code; never work round the rule. `known-hosts-only` is a
|
|
602
|
+
warning: the build goes on; mention it to the human in one line. Cards may draw
|
|
603
|
+
any markup and code they need - that is what they are for.
|
|
604
|
+
|
|
605
|
+
| Rule | Refuses | Do instead |
|
|
606
|
+
|---|---|---|
|
|
607
|
+
| `no-fixed-customer-address` | One customer's address in `API_BASE` or anywhere in `agent/`, on a product with a subdomain per customer - or one that may be, while `PER_CUSTOMER_SUBDOMAIN` is undecided | `"/api"` or `"https://{subdomain}.…"`; a path for a link (**Each customer on their own subdomain**) |
|
|
608
|
+
| `token-stays-home` | The user's sign-in token returned from `execute` or a card, handed to Foldspace, logged, stored again (storage, a cookie, `window`) or sent to a host that is not the product's backend | Let `apiFetch` add it; log `redact(token)` |
|
|
609
|
+
| `no-secrets` | API keys, JWTs, bearer tokens, cookie values, `.har` captures, storage dumps and `.env` files anywhere in the project | Names and where things live go in `docs/app-profile.md`; values never |
|
|
610
|
+
| `known-hosts-only` | A call (`fetch`, `sendBeacon`, `WebSocket`, `XMLHttpRequest`, `publicFetch`) to a host that is neither the product's own, Foldspace's (`*.foldspace.ai`), nor named in `agent/constants.ts` | The product's backend through `apiFetch`; another host only with the human's yes, as a constant in `agent/constants.ts` |
|
|
611
|
+
|
|
511
612
|
## Rules that are not negotiable
|
|
512
613
|
|
|
513
614
|
1. **Handlers return data only.** Never `directive`, `instructions`, or a
|
|
514
615
|
paragraph telling the agent what to say - those live in Agent Studio.
|
|
515
616
|
Never return from `execute` a token, a cookie, any other secret or
|
|
516
|
-
`ApiResult.detail` (it is for the console)
|
|
617
|
+
`ApiResult.detail` (it is for the console) - the build refuses the
|
|
618
|
+
user's token there (`token-stays-home`): the agent reads what
|
|
517
619
|
`execute` returns for an action with no card or a view card (rule 2).
|
|
518
620
|
Validate parameters and return sanitised errors.
|
|
519
621
|
**What goes back to the agent states what happened - it never says what to
|
|
@@ -612,14 +714,29 @@ wins**. <https://docs.foldspace.ai/customize/configuration/>
|
|
|
612
714
|
list. It changes their Foldspace account, so it needs their yes. Offer it in
|
|
613
715
|
the second wave, replacing the sign-up starters that point at actions the
|
|
614
716
|
agent does not have.
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
717
|
+
1. **At most 3, both groups together.** They are also the agent panel's
|
|
718
|
+
opening screen, and a long list buries the one that matters. When the
|
|
719
|
+
list already holds more, say which 3 you would keep and ask.
|
|
720
|
+
2. **Each in its group.** `conversation_starters` takes
|
|
721
|
+
`{"KNOWLEDGE": [{"title": ...}], "ACTION": [{"title": ...}]}`, both keys
|
|
722
|
+
always. `ACTION` when clicking it runs one of the agent's actions (it
|
|
723
|
+
shows the action icon); `KNOWLEDGE` when the answer comes from the
|
|
724
|
+
knowledge base. Starters for the experiences you built go in `ACTION`.
|
|
725
|
+
3. **`default_type`** picks the group shown first: `ACTION` when only that
|
|
726
|
+
group has starters.
|
|
727
|
+
- **Page starters are code** - `setConversationStarters` (`null` restores
|
|
728
|
+
Agent Studio's), the same 3-at-most and the same two groups;
|
|
729
|
+
`npx foldspace lint` refuses more than 3. That code lives in this project's
|
|
730
|
+
start-up (`recipes/bottom-bar`, kind `page-setup`) and ships with the
|
|
731
|
+
actions bundle, so nothing in the customer's source changes; their own mount
|
|
732
|
+
code is the other place it can live, their engineers' call. The first
|
|
733
|
+
session writes them for the main-menu pages (second wave, item 2).
|
|
734
|
+
- **The bottom bar is on by default** - Foldspace turns it on from its own
|
|
735
|
+
settings, so code never sends `enabled`. Send `bottomBarSettings` only for a
|
|
736
|
+
setting the human asks to change, one whole settings group per call, as the
|
|
737
|
+
docs do, with `enabled: true` in it (⚠️ whether a partial group merges with
|
|
738
|
+
Foldspace's own settings is not verified). `maxVisibleStarters` caps the
|
|
739
|
+
bar only, never the panel.
|
|
623
740
|
- **An action's `execute` never calls `setConfiguration`**: a partial call from
|
|
624
741
|
one can collapse the agent's panel to nothing.
|
|
625
742
|
|
|
@@ -712,7 +829,8 @@ Each of these cost a real build.
|
|
|
712
829
|
a mismatch does not error, the action simply never fires.
|
|
713
830
|
- `agent/api/` — one HTTP helper per endpoint
|
|
714
831
|
- `agent/constants.ts` — agent, product, domain, plus empty `API_BASE` /
|
|
715
|
-
`AUTH_SOURCE` and `LOAD_MODE
|
|
832
|
+
`AUTH_SOURCE`, undecided `PER_CUSTOMER_SUBDOMAIN` and `LOAD_MODE`. Any
|
|
833
|
+
host outside the product the human agreed to is named here too
|
|
716
834
|
- `agent/utils.ts` — configures the harness runtime and re-exports it. **The
|
|
717
835
|
only import surface for actions.**
|
|
718
836
|
- `agent/runtime/` — a code project only: this harness's runtime, which every
|
|
@@ -760,6 +878,7 @@ not import it — fix the import when you implement.
|
|
|
760
878
|
| Helper | Use when | Do not use when |
|
|
761
879
|
|---|---|---|
|
|
762
880
|
| `apiFetch` / `apiFetchBinary` | The app's own JSON/file API, after a real 200 | Public marketing hosts (`publicFetch`); custom auth headers |
|
|
881
|
+
| `apiBaseUrl` | Your own `apiFetch` (custom header, cookies) builds its URLs from it - `"/api"` and `{subdomain}` resolved for this page | Reading `API_BASE` or `getConfig().apiBase` directly: on a product with a subdomain per customer that is the wrong backend |
|
|
763
882
|
| `publicFetch` | Unauthenticated / marketing origin | Signed-in product APIs |
|
|
764
883
|
| `getAuthToken` / `parseJwt` | `AUTH_SOURCE` is `localStorage` or `cookie` | Custom header schemes — override `apiFetch` in `utils.ts` |
|
|
765
884
|
| `rankBy` | User typed a name; API search is exact or ignored | Domain ranking (invoices, coverage, industry classification codes) |
|
|
@@ -790,8 +909,8 @@ Import the helpers the way the project's own `agent/utils.ts` does - from
|
|
|
790
909
|
`./runtime` - in a recipe file you copy too. `agent/runtime/` is this harness's
|
|
791
910
|
runtime, copied in for that reason. A build that changes files there names
|
|
792
911
|
them: save exactly those files in a save of their own as soon as the agent is
|
|
793
|
-
on their page
|
|
794
|
-
it changes nothing for their users. If their account cannot save, carry on.
|
|
912
|
+
on their page; a save of only those files needs no `review-experience`. Say you are bringing their project up to date - no
|
|
913
|
+
need to ask, it changes nothing for their users. If their account cannot save, carry on.
|
|
795
914
|
Never edit a file there: the next build puts the harness's copy back. To change
|
|
796
915
|
how a helper behaves, define it in `agent/utils.ts` and export it from there in
|
|
797
916
|
place of the runtime's. A project with an `agent/utils/` folder has its own
|
|
@@ -830,7 +949,8 @@ import is right.
|
|
|
830
949
|
goes. **Never widen your own access**: if something is refused for a site or
|
|
831
950
|
a permission, say so and ask; do not try others.
|
|
832
951
|
- Do not commit secrets, cookies, HAR files, browser storage, or
|
|
833
|
-
`.foldspace-dev
|
|
952
|
+
`.foldspace-dev/` - the build refuses them in the project (`no-secrets`).
|
|
953
|
+
Nothing transient goes in `docs/`.
|
|
834
954
|
- Local handler changes are not cloud publication.
|
|
835
955
|
- Creating or publishing Foldspace resources needs the human's yes - except
|
|
836
956
|
the agent's code: creating its code project, saving to it and publishing it
|
package/README.md
CHANGED
|
@@ -157,11 +157,14 @@ to that import with `foldspace upgrade --refresh-instructions`.
|
|
|
157
157
|
### Keep the harness current
|
|
158
158
|
|
|
159
159
|
Tenant projects pin an exact `@foldspace_npm/harness` version. Publishing a
|
|
160
|
-
newer package does not move them
|
|
160
|
+
newer package does not move them: each session checks and bumps. Several
|
|
161
|
+
builders can work on one agent, so within one line (0.1.x to a newer 0.1.y,
|
|
162
|
+
`update.autoUpgrade`) the coding agent upgrades without asking and says so in
|
|
163
|
+
one line; across a line (0.1 to 0.2) it asks the human first.
|
|
161
164
|
|
|
162
165
|
```bash
|
|
163
166
|
npx --yes @foldspace_npm/harness@latest upgrade --check
|
|
164
|
-
foldspace upgrade --yes #
|
|
167
|
+
foldspace upgrade --yes # within the line: no question; across a line: after the human agrees
|
|
165
168
|
```
|
|
166
169
|
|
|
167
170
|
`--check` (and a non-interactive `upgrade` with no `--yes`) never installs.
|
|
@@ -180,6 +183,27 @@ FOLDSPACE_SKIP_UPDATE_CHECK=1 npm run build
|
|
|
180
183
|
|
|
181
184
|
That turns the check off entirely; it does not fix the withdrawn version.
|
|
182
185
|
|
|
186
|
+
### Keep up with the saved code
|
|
187
|
+
|
|
188
|
+
With a code project, several builders can save to one agent. `foldspace sync`
|
|
189
|
+
keeps a folder's `agent/` up to date: it never reaches Foldspace itself - the
|
|
190
|
+
coding agent reads the saved code with the Foldspace tools and passes the
|
|
191
|
+
heads it read.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npx foldspace sync check --develop <sha> [--line <sha>] # what to pull, and where to write it
|
|
195
|
+
npx foldspace sync apply # merge it into agent/
|
|
196
|
+
npx foldspace sync saved --head <commit_sha> --paths <a,b> # after a save
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`--line` is the head of `foldspace/<Agent Key>`, the line of work the code
|
|
200
|
+
project's instructions save to, once it exists. `apply` keeps a copy of
|
|
201
|
+
`agent/` in `.foldspace-dev/agent-kept/<time>/` and merges file by file
|
|
202
|
+
against what the folder last pulled (kept in `.foldspace-dev/saved-code/`): a
|
|
203
|
+
file only one side changed takes that side; a file both changed takes the
|
|
204
|
+
saved version and is listed under `conflicts`. Saving and publishing are
|
|
205
|
+
unchanged: they follow the code project's instructions.
|
|
206
|
+
|
|
183
207
|
### Withdrawing a release (maintainers)
|
|
184
208
|
|
|
185
209
|
When a published version is broken badly enough that nobody should build on
|
|
@@ -234,6 +258,10 @@ v1 flags:
|
|
|
234
258
|
- **error** `no-runtask-prompt` — `runTask({ data: { prompt } })` and the same keys; a customer API field named `prompt` is allowed
|
|
235
259
|
- **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
|
|
236
260
|
- **error** `no-test-mode-in-bundle` - `armAllInstances` used anywhere in `agent/` (called, or handed on to be called later), or `setTestMode(...)` with anything but a literal `false`. A code project's own copy of the runtime in `agent/runtime/` is not refused for the `setTestMode` inside its `armAllInstances`
|
|
261
|
+
- **error** `no-fixed-customer-address` - on a product with a subdomain per customer (`PER_CUSTOMER_SUBDOMAIN` in `agent/constants.ts`), one customer's address in `API_BASE` or anywhere in `agent/`; while that is undecided, an address whose first part looks like a customer's name (`acme.` - not `app.`, `eu1.`, `reports.`)
|
|
262
|
+
- **error** `token-stays-home` - the user's sign-in token (`getAuthToken`, a session-named storage key, `document.cookie`, followed through variables and helper functions) returned from `execute` or a card, handed to Foldspace, logged, stored again, or sent to a host that is not the product's backend
|
|
263
|
+
- **error** `no-secrets` - API keys, JWTs, bearer tokens, cookie values, secret-named strings, `.har` captures, storage dumps and `.env` files anywhere in the project (not `node_modules/`, `dist/`, `.foldspace-dev/`, `agent/runtime/`)
|
|
264
|
+
- **warning** `known-hosts-only` - `fetch`, `sendBeacon`, `WebSocket`, `EventSource`, `XMLHttpRequest`, `publicFetch` or `import()` to an address written out whose host is neither Foldspace's (`*.foldspace.ai`), the product's (`foldspace.dev.json` hosts, `API_BASE`, the app's own domain) nor written in `agent/constants.ts`
|
|
237
265
|
- **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)
|
|
238
266
|
- **warning** `no-emoji` — emoji in handler or `agent/views` string literals
|
|
239
267
|
- **warning** `registry-integrity` — duplicate keys, `{ execute }` not registered, registry value missing `execute`
|
package/bin/inject.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
waitForOwnedCdp,
|
|
21
21
|
writeLaunchSentinel,
|
|
22
22
|
} from "../src/cdp-ownership.mjs";
|
|
23
|
-
import { findChrome } from "../src/chrome-binary.mjs";
|
|
23
|
+
import { findChrome, testWindowArgs } from "../src/chrome-binary.mjs";
|
|
24
24
|
|
|
25
25
|
// Resolve the CONSUMING repo, not this package. FOLDSPACE_PROJECT_DIR lets a
|
|
26
26
|
// hosted builder point the harness at a workspace it controls.
|
|
@@ -205,23 +205,7 @@ function nameProfile() {
|
|
|
205
205
|
}
|
|
206
206
|
nameProfile();
|
|
207
207
|
|
|
208
|
-
const chromeArgs =
|
|
209
|
-
`--user-data-dir=${profileDir}`,
|
|
210
|
-
`--remote-debugging-port=${debugPort}`,
|
|
211
|
-
// The theme only paints the isolated window Foldspace blue. SDK and action
|
|
212
|
-
// code are loaded later by attach over CDP.
|
|
213
|
-
`--load-extension=${themeDir}`,
|
|
214
|
-
`--disable-extensions-except=${themeDir}`,
|
|
215
|
-
// Chrome 136+ dropped --load-extension. Keep the legacy switch for older
|
|
216
|
-
// builds, and enable the CDP Extensions domain so we can load it at runtime.
|
|
217
|
-
"--disable-features=DisableLoadExtensionCommandLineSwitch",
|
|
218
|
-
"--enable-unsafe-extension-debugging",
|
|
219
|
-
"--no-first-run",
|
|
220
|
-
"--no-default-browser-check",
|
|
221
|
-
"--test-type",
|
|
222
|
-
"--remote-allow-origins=*",
|
|
223
|
-
startUrl,
|
|
224
|
-
];
|
|
208
|
+
const chromeArgs = testWindowArgs({ profileDir, debugPort, themeDir, startUrl });
|
|
225
209
|
|
|
226
210
|
console.log(`\nOpening the test window - profile ${path.relative(root, profileDir)}, debugging port :${debugPort}.`);
|
|
227
211
|
console.log("The human signs in there; the profile keeps what the site remembers between runs.\n");
|
package/bin/observe.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
summarizeJson,
|
|
51
51
|
toHex,
|
|
52
52
|
} from "../src/observe-core.mjs";
|
|
53
|
+
import { customerSubdomainHint, readCustomerSubdomainSetting } from "../src/customer-subdomain.mjs";
|
|
53
54
|
import { recordEvent } from "../src/session-events.mjs";
|
|
54
55
|
import { resolveProjectDir } from "../src/upgrade.mjs";
|
|
55
56
|
|
|
@@ -326,6 +327,21 @@ async function runPages() {
|
|
|
326
327
|
};
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
// Whether the project has settled PER_CUSTOMER_SUBDOMAIN in agent/constants.ts:
|
|
331
|
+
// once it has, observe stops asking.
|
|
332
|
+
function customerSubdomainSetting() {
|
|
333
|
+
try {
|
|
334
|
+
return readCustomerSubdomainSetting(fs.readFileSync(path.join(projectDir, "agent", "constants.ts"), "utf8"));
|
|
335
|
+
} catch {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function customerSubdomainFields(pageUrl, requestUrls) {
|
|
341
|
+
const found = customerSubdomainHint(pageUrl, requestUrls, customerSubdomainSetting());
|
|
342
|
+
return found ? { customerSubdomain: found } : {};
|
|
343
|
+
}
|
|
344
|
+
|
|
329
345
|
async function runMenu(page) {
|
|
330
346
|
const perFrame = await evaluateInFrames(page, navigationScript(null));
|
|
331
347
|
const seen = new Set();
|
|
@@ -351,6 +367,7 @@ async function runMenu(page) {
|
|
|
351
367
|
count: items.length,
|
|
352
368
|
items,
|
|
353
369
|
...(siblings ? { otherHosts: siblings.hosts, hostsHint: siblings.hint } : {}),
|
|
370
|
+
...customerSubdomainFields(url, []),
|
|
354
371
|
...(items.length ? {} : { note: "No visible navigation found. The app may still be loading, or showing a login page." }),
|
|
355
372
|
};
|
|
356
373
|
}
|
|
@@ -440,6 +457,7 @@ async function runScreen(page, flags) {
|
|
|
440
457
|
requestsSeen: all.filter(isApiCall).length,
|
|
441
458
|
candidates: ranked,
|
|
442
459
|
writesSeen: [...writes.values()].slice(0, 20),
|
|
460
|
+
...customerSubdomainFields(url, all.filter(isApiCall).map((request) => request.url)),
|
|
443
461
|
...(ranked.length
|
|
444
462
|
? {}
|
|
445
463
|
: { note: "No successful JSON GET was seen. The data may load after another click, or arrive inside the HTML." }),
|
package/bin/record.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
checkRecordingMatches,
|
|
38
38
|
closeAllHeadless,
|
|
39
39
|
loadRecording,
|
|
40
|
+
wideWindowNote,
|
|
40
41
|
needsChrome,
|
|
41
42
|
probeEncoder,
|
|
42
43
|
renderInChrome,
|
|
@@ -337,6 +338,7 @@ async function compose(config, recordDir, chromePath) {
|
|
|
337
338
|
note: `${Math.round(plan.fit.totalS)} s, longer than the ${TIMING.maxTotalS} s it should be at most, with the waits already shortened: drop a scene, or pick questions with shorter answers.`,
|
|
338
339
|
}
|
|
339
340
|
: {}),
|
|
341
|
+
...(wideWindowNote(recording) ? { wideWindow: wideWindowNote(recording) } : {}),
|
|
340
342
|
sceneSeconds: plan.clips.filter((clip) => clip.kind === "scene").map((clip) => Math.round(clip.durationS * 10) / 10),
|
|
341
343
|
lastFrames: lastFrames(recording),
|
|
342
344
|
};
|
package/bin/sync.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* foldspace sync <check|apply|saved> [options]
|
|
4
|
+
*
|
|
5
|
+
* Keeps this folder's agent/ up to date with the agent's saved code, which
|
|
6
|
+
* several builders can work on. The coding agent reads the saved code with the
|
|
7
|
+
* Foldspace tools and passes the heads it read; this command remembers what
|
|
8
|
+
* this folder last pulled (.foldspace-dev/saved-code/) and merges.
|
|
9
|
+
*
|
|
10
|
+
* check --develop <sha> [--line <sha>]
|
|
11
|
+
* apply
|
|
12
|
+
* saved --head <commit_sha> --paths <a,b,...>
|
|
13
|
+
*
|
|
14
|
+
* Saving and publishing follow the code project's own instructions.
|
|
15
|
+
*/
|
|
16
|
+
import { resolveProjectDir } from "../src/upgrade.mjs";
|
|
17
|
+
import { syncApply, syncCheck, syncSaved } from "../src/sync-core.mjs";
|
|
18
|
+
|
|
19
|
+
const VALUE_FLAGS = new Set(["--develop", "--line", "--head", "--paths"]);
|
|
20
|
+
|
|
21
|
+
function parse(argv) {
|
|
22
|
+
const flags = {};
|
|
23
|
+
const rest = [];
|
|
24
|
+
for (let index = 0; index < argv.length; index++) {
|
|
25
|
+
const arg = argv[index];
|
|
26
|
+
if (VALUE_FLAGS.has(arg)) flags[arg.slice(2)] = argv[++index];
|
|
27
|
+
else if (arg.startsWith("--")) throw new Error(`unknown option '${arg}'`);
|
|
28
|
+
else rest.push(arg);
|
|
29
|
+
}
|
|
30
|
+
return { flags, rest };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const projectDir = resolveProjectDir();
|
|
35
|
+
const { flags, rest } = parse(process.argv.slice(2));
|
|
36
|
+
const [verb, extra] = rest;
|
|
37
|
+
if (extra) throw new Error(`unexpected argument '${extra}'`);
|
|
38
|
+
const allowed = { check: ["develop", "line"], apply: [], saved: ["head", "paths"] }[verb];
|
|
39
|
+
if (!allowed) throw new Error("usage: foldspace sync <check|apply|saved> - see foldspace help sync");
|
|
40
|
+
const stray = Object.keys(flags).find((name) => !allowed.includes(name));
|
|
41
|
+
if (stray) throw new Error(`option '--${stray}' does not go with sync ${verb}`);
|
|
42
|
+
const report =
|
|
43
|
+
verb === "check" ? syncCheck(projectDir, flags) : verb === "apply" ? syncApply(projectDir) : syncSaved(projectDir, flags);
|
|
44
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`sync: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
package/package.json
CHANGED
package/recipes/INDEX.md
CHANGED
|
@@ -8,7 +8,7 @@ Read this whole table, then open the recipe closest to the outcome.
|
|
|
8
8
|
| [`find-by-name`](find-by-name/) | **L2** | The agent turns a name the user said into the id the next action needs; a count when no name is given; every tie when the name is ambiguous; near-misses when nothing matches | A data action. **No card** | 3 production builds |
|
|
9
9
|
| [`lookup-then-view`](lookup-then-view/) | **L2** | Show one account the user names - **start here**. One clear match: its card, no question. Several or none: the **pick-one** list - search, pages, one click - then the card: who it is, the money, open deals, people, open cases. The card action's **required** `accountId`, described as coming from the lookup, makes the agent call the lookup first. The app already hands over the id → `account-overview` | Two actions - an input card (`find_account`) and a view card (`show_account_overview`) - with their Agent Studio definitions word for word | 1 production build |
|
|
10
10
|
| [`pick-from-a-list`](pick-from-a-list/) | **L2** | A clickable list, shown **only** when the user has to choose — with "none of these" and cancel | An input card: the agent waits for the user's answer | 1 production build |
|
|
11
|
-
| [`bottom-bar`](bottom-bar/) | **L0** | The agent's resting entry point is a bar at the foot of the page whose starters change with the screen the user is on
|
|
11
|
+
| [`bottom-bar`](bottom-bar/) | **L0** | The agent's resting entry point is a bar at the foot of the page (Foldspace turns it on) whose starters - at most 3 - change with the screen the user is on, set from code | Page set-up, runs once at start-up | 2 production builds |
|
|
12
12
|
| [`swap-the-login-method`](swap-the-login-method/) | any | `apiFetch` for an app that authenticates with a custom header, or with cookies, instead of a bearer token | A replacement `agent/utils.ts` | 2 production builds (a third uses the default bearer token) |
|
|
13
13
|
| [`account-overview`](account-overview/) | **L2** | One account on a card, when the app already hands over its id: identity, pipeline total, contacts, cases, meetings. The agent gets what the card shows. An account the user names → `lookup-then-view` | A view card: no `callback`, no `awaitUserInput` | 1 production build |
|
|
14
14
|
| [`upload-contacts`](upload-contacts/) | **L4** | A CSV is previewed, then written only after the user chooses Import | An input card: the agent waits for the user's answer | 1 production build |
|
|
@@ -5,17 +5,19 @@ page that shows a few starters and opens the agent when one is clicked. Its
|
|
|
5
5
|
starters change with the screen the user is on, so an agent with two actions
|
|
6
6
|
still looks useful everywhere.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
product's own source, one from
|
|
8
|
+
**Foldspace turns the bar on** from its own settings - this code does not.
|
|
9
|
+
What the code does is pick each page's starters.
|
|
10
|
+
**Proven by 2 production builds** (one in the product's own source, one from
|
|
11
|
+
the agent project behind a feature flag).
|
|
11
12
|
|
|
12
13
|
## Adapt it
|
|
13
14
|
|
|
14
15
|
| In `agent/bottomBar.ts` | Change |
|
|
15
16
|
|---|---|
|
|
16
|
-
| `PAGE_STARTERS` |
|
|
17
|
-
| `
|
|
18
|
-
|
|
17
|
+
| `PAGE_STARTERS` | The product's main-menu screens (`npx foldspace observe menu`: path prefixes) and, for each, **at most 3 starters** the agent can answer today. A starter for something the agent cannot do is worse than none: leave that page out |
|
|
18
|
+
| `ACTION` or `KNOWLEDGE` | `ACTION` when clicking it runs one of the agent's actions - the bar shows the action icon. `KNOWLEDGE` when the answer comes from what the agent knows (its knowledge base) |
|
|
19
|
+
|
|
20
|
+
A page that is not listed shows Agent Studio's starters (`setConversationStarters(null)`).
|
|
19
21
|
|
|
20
22
|
Then call it from the bundle's entry point:
|
|
21
23
|
|
|
@@ -30,14 +32,25 @@ installBottomBar();
|
|
|
30
32
|
- **`setConversationStarters` replaces the whole set for the page** — both
|
|
31
33
|
groups. `null` restores Agent Studio's. So always pass the full list you
|
|
32
34
|
want shown.
|
|
35
|
+
- **It also sets the agent panel's opening screen**, not only the bar. One
|
|
36
|
+
customer's list had 7 there. At most 3 per page, both groups together:
|
|
37
|
+
`npx foldspace lint` refuses more. `maxVisibleStarters` caps the bar only.
|
|
38
|
+
- **Route changes in a single-page app don't reload**, so the starters must be
|
|
39
|
+
re-applied on navigation. The recipe uses the browser's own page-change
|
|
40
|
+
event (`navigation`'s `currententrychange`); where a browser has no
|
|
41
|
+
Navigation API (⚠️ some Safari and Firefox versions, not tried live) it
|
|
42
|
+
wraps `pushState` / `replaceState` and listens to `popstate`. It brings the
|
|
43
|
+
bar back to full only when the starters change; a navigation that keeps the
|
|
44
|
+
same starters leaves a bar that has folded to compact as it is.
|
|
45
|
+
- **An older SDK may lack these calls.** The recipe then logs once in the
|
|
46
|
+
console and leaves Agent Studio's starters.
|
|
47
|
+
- **No `bottomBarSettings`.** Send one only if the human asks for a setting
|
|
48
|
+
that differs from the default (the fields are on the Bottom Bar page), and
|
|
49
|
+
send `enabled: true` in the same group. ⚠️ Whether a partial group merges
|
|
50
|
+
with Foldspace's own settings is not verified.
|
|
33
51
|
- **In embedded mode the bar needs `openEmbeddedCallback`**, or the bar does
|
|
34
52
|
not render at all. This recipe is for the overlay agent; see the docs for
|
|
35
53
|
embedded.
|
|
36
|
-
- **Route changes in a single-page app don't reload**, so the starters must be
|
|
37
|
-
re-applied on navigation - this recipe wraps `pushState` / `replaceState`
|
|
38
|
-
and listens to `popstate`. It brings the bar back to full only when the
|
|
39
|
-
starters change; a navigation that keeps the same starters leaves a bar
|
|
40
|
-
that has folded to compact (after `idleTimeoutMs`) as it is.
|
|
41
54
|
- **Dark mode**: the SDK's default colours do not follow a dark theme yet
|
|
42
55
|
(PLG-5891, in the backlog); a product in dark mode may need `theme` /
|
|
43
56
|
`darkModeSettings` set.
|