@foldspace_npm/harness 0.1.10 → 0.1.12
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 +328 -0
- package/README.md +24 -5
- package/bin/cli.mjs +29 -1
- package/package.json +4 -2
- package/recipes/INDEX.md +15 -0
- package/recipes/README.md +46 -0
- package/recipes/find-by-name/README.md +47 -0
- package/recipes/find-by-name/agent/actions/find_project_id.ts +101 -0
- package/recipes/find-by-name/agent/api/projects.ts +36 -0
- package/recipes/find-by-name/agent/projects.ts +40 -0
- package/recipes/find-by-name/fixtures/projects.all.json +29 -0
- package/recipes/find-by-name/fixtures/projects.empty-account.json +4 -0
- package/recipes/find-by-name/fixtures/projects.none.json +4 -0
- package/recipes/find-by-name/recipe.json +10 -0
- package/recipes/pick-from-a-list/README.md +44 -0
- package/recipes/pick-from-a-list/agent/actions/choose_project.ts +161 -0
- package/recipes/pick-from-a-list/agent/api/projects.ts +36 -0
- package/recipes/pick-from-a-list/agent/projects.ts +40 -0
- package/recipes/pick-from-a-list/agent/views/brand.ts +14 -0
- package/recipes/pick-from-a-list/agent/views/picker.ts +119 -0
- package/recipes/pick-from-a-list/fixtures/projects.all.json +29 -0
- package/recipes/pick-from-a-list/fixtures/projects.empty-account.json +4 -0
- package/recipes/pick-from-a-list/recipe.json +10 -0
- package/recipes/swap-the-login-method/README.md +40 -0
- package/recipes/swap-the-login-method/agent/utils.ts +73 -0
- package/recipes/swap-the-login-method/fixtures/anything.ok.json +8 -0
- package/recipes/swap-the-login-method/recipe.json +12 -0
- package/recipes/swap-the-login-method/variants/utils.cookies.ts +64 -0
- package/recipes/who-is-the-user/README.md +56 -0
- package/recipes/who-is-the-user/agent/identify.ts +89 -0
- package/recipes/who-is-the-user/fixtures/profile.ok.json +6 -0
- package/recipes/who-is-the-user/recipe.json +9 -0
- package/src/cli-help.mjs +1 -0
- package/src/cli-registry.mjs +48 -3
- package/src/init.mjs +16 -8
- package/src/runtime/config.ts +1 -1
- package/src/runtime/http.ts +104 -53
- package/src/runtime/index.ts +4 -1
- package/src/runtime/match.ts +1 -1
- package/src/runtime/render.ts +42 -1
- package/src/upgrade.mjs +469 -0
- package/templates/agent-starter/CLAUDE.md +11 -220
- package/templates/agent-starter/README.md +1 -1
- package/templates/agent-starter/agent/actions/_example.ts +9 -2
- package/templates/agent-starter/agent/utils.ts +2 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Foldspace Agent
|
|
2
|
+
|
|
3
|
+
You are adding an AI agent to a product that already exists. It answers
|
|
4
|
+
questions and *does things* inside that product, with the user's own session.
|
|
5
|
+
|
|
6
|
+
The person you are working with may not be a developer. **You run the
|
|
7
|
+
commands; they sign in and talk to the agent.** Their first session ends at
|
|
8
|
+
something they can **see on their own page** — not at "it builds".
|
|
9
|
+
|
|
10
|
+
## Every session starts the same way
|
|
11
|
+
|
|
12
|
+
1. **Harness version.** Run `npx foldspace help --json` (from an older install,
|
|
13
|
+
`npx --yes @foldspace_npm/harness@latest upgrade --check`). If
|
|
14
|
+
`update.outdated` is true, say the pinned and latest versions and **ask
|
|
15
|
+
before upgrading**; then `foldspace upgrade --yes` and `npm run build`. Never
|
|
16
|
+
upgrade as a side effect of build, attach or implementing an action. If the
|
|
17
|
+
project `CLAUDE.md` does not contain
|
|
18
|
+
`@node_modules/@foldspace_npm/harness/CLAUDE.md`, ask before
|
|
19
|
+
`foldspace upgrade --refresh-instructions`.
|
|
20
|
+
2. **Is Foldspace connected? Check — never assume.** Call `list_agents`:
|
|
21
|
+
|
|
22
|
+
| Result | Do |
|
|
23
|
+
|---|---|
|
|
24
|
+
| The tool does not exist | The Foldspace MCP is not installed in this editor. Give the one install step for *this* editor — Claude Code: `claude plugin install foldspace-agent-builder@foldspace-plugins`; anything else: the MCP block on <https://foldspace.ai/agents.md> — then check again |
|
|
25
|
+
| It exists but fails on auth | Installed, not signed in. Only the human can approve sign-in. Tell them exactly where — Claude Code: `/mcp` → **foldspace** → finish in the browser — then check again |
|
|
26
|
+
| It returns agents | One agent: state its name, Agent Key and product in a line and carry on; the human objects if it is wrong. Several: ask which, as a choice |
|
|
27
|
+
|
|
28
|
+
3. **Orient** — `get_agent_settings`, `list_actions`, `list_task_agents`,
|
|
29
|
+
`list_navigation_routes`. Record current versions before changing anything;
|
|
30
|
+
if something breaks, this is how you prove what moved. Conversation starters
|
|
31
|
+
seeded at sign-up are often a specification someone already wrote.
|
|
32
|
+
4. **Read `docs/app-profile.md`.** It is what is already known about this app,
|
|
33
|
+
with how each fact was established.
|
|
34
|
+
|
|
35
|
+
Before an unfamiliar harness operation, `npx foldspace help --json` is the
|
|
36
|
+
current CLI contract — risk, prerequisites, effects, next step.
|
|
37
|
+
|
|
38
|
+
## Aim at a level, then choose the cheapest experience that reaches it
|
|
39
|
+
|
|
40
|
+
| Level | Reached when |
|
|
41
|
+
|---|---|
|
|
42
|
+
| **L0 · agent live** | injected, visible on the page, `identify` fired |
|
|
43
|
+
| **L1 · read settings** | the user's own account, plan or settings in a widget, showing their real values |
|
|
44
|
+
| **L2 · look up entities** | a search from the user's own words, showing rows |
|
|
45
|
+
|
|
46
|
+
Navigation, uploads, extraction and anything that writes are a later session.
|
|
47
|
+
Say the target out loud, then offer three or four read-only experiences that
|
|
48
|
+
reach it, **cheapest first**: an endpoint the profile already verifies → the
|
|
49
|
+
user's own data with no parameter (L1) → a search with one parameter (L2).
|
|
50
|
+
`discover_actions` gives candidates, not an inventory.
|
|
51
|
+
|
|
52
|
+
**Check row counts before recommending.** An experience on a resource the
|
|
53
|
+
account has zero of is a demo of an empty state. On an empty account the useful
|
|
54
|
+
first action is one that creates data — say so instead.
|
|
55
|
+
|
|
56
|
+
### Ask as a choice, and ask almost nothing
|
|
57
|
+
|
|
58
|
+
Every question to the human is an **enumerated choice with the recommended
|
|
59
|
+
option first** (in Claude Code, `AskUserQuestion`). A first session has four
|
|
60
|
+
decision points: which agent (only if several), the app's hostname, which
|
|
61
|
+
experience first, and whether to publish. The hostname is the only free-text
|
|
62
|
+
question. Everything else — attach mode, folder, name, level — you **state and
|
|
63
|
+
move on**.
|
|
64
|
+
|
|
65
|
+
Then agree the experience in the customer's words, before creating anything:
|
|
66
|
+
*"Show my plan and credits needs one thing the agent can call and one card to
|
|
67
|
+
show it in. Sound right?"*
|
|
68
|
+
|
|
69
|
+
## Find the call yourself
|
|
70
|
+
|
|
71
|
+
Never guess `API_BASE`, `AUTH_SOURCE`, an endpoint or a schema. But guessing
|
|
72
|
+
and asking are not the only options — **observing is, and it comes first**:
|
|
73
|
+
|
|
74
|
+
1. **Already known** — `docs/app-profile.md`. A verified endpoint needs no
|
|
75
|
+
browsing.
|
|
76
|
+
2. **You drive.** `npm run inject`, the human signs in, and *you* go to the
|
|
77
|
+
screen: read the app's bundle for API paths and watch the requests it makes
|
|
78
|
+
(chrome-devtools MCP against the inject Chrome, or a page-context `fetch`,
|
|
79
|
+
**before** `attach` owns the debug port). Read the auth method off a request
|
|
80
|
+
the page already sent — bearer from `localStorage`, cookie, custom header.
|
|
81
|
+
Delegate the watching to a read-only subagent so the traffic does not flood
|
|
82
|
+
your context.
|
|
83
|
+
3. **Ask the human to perform the workflow** — only for what you cannot reach:
|
|
84
|
+
an MFA step, a paywall, a screen that needs a record the account does not
|
|
85
|
+
have, or anything that would write.
|
|
86
|
+
|
|
87
|
+
Signing in is the human's step, because `inject` launches a fresh profile.
|
|
88
|
+
Navigating is yours. **While you drive a signed-in account you are read-only:**
|
|
89
|
+
GET only, never submit a form, never click anything that creates, sends or
|
|
90
|
+
deletes.
|
|
91
|
+
|
|
92
|
+
Capture at least one real HTTP 200 for the data the action needs. If there is
|
|
93
|
+
none, pivot; do not create Foldspace resources. Write what you establish into
|
|
94
|
+
`docs/app-profile.md` **with how you know it**, at that moment. Do not promote
|
|
95
|
+
an assumption by quoting that file later.
|
|
96
|
+
|
|
97
|
+
## Build: objects first, code second
|
|
98
|
+
|
|
99
|
+
Four kinds of thing. Never make the customer learn these words — describe them
|
|
100
|
+
by what they do.
|
|
101
|
+
|
|
102
|
+
| Thing | Answers | Lives in | You write |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| **Action** | *when* should the agent invoke this? | Agent Studio | the contract |
|
|
105
|
+
| **Action handler** | what happens when it does | `agent/actions/<name>.ts` | the code |
|
|
106
|
+
| **Task agent** | *how* to do one reasoning job | Agent Studio | instructions + response schema |
|
|
107
|
+
| **Navigation route** | where the agent can send the user | Agent Studio | a URL template + what the page is for |
|
|
108
|
+
|
|
109
|
+
Knowledge search, navigation and sharing the current tab are platform
|
|
110
|
+
built-ins — no code, and a first experience can lean on them.
|
|
111
|
+
|
|
112
|
+
1. `create_action` as a **draft**. `generate_action_handler` works from a draft
|
|
113
|
+
schema; do not publish yet. Generating the handler from the schema is what
|
|
114
|
+
keeps the two from drifting.
|
|
115
|
+
2. Implement the handler from the observed request and response shapes. A
|
|
116
|
+
handler is `fetch` plus `runTask` and nothing more. Import helpers from
|
|
117
|
+
`agent/utils.ts` — inspect that file before writing another `fetch` or rank
|
|
118
|
+
helper.
|
|
119
|
+
3. Register it in `agent/actions/index.ts` and build (`foldspace build` lints
|
|
120
|
+
first).
|
|
121
|
+
|
|
122
|
+
**Task agents** are for the one LLM subtask deterministic code cannot do —
|
|
123
|
+
extraction, summarisation, classification, normalisation. Not for API calls,
|
|
124
|
+
CRUD, routing or parsing a known shape. Created in Agent Studio, called with
|
|
125
|
+
`runTask({ taskKey, data })` — published, or it does not resolve. `data` carries
|
|
126
|
+
facts, never `prompt` or `instructions`. One owner per concern: extraction
|
|
127
|
+
reads, normalisation converts, nothing does both.
|
|
128
|
+
<https://docs.foldspace.ai/user-guides/task-agents/>
|
|
129
|
+
|
|
130
|
+
**Changing something already live is a production deploy** — immediate, no
|
|
131
|
+
review, no rollback. Never edit it to test. Stand up a `_gen2` beside it, point
|
|
132
|
+
the code at that, and roll back by pointing back. `_gen2`, never `_v2` —
|
|
133
|
+
Foldspace owns the `v` counter. The suffix is permanent.
|
|
134
|
+
|
|
135
|
+
### The moment a handler exists, prove it
|
|
136
|
+
|
|
137
|
+
Do not batch verification to the end. As soon as a handler is registered:
|
|
138
|
+
build, confirm it reached `dist/index.js`, attach, confirm
|
|
139
|
+
`registration_ok`, execute it against the live endpoint, and check the result
|
|
140
|
+
is **not empty**. An empty widget that renders cleanly is not a working
|
|
141
|
+
experience.
|
|
142
|
+
|
|
143
|
+
Say what that proved and what it did not: executing a handler proves the
|
|
144
|
+
handler; only a real chat turn proves the experience (gate 5). Then record the
|
|
145
|
+
endpoint, auth source, response shape and row count in `docs/app-profile.md`,
|
|
146
|
+
with how you established each.
|
|
147
|
+
|
|
148
|
+
## Reachability: everything must be published
|
|
149
|
+
|
|
150
|
+
| | Published to run? | Second gate |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| Action | **yes** | handler registered in `agent/actions/index.ts` |
|
|
153
|
+
| Task agent | **yes** | a `taskKey` in code names it |
|
|
154
|
+
| Navigation route | **yes** | `isActive: true` |
|
|
155
|
+
|
|
156
|
+
Publishing a **new** object is inert; publishing an **edit to a live one** is
|
|
157
|
+
the deploy above. Navigation has **no publish tool on the MCP** and
|
|
158
|
+
`get_navigation_route` returns only `isActive` — publish routes in Agent Studio
|
|
159
|
+
and say so. Route descriptions are a retrieval surface: write what the page is
|
|
160
|
+
*for*, in the user's words, and pair every parameterised route with the lookup
|
|
161
|
+
action that produces its parameter — or ship neither.
|
|
162
|
+
|
|
163
|
+
**Ask before publishing.** It is required only so the copilot can call the
|
|
164
|
+
action, and it is a live product change when the agent has real users.
|
|
165
|
+
|
|
166
|
+
## Local loop
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm run dev
|
|
170
|
+
npm run inject
|
|
171
|
+
npx foldspace attach --daemon
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`inject` launches an isolated Chrome profile and records its debug port; it
|
|
175
|
+
loads nothing. Observe after inject and **before** attach, while chrome-devtools
|
|
176
|
+
MCP can use the same Chrome. `attach` prepares the page and loads the local
|
|
177
|
+
`dist/index.js` over CDP. Coding agents use `--daemon` so the tool returns after
|
|
178
|
+
`[lifecycle] inspect_registration:…`. An empty local registry is valid; `npm run
|
|
179
|
+
build` is still required so `dist/index.js` exists.
|
|
180
|
+
|
|
181
|
+
Attach mode is read off the page, never asked: default swap when the page
|
|
182
|
+
already has the configured product and agent; `--bootstrap` when it has no
|
|
183
|
+
Foldspace SDK; `--replace` when it embeds a different product or agent, or an
|
|
184
|
+
SDK without the configured agent.
|
|
185
|
+
|
|
186
|
+
`inspect_registration:registration_ok` is the signal. On `registration_mismatch`
|
|
187
|
+
read `npx foldspace help attach --json` and map `missingActionNames`,
|
|
188
|
+
`unexpectedActionNames`, `diagnosticError`. While attach runs it owns the debug
|
|
189
|
+
port — `attach --stop` before using chrome-devtools MCP again.
|
|
190
|
+
|
|
191
|
+
Prove a named action through the visible agent: after the human talks to the
|
|
192
|
+
copilot, read the daemon log for `[actions]` SDK callback and local
|
|
193
|
+
execute/render lines. Those record names, statuses, durations and parameter keys
|
|
194
|
+
only — never results or error bodies.
|
|
195
|
+
|
|
196
|
+
## Verification gates
|
|
197
|
+
|
|
198
|
+
Do not report success without all six:
|
|
199
|
+
|
|
200
|
+
1. `npm run typecheck` compiles (`tsc --noEmit -p tsconfig.json`). Do not run
|
|
201
|
+
`npx tsc` — it can install the wrong package.
|
|
202
|
+
2. `foldspace lint` reports no errors (`foldspace build` runs it first).
|
|
203
|
+
3. The handler appears in `dist/index.js`.
|
|
204
|
+
4. The browser reports `inspect_registration:registration_ok`, and for a named
|
|
205
|
+
action the captured registry includes it.
|
|
206
|
+
5. The action behaves against the real workflow: `[actions]
|
|
207
|
+
local-handler:execute` in the attach log after the human exercises the
|
|
208
|
+
visible agent. A widget also needs `local-handler:render` from a real chat
|
|
209
|
+
turn — not a hand-built `render()` call. `runAction` refuses render actions
|
|
210
|
+
and driving `render()` yourself never runs `execute()`.
|
|
211
|
+
6. Browser evidence came from the live target, not hand-authored examples.
|
|
212
|
+
Neighbour fixtures still pass when they exist.
|
|
213
|
+
|
|
214
|
+
## Rules that are not negotiable
|
|
215
|
+
|
|
216
|
+
1. **Handlers return data only.** Never `directive`, `instructions`, or a
|
|
217
|
+
paragraph telling the copilot what to say — those live in Agent Studio.
|
|
218
|
+
Never return `ApiResult.detail` from `execute`; it is for the console.
|
|
219
|
+
Validate parameters and return sanitised errors.
|
|
220
|
+
2. **`render` receives `execute`'s return value**, not the input params. A
|
|
221
|
+
widget that lacks the ids the card needs passes in isolation and fails in the
|
|
222
|
+
real chat. <https://docs.foldspace.ai/guides/in-chat-ui/>
|
|
223
|
+
3. **Say what you verified and what you assumed.** "The docs say X" and "I
|
|
224
|
+
think X" are different claims — mark which. Search `docs.foldspace.ai` over
|
|
225
|
+
MCP (`search_docs`, `fetch_page`) before asserting how the platform behaves;
|
|
226
|
+
cite the page, or say plainly that the docs do not cover it and write down
|
|
227
|
+
what you learned.
|
|
228
|
+
4. **Do not send chat messages to test.** Every message is a real conversation
|
|
229
|
+
in the customer's list. `runTask` creates no conversation record — use it.
|
|
230
|
+
5. **Test mode is off by default.** A new agent has no traffic to pollute, and
|
|
231
|
+
watching real conversations appear is the point of the first run. Arm it
|
|
232
|
+
only when the tenant already has real conversations — and then on **every**
|
|
233
|
+
instance via `armAllInstances`, because `foldspace.agent({apiName})` returns
|
|
234
|
+
the overlay handle, which is not the one serving an embedded chat. Nothing
|
|
235
|
+
can confirm it is on afterwards; never send probe messages to find out.
|
|
236
|
+
6. **The human is not necessarily a developer.** Never hand over a terminal
|
|
237
|
+
line, a stack trace, or a choice between flags. Name experiences by outcome
|
|
238
|
+
— *"show my plan and credits"*, not `get_account_status`. Their two jobs:
|
|
239
|
+
sign in, and talk to the agent.
|
|
240
|
+
7. **Be short.** No recap of what was established, no restating the plan before
|
|
241
|
+
each step. Progress is one line: what you are doing, the level reached, and
|
|
242
|
+
the question if there is one.
|
|
243
|
+
8. **Subagents investigate; they do not build.** A read-only investigation
|
|
244
|
+
that returns a summary belongs in a subagent because it floods context.
|
|
245
|
+
Never delegate a handler or a gate — a summary of a gate is not a gate.
|
|
246
|
+
|
|
247
|
+
## Product defaults
|
|
248
|
+
|
|
249
|
+
Lint catches some of these in handler code; Agent Studio copy is on you.
|
|
250
|
+
|
|
251
|
+
- **No emoji** in copilot replies, cards or handler strings. Put that in the
|
|
252
|
+
agent's Behavior instructions, and in each new action's Studio `instructions`.
|
|
253
|
+
- **Never send the user out of the host app.** No "open in <product>" button,
|
|
254
|
+
no pasted URLs — navigate with a route, same tab.
|
|
255
|
+
|
|
256
|
+
## Layout
|
|
257
|
+
|
|
258
|
+
- `agent/actions/` — one handler per action (`execute`, optional `render`),
|
|
259
|
+
registered in `index.ts`. The key must equal the action key in Agent Studio;
|
|
260
|
+
a mismatch does not error, the action simply never fires.
|
|
261
|
+
- `agent/api/` — one HTTP helper per endpoint
|
|
262
|
+
- `agent/constants.ts` — agent, product, domain, plus empty `API_BASE` /
|
|
263
|
+
`AUTH_SOURCE` and `LOAD_MODE`
|
|
264
|
+
- `agent/utils.ts` — configures the harness runtime and re-exports it. **The
|
|
265
|
+
only import surface for actions.**
|
|
266
|
+
- `agent/views/` — branded cards
|
|
267
|
+
- `docs/app-profile.md` — what is known about this app, and how it was established
|
|
268
|
+
- `foldspace.dev.json` — local harness target configuration
|
|
269
|
+
- `CLAUDE.md` — this file, imported — plus what is specific to this product
|
|
270
|
+
|
|
271
|
+
Do not introduce another bundler or bundle format.
|
|
272
|
+
|
|
273
|
+
## Working examples to copy from
|
|
274
|
+
|
|
275
|
+
Before writing a handler or a card, read
|
|
276
|
+
`node_modules/@foldspace_npm/harness/recipes/INDEX.md` — the whole table, it is
|
|
277
|
+
short — and open the recipe closest to the outcome. Each is a finished
|
|
278
|
+
piece of a build, generalised from production builds with the customer taken
|
|
279
|
+
out: the API helper, the handler or card, and a recorded response per state. Copy its `agent/` files across, then replace the
|
|
280
|
+
placeholder path and fields with what you observed. They match the harness
|
|
281
|
+
version you have installed, and every one is built and run on each release.
|
|
282
|
+
|
|
283
|
+
L0 → `who-is-the-user`. L2 → `find-by-name`, and `pick-from-a-list` when the
|
|
284
|
+
user has to choose. App does not use a bearer token → `swap-the-login-method`.
|
|
285
|
+
Each says how many production builds carry the pattern and what they learned.
|
|
286
|
+
If nothing fits — there is no L1 card recipe yet — say so and build from
|
|
287
|
+
`agent/actions/_example.ts`.
|
|
288
|
+
|
|
289
|
+
## What you already have
|
|
290
|
+
|
|
291
|
+
Import from `../utils`. `generate_action_handler` may emit a skeleton that does
|
|
292
|
+
not import it — fix the import when you implement.
|
|
293
|
+
|
|
294
|
+
| Helper | Use when | Do not use when |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| `apiFetch` / `apiFetchBinary` | The app's own JSON/file API, after a real 200 | Public marketing hosts (`publicFetch`); custom auth headers |
|
|
297
|
+
| `publicFetch` | Unauthenticated / marketing origin | Signed-in product APIs |
|
|
298
|
+
| `getAuthToken` / `parseJwt` | `AUTH_SOURCE` is `localStorage` or `cookie` | Custom header schemes — override `apiFetch` in `utils.ts` |
|
|
299
|
+
| `rankBy` | User typed a name; API search is exact or ignored | Domain ranking (invoices, coverage, industry classification codes) |
|
|
300
|
+
| `renderLoading` / `Empty` / `Error` / `Fatal` | Chatterblock states | Branded cards — those stay in `agent/views/` |
|
|
301
|
+
| `renderFailure` | Any `{ ok: false }` from `apiFetch` — it picks the state from `reason` | You need bespoke copy for one reason; then branch on `reason` yourself |
|
|
302
|
+
| `httpFailure` / `networkFailure` | Your own `apiFetch` (custom header, cookies) — keeps the same `reason`s | The default `apiFetch` fits; then you never call these |
|
|
303
|
+
| `getAgent` | Talking to Foldspace | You need a specific instance — then `agentIds()` |
|
|
304
|
+
| `armAllInstances` | Attach/bootstrap setup | Inside `execute` |
|
|
305
|
+
| `redact` / `mapWithConcurrency` | Logging tokens; batching fetches | — |
|
|
306
|
+
|
|
307
|
+
Every failed call carries a `reason` — `signed_out`, `forbidden`, `not_found`,
|
|
308
|
+
`rate_limited`, `timeout`, `unreachable`, `config`, `server`. Branch on that,
|
|
309
|
+
not on `status`, and do not re-detect it. The session you observed while
|
|
310
|
+
building is not the session at run time: **expect `signed_out`** and let
|
|
311
|
+
`renderFailure` show it. `not_found` and zero rows are both `renderEmpty` with
|
|
312
|
+
the query named — never an error, never a blank card.
|
|
313
|
+
|
|
314
|
+
If the observed auth is not Bearer + `localStorage`/`cookie`, stop re-exporting
|
|
315
|
+
that one function and keep the rest:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
export { getAgent, rankBy, renderEmpty } from "@foldspace_npm/harness/runtime";
|
|
319
|
+
export { apiFetch } from "./api";
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Safety
|
|
323
|
+
|
|
324
|
+
- Do not commit secrets, cookies, HAR files, browser storage, or
|
|
325
|
+
`.foldspace-dev/`. Nothing transient goes in `docs/`.
|
|
326
|
+
- Local handler changes are not cloud publication.
|
|
327
|
+
- Creating or publishing Foldspace resources requires explicit approval.
|
|
328
|
+
- Action keys and parameter names must match the published schema.
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ constraint this package exists to protect.
|
|
|
26
26
|
### What is genuinely shared
|
|
27
27
|
|
|
28
28
|
- `agent/actions/*` and `agent/api/*` — a handler is `fetch` plus `runTask`,
|
|
29
|
-
nothing more.
|
|
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
32
|
- `foldspace deploy` — publish to `agent/actions/<env>/<productId>/<agentApiName>`
|
|
@@ -97,7 +97,7 @@ Non-interactive / CI form:
|
|
|
97
97
|
npx --yes @foldspace_npm/harness init foldspace-agent \
|
|
98
98
|
--product-id FR8JUQZAQRZB \
|
|
99
99
|
--agent-key my-agent \
|
|
100
|
-
--domain app.example.com \
|
|
100
|
+
--app-domain app.example.com \
|
|
101
101
|
--name "My Agent"
|
|
102
102
|
```
|
|
103
103
|
|
|
@@ -107,14 +107,15 @@ When running directly from a harness checkout during development:
|
|
|
107
107
|
node bin/cli.mjs init ../foldspace-agent \
|
|
108
108
|
--product-id FR8JUQZAQRZB \
|
|
109
109
|
--agent-key my-agent \
|
|
110
|
-
--domain app.example.com
|
|
110
|
+
--app-domain app.example.com
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
The Agent Key is the value shown in Agent Studio, such as `my-agent`. It is
|
|
114
114
|
not the sidecar directory name. `--agent-api-name` remains a deprecated alias
|
|
115
|
-
for `--agent-key`. `--
|
|
115
|
+
for `--agent-key`. `--domain` remains a deprecated alias for `--app-domain`.
|
|
116
|
+
`--name` is optional and defaults to the target directory
|
|
116
117
|
name (`foldspace-agent` unless you pass a directory). The product ID must be
|
|
117
|
-
the bare ID, not the `EU-…-1-1` SDK loader key. The domain may be a hostname
|
|
118
|
+
the bare ID, not the `EU-…-1-1` SDK loader key. The app domain may be a hostname
|
|
118
119
|
or an HTTP(S) URL without a port or path.
|
|
119
120
|
|
|
120
121
|
For safety, `init` requires a target path that does not exist. It does not
|
|
@@ -132,6 +133,24 @@ npm run attach
|
|
|
132
133
|
The generated npm scripts intentionally remain the normal project interface;
|
|
133
134
|
`foldspace init` is the one-time project creation command.
|
|
134
135
|
|
|
136
|
+
Generated `CLAUDE.md` imports platform instructions from
|
|
137
|
+
`@foldspace_npm/harness` instead of copying them. Existing projects can switch
|
|
138
|
+
to that import with `foldspace upgrade --refresh-instructions`.
|
|
139
|
+
|
|
140
|
+
### Keep the harness current
|
|
141
|
+
|
|
142
|
+
Tenant projects pin an exact `@foldspace_npm/harness` version. Publishing a
|
|
143
|
+
newer package does not move them. Check, ask the user, then bump:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npx --yes @foldspace_npm/harness@latest upgrade --check
|
|
147
|
+
foldspace upgrade --yes # only after the user agrees
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`--check` (and a non-interactive `upgrade` with no `--yes`) never installs.
|
|
151
|
+
`--yes` rewrites the exact pin and runs `npm install --ignore-scripts`. Rebuild
|
|
152
|
+
afterward; that is still not a deploy.
|
|
153
|
+
|
|
135
154
|
### Lint handlers before they ship
|
|
136
155
|
|
|
137
156
|
`foldspace build` runs `foldspace lint` first. Errors skip bundling; warnings
|
package/bin/cli.mjs
CHANGED
|
@@ -5,6 +5,12 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { runInit } from "../src/init.mjs";
|
|
8
|
+
import {
|
|
9
|
+
checkForUpdate,
|
|
10
|
+
nagIfOutdated,
|
|
11
|
+
resolveProjectDir,
|
|
12
|
+
runUpgrade,
|
|
13
|
+
} from "../src/upgrade.mjs";
|
|
8
14
|
import {
|
|
9
15
|
classifyAttachControl,
|
|
10
16
|
runAttachControl,
|
|
@@ -56,10 +62,13 @@ function printHelp(topic, json) {
|
|
|
56
62
|
);
|
|
57
63
|
return;
|
|
58
64
|
}
|
|
65
|
+
const update = checkForUpdate(resolveProjectDir());
|
|
59
66
|
if (json) {
|
|
60
|
-
|
|
67
|
+
const payload = topic ? document : { ...document, update };
|
|
68
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
61
69
|
return;
|
|
62
70
|
}
|
|
71
|
+
nagIfOutdated(update);
|
|
63
72
|
console.log(
|
|
64
73
|
topic ? renderCommandHelp(registry, topic) : renderGeneralHelp(registry),
|
|
65
74
|
);
|
|
@@ -118,6 +127,7 @@ const [command, ...args] = process.argv.slice(2);
|
|
|
118
127
|
if (!command || command === "--help" || command === "-h") {
|
|
119
128
|
printHelp(null, args.includes("--json"));
|
|
120
129
|
} else if (command === "--version" || command === "-v") {
|
|
130
|
+
nagIfOutdated(checkForUpdate(resolveProjectDir()));
|
|
121
131
|
console.log(
|
|
122
132
|
`${registry.package.name} ${registry.package.version} ` +
|
|
123
133
|
`(protocol ${registry.protocolVersion}, CLI schema ${registry.schemaVersion})`,
|
|
@@ -149,6 +159,24 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
149
159
|
fail(error instanceof Error ? error.message : String(error));
|
|
150
160
|
}
|
|
151
161
|
}
|
|
162
|
+
} else if (command === "upgrade") {
|
|
163
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
164
|
+
printHelp("upgrade", args.includes("--json"));
|
|
165
|
+
} else {
|
|
166
|
+
try {
|
|
167
|
+
const normalized = normalizeCommandArgs(commandByName("upgrade"), args);
|
|
168
|
+
Promise.resolve()
|
|
169
|
+
.then(() => runUpgrade(normalized))
|
|
170
|
+
.then((report) => {
|
|
171
|
+
console.log(JSON.stringify(report, null, 2));
|
|
172
|
+
})
|
|
173
|
+
.catch((error) => {
|
|
174
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
152
180
|
} else if (commandByName(command)) {
|
|
153
181
|
if (args.includes("--help") || args.includes("-h")) {
|
|
154
182
|
printHelp(command, args.includes("--json"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldspace_npm/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Build and verify portable Foldspace action artifacts against a live app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"bin",
|
|
22
22
|
"src",
|
|
23
23
|
"templates",
|
|
24
|
-
"
|
|
24
|
+
"recipes",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CLAUDE.md"
|
|
25
27
|
],
|
|
26
28
|
"dependencies": {
|
|
27
29
|
"esbuild": "^0.20.0",
|
package/recipes/INDEX.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Recipe index
|
|
2
|
+
|
|
3
|
+
Read this whole table, then open the recipe closest to the outcome.
|
|
4
|
+
|
|
5
|
+
| Recipe | Level | What it gives | Kind | Proven by |
|
|
6
|
+
|---|---|---|---|---|
|
|
7
|
+
| [`who-is-the-user`](who-is-the-user/) | **L0** | Foldspace knows who is signed in — id, email, name, role, subscription — so conversations and analytics are not anonymous | Runs once at start-up. No action, no card | 4 production builds |
|
|
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
|
+
| [`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 action with a card that waits for the user | 1 production build |
|
|
10
|
+
| [`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 (3 builds, 3 different schemes) |
|
|
11
|
+
|
|
12
|
+
**Not here yet — no production build proves it:** the user's own plan or
|
|
13
|
+
settings drawn as a card (L1). In production that data is sent to `identify`
|
|
14
|
+
(`who-is-the-user`), not rendered. Navigation (L3), writes (L4), uploads and
|
|
15
|
+
task agents are also still to come.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Finished pieces of a build to **read and adapt** — each one generalised from a
|
|
4
|
+
pattern that is live in production builds, with the customer taken out.
|
|
5
|
+
`_example.ts` is an empty skeleton; these are complete.
|
|
6
|
+
|
|
7
|
+
They ship inside the harness on purpose: a recipe always matches the runtime it
|
|
8
|
+
imports, and every one is installed into a freshly scaffolded project, built,
|
|
9
|
+
typechecked and run against its fixtures on every harness change
|
|
10
|
+
(`test/recipes.test.mjs`). A recipe that stops working fails the release instead
|
|
11
|
+
of a customer session.
|
|
12
|
+
|
|
13
|
+
## Use one
|
|
14
|
+
|
|
15
|
+
1. Read `INDEX.md` and pick the closest outcome.
|
|
16
|
+
2. Copy its `agent/` files into your project's `agent/` — the layout is the
|
|
17
|
+
same, so the imports already resolve.
|
|
18
|
+
3. Replace every `__observe_me` placeholder — paths, storage keys, claim names —
|
|
19
|
+
with what you **observed** in the live app, and record how you know in
|
|
20
|
+
`docs/app-profile.md`. The placeholders are deliberate: none can succeed.
|
|
21
|
+
4. Register actions in `agent/actions/index.ts` under the **same key** as in
|
|
22
|
+
Agent Studio, then run the six gates.
|
|
23
|
+
|
|
24
|
+
If nothing here fits, say so and build from `_example.ts`.
|
|
25
|
+
|
|
26
|
+
## Where they come from
|
|
27
|
+
|
|
28
|
+
Each recipe's README says how many production builds carry the pattern and what
|
|
29
|
+
those builds learned the hard way. **What is taken is the shape** — the order of
|
|
30
|
+
calls, what is returned to the agent, where the traps are. **What is not taken**
|
|
31
|
+
is any customer's name, endpoint, identifier, styling or code. Contracts are the
|
|
32
|
+
documented ones: `execute(params)` and
|
|
33
|
+
`render(data, host, header, callback, cancel)`
|
|
34
|
+
(<https://docs.foldspace.ai/guides/in-chat-ui/>), `foldspace.identify(context)`
|
|
35
|
+
(<https://docs.foldspace.ai/start/user-context/>), and the harness runtime.
|
|
36
|
+
|
|
37
|
+
Error handling is **not** taken from those builds. They predate `reason`: one
|
|
38
|
+
turns every failure into "unexpected error", another reports a missing session
|
|
39
|
+
as "no projects found". Recipes use the runtime's failure reasons instead.
|
|
40
|
+
|
|
41
|
+
## What every recipe demonstrates
|
|
42
|
+
|
|
43
|
+
- `execute` returns **data and facts** — never `detail`, never instructions.
|
|
44
|
+
- A missing or expired session is `signed_out`, not an empty result.
|
|
45
|
+
- DOM is built with `textContent`, never `innerHTML` with API data, and styles
|
|
46
|
+
go in `header` — `host` lives inside the component's frame.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Find by name — L2
|
|
2
|
+
|
|
3
|
+
**The user says** "add it to the Maple Street project". **The agent needs** an id.
|
|
4
|
+
|
|
5
|
+
**Proven by 3 production builds**, all the same shape: a name from the user's
|
|
6
|
+
words → the app's own list → a ranked answer **back to the agent as data**. None
|
|
7
|
+
of them draws a card. A resolver answers questions; it never asks one. When the
|
|
8
|
+
user genuinely has to choose, that is a different action —
|
|
9
|
+
[`pick-from-a-list`](../pick-from-a-list/).
|
|
10
|
+
|
|
11
|
+
In Agent Studio: an action with key `find_project_id` and one optional string
|
|
12
|
+
parameter `name` — *the name, or part of the name, the user said*.
|
|
13
|
+
|
|
14
|
+
## What it returns
|
|
15
|
+
|
|
16
|
+
| The user's words | The agent gets |
|
|
17
|
+
|---|---|
|
|
18
|
+
| no name ("how many projects do I have?") | the account's **count**, plus a small sample — never the whole account |
|
|
19
|
+
| one clear match | that project, its match strength, and up to three alternatives |
|
|
20
|
+
| several equally good matches | **all of them**, marked `ambiguous` — never a silent pick |
|
|
21
|
+
| nothing matches | `success: false`, plus near-misses for a typo |
|
|
22
|
+
| the account is empty | a count of zero — not an error |
|
|
23
|
+
| signed out, no access, throttled, unreachable | `success: false` with the `reason` |
|
|
24
|
+
|
|
25
|
+
## Adapt it
|
|
26
|
+
|
|
27
|
+
| File | Change |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `agent/api/projects.ts` | Path, parameter names, envelope — from a captured response |
|
|
30
|
+
| `agent/projects.ts` | What a summary carries, and which keys a user might name out loud |
|
|
31
|
+
| `agent/actions/find_project_id.ts` | The entity. The logic rarely needs touching |
|
|
32
|
+
|
|
33
|
+
## What those builds learned the hard way
|
|
34
|
+
|
|
35
|
+
- **Hand back every tie.** Picking one of two equally good matches is how a
|
|
36
|
+
later action changes the wrong project.
|
|
37
|
+
- **Count on the server.** Fetching every row to count them is the slowest thing
|
|
38
|
+
an agent can do, and it puts the whole account into the model's context.
|
|
39
|
+
- **Return summaries, not rows.** Two of the three builds send ten raw fields
|
|
40
|
+
per row to the agent; the third sends three and is the one that scales.
|
|
41
|
+
- **A missing session is not "no projects".** One build reports a missing token
|
|
42
|
+
as an empty list, and the agent cheerfully tells a signed-out user they have
|
|
43
|
+
none.
|
|
44
|
+
- **Server search is usually a plain substring**, so a typo finds nothing. Try
|
|
45
|
+
the near-miss pass before saying no.
|
|
46
|
+
- **Probe with a name a real record has.** A search that returns zero proves
|
|
47
|
+
nothing unless a matching record exists.
|