@foldspace_npm/harness 0.1.10 → 0.1.11
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 +235 -0
- package/README.md +23 -4
- package/bin/cli.mjs +29 -1
- package/package.json +3 -2
- package/src/cli-help.mjs +1 -0
- package/src/cli-registry.mjs +48 -3
- package/src/init.mjs +16 -8
- package/src/upgrade.mjs +469 -0
- package/templates/agent-starter/CLAUDE.md +11 -220
- package/templates/agent-starter/README.md +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Foldspace Agent
|
|
2
|
+
|
|
3
|
+
## Harness version
|
|
4
|
+
|
|
5
|
+
At the start of a session, run `npx foldspace help --json` (or, from an older
|
|
6
|
+
install, `npx --yes @foldspace_npm/harness@latest upgrade --check`).
|
|
7
|
+
|
|
8
|
+
If `update.outdated` is true, tell the user the pinned and latest versions and
|
|
9
|
+
**ask before upgrading**. After they agree, run `foldspace upgrade --yes`, then
|
|
10
|
+
`npm run build`. Do not upgrade as a side effect of build, attach, or
|
|
11
|
+
implementing an action. A harness bump is not a deploy.
|
|
12
|
+
|
|
13
|
+
If the tenant `CLAUDE.md` does not contain
|
|
14
|
+
`@node_modules/@foldspace_npm/harness/CLAUDE.md`, ask before
|
|
15
|
+
`foldspace upgrade --refresh-instructions`. That replaces the snapshot with an
|
|
16
|
+
import of this file.
|
|
17
|
+
|
|
18
|
+
## Before building
|
|
19
|
+
|
|
20
|
+
1. Run `npx foldspace help --json` before an unfamiliar harness operation.
|
|
21
|
+
Treat its risk, prerequisites, effects, next-step, and `update` fields as
|
|
22
|
+
the current CLI contract.
|
|
23
|
+
2. Connect Product MCP and verify that `list_agents` works.
|
|
24
|
+
3. Inspect the selected agent with `get_agent_settings`, `list_actions`, and
|
|
25
|
+
`list_task_agents`.
|
|
26
|
+
4. Use `discover_actions` to identify candidate experiences, then let the user
|
|
27
|
+
choose what to build. Treat those ideas as candidates, not an inventory.
|
|
28
|
+
|
|
29
|
+
## Build workflow
|
|
30
|
+
|
|
31
|
+
An experience can require several reusable actions. Separate lookup actions
|
|
32
|
+
from actions that read or mutate a selected resource.
|
|
33
|
+
|
|
34
|
+
1. Agree on the candidate experience and how its actions would compose. This is
|
|
35
|
+
not a publish and not a commitment.
|
|
36
|
+
2. Run `npm run inject`. Ask the user to sign in and perform the real target
|
|
37
|
+
workflow in that Chrome window.
|
|
38
|
+
3. Capture at least one real HTTP 200 for the data the action needs. Use
|
|
39
|
+
chrome-devtools MCP against the inject Chrome, or a page-context `fetch`,
|
|
40
|
+
**before** `attach` owns the debug port. If there is no 200,
|
|
41
|
+
pivot; do not create Foldspace resources. Write what you establish into
|
|
42
|
+
`docs/app-profile.md` with **how you know it**. Do not promote an assumption
|
|
43
|
+
by quoting that file.
|
|
44
|
+
4. Create action metadata as a draft. `generate_action_handler` works from a
|
|
45
|
+
draft schema; do not publish yet.
|
|
46
|
+
5. Implement the handler using the observed request and response shapes.
|
|
47
|
+
Import helpers from `agent/utils.ts` — inspect that file before writing
|
|
48
|
+
another `fetch` or rank helper.
|
|
49
|
+
6. Register the handler in `agent/actions/index.ts` and build (`foldspace build` lints first).
|
|
50
|
+
7. Run `npx foldspace attach --daemon` (add `--bootstrap` or `--replace` when
|
|
51
|
+
the page requires it). An empty local registry is valid if you only want to
|
|
52
|
+
see how the agent works.
|
|
53
|
+
8. Ask before publishing. Publishing is required only so the copilot can call
|
|
54
|
+
the action, and it is a live product change when the agent has real users.
|
|
55
|
+
9. Complete the verification gates below.
|
|
56
|
+
|
|
57
|
+
## API rules
|
|
58
|
+
|
|
59
|
+
Actions execute in the user's signed-in browser session.
|
|
60
|
+
|
|
61
|
+
- Import HTTP, ranking, and widget helpers from `../utils`, not a new
|
|
62
|
+
`agent/api.ts` copy of `fetch`.
|
|
63
|
+
- Do not guess `API_BASE` or `AUTH_SOURCE`. Capture at least one real 200
|
|
64
|
+
(and the auth header the page actually sends) before filling them in.
|
|
65
|
+
`credentials: "include"` is correct only when you have observed the app
|
|
66
|
+
using cookies that way. Many apps use `Authorization: Bearer` from
|
|
67
|
+
`localStorage` instead; some use a custom header.
|
|
68
|
+
- Never guess endpoints or schemas. Capture at least one real 200 before
|
|
69
|
+
implementing a parser. Do not implement a path that was not observed.
|
|
70
|
+
- Verify that the user is signed in before observing a workflow.
|
|
71
|
+
- Do not substitute a public developer API when the browser session is missing;
|
|
72
|
+
ask the user to sign in.
|
|
73
|
+
- Validate parameters and return sanitized errors. Return **data only** — never
|
|
74
|
+
`directive`, `instructions`, or a paragraph telling the copilot what to say.
|
|
75
|
+
Action and agent instructions live in Agent Studio / MCP. Never return
|
|
76
|
+
`ApiResult.detail` from `execute` — it is for the console.
|
|
77
|
+
- Actions that return data the user will inspect should include a `render`
|
|
78
|
+
function for in-chat UI (a chatterblock). If it makes more sense to output the data
|
|
79
|
+
in a UI component instead of text then consider using render to show a component.
|
|
80
|
+
- `render` receives **`execute`'s return value**, not the action's input params.
|
|
81
|
+
Returning anything that lacks the ids the card needs is why widgets pass in
|
|
82
|
+
isolation and fail in the real chat.
|
|
83
|
+
- `runAction` refuses render actions (`cannot be executed silently`). Driving
|
|
84
|
+
`render()` yourself never runs `execute()`, so it does not test that contract.
|
|
85
|
+
- Check https://docs.foldspace.ai/guides/in-chat-ui/ for more information
|
|
86
|
+
|
|
87
|
+
## Task agents
|
|
88
|
+
|
|
89
|
+
Use a Task Agent when the handler needs a one-time LLM subtask that
|
|
90
|
+
deterministic code cannot do well: extraction, summarization,
|
|
91
|
+
classification, normalization, enrichment, or generation.
|
|
92
|
+
|
|
93
|
+
Do not use a Task Agent for API calls, CRUD, routing, or parsing a
|
|
94
|
+
known response shape. Those stay in `execute`.
|
|
95
|
+
|
|
96
|
+
Task agents are created in Agent Studio, not in this repo. Ask before
|
|
97
|
+
creating or publishing one. Call a published task agent from the
|
|
98
|
+
handler with `runTask({ taskKey, data })` (if not published the runTask won't work).
|
|
99
|
+
`data` carries extracted facts only — not `prompt` / `instructions` strings.
|
|
100
|
+
Prefer JSON output when the handler must consume the result.
|
|
101
|
+
|
|
102
|
+
See https://docs.foldspace.ai/user-guides/task-agents/ and
|
|
103
|
+
https://docs.foldspace.ai/reference/task-agent-api/
|
|
104
|
+
|
|
105
|
+
## Local harness loop
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run dev
|
|
109
|
+
npm run inject
|
|
110
|
+
npx foldspace attach --daemon
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`inject` launches an isolated Chrome profile and records its debug port.
|
|
114
|
+
It does not generate or load an application extension. Observe the customer's
|
|
115
|
+
workflow after inject and **before** attach, while chrome-devtools MCP can use
|
|
116
|
+
the same Chrome. `attach` prepares the page and loads the local `dist/index.js`
|
|
117
|
+
bundle through CDP. Coding agents must use `--daemon` so the invoking tool
|
|
118
|
+
returns after `[lifecycle] inspect_registration:…`; foreground `npm run attach`
|
|
119
|
+
is for humans watching the terminal. An empty local registry is valid. `npm run
|
|
120
|
+
build` is still required so `dist/index.js` exists.
|
|
121
|
+
|
|
122
|
+
Use the default swap only when the page already has the configured product and
|
|
123
|
+
agent. Use `--bootstrap` only when the page has no Foldspace SDK, and
|
|
124
|
+
`--replace` when it embeds a different product or agent, or when an SDK is
|
|
125
|
+
present without the configured agent.
|
|
126
|
+
|
|
127
|
+
The attach log must report `inspect_registration:registration_ok` before
|
|
128
|
+
treating the page as registered. Zero captured actions is success when the
|
|
129
|
+
local registry is empty. On `registration_mismatch`, read
|
|
130
|
+
`npx foldspace help attach --json` diagnostics and map those names onto the
|
|
131
|
+
lifecycle details (`missingActionNames`, `unexpectedActionNames`,
|
|
132
|
+
`diagnosticError`).
|
|
133
|
+
|
|
134
|
+
While attach is running it owns the debug port. Detach (`attach --stop`, or
|
|
135
|
+
Ctrl-C in the foreground) before using chrome-devtools MCP against the same
|
|
136
|
+
Chrome.
|
|
137
|
+
|
|
138
|
+
Prove a named action through the visible agent. After the user talks to the
|
|
139
|
+
copilot, read the daemon log for `[actions]` SDK callback and local
|
|
140
|
+
execute/render lines; do not invoke the handler directly. Those lines record
|
|
141
|
+
names, statuses, durations, and parameter keys only — not results or error
|
|
142
|
+
bodies.
|
|
143
|
+
|
|
144
|
+
## Product defaults
|
|
145
|
+
|
|
146
|
+
These are not product-specific. Lint can catch some of them in handler code;
|
|
147
|
+
Agent Studio copy is on you.
|
|
148
|
+
|
|
149
|
+
- **No emoji** in copilot replies, cards, or handler strings. Put that in the
|
|
150
|
+
agent's Behavior instructions (MCP cannot write that field, so each new
|
|
151
|
+
action's Studio `instructions` must carry the line too).
|
|
152
|
+
- **Never send the user out of the host app.** No "open in <product>"
|
|
153
|
+
button and no pasted URLs — navigate with a navigation route, same tab.
|
|
154
|
+
- **Check row counts before choosing the experience.** On an empty account the
|
|
155
|
+
useful first action is one that creates data.
|
|
156
|
+
|
|
157
|
+
## Verification gates
|
|
158
|
+
|
|
159
|
+
Do not report success without all six:
|
|
160
|
+
|
|
161
|
+
1. TypeScript compiles with `npm run typecheck` (`tsc --noEmit -p tsconfig.json`).
|
|
162
|
+
Do not run `npx tsc` — that can install the wrong package.
|
|
163
|
+
2. `foldspace lint` reports no errors (`foldspace build` runs this first).
|
|
164
|
+
3. The expected handler appears in `dist/index.js`.
|
|
165
|
+
4. The browser reports `inspect_registration:registration_ok`. For a named
|
|
166
|
+
action, the captured registry includes that handler.
|
|
167
|
+
5. The action behaves correctly against the real target workflow. Confirm
|
|
168
|
+
`[actions] local-handler:execute` in the attach log after the user exercises
|
|
169
|
+
the visible agent. For a widget, that log must include `local-handler:render`
|
|
170
|
+
from a real chat turn — not a hand-built `render()` call.
|
|
171
|
+
6. Browser evidence came from the live target, not from hand-authored examples.
|
|
172
|
+
Neighbour fixtures still pass when they exist.
|
|
173
|
+
|
|
174
|
+
## Layout
|
|
175
|
+
|
|
176
|
+
- `agent/actions/` — one handler per action (`execute`, optional `render`),
|
|
177
|
+
registered in `index.ts`
|
|
178
|
+
- `agent/api/` — one HTTP helper per endpoint
|
|
179
|
+
- `agent/constants.ts` — agent, product, domain, plus empty `API_BASE` /
|
|
180
|
+
`AUTH_SOURCE` and `LOAD_MODE`
|
|
181
|
+
- `agent/utils.ts` — configure the harness runtime and re-export it. **This is
|
|
182
|
+
the only import surface for actions.**
|
|
183
|
+
- `foldspace.dev.json` — local harness target configuration
|
|
184
|
+
- `docs/app-profile.md` — what is known about this app, and how it was established
|
|
185
|
+
- `CLAUDE.md` — this file, imported — plus what is specific to this product
|
|
186
|
+
|
|
187
|
+
Do not introduce another bundler or bundle format.
|
|
188
|
+
|
|
189
|
+
## What you already have
|
|
190
|
+
|
|
191
|
+
Import from `../utils`. Inspect that file before implementing another
|
|
192
|
+
general-purpose helper. MCP `generate_action_handler` may still emit a
|
|
193
|
+
skeleton that does not import it — fix the import when you implement.
|
|
194
|
+
|
|
195
|
+
| Helper | Use when | Do not use when |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `apiFetch` / `apiFetchBinary` | The app's own JSON/file API, after a real 200 | Public marketing hosts (`publicFetch`); custom auth headers |
|
|
198
|
+
| `publicFetch` | Unauthenticated / marketing origin | Signed-in product APIs |
|
|
199
|
+
| `getAuthToken` / `parseJwt` | `AUTH_SOURCE` is `localStorage` or `cookie` | Custom header schemes — override `apiFetch` in `utils.ts` |
|
|
200
|
+
| `rankBy` | User typed a name; API search is exact or ignored | Domain ranking (invoices, coverage, MasterFormat) |
|
|
201
|
+
| `renderLoading` / `Empty` / `Error` / `Fatal` | Chatterblock empty/error paths | Branded cards — those stay in `agent/views/` |
|
|
202
|
+
| `getAgent` | Talking to Foldspace | You need a specific instance — then `agentIds()` |
|
|
203
|
+
| `armAllInstances` | Attach/bootstrap setup | Inside `execute` (can ship into `dist`; attach already arms test mode) |
|
|
204
|
+
| `redact` / `mapWithConcurrency` | Logging tokens; batching fetches | — |
|
|
205
|
+
|
|
206
|
+
If the observed auth is not Bearer + `localStorage`/`cookie`, stop re-exporting
|
|
207
|
+
that one function and keep the rest:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
export { getAgent, rankBy, renderEmpty } from "@foldspace_npm/harness/runtime";
|
|
211
|
+
export { apiFetch } from "./api";
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Agent learnings
|
|
215
|
+
|
|
216
|
+
`docs/app-profile.md` is the durable record of this app. Fill it as you probe,
|
|
217
|
+
not afterwards. Other notes in `docs/` are fine for session-specific lessons.
|
|
218
|
+
|
|
219
|
+
- Before similar work, read `docs/app-profile.md` and any other notes in `docs/`.
|
|
220
|
+
- After non-obvious discoveries, add them to the profile (or a short extra
|
|
221
|
+
note) covering verified gotchas, failed approaches, and how they were
|
|
222
|
+
established.
|
|
223
|
+
- Keep notes concise and evidence-based.
|
|
224
|
+
- Do not store secrets, cookies, HAR files, browser storage, or other transient
|
|
225
|
+
session data in `docs/`.
|
|
226
|
+
|
|
227
|
+
## Safety
|
|
228
|
+
|
|
229
|
+
- Do not commit secrets, cookies, HAR files, browser storage, or
|
|
230
|
+
`.foldspace-dev/`.
|
|
231
|
+
- Local handler changes are not cloud publication.
|
|
232
|
+
- Creating or publishing Foldspace resources requires explicit approval.
|
|
233
|
+
- Action keys and parameter names must match the published schema.
|
|
234
|
+
- Search Foldspace documentation before asserting unfamiliar platform
|
|
235
|
+
behaviour.
|
package/README.md
CHANGED
|
@@ -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.11",
|
|
4
4
|
"description": "Build and verify portable Foldspace action artifacts against a live app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"bin",
|
|
22
22
|
"src",
|
|
23
23
|
"templates",
|
|
24
|
-
"README.md"
|
|
24
|
+
"README.md",
|
|
25
|
+
"CLAUDE.md"
|
|
25
26
|
],
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"esbuild": "^0.20.0",
|
package/src/cli-help.mjs
CHANGED
|
@@ -46,6 +46,7 @@ export function renderGeneralHelp(registry) {
|
|
|
46
46
|
" foldspace help --json Print the machine-readable CLI contract",
|
|
47
47
|
" foldspace help <command> --json Print one command contract",
|
|
48
48
|
" foldspace --version Show package and protocol versions",
|
|
49
|
+
" foldspace upgrade --check Compare the installed pin to npm latest",
|
|
49
50
|
"",
|
|
50
51
|
"attach loads local actions and observes the normal agent experience.",
|
|
51
52
|
"deploy is a separate remote publication step.",
|
package/src/cli-registry.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
21
21
|
group: "start",
|
|
22
22
|
summary: "Create a configured Foldspace actions project",
|
|
23
23
|
usage:
|
|
24
|
-
"foldspace init [<directory>] [--product-id <id>] [--agent-key <key>] [--domain <host>] [--name <display-name>]",
|
|
24
|
+
"foldspace init [<directory>] [--product-id <id>] [--agent-key <key>] [--app-domain <host>] [--name <display-name>]",
|
|
25
25
|
risk: "local-write",
|
|
26
26
|
environment: "node",
|
|
27
27
|
environmentVariables: [],
|
|
@@ -41,8 +41,9 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
41
41
|
required: "non-interactive",
|
|
42
42
|
deprecatedAliases: ["--agent-api-name"],
|
|
43
43
|
}),
|
|
44
|
-
value("--domain", "host", "
|
|
44
|
+
value("--app-domain", "host", "Live app hostname or HTTP(S) URL", {
|
|
45
45
|
required: "non-interactive",
|
|
46
|
+
deprecatedAliases: ["--domain"],
|
|
46
47
|
}),
|
|
47
48
|
value("--name", "display-name", "Display name", {
|
|
48
49
|
default: "directory name",
|
|
@@ -51,7 +52,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
51
52
|
prerequisites: [
|
|
52
53
|
"Node 20 or newer",
|
|
53
54
|
"A target directory that does not already exist",
|
|
54
|
-
"Non-interactive use requires directory, product ID, Agent Key, and domain",
|
|
55
|
+
"Non-interactive use requires directory, product ID, Agent Key, and app domain",
|
|
55
56
|
],
|
|
56
57
|
effects: ["Creates a new local project; never initializes Git"],
|
|
57
58
|
next: [
|
|
@@ -61,6 +62,48 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
61
62
|
"Run foldspace attach",
|
|
62
63
|
],
|
|
63
64
|
}),
|
|
65
|
+
Object.freeze({
|
|
66
|
+
name: "upgrade",
|
|
67
|
+
entry: null,
|
|
68
|
+
group: "start",
|
|
69
|
+
summary: "Check for a newer harness and update the exact pin after asking",
|
|
70
|
+
usage:
|
|
71
|
+
"foldspace upgrade [--check | --yes] [--refresh-instructions]",
|
|
72
|
+
risk: "local-write",
|
|
73
|
+
environment: "node",
|
|
74
|
+
environmentVariables: [
|
|
75
|
+
"FOLDSPACE_PROJECT_DIR",
|
|
76
|
+
"FOLDSPACE_SKIP_UPDATE_CHECK",
|
|
77
|
+
],
|
|
78
|
+
capabilities: ["project.upgrade"],
|
|
79
|
+
positionals: [],
|
|
80
|
+
options: [
|
|
81
|
+
flag("--check", "Compare versions and print JSON; never install"),
|
|
82
|
+
flag(
|
|
83
|
+
"--yes",
|
|
84
|
+
"Install the latest exact pin after the user has already agreed",
|
|
85
|
+
),
|
|
86
|
+
flag(
|
|
87
|
+
"--refresh-instructions",
|
|
88
|
+
"Replace CLAUDE.md with an import of the package instructions",
|
|
89
|
+
),
|
|
90
|
+
],
|
|
91
|
+
prerequisites: [
|
|
92
|
+
"A consumer project with package.json",
|
|
93
|
+
"An exact @foldspace_npm/harness dependency unless only refreshing instructions",
|
|
94
|
+
],
|
|
95
|
+
effects: [
|
|
96
|
+
"Without --yes, writes nothing except an optional update cache",
|
|
97
|
+
"With --yes, rewrites the exact harness pin and runs npm install --ignore-scripts",
|
|
98
|
+
"Does not rebuild dist/index.js or deploy",
|
|
99
|
+
"--refresh-instructions copies CLAUDE.md to CLAUDE.md.bak then writes the stub",
|
|
100
|
+
],
|
|
101
|
+
next: [
|
|
102
|
+
"If outdated, ask the user, then run foldspace upgrade --yes",
|
|
103
|
+
"Run npm run build after a pin bump",
|
|
104
|
+
"Deploy only if product users should receive the rebuilt runtime",
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
64
107
|
Object.freeze({
|
|
65
108
|
name: "build",
|
|
66
109
|
entry: "build-cli.mjs",
|
|
@@ -272,6 +315,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
272
315
|
|
|
273
316
|
export const CAPABILITY_CATALOGUE = Object.freeze([
|
|
274
317
|
["project.scaffold", "Create a new configured project on local disk"],
|
|
318
|
+
["project.upgrade", "Update the consuming project's exact harness pin"],
|
|
275
319
|
["artifact.build", "Build the portable dist/index.js action artifact"],
|
|
276
320
|
["browser.launch", "Launch an isolated local Chrome profile"],
|
|
277
321
|
["browser.cdp", "Connect to local Chrome through CDP"],
|
|
@@ -342,6 +386,7 @@ export function createCliRegistry({ packageName, packageVersion }) {
|
|
|
342
386
|
"The harness bin alias is equivalent to foldspace.",
|
|
343
387
|
"attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
|
|
344
388
|
"Coding agents should run attach --daemon; foreground attach is for humans watching the terminal.",
|
|
389
|
+
"If update.outdated is true, ask the user before foldspace upgrade --yes.",
|
|
345
390
|
],
|
|
346
391
|
};
|
|
347
392
|
}
|
package/src/init.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const allowedFlags = new Set([
|
|
|
13
13
|
"product-id",
|
|
14
14
|
"agent-key",
|
|
15
15
|
"agent-api-name",
|
|
16
|
+
"app-domain",
|
|
16
17
|
"domain",
|
|
17
18
|
]);
|
|
18
19
|
const tokenPattern = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
@@ -86,13 +87,18 @@ export function parseInitArgs(argv) {
|
|
|
86
87
|
"Use --agent-key only; --agent-api-name is its deprecated alias.",
|
|
87
88
|
);
|
|
88
89
|
}
|
|
90
|
+
if (flags["app-domain"] && flags.domain) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Use --app-domain only; --domain is its deprecated alias.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
89
95
|
|
|
90
96
|
return {
|
|
91
97
|
directory: positional[0] || null,
|
|
92
98
|
displayName: flags.name || null,
|
|
93
99
|
productId: flags["product-id"] || null,
|
|
94
100
|
agentApiName: flags["agent-key"] || flags["agent-api-name"] || null,
|
|
95
|
-
domain: flags.domain || null,
|
|
101
|
+
domain: flags["app-domain"] || flags.domain || null,
|
|
96
102
|
};
|
|
97
103
|
}
|
|
98
104
|
|
|
@@ -101,7 +107,7 @@ function missingInitFields(parsed) {
|
|
|
101
107
|
if (!parsed.directory) missing.push("directory");
|
|
102
108
|
if (!parsed.productId) missing.push("product-id");
|
|
103
109
|
if (!parsed.agentApiName) missing.push("agent-key");
|
|
104
|
-
if (!parsed.domain) missing.push("domain");
|
|
110
|
+
if (!parsed.domain) missing.push("app-domain");
|
|
105
111
|
return missing;
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -134,14 +140,14 @@ function validateProductId(value) {
|
|
|
134
140
|
function normalizeTarget(value) {
|
|
135
141
|
const raw = value.trim();
|
|
136
142
|
if (!raw || raw.includes("*")) {
|
|
137
|
-
throw new Error("
|
|
143
|
+
throw new Error("App domain must be a concrete hostname without a wildcard.");
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
let url;
|
|
141
147
|
try {
|
|
142
148
|
url = new URL(raw.includes("://") ? raw : `https://${raw}`);
|
|
143
149
|
} catch {
|
|
144
|
-
throw new Error(`Invalid domain: ${value}`);
|
|
150
|
+
throw new Error(`Invalid app domain: ${value}`);
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
if (
|
|
@@ -153,12 +159,12 @@ function normalizeTarget(value) {
|
|
|
153
159
|
url.search ||
|
|
154
160
|
url.hash
|
|
155
161
|
) {
|
|
156
|
-
throw new Error("
|
|
162
|
+
throw new Error("App domain must contain only an HTTP(S) hostname, without credentials, a port, path, query, or fragment.");
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
const domain = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
160
166
|
if (!domain || domain.includes("..")) {
|
|
161
|
-
throw new Error(`Invalid domain: ${value}`);
|
|
167
|
+
throw new Error(`Invalid app domain: ${value}`);
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
return {
|
|
@@ -227,7 +233,7 @@ async function promptForMissingFields(parsed, options = {}) {
|
|
|
227
233
|
|
|
228
234
|
if (!next.domain) {
|
|
229
235
|
next.domain = await askUntilValid(
|
|
230
|
-
"
|
|
236
|
+
"App domain",
|
|
231
237
|
(value) => {
|
|
232
238
|
normalizeTarget(value);
|
|
233
239
|
return value.trim();
|
|
@@ -255,7 +261,7 @@ function finalizeInitConfig(parsed) {
|
|
|
255
261
|
for (const [key, label] of [
|
|
256
262
|
["productId", "product-id"],
|
|
257
263
|
["agentApiName", "agent-key"],
|
|
258
|
-
["domain", "domain"],
|
|
264
|
+
["domain", "app-domain"],
|
|
259
265
|
]) {
|
|
260
266
|
if (!parsed[key]) {
|
|
261
267
|
throw new Error(`Missing required option: --${label}\nUsage: ${initUsage}`);
|
|
@@ -304,7 +310,9 @@ function createTemplateValues(config, harnessVersion) {
|
|
|
304
310
|
DISPLAY_NAME: displayName,
|
|
305
311
|
PACKAGE_NAME: packageName,
|
|
306
312
|
PACKAGE_DESCRIPTION_JSON: JSON.stringify(`Foldspace browser actions for ${displayName}.`),
|
|
313
|
+
PRODUCT_ID: productId,
|
|
307
314
|
PRODUCT_ID_JSON: JSON.stringify(productId),
|
|
315
|
+
AGENT_API_NAME: agentApiName,
|
|
308
316
|
AGENT_API_NAME_JSON: JSON.stringify(agentApiName),
|
|
309
317
|
APP_DOMAIN: target.domain,
|
|
310
318
|
APP_DOMAIN_JSON: JSON.stringify(target.domain),
|
package/src/upgrade.mjs
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input } from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
export const HARNESS_PACKAGE = "@foldspace_npm/harness";
|
|
9
|
+
export const INSTRUCTION_IMPORT = `@node_modules/${HARNESS_PACKAGE}/CLAUDE.md`;
|
|
10
|
+
export const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
11
|
+
export const NPM_VIEW_TIMEOUT_MS = 2000;
|
|
12
|
+
export const UPGRADE_NEXT = "Ask the user, then run foldspace upgrade --yes";
|
|
13
|
+
|
|
14
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const instructionTemplatePath = path.join(
|
|
16
|
+
packageRoot,
|
|
17
|
+
"templates",
|
|
18
|
+
"agent-starter",
|
|
19
|
+
"CLAUDE.md",
|
|
20
|
+
);
|
|
21
|
+
const tokenPattern = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
22
|
+
|
|
23
|
+
export function resolveProjectDir(env = process.env, cwd = process.cwd()) {
|
|
24
|
+
return env.FOLDSPACE_PROJECT_DIR || cwd;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseUpgradeArgs(argv) {
|
|
28
|
+
const flags = new Set();
|
|
29
|
+
for (const token of argv) {
|
|
30
|
+
if (token === "--check" || token === "--yes" || token === "--refresh-instructions") {
|
|
31
|
+
if (flags.has(token)) {
|
|
32
|
+
throw new Error(`option '${token}' was provided twice`);
|
|
33
|
+
}
|
|
34
|
+
flags.add(token);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`unknown option '${token}'`);
|
|
38
|
+
}
|
|
39
|
+
if (flags.has("--check") && flags.has("--yes")) {
|
|
40
|
+
throw new Error("options '--check' and '--yes' cannot be used together");
|
|
41
|
+
}
|
|
42
|
+
if (flags.has("--check") && flags.has("--refresh-instructions")) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"options '--check' and '--refresh-instructions' cannot be used together",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
checkOnly: flags.has("--check"),
|
|
49
|
+
yes: flags.has("--yes"),
|
|
50
|
+
refreshInstructions: flags.has("--refresh-instructions"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeVersion(spec) {
|
|
55
|
+
if (typeof spec !== "string" || !spec.trim()) return null;
|
|
56
|
+
const trimmed = spec.trim();
|
|
57
|
+
if (trimmed.startsWith("file:") || trimmed.startsWith("link:") || trimmed.startsWith("github:")) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const match = trimmed.match(/^[~^]?(\d+\.\d+\.\d+)/);
|
|
61
|
+
return match ? match[1] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function compareVersions(left, right) {
|
|
65
|
+
const a = parseSemver(left);
|
|
66
|
+
const b = parseSemver(right);
|
|
67
|
+
if (!a || !b) return 0;
|
|
68
|
+
for (let index = 0; index < 3; index += 1) {
|
|
69
|
+
if (a[index] > b[index]) return 1;
|
|
70
|
+
if (a[index] < b[index]) return -1;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseSemver(value) {
|
|
76
|
+
const version = normalizeVersion(value);
|
|
77
|
+
if (!version) return null;
|
|
78
|
+
return version.split(".").map((part) => Number(part));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isBehind(current, latest) {
|
|
82
|
+
if (!current || !latest) return false;
|
|
83
|
+
return compareVersions(current, latest) < 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readJson(filePath) {
|
|
87
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeJson(filePath, value) {
|
|
91
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function readPackageManifest(projectDir) {
|
|
95
|
+
const manifestPath = path.join(projectDir, "package.json");
|
|
96
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
97
|
+
try {
|
|
98
|
+
return readJson(manifestPath);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isHarnessSourceTree(projectDir) {
|
|
105
|
+
const manifest = readPackageManifest(projectDir);
|
|
106
|
+
return Boolean(
|
|
107
|
+
manifest?.name === HARNESS_PACKAGE &&
|
|
108
|
+
fs.existsSync(path.join(projectDir, "templates", "agent-starter")),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function readPinnedVersion(manifest) {
|
|
113
|
+
if (!manifest || typeof manifest !== "object") return null;
|
|
114
|
+
const spec =
|
|
115
|
+
manifest.devDependencies?.[HARNESS_PACKAGE] ||
|
|
116
|
+
manifest.dependencies?.[HARNESS_PACKAGE] ||
|
|
117
|
+
null;
|
|
118
|
+
return normalizeVersion(spec);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function readInstalledVersion(projectDir) {
|
|
122
|
+
const installedPath = path.join(
|
|
123
|
+
projectDir,
|
|
124
|
+
"node_modules",
|
|
125
|
+
HARNESS_PACKAGE,
|
|
126
|
+
"package.json",
|
|
127
|
+
);
|
|
128
|
+
if (!fs.existsSync(installedPath)) return null;
|
|
129
|
+
try {
|
|
130
|
+
return normalizeVersion(readJson(installedPath).version);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hasLiveInstructionImport(content) {
|
|
137
|
+
return typeof content === "string" && content.includes(INSTRUCTION_IMPORT);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function instructionValuesFromProject(projectDir) {
|
|
141
|
+
const configPath = path.join(projectDir, "foldspace.dev.json");
|
|
142
|
+
if (!fs.existsSync(configPath)) {
|
|
143
|
+
throw new Error("foldspace.dev.json is required to refresh CLAUDE.md");
|
|
144
|
+
}
|
|
145
|
+
const config = readJson(configPath);
|
|
146
|
+
const targetName = config.defaultTarget;
|
|
147
|
+
const target = config.targets?.[targetName];
|
|
148
|
+
if (!target?.productId || !target?.agentApiName) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`foldspace.dev.json target '${targetName || "default"}' is missing productId or agentApiName`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
let domain = null;
|
|
154
|
+
if (typeof target.startUrl === "string") {
|
|
155
|
+
try {
|
|
156
|
+
domain = new URL(target.startUrl).hostname;
|
|
157
|
+
} catch {
|
|
158
|
+
domain = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!domain && Array.isArray(target.hosts) && target.hosts[0]) {
|
|
162
|
+
domain = String(target.hosts[0]).replace(/^\*\./, "");
|
|
163
|
+
}
|
|
164
|
+
if (!domain) {
|
|
165
|
+
throw new Error("Could not resolve the app domain from foldspace.dev.json");
|
|
166
|
+
}
|
|
167
|
+
const manifest = readPackageManifest(projectDir);
|
|
168
|
+
return {
|
|
169
|
+
DISPLAY_NAME: manifest?.name || path.basename(projectDir),
|
|
170
|
+
PRODUCT_ID: String(target.productId),
|
|
171
|
+
AGENT_API_NAME: String(target.agentApiName),
|
|
172
|
+
APP_DOMAIN: domain,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function renderInstructionStub(values, template = fs.readFileSync(instructionTemplatePath, "utf8")) {
|
|
177
|
+
const rendered = template.replace(tokenPattern, (_match, key) => {
|
|
178
|
+
if (!(key in values)) {
|
|
179
|
+
throw new Error(`Missing value for {{${key}}} in CLAUDE.md stub`);
|
|
180
|
+
}
|
|
181
|
+
return values[key];
|
|
182
|
+
});
|
|
183
|
+
const unresolved = rendered.match(tokenPattern);
|
|
184
|
+
if (unresolved) {
|
|
185
|
+
throw new Error(`Unresolved template token ${unresolved[0]} in CLAUDE.md stub`);
|
|
186
|
+
}
|
|
187
|
+
return rendered;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function refreshInstructionStub(projectDir, options = {}) {
|
|
191
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
192
|
+
const previous = fs.existsSync(claudePath)
|
|
193
|
+
? fs.readFileSync(claudePath, "utf8")
|
|
194
|
+
: null;
|
|
195
|
+
const next = renderInstructionStub(
|
|
196
|
+
options.values || instructionValuesFromProject(projectDir),
|
|
197
|
+
options.template,
|
|
198
|
+
);
|
|
199
|
+
if (previous === next) {
|
|
200
|
+
return { written: false, backedUp: false, importPresent: true };
|
|
201
|
+
}
|
|
202
|
+
let backedUp = false;
|
|
203
|
+
if (previous !== null) {
|
|
204
|
+
fs.writeFileSync(path.join(projectDir, "CLAUDE.md.bak"), previous);
|
|
205
|
+
backedUp = true;
|
|
206
|
+
}
|
|
207
|
+
fs.writeFileSync(claudePath, next);
|
|
208
|
+
return { written: true, backedUp, importPresent: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function cachePath(projectDir) {
|
|
212
|
+
return path.join(projectDir, ".foldspace-dev", "harness-update.json");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readCache(projectDir, now) {
|
|
216
|
+
const filePath = cachePath(projectDir);
|
|
217
|
+
if (!fs.existsSync(filePath)) return null;
|
|
218
|
+
try {
|
|
219
|
+
const cached = readJson(filePath);
|
|
220
|
+
if (typeof cached.checkedAt !== "number" || typeof cached.latest !== "string") {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (now - cached.checkedAt > CACHE_TTL_MS) return null;
|
|
224
|
+
return cached;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writeCache(projectDir, latest, now) {
|
|
231
|
+
const dir = path.dirname(cachePath(projectDir));
|
|
232
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
233
|
+
writeJson(cachePath(projectDir), { checkedAt: now, latest });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function fetchLatestFromNpm(options = {}) {
|
|
237
|
+
const spawn = options.spawnSyncFn || spawnSync;
|
|
238
|
+
const result = spawn("npm", ["view", HARNESS_PACKAGE, "version"], {
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
timeout: options.timeoutMs ?? NPM_VIEW_TIMEOUT_MS,
|
|
241
|
+
env: options.env || process.env,
|
|
242
|
+
});
|
|
243
|
+
if (result.error || result.status !== 0) return null;
|
|
244
|
+
return normalizeVersion(result.stdout);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function nextAction(report) {
|
|
248
|
+
if (report.status === "outdated") {
|
|
249
|
+
return report.instructionsImport === false
|
|
250
|
+
? `${UPGRADE_NEXT}. If CLAUDE.md is still a snapshot, also pass --refresh-instructions`
|
|
251
|
+
: UPGRADE_NEXT;
|
|
252
|
+
}
|
|
253
|
+
if (report.status === "upgraded") {
|
|
254
|
+
return "Run npm run build. Deploy only if product users should receive the rebuilt runtime.";
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function checkForUpdate(projectDir, options = {}) {
|
|
260
|
+
const env = options.env || process.env;
|
|
261
|
+
const now = options.now ?? Date.now();
|
|
262
|
+
const manifest = readPackageManifest(projectDir);
|
|
263
|
+
const harnessSource = isHarnessSourceTree(projectDir);
|
|
264
|
+
const pinned = harnessSource ? null : readPinnedVersion(manifest);
|
|
265
|
+
const installed = harnessSource
|
|
266
|
+
? normalizeVersion(manifest?.version)
|
|
267
|
+
: readInstalledVersion(projectDir);
|
|
268
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
269
|
+
const instructionsImport = fs.existsSync(claudePath)
|
|
270
|
+
? hasLiveInstructionImport(fs.readFileSync(claudePath, "utf8"))
|
|
271
|
+
: false;
|
|
272
|
+
|
|
273
|
+
const skipped =
|
|
274
|
+
options.skipCheck === true ||
|
|
275
|
+
(options.skipCheck !== false && env.FOLDSPACE_SKIP_UPDATE_CHECK === "1");
|
|
276
|
+
const consumer = !harnessSource && Boolean(pinned || installed);
|
|
277
|
+
|
|
278
|
+
let latest = null;
|
|
279
|
+
let status = "current";
|
|
280
|
+
|
|
281
|
+
if (!consumer) {
|
|
282
|
+
status = "not_a_consumer";
|
|
283
|
+
} else if (skipped) {
|
|
284
|
+
status = "check_skipped";
|
|
285
|
+
} else {
|
|
286
|
+
const cached = options.cache === false ? null : readCache(projectDir, now);
|
|
287
|
+
if (cached?.latest) {
|
|
288
|
+
latest = cached.latest;
|
|
289
|
+
} else {
|
|
290
|
+
const fetched =
|
|
291
|
+
typeof options.fetchLatest === "function"
|
|
292
|
+
? options.fetchLatest()
|
|
293
|
+
: fetchLatestFromNpm(options);
|
|
294
|
+
latest = normalizeVersion(fetched);
|
|
295
|
+
if (latest && options.cache !== false) {
|
|
296
|
+
writeCache(projectDir, latest, now);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!latest) {
|
|
300
|
+
status = "check_skipped";
|
|
301
|
+
} else {
|
|
302
|
+
const outdated = isBehind(pinned, latest) || isBehind(installed, latest);
|
|
303
|
+
status = outdated ? "outdated" : "current";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const report = {
|
|
308
|
+
kind: "foldspace.cli.update",
|
|
309
|
+
package: HARNESS_PACKAGE,
|
|
310
|
+
pinned,
|
|
311
|
+
installed,
|
|
312
|
+
latest,
|
|
313
|
+
outdated: status === "outdated",
|
|
314
|
+
status,
|
|
315
|
+
instructionsImport,
|
|
316
|
+
next: null,
|
|
317
|
+
};
|
|
318
|
+
report.next = nextAction(report);
|
|
319
|
+
return report;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function writeExactPin(projectDir, version) {
|
|
323
|
+
const manifestPath = path.join(projectDir, "package.json");
|
|
324
|
+
const manifest = readPackageManifest(projectDir);
|
|
325
|
+
if (!manifest) {
|
|
326
|
+
throw new Error(`package.json not found in ${projectDir}`);
|
|
327
|
+
}
|
|
328
|
+
if (manifest.dependencies?.[HARNESS_PACKAGE] !== undefined) {
|
|
329
|
+
manifest.dependencies[HARNESS_PACKAGE] = version;
|
|
330
|
+
} else {
|
|
331
|
+
manifest.devDependencies = manifest.devDependencies || {};
|
|
332
|
+
manifest.devDependencies[HARNESS_PACKAGE] = version;
|
|
333
|
+
}
|
|
334
|
+
writeJson(manifestPath, manifest);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function defaultInstall(projectDir) {
|
|
338
|
+
execFileSync("npm", ["install", "--ignore-scripts"], {
|
|
339
|
+
cwd: projectDir,
|
|
340
|
+
stdio: "inherit",
|
|
341
|
+
env: process.env,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isInteractive(options = {}) {
|
|
346
|
+
if (typeof options.interactive === "boolean") return options.interactive;
|
|
347
|
+
return Boolean(options.stdin?.isTTY ?? input.isTTY);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isAffirmative(value) {
|
|
351
|
+
const normalized = value.trim().toLowerCase();
|
|
352
|
+
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function ask(question, options = {}) {
|
|
356
|
+
if (typeof options.ask === "function") return options.ask(question);
|
|
357
|
+
const rl = readline.createInterface({
|
|
358
|
+
input: options.stdin || input,
|
|
359
|
+
output: options.stderr || process.stderr,
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
return await rl.question(question);
|
|
363
|
+
} finally {
|
|
364
|
+
rl.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function runUpgrade(argv, options = {}) {
|
|
369
|
+
const parsed = parseUpgradeArgs(argv);
|
|
370
|
+
const projectDir = options.projectDir || resolveProjectDir(options.env);
|
|
371
|
+
const log = options.log || ((message) => {
|
|
372
|
+
console.error(message);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (isHarnessSourceTree(projectDir) && (parsed.yes || parsed.refreshInstructions)) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"this directory is the harness source, not a consumer project",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const report = checkForUpdate(projectDir, options);
|
|
382
|
+
|
|
383
|
+
if (parsed.checkOnly || (!parsed.yes && !isInteractive(options) && !parsed.refreshInstructions)) {
|
|
384
|
+
return report;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (parsed.refreshInstructions && !parsed.yes && report.status !== "outdated") {
|
|
388
|
+
const refreshed = refreshInstructionStub(projectDir, options);
|
|
389
|
+
return {
|
|
390
|
+
...report,
|
|
391
|
+
status: refreshed.written ? "instructions_refreshed" : "current",
|
|
392
|
+
instructionsImport: true,
|
|
393
|
+
refreshed: refreshed.written,
|
|
394
|
+
backedUp: refreshed.backedUp,
|
|
395
|
+
next: refreshed.written
|
|
396
|
+
? "Imported platform instructions from the installed harness."
|
|
397
|
+
: report.next,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (report.status === "not_a_consumer") {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`no ${HARNESS_PACKAGE} dependency found in ${projectDir}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (parsed.yes && report.status === "check_skipped") {
|
|
408
|
+
throw new Error("cannot upgrade without a registry version");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let shouldInstall = parsed.yes && report.status === "outdated";
|
|
412
|
+
let shouldRefresh = parsed.refreshInstructions;
|
|
413
|
+
|
|
414
|
+
if (!parsed.yes && !parsed.checkOnly && isInteractive(options) && report.status === "outdated") {
|
|
415
|
+
log(
|
|
416
|
+
`${HARNESS_PACKAGE} ${report.pinned || report.installed} -> ${report.latest} is available`,
|
|
417
|
+
);
|
|
418
|
+
const answer = await ask("Upgrade the exact pin in this project? [Y/n] ", options);
|
|
419
|
+
shouldInstall = isAffirmative(answer);
|
|
420
|
+
if (!shouldInstall) {
|
|
421
|
+
return { ...report, status: "declined", next: UPGRADE_NEXT };
|
|
422
|
+
}
|
|
423
|
+
if (!report.instructionsImport && !shouldRefresh) {
|
|
424
|
+
const refreshAnswer = await ask(
|
|
425
|
+
"Replace CLAUDE.md with an import of the package instructions? [Y/n] ",
|
|
426
|
+
options,
|
|
427
|
+
);
|
|
428
|
+
shouldRefresh = isAffirmative(refreshAnswer);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!shouldInstall && !shouldRefresh) {
|
|
433
|
+
return report;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (shouldInstall) {
|
|
437
|
+
if (!report.latest) {
|
|
438
|
+
throw new Error("cannot upgrade without a registry version");
|
|
439
|
+
}
|
|
440
|
+
writeExactPin(projectDir, report.latest);
|
|
441
|
+
const install = options.install || defaultInstall;
|
|
442
|
+
install(projectDir, report.latest);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let refreshed = { written: false, backedUp: false, importPresent: report.instructionsImport };
|
|
446
|
+
if (shouldRefresh) {
|
|
447
|
+
refreshed = refreshInstructionStub(projectDir, options);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const nextReport = {
|
|
451
|
+
...report,
|
|
452
|
+
pinned: shouldInstall ? report.latest : report.pinned,
|
|
453
|
+
installed: shouldInstall ? report.latest : report.installed,
|
|
454
|
+
outdated: false,
|
|
455
|
+
status: shouldInstall ? "upgraded" : refreshed.written ? "instructions_refreshed" : report.status,
|
|
456
|
+
instructionsImport: shouldRefresh ? true : report.instructionsImport,
|
|
457
|
+
refreshed: refreshed.written,
|
|
458
|
+
backedUp: refreshed.backedUp,
|
|
459
|
+
};
|
|
460
|
+
nextReport.next = nextAction(nextReport);
|
|
461
|
+
return nextReport;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function nagIfOutdated(report, write = (message) => console.error(message)) {
|
|
465
|
+
if (!report?.outdated) return;
|
|
466
|
+
write(
|
|
467
|
+
`foldspace: ${HARNESS_PACKAGE} ${report.pinned || report.installed} is behind ${report.latest}. ${UPGRADE_NEXT}.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
@@ -1,225 +1,16 @@
|
|
|
1
|
-
# Foldspace
|
|
1
|
+
# Foldspace actions for {{APP_DOMAIN}}
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
@node_modules/@foldspace_npm/harness/CLAUDE.md
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
changes there.
|
|
5
|
+
If that import did not resolve, read `node_modules/@foldspace_npm/harness/CLAUDE.md`
|
|
6
|
+
before building. What follows is only what is true of **this** product.
|
|
8
7
|
|
|
9
|
-
##
|
|
8
|
+
## This product
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`list_task_agents`.
|
|
17
|
-
4. Use `discover_actions` to identify candidate experiences, then let the user
|
|
18
|
-
choose what to build. Treat those ideas as candidates, not an inventory.
|
|
10
|
+
| | |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Agent | `{{AGENT_API_NAME}}` |
|
|
13
|
+
| Product | `{{PRODUCT_ID}}` |
|
|
14
|
+
| App | `{{APP_DOMAIN}}` |
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
An experience can require several reusable actions. Separate lookup actions
|
|
23
|
-
from actions that read or mutate a selected resource.
|
|
24
|
-
|
|
25
|
-
1. Agree on the candidate experience and how its actions would compose. This is
|
|
26
|
-
not a publish and not a commitment.
|
|
27
|
-
2. Run `npm run inject`. Ask the user to sign in and perform the real target
|
|
28
|
-
workflow in that Chrome window.
|
|
29
|
-
3. Capture at least one real HTTP 200 for the data the action needs. Use
|
|
30
|
-
chrome-devtools MCP against the inject Chrome, or a page-context `fetch`,
|
|
31
|
-
**before** `attach` owns the debug port. If there is no 200,
|
|
32
|
-
pivot; do not create Foldspace resources. Write what you establish into
|
|
33
|
-
`docs/app-profile.md` with **how you know it**. Do not promote an assumption
|
|
34
|
-
by quoting that file.
|
|
35
|
-
4. Create action metadata as a draft. `generate_action_handler` works from the
|
|
36
|
-
draft schema; do not publish yet.
|
|
37
|
-
5. Implement the handler using the observed request and response shapes.
|
|
38
|
-
Import helpers from `agent/utils.ts` — inspect that file before writing
|
|
39
|
-
another `fetch` or rank helper.
|
|
40
|
-
6. Register the handler in `agent/actions/index.ts` and build (`foldspace build` lints first).
|
|
41
|
-
7. Run `npx foldspace attach --daemon` (add `--bootstrap` or `--replace` when
|
|
42
|
-
the page requires it). An empty local registry is valid if you only want to
|
|
43
|
-
see how the agent works.
|
|
44
|
-
8. Ask before publishing. Publishing is required only so the copilot can call
|
|
45
|
-
the action, and it is a live product change when the agent has real users.
|
|
46
|
-
9. Complete the verification gates below.
|
|
47
|
-
|
|
48
|
-
## API rules
|
|
49
|
-
|
|
50
|
-
Actions execute in the user's signed-in browser session.
|
|
51
|
-
|
|
52
|
-
- Import HTTP, ranking, and widget helpers from `../utils`, not a new
|
|
53
|
-
`agent/api.ts` copy of `fetch`.
|
|
54
|
-
- Do not guess `API_BASE` or `AUTH_SOURCE`. Capture at least one real 200
|
|
55
|
-
(and the auth header the page actually sends) before filling them in.
|
|
56
|
-
`credentials: "include"` is correct only when you have observed the app
|
|
57
|
-
using cookies that way. Many apps use `Authorization: Bearer` from
|
|
58
|
-
`localStorage` instead; some use a custom header.
|
|
59
|
-
- Never guess endpoints or schemas. Capture at least one real 200 before
|
|
60
|
-
implementing a parser. Do not implement a path that was not observed.
|
|
61
|
-
- Verify that the user is signed in before observing a workflow.
|
|
62
|
-
- Do not substitute a public developer API when the browser session is missing;
|
|
63
|
-
ask the user to sign in.
|
|
64
|
-
- Validate parameters and return sanitized errors. Return **data only** — never
|
|
65
|
-
`directive`, `instructions`, or a paragraph telling the copilot what to say.
|
|
66
|
-
Action and agent instructions live in Agent Studio / MCP. Never return
|
|
67
|
-
`ApiResult.detail` from `execute` — it is for the console.
|
|
68
|
-
- Actions that return data the user will inspect should include a `render`
|
|
69
|
-
function for in-chat UI (a chatterblock). If it makes more sense to output the data
|
|
70
|
-
in a UI component instead of text then consider using render to show a component.
|
|
71
|
-
- `render` receives **`execute`'s return value**, not the action's input params.
|
|
72
|
-
Returning anything that lacks the ids the card needs is why widgets pass in
|
|
73
|
-
isolation and fail in the real chat.
|
|
74
|
-
- `runAction` refuses render actions (`cannot be executed silently`). Driving
|
|
75
|
-
`render()` yourself never runs `execute()`, so it does not test that contract.
|
|
76
|
-
- Check https://docs.foldspace.ai/guides/in-chat-ui/ for more information
|
|
77
|
-
|
|
78
|
-
## Task agents
|
|
79
|
-
|
|
80
|
-
Use a Task Agent when the handler needs a one-time LLM subtask that
|
|
81
|
-
deterministic code cannot do well: extraction, summarization,
|
|
82
|
-
classification, normalization, enrichment, or generation.
|
|
83
|
-
|
|
84
|
-
Do not use a Task Agent for API calls, CRUD, routing, or parsing a
|
|
85
|
-
known response shape. Those stay in `execute`.
|
|
86
|
-
|
|
87
|
-
Task agents are created in Agent Studio, not in this repo. Ask before
|
|
88
|
-
creating or publishing one. Call a published task agent from the
|
|
89
|
-
handler with `runTask({ taskKey, data })` (if not published the runTask won't work).
|
|
90
|
-
`data` carries extracted facts only — not `prompt` / `instructions` strings.
|
|
91
|
-
Prefer JSON output when the handler must consume the result.
|
|
92
|
-
|
|
93
|
-
See https://docs.foldspace.ai/user-guides/task-agents/ and
|
|
94
|
-
https://docs.foldspace.ai/reference/task-agent-api/
|
|
95
|
-
|
|
96
|
-
## Local harness loop
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
npm run dev
|
|
100
|
-
npm run inject
|
|
101
|
-
npx foldspace attach --daemon
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
`inject` launches an isolated Chrome profile and records its debug port.
|
|
105
|
-
It does not generate or load an application extension. Observe the customer's
|
|
106
|
-
workflow after inject and **before** attach, while chrome-devtools MCP can use
|
|
107
|
-
the same Chrome. `attach` prepares the page and loads the local `dist/index.js`
|
|
108
|
-
bundle through CDP. Coding agents must use `--daemon` so the invoking tool
|
|
109
|
-
returns after `[lifecycle] inspect_registration:…`; foreground `npm run attach`
|
|
110
|
-
is for humans watching the terminal. An empty local registry is valid. `npm run
|
|
111
|
-
build` is still required so `dist/index.js` exists.
|
|
112
|
-
|
|
113
|
-
Use the default swap only when the page already has the configured product and
|
|
114
|
-
agent. Use `--bootstrap` only when the page has no Foldspace SDK, and
|
|
115
|
-
`--replace` when it embeds a different product or agent, or when an SDK is
|
|
116
|
-
present without the configured agent.
|
|
117
|
-
|
|
118
|
-
The attach log must report `inspect_registration:registration_ok` before
|
|
119
|
-
treating the page as registered. Zero captured actions is success when the
|
|
120
|
-
local registry is empty. On `registration_mismatch`, read
|
|
121
|
-
`npx foldspace help attach --json` diagnostics and map those names onto the
|
|
122
|
-
lifecycle details (`missingActionNames`, `unexpectedActionNames`,
|
|
123
|
-
`diagnosticError`).
|
|
124
|
-
|
|
125
|
-
While attach is running it owns the debug port. Detach (`attach --stop`, or
|
|
126
|
-
Ctrl-C in the foreground) before using chrome-devtools MCP against the same
|
|
127
|
-
Chrome.
|
|
128
|
-
|
|
129
|
-
Prove a named action through the visible agent. After the user talks to the
|
|
130
|
-
copilot, read the daemon log for `[actions]` SDK callback and local
|
|
131
|
-
execute/render lines; do not invoke the handler directly. Those lines record
|
|
132
|
-
names, statuses, durations, and parameter keys only — not results or error
|
|
133
|
-
bodies.
|
|
134
|
-
|
|
135
|
-
## Product defaults
|
|
136
|
-
|
|
137
|
-
These are not Joist-specific. Lint can catch some of them in handler code;
|
|
138
|
-
Agent Studio copy is on you.
|
|
139
|
-
|
|
140
|
-
- **No emoji** in copilot replies, cards, or handler strings. Put that in the
|
|
141
|
-
agent's Behavior instructions (MCP cannot write that field, so each new
|
|
142
|
-
action's Studio `instructions` must carry the line too).
|
|
143
|
-
- **Never send the user out of the host app.** No "open in <product>"
|
|
144
|
-
button and no pasted URLs — navigate with a navigation route, same tab.
|
|
145
|
-
- **Check row counts before choosing the experience.** On an empty account the
|
|
146
|
-
useful first action is one that creates data.
|
|
147
|
-
|
|
148
|
-
## Verification gates
|
|
149
|
-
|
|
150
|
-
Do not report success without all six:
|
|
151
|
-
|
|
152
|
-
1. TypeScript compiles with `npm run typecheck` (`tsc --noEmit -p tsconfig.json`).
|
|
153
|
-
Do not run `npx tsc` — that can install the wrong package.
|
|
154
|
-
2. `foldspace lint` reports no errors (`foldspace build` runs this first).
|
|
155
|
-
3. The expected handler appears in `dist/index.js`.
|
|
156
|
-
4. The browser reports `inspect_registration:registration_ok`. For a named
|
|
157
|
-
action, the captured registry includes that handler.
|
|
158
|
-
5. The action behaves correctly against the real target workflow. Confirm
|
|
159
|
-
`[actions] local-handler:execute` in the attach log after the user exercises
|
|
160
|
-
the visible agent. For a widget, that log must include `local-handler:render`
|
|
161
|
-
from a real chat turn — not a hand-built `render()` call.
|
|
162
|
-
6. Browser evidence came from the live target, not from hand-authored examples.
|
|
163
|
-
Neighbour fixtures still pass when they exist.
|
|
164
|
-
|
|
165
|
-
## Layout
|
|
166
|
-
|
|
167
|
-
- `agent/actions/` — one handler per action (`execute`, optional `render`),
|
|
168
|
-
registered in `index.ts`
|
|
169
|
-
- `agent/api/` — one HTTP helper per endpoint
|
|
170
|
-
- `agent/constants.ts` — agent, product, domain, plus empty `API_BASE` /
|
|
171
|
-
`AUTH_SOURCE` and `LOAD_MODE`
|
|
172
|
-
- `agent/utils.ts` — configure the harness runtime and re-export it. **This is
|
|
173
|
-
the only import surface for actions.**
|
|
174
|
-
- `foldspace.dev.json` — local harness target configuration
|
|
175
|
-
- `docs/app-profile.md` — what is known about this app, and how it was established
|
|
176
|
-
|
|
177
|
-
Do not introduce another bundler or bundle format.
|
|
178
|
-
|
|
179
|
-
## What you already have
|
|
180
|
-
|
|
181
|
-
Import from `../utils`. Inspect that file before implementing another
|
|
182
|
-
general-purpose helper. MCP `generate_action_handler` may still emit a
|
|
183
|
-
skeleton that does not import it — fix the import when you implement.
|
|
184
|
-
|
|
185
|
-
| Helper | Use when | Do not use when |
|
|
186
|
-
|---|---|---|
|
|
187
|
-
| `apiFetch` / `apiFetchBinary` | The app's own JSON/file API, after a real 200 | Public marketing hosts (`publicFetch`); custom auth headers |
|
|
188
|
-
| `publicFetch` | Unauthenticated / marketing origin | Signed-in product APIs |
|
|
189
|
-
| `getAuthToken` / `parseJwt` | `AUTH_SOURCE` is `localStorage` or `cookie` | Custom header schemes — override `apiFetch` in `utils.ts` |
|
|
190
|
-
| `rankBy` | User typed a name; API search is exact or ignored | Domain ranking (invoices, coverage, MasterFormat) |
|
|
191
|
-
| `renderLoading` / `Empty` / `Error` / `Fatal` | Chatterblock empty/error paths | Branded cards — those stay in `agent/views/` |
|
|
192
|
-
| `getAgent` | Talking to Foldspace | You need a specific instance — then `agentIds()` |
|
|
193
|
-
| `armAllInstances` | Attach/bootstrap setup | Inside `execute` (can ship into `dist`; attach already arms test mode) |
|
|
194
|
-
| `redact` / `mapWithConcurrency` | Logging tokens; batching fetches | — |
|
|
195
|
-
|
|
196
|
-
If the observed auth is not Bearer + `localStorage`/`cookie`, stop re-exporting
|
|
197
|
-
that one function and keep the rest:
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
export { getAgent, rankBy, renderEmpty } from "@foldspace_npm/harness/runtime";
|
|
201
|
-
export { apiFetch } from "./api";
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## Agent learnings
|
|
205
|
-
|
|
206
|
-
`docs/app-profile.md` is the durable record of this app. Fill it as you probe,
|
|
207
|
-
not afterwards. Other notes in `docs/` are fine for session-specific lessons.
|
|
208
|
-
|
|
209
|
-
- Before similar work, read `docs/app-profile.md` and any other notes in `docs/`.
|
|
210
|
-
- After non-obvious discoveries, add them to the profile (or a short extra
|
|
211
|
-
note) covering verified gotchas, failed approaches, and how they were
|
|
212
|
-
established.
|
|
213
|
-
- Keep notes concise and evidence-based.
|
|
214
|
-
- Do not store secrets, cookies, HAR files, browser storage, or other transient
|
|
215
|
-
session data in `docs/`.
|
|
216
|
-
|
|
217
|
-
## Safety
|
|
218
|
-
|
|
219
|
-
- Do not commit secrets, cookies, HAR files, browser storage, or
|
|
220
|
-
`.foldspace-dev/`.
|
|
221
|
-
- Local handler changes are not cloud publication.
|
|
222
|
-
- Creating or publishing Foldspace resources requires explicit approval.
|
|
223
|
-
- Action keys and parameter names must match the published schema.
|
|
224
|
-
- Search Foldspace documentation before asserting unfamiliar platform
|
|
225
|
-
behaviour.
|
|
16
|
+
Observed API, auth, and account facts for this app live in `docs/app-profile.md`.
|