@foldspace_npm/harness 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +402 -204
- package/README.md +84 -36
- package/bin/ask.mjs +19 -9
- package/bin/attach.mjs +91 -56
- package/bin/badge.mjs +28 -8
- package/bin/cli.mjs +23 -6
- package/bin/inject.mjs +3 -4
- package/bin/observe.mjs +22 -6
- package/bin/published.mjs +141 -0
- package/package.json +6 -2
- package/recipes/INDEX.md +13 -8
- package/recipes/README.md +30 -7
- package/recipes/account-overview/README.md +6 -6
- package/recipes/account-overview/agent/actions/show_account_overview.ts +11 -16
- package/recipes/account-overview/agent/views/brand.ts +23 -14
- package/recipes/account-overview/agent/views/overview.ts +13 -17
- package/recipes/bottom-bar/README.md +12 -9
- package/recipes/bottom-bar/agent/bottomBar.ts +14 -12
- package/recipes/create-a-record/README.md +97 -0
- package/recipes/create-a-record/agent/actions/create_project.ts +26 -0
- package/recipes/create-a-record/agent/api/projects.ts +62 -0
- package/recipes/create-a-record/agent/projects.ts +145 -0
- package/recipes/create-a-record/agent/views/approval.ts +181 -0
- package/recipes/create-a-record/agent/views/brand.ts +35 -0
- package/recipes/create-a-record/agent/views/fields.ts +252 -0
- package/recipes/create-a-record/agent/views/project-card.ts +253 -0
- package/recipes/create-a-record/agent/views/steps.ts +92 -0
- package/recipes/create-a-record/fixtures/owners.ok.json +9 -0
- package/recipes/create-a-record/fixtures/project.created.json +1 -0
- package/recipes/create-a-record/fixtures/project.rejected.json +1 -0
- package/recipes/create-a-record/recipe.json +13 -0
- package/recipes/create-a-record/variants/actions/create_project.steps.ts +160 -0
- package/recipes/find-by-name/README.md +3 -1
- package/recipes/find-by-name/agent/actions/find_project_id.ts +1 -1
- package/recipes/lookup-then-view/README.md +73 -0
- package/recipes/lookup-then-view/agent/accounts.ts +174 -0
- package/recipes/lookup-then-view/agent/actions/find_account.ts +108 -0
- package/recipes/lookup-then-view/agent/actions/show_account_overview.ts +42 -0
- package/recipes/lookup-then-view/agent/api/accounts.ts +68 -0
- package/recipes/lookup-then-view/agent/views/account.ts +183 -0
- package/recipes/lookup-then-view/agent/views/brand.ts +35 -0
- package/recipes/lookup-then-view/agent/views/format.ts +73 -0
- package/recipes/lookup-then-view/agent/views/pick-one.ts +240 -0
- package/recipes/lookup-then-view/fixtures/accounts.json +319 -0
- package/recipes/lookup-then-view/fixtures/overview.ok.json +122 -0
- package/recipes/lookup-then-view/fixtures/overview.unsigned.json +14 -0
- package/recipes/lookup-then-view/recipe.json +11 -0
- package/recipes/lookup-then-view/studio/find_account.json +10 -0
- package/recipes/lookup-then-view/studio/show_account_overview.json +15 -0
- package/recipes/opportunities-at-risk/README.md +5 -5
- package/recipes/opportunities-at-risk/agent/actions/show_opportunities_at_risk.ts +9 -7
- package/recipes/opportunities-at-risk/agent/opportunities.ts +11 -10
- package/recipes/opportunities-at-risk/agent/views/at-risk.ts +1 -1
- package/recipes/opportunities-at-risk/agent/views/brand.ts +26 -15
- package/recipes/pick-from-a-list/README.md +7 -6
- package/recipes/pick-from-a-list/agent/actions/choose_project.ts +27 -12
- package/recipes/pick-from-a-list/agent/views/brand.ts +30 -9
- package/recipes/pick-from-a-list/agent/views/picker.ts +3 -3
- package/recipes/prepare-for-a-meeting/README.md +4 -4
- package/recipes/prepare-for-a-meeting/agent/actions/prepare_for_meeting.ts +3 -3
- package/recipes/records-list/README.md +6 -6
- package/recipes/records-list/agent/accounts.ts +3 -1
- package/recipes/records-list/agent/actions/list_accounts.ts +7 -7
- package/recipes/records-list/agent/views/brand.ts +26 -14
- package/recipes/server-rendered-forms/README.md +35 -0
- package/recipes/server-rendered-forms/agent/actions/create_record_via_form.ts +62 -0
- package/recipes/server-rendered-forms/agent/api/pages.ts +57 -0
- package/recipes/server-rendered-forms/agent/forms.ts +192 -0
- package/recipes/server-rendered-forms/fixtures/form.errors.html +9 -0
- package/recipes/server-rendered-forms/fixtures/form.html +17 -0
- package/recipes/server-rendered-forms/fixtures/list.html +9 -0
- package/recipes/server-rendered-forms/fixtures/record.html +4 -0
- package/recipes/server-rendered-forms/recipe.json +10 -0
- package/recipes/several-hosts/README.md +10 -13
- package/recipes/several-hosts/agent/hosts.ts +19 -136
- package/recipes/several-hosts/fixtures/wrong-page.json +6 -0
- package/recipes/several-hosts/recipe.json +3 -3
- package/recipes/swap-the-login-method/README.md +16 -5
- package/recipes/swap-the-login-method/agent/utils.ts +52 -3
- package/recipes/swap-the-login-method/variants/utils.cookies.ts +59 -6
- package/recipes/update-meeting-notes/README.md +10 -4
- package/recipes/update-meeting-notes/agent/actions/update_meeting_notes.ts +16 -5
- package/recipes/upload-contacts/README.md +5 -5
- package/recipes/upload-contacts/agent/actions/upload_contacts.ts +7 -7
- package/recipes/upload-contacts/agent/views/brand.ts +24 -14
- package/recipes/upload-contacts/agent/views/uploader.ts +28 -17
- package/recipes/who-is-the-user/README.md +7 -3
- package/src/ask-core.mjs +16 -4
- package/src/attach-daemon.mjs +9 -2
- package/src/attach-helpers.mjs +105 -0
- package/src/attach-preflight.mjs +34 -11
- package/src/badge-core.mjs +1 -1
- package/src/cdp-client.mjs +15 -19
- package/src/cdp-ownership.mjs +41 -20
- package/src/cli-help.mjs +7 -5
- package/src/cli-registry.mjs +102 -47
- package/src/diagnostics.mjs +1 -1
- package/src/init.mjs +5 -3
- package/src/keep-focus.mjs +17 -13
- package/src/lint/index.mjs +8 -1
- package/src/lint/rules.mjs +240 -43
- package/src/observe-core.mjs +163 -34
- package/src/published-core.mjs +392 -0
- package/src/region.mjs +1 -1
- package/src/runtime/agent.ts +16 -12
- package/src/runtime/http.ts +25 -15
- package/src/runtime/match.ts +1 -1
- package/src/runtime/render.ts +4 -4
- package/src/transports/README.md +2 -2
- package/src/upgrade.mjs +33 -10
- package/templates/agent-starter/CLAUDE.md +8 -5
- package/templates/agent-starter/README.md +32 -33
- package/templates/agent-starter/agent/actions/_example.ts +4 -4
- package/templates/agent-starter/agent/actions/index.ts +2 -2
- package/templates/agent-starter/agent/constants.ts +4 -3
- package/templates/agent-starter/agent/utils.ts +15 -18
- package/templates/agent-starter/claude-settings.json +2 -1
- package/templates/agent-starter/docs/app-profile.md +19 -14
- package/templates/agent-starter/foldspace.dev.json +1 -1
- package/recipes/several-hosts/fixtures/nav-record.json +0 -5
package/src/region.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// points at that region's agent, recording and tracking hosts - the US SDK on
|
|
8
8
|
// an EU page talks to a US backend that does not have the EU agents.
|
|
9
9
|
//
|
|
10
|
-
// `server`: the
|
|
10
|
+
// `server`: the Foldspace tools' server name in the editor, as the region's playbook
|
|
11
11
|
// installs it. Distinct per region, so both can sit side by side.
|
|
12
12
|
export const REGIONS = Object.freeze({
|
|
13
13
|
us: Object.freeze({
|
package/src/runtime/agent.ts
CHANGED
|
@@ -6,7 +6,7 @@ let cached: any | null = null;
|
|
|
6
6
|
* Foldspace SDK handle for this agent's **overlay** instance.
|
|
7
7
|
*
|
|
8
8
|
* Takes no `mode` on purpose: `foldspace.agent({ apiName })` returns the overlay
|
|
9
|
-
* handle. On an app that embeds the
|
|
9
|
+
* handle. On an app that embeds the agent in-page that is not the instance
|
|
10
10
|
* serving the chat, so arming only it succeeds and leaves real conversations
|
|
11
11
|
* untagged. For a specific instance, enumerate `window.foldspace.agentIds()`
|
|
12
12
|
* (`"<mode>-<apiName>"`) and call `foldspace.agent({ apiName, mode })`.
|
|
@@ -25,20 +25,24 @@ export function getAgent(): any | null {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Turns test mode on or off (`testMode`) on every Foldspace instance on the
|
|
29
|
+
* page (`window.foldspace.agentIds()`) and turns on their remote actions: SDK
|
|
30
|
+
* `setTestMode(testMode)` and `setConfiguration({ remoteActionsSettings })`.
|
|
29
31
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* It does not belong in agent code: any file in `agent/` can ship to
|
|
33
|
+
* production, where test mode would stop real users' conversations being
|
|
34
|
+
* tracked. Leave it out even with `false`, which lint refuses too: it still
|
|
35
|
+
* calls `setConfiguration` on every instance, and a partial `setConfiguration`
|
|
36
|
+
* can shrink the agent's panel to 0×0. Test mode is only for an agent that
|
|
37
|
+
* already has real users, and only through `npx foldspace attach --test-mode`,
|
|
38
|
+
* which never touches the bundle. For an agent with no real users yet, leave
|
|
39
|
+
* test mode off.
|
|
34
40
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* @param testMode - When true, mark conversations as test traffic
|
|
39
|
-
* @returns Instance ids that armed vs failed
|
|
41
|
+
* @param testMode - `true` marks new conversations on this page as test
|
|
42
|
+
* traffic; `false` (the default) turns that off.
|
|
43
|
+
* @returns The instance ids it set (`armed`) and the ones that threw (`failed`)
|
|
40
44
|
*/
|
|
41
|
-
export function armAllInstances(testMode =
|
|
45
|
+
export function armAllInstances(testMode = false): {
|
|
42
46
|
armed: string[];
|
|
43
47
|
failed: string[];
|
|
44
48
|
} {
|
package/src/runtime/http.ts
CHANGED
|
@@ -96,16 +96,17 @@ export interface ApiSuccess<T> {
|
|
|
96
96
|
/**
|
|
97
97
|
* Failed request. Errors are **returned**, never thrown.
|
|
98
98
|
*
|
|
99
|
-
* Show `error` to a person. Log `detail` to the console only
|
|
100
|
-
* `detail` in an action return value;
|
|
99
|
+
* Show `error` to a person. Log `detail` to the console only - never put
|
|
100
|
+
* `detail` in an action return value; the agent may quote it to the end user.
|
|
101
101
|
*
|
|
102
102
|
* `unreachable` is true when the browser never got a response (DNS, CORS,
|
|
103
103
|
* offline, connection refused). Timeouts set `unreachable: false` and
|
|
104
104
|
* `error: "Request timed out."`. Empty `API_BASE` / `AUTH_SOURCE` also
|
|
105
|
-
* return `{ ok: false }` with status 0
|
|
105
|
+
* return `{ ok: false }` with status 0 - they do not throw: `reason: "config"`,
|
|
106
|
+
* `error: "This is not set up yet."`, and which setting is missing in `detail`.
|
|
106
107
|
*
|
|
107
108
|
* Branch on `reason`, not on `status`: a search that finds nothing, an expired
|
|
108
|
-
* session and a throttled call are three different
|
|
109
|
+
* session and a throttled call are three different card states, and
|
|
109
110
|
* `renderFailure` picks the right one for you.
|
|
110
111
|
*/
|
|
111
112
|
export interface ApiFailure {
|
|
@@ -121,7 +122,7 @@ export interface ApiFailure {
|
|
|
121
122
|
status: number;
|
|
122
123
|
/** Safe, user-facing sentence. Pass this to `renderError`. */
|
|
123
124
|
error: string;
|
|
124
|
-
/**
|
|
125
|
+
/** For the console only: the API's body (truncated), or which setting is missing. Never show this in the card. */
|
|
125
126
|
detail?: string;
|
|
126
127
|
/**
|
|
127
128
|
* True when the network itself failed (not HTTP 4xx/5xx, not abort).
|
|
@@ -131,7 +132,7 @@ export interface ApiFailure {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
|
-
* Why a call failed, in terms a
|
|
135
|
+
* Why a call failed, in terms a card can act on.
|
|
135
136
|
*
|
|
136
137
|
* - `signed_out` — no token on the page, or the API answered 401. The session
|
|
137
138
|
* observed at build time is not the session at run time; expect this.
|
|
@@ -155,6 +156,11 @@ export type FailureReason =
|
|
|
155
156
|
/** Discriminated result of a customer-API call. Branch on `ok`. */
|
|
156
157
|
export type ApiResult<T> = ApiSuccess<T> | ApiFailure;
|
|
157
158
|
|
|
159
|
+
// What reaches the agent and the card when the build is not configured yet:
|
|
160
|
+
// a state, never a developer instruction. The developer hint is in `detail`,
|
|
161
|
+
// for the console only: nothing shows it unless the handler logs it.
|
|
162
|
+
const NOT_SET_UP = "This is not set up yet.";
|
|
163
|
+
|
|
158
164
|
function missingConfigResult(): ApiResult<never> | null {
|
|
159
165
|
const config = getConfig();
|
|
160
166
|
if (!config.apiBase) {
|
|
@@ -162,7 +168,8 @@ function missingConfigResult(): ApiResult<never> | null {
|
|
|
162
168
|
ok: false,
|
|
163
169
|
status: 0,
|
|
164
170
|
reason: "config",
|
|
165
|
-
error:
|
|
171
|
+
error: NOT_SET_UP,
|
|
172
|
+
detail: "API_BASE is not set - capture the app's XHR first.",
|
|
166
173
|
};
|
|
167
174
|
}
|
|
168
175
|
if (!config.authSource) {
|
|
@@ -170,7 +177,8 @@ function missingConfigResult(): ApiResult<never> | null {
|
|
|
170
177
|
ok: false,
|
|
171
178
|
status: 0,
|
|
172
179
|
reason: "config",
|
|
173
|
-
error:
|
|
180
|
+
error: NOT_SET_UP,
|
|
181
|
+
detail: "AUTH_SOURCE is not set - capture the app's session first.",
|
|
174
182
|
};
|
|
175
183
|
}
|
|
176
184
|
return null;
|
|
@@ -196,7 +204,7 @@ function retryAfterMs(res: Response): number | undefined {
|
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
/**
|
|
199
|
-
* One mapping from HTTP status to a failure a
|
|
207
|
+
* One mapping from HTTP status to a failure a card can act on.
|
|
200
208
|
*
|
|
201
209
|
* Exported for a tenant's own `apiFetch` — an app that authenticates with a
|
|
202
210
|
* custom header or cookies replaces the transport, and should not also have to
|
|
@@ -206,7 +214,7 @@ export function httpFailure(res: Response, fallback: string, detail?: string): A
|
|
|
206
214
|
const base = { ok: false as const, status: res.status, detail };
|
|
207
215
|
switch (res.status) {
|
|
208
216
|
case 401:
|
|
209
|
-
return { ...base, reason: "signed_out", error: "Your session expired.
|
|
217
|
+
return { ...base, reason: "signed_out", error: "Your session has expired." };
|
|
210
218
|
case 403:
|
|
211
219
|
return { ...base, reason: "forbidden", error: "You do not have access to that." };
|
|
212
220
|
case 404:
|
|
@@ -216,7 +224,7 @@ export function httpFailure(res: Response, fallback: string, detail?: string): A
|
|
|
216
224
|
...base,
|
|
217
225
|
reason: "rate_limited",
|
|
218
226
|
retryAfterMs: retryAfterMs(res),
|
|
219
|
-
error: "Too many requests
|
|
227
|
+
error: "Too many requests right now.",
|
|
220
228
|
};
|
|
221
229
|
default:
|
|
222
230
|
return { ...base, reason: "server", error: fallback };
|
|
@@ -245,9 +253,10 @@ const SIGNED_OUT: ApiFailure = {
|
|
|
245
253
|
/**
|
|
246
254
|
* Authenticated JSON request against `apiBase` with `Authorization: Bearer`.
|
|
247
255
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* `
|
|
256
|
+
* For an app whose own requests carry `Authorization: Bearer`. If the live
|
|
257
|
+
* page sends something else (a custom header, or cookies with no token),
|
|
258
|
+
* follow `recipes/swap-the-login-method/README.md`: it names the file to copy
|
|
259
|
+
* over `agent/utils.ts` for each case.
|
|
251
260
|
*
|
|
252
261
|
* Empty `API_BASE` / `AUTH_SOURCE` return `{ ok: false }` so a guessed host
|
|
253
262
|
* cannot look like success. Errors never throw and never include the token.
|
|
@@ -401,7 +410,8 @@ export async function publicFetch<T = unknown>(
|
|
|
401
410
|
ok: false,
|
|
402
411
|
status: 0,
|
|
403
412
|
reason: "config",
|
|
404
|
-
error:
|
|
413
|
+
error: NOT_SET_UP,
|
|
414
|
+
detail: "No public origin is set - pass one to publicFetch, or set publicOrigin in configure().",
|
|
405
415
|
};
|
|
406
416
|
}
|
|
407
417
|
const controller = new AbortController();
|
package/src/runtime/match.ts
CHANGED
|
@@ -41,7 +41,7 @@ const PLAUSIBLE = 0.45;
|
|
|
41
41
|
* SDK method degrades the result rather than breaking the action.
|
|
42
42
|
*
|
|
43
43
|
* @param items - Records already loaded (e.g. from {@link apiFetch})
|
|
44
|
-
* @param query - What the user typed
|
|
44
|
+
* @param query - What the end user typed
|
|
45
45
|
* @param opts - Fields, identity, optional limit
|
|
46
46
|
* @returns Up to `limit` items, substring matches first
|
|
47
47
|
*/
|
package/src/runtime/render.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unstyled
|
|
2
|
+
* Unstyled card states. Tenant brand belongs in `agent/views/`.
|
|
3
3
|
*
|
|
4
4
|
* An action's `render` output cannot be typechecked. Look at a frame. Never
|
|
5
5
|
* use `innerHTML` with API data.
|
|
@@ -38,7 +38,7 @@ function replace(host: ViewHost, node: HTMLElement): void {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* In-progress
|
|
41
|
+
* In-progress card. Unstyled scaffolding - replace with branded UI
|
|
42
42
|
* when the card is the experience.
|
|
43
43
|
*
|
|
44
44
|
* @param host - Foldspace `render` host
|
|
@@ -60,8 +60,8 @@ export function renderEmpty(host: ViewHost, message: string): void {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Recoverable error: the user can retry or narrow. Pass `ApiResult.error`,
|
|
64
|
-
* never `detail`
|
|
63
|
+
* Recoverable error: the end user can retry or narrow. Pass `ApiResult.error`,
|
|
64
|
+
* never `detail` - it is for the console.
|
|
65
65
|
*
|
|
66
66
|
* @param host - Foldspace `render` host
|
|
67
67
|
* @param message - Safe, user-facing sentence
|
package/src/transports/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The harness does the same three jobs regardless of where the browser is:
|
|
|
6
6
|
|
|
7
7
|
Track A (`local`) does them over CDP against a browser on this machine.
|
|
8
8
|
Track B (`extension`) will do them over a websocket to a Web Store extension in
|
|
9
|
-
the
|
|
9
|
+
the human's own Chrome.
|
|
10
10
|
|
|
11
11
|
`agent/`, `fixtures/` and the build are identical in both — only this layer
|
|
12
12
|
differs. Keep it that way: "export to repo" from a hosted build is a copy only
|
|
@@ -21,4 +21,4 @@ while the two tracks share an identical `agent/` directory.
|
|
|
21
21
|
| `navigate` | `Page.navigate` | `tabs.update` |
|
|
22
22
|
|
|
23
23
|
`local` is implemented today as `bin/inject.mjs` + `bin/attach.mjs`.
|
|
24
|
-
`extension` is not built
|
|
24
|
+
`extension` is not built.
|
package/src/upgrade.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export const HARNESS_PACKAGE = "@foldspace_npm/harness";
|
|
|
9
9
|
export const INSTRUCTION_IMPORT = `@node_modules/${HARNESS_PACKAGE}/CLAUDE.md`;
|
|
10
10
|
export const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
11
11
|
export const NPM_VIEW_TIMEOUT_MS = 2000;
|
|
12
|
-
export const UPGRADE_NEXT = "Ask the
|
|
12
|
+
export const UPGRADE_NEXT = "Ask the human, then run npx foldspace upgrade --yes";
|
|
13
13
|
|
|
14
14
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
15
|
const instructionTemplatePath = path.join(
|
|
@@ -267,11 +267,11 @@ function nextAction(report) {
|
|
|
267
267
|
}
|
|
268
268
|
if (report.status === "outdated") {
|
|
269
269
|
return report.instructionsImport === false
|
|
270
|
-
?
|
|
270
|
+
? "CLAUDE.md does not import the package instructions. Ask the human, then run npx foldspace upgrade --yes --refresh-instructions (the old CLAUDE.md is kept as CLAUDE.md.bak)."
|
|
271
271
|
: UPGRADE_NEXT;
|
|
272
272
|
}
|
|
273
273
|
if (report.status === "upgraded") {
|
|
274
|
-
return "Run npm run build
|
|
274
|
+
return "Run `npm run build`. If attach is running, run `npx foldspace attach --stop`, then `npx foldspace attach --daemon` with the flags it ran with, to load the rebuild.";
|
|
275
275
|
}
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
@@ -311,8 +311,14 @@ export function checkForUpdate(projectDir, options = {}) {
|
|
|
311
311
|
// new answer down. `foldspace upgrade` is always fresh: someone asking to
|
|
312
312
|
// upgrade minutes after a release was told the previous version was the
|
|
313
313
|
// latest, for up to 12 hours (seen 2026-09-28).
|
|
314
|
+
// `preferFresh` (help --json, which the manual reads first) asks the
|
|
315
|
+
// registry too, but falls back to the remembered answer when the registry
|
|
316
|
+
// cannot be reached, so an offline session still hears something.
|
|
317
|
+
const preferFresh = options.preferFresh === true;
|
|
314
318
|
const cached =
|
|
315
|
-
options.cache === false || options.fresh === true
|
|
319
|
+
options.cache === false || options.fresh === true || preferFresh
|
|
320
|
+
? null
|
|
321
|
+
: readCache(projectDir, now);
|
|
316
322
|
const cachedDeprecation =
|
|
317
323
|
cached?.deprecation?.version === running ? cached.deprecation : null;
|
|
318
324
|
if (cached?.latest && cachedDeprecation) {
|
|
@@ -329,12 +335,25 @@ export function checkForUpdate(projectDir, options = {}) {
|
|
|
329
335
|
? options.fetchDeprecation(running)
|
|
330
336
|
: fetchDeprecationFromNpm(running, options);
|
|
331
337
|
deprecated = message || null;
|
|
332
|
-
|
|
333
|
-
|
|
338
|
+
// Each of the two answers falls back on its own: the version can arrive
|
|
339
|
+
// while the withdrawn-version check times out, and that warning is the
|
|
340
|
+
// one step 1 must not lose.
|
|
341
|
+
const fetchedLatest = latest;
|
|
342
|
+
const remembered = preferFresh && options.cache !== false ? readCache(projectDir, now) : null;
|
|
343
|
+
const rememberedDeprecation =
|
|
344
|
+
remembered?.deprecation?.version === running ? remembered.deprecation : null;
|
|
345
|
+
if (!fetchedLatest && remembered?.latest) latest = remembered.latest;
|
|
346
|
+
if (typeof message !== "string" && rememberedDeprecation) {
|
|
347
|
+
deprecated = rememberedDeprecation.message || null;
|
|
348
|
+
}
|
|
349
|
+
if (fetchedLatest && options.cache !== false) {
|
|
350
|
+
const entry = { latest: fetchedLatest };
|
|
334
351
|
// Only cache an answer the registry actually gave, so an offline
|
|
335
352
|
// run is asked again next time instead of remembered as "fine".
|
|
336
353
|
if (typeof message === "string") {
|
|
337
354
|
entry.deprecation = { version: running, message };
|
|
355
|
+
} else if (rememberedDeprecation) {
|
|
356
|
+
entry.deprecation = rememberedDeprecation;
|
|
338
357
|
}
|
|
339
358
|
writeCache(projectDir, entry, now);
|
|
340
359
|
}
|
|
@@ -367,7 +386,7 @@ export function checkForUpdate(projectDir, options = {}) {
|
|
|
367
386
|
/**
|
|
368
387
|
* What `build` and `attach` do with an update report before they run:
|
|
369
388
|
* `block: true` stops the command, `message` (if any) is printed to stderr.
|
|
370
|
-
* Never upgrades - that stays the
|
|
389
|
+
* Never upgrades - that stays the human's decision (CLAUDE.md, step 1).
|
|
371
390
|
* The escape hatch is FOLDSPACE_SKIP_UPDATE_CHECK=1: the report comes back
|
|
372
391
|
* check_skipped, so nothing here blocks.
|
|
373
392
|
*/
|
|
@@ -379,7 +398,7 @@ export function gateForCommand(report) {
|
|
|
379
398
|
block: true,
|
|
380
399
|
message:
|
|
381
400
|
`${HARNESS_PACKAGE} ${report.installed || report.pinned} was withdrawn: ${report.deprecated}. ` +
|
|
382
|
-
`${UPGRADE_NEXT}. If the
|
|
401
|
+
`${UPGRADE_NEXT}. If the human declines, rerun with FOLDSPACE_SKIP_UPDATE_CHECK=1 to run this version anyway.`,
|
|
383
402
|
};
|
|
384
403
|
}
|
|
385
404
|
if (report.outdated) {
|
|
@@ -487,10 +506,14 @@ export async function runUpgrade(argv, options = {}) {
|
|
|
487
506
|
log(
|
|
488
507
|
`${HARNESS_PACKAGE} ${report.pinned || report.installed} -> ${report.latest} is available`,
|
|
489
508
|
);
|
|
490
|
-
const answer = await ask("Upgrade the
|
|
509
|
+
const answer = await ask("Upgrade the version this project uses? [Y/n] ", options);
|
|
491
510
|
shouldInstall = isAffirmative(answer);
|
|
492
511
|
if (!shouldInstall) {
|
|
493
|
-
|
|
512
|
+
// The answer was no: asking again is not the next step.
|
|
513
|
+
const next = report.broken
|
|
514
|
+
? "The human declined: do not ask again this session. This version is withdrawn, so build and attach refuse it; run them with FOLDSPACE_SKIP_UPDATE_CHECK=1."
|
|
515
|
+
: "The human declined: carry on with this version, and do not ask again this session.";
|
|
516
|
+
return { ...report, status: "declined", next };
|
|
494
517
|
}
|
|
495
518
|
if (!report.instructionsImport && !shouldRefresh) {
|
|
496
519
|
const refreshAnswer = await ask(
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
@node_modules/@foldspace_npm/harness/CLAUDE.md
|
|
4
4
|
|
|
5
|
-
If
|
|
6
|
-
|
|
5
|
+
That import is the manual for this build. If it did not resolve, read
|
|
6
|
+
`node_modules/@foldspace_npm/harness/CLAUDE.md` before anything else; if that
|
|
7
|
+
file is missing too, run `npm install --ignore-scripts` first. What follows is
|
|
8
|
+
only what is true of **this** product.
|
|
7
9
|
|
|
8
10
|
## This product
|
|
9
11
|
|
|
10
12
|
| | |
|
|
11
13
|
|---|---|
|
|
12
|
-
| Agent | `{{AGENT_API_NAME}}` |
|
|
13
|
-
| Product | `{{PRODUCT_ID}}` |
|
|
14
|
+
| Agent Key | `{{AGENT_API_NAME}}` |
|
|
15
|
+
| Product ID | `{{PRODUCT_ID}}` |
|
|
14
16
|
| App | `{{APP_DOMAIN}}` |
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
What is known about this app, and how each fact was established, is in
|
|
19
|
+
`docs/app-profile.md`.
|
|
@@ -24,63 +24,62 @@ npm run build
|
|
|
24
24
|
agent/
|
|
25
25
|
actions/ one file per action; register actions in index.ts
|
|
26
26
|
api/ one file per HTTP helper
|
|
27
|
+
views/ cards; brand.ts comes from `npx foldspace observe styles --write`
|
|
27
28
|
constants.ts product, agent, and domain identifiers
|
|
28
|
-
utils.ts
|
|
29
|
-
docs/
|
|
29
|
+
utils.ts configures the harness runtime; the only import surface for actions
|
|
30
|
+
docs/
|
|
31
|
+
app-profile.md what is known about this app, and how each fact was established
|
|
30
32
|
foldspace.dev.json
|
|
31
33
|
CLAUDE.md imports platform instructions from the installed harness
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
##
|
|
36
|
+
## What is known about this app
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Good notes cover verified gotchas, failed approaches, design rationale, and
|
|
41
|
-
useful verification commands. Keep them concise and evidence-based. Before
|
|
42
|
-
similar work, read any relevant notes in `docs/`. Do not store secrets, cookies,
|
|
43
|
-
HAR files, browser storage, or other transient session data there.
|
|
38
|
+
`docs/app-profile.md` holds every fact established about this app, each with
|
|
39
|
+
how it was established, and every workaround. Never store secrets, cookies, HAR
|
|
40
|
+
files or browser storage there.
|
|
44
41
|
|
|
45
42
|
## Commands
|
|
46
43
|
|
|
47
44
|
```bash
|
|
48
|
-
npx foldspace help
|
|
49
|
-
npm run build
|
|
50
|
-
npm run dev
|
|
51
|
-
npm run inject
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
npx foldspace help # commands, risks, and next steps
|
|
46
|
+
npm run build # create dist/index.js
|
|
47
|
+
npm run dev # rebuild dist/index.js on changes
|
|
48
|
+
npm run inject # open the test window; the human signs in there
|
|
49
|
+
npx foldspace observe wait-login # returns when they are signed in
|
|
50
|
+
npx foldspace attach --daemon # load this build into the test window
|
|
54
51
|
```
|
|
55
52
|
|
|
56
|
-
Run `inject` before `attach`.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`dist/index.js` directly through CDP. An empty local registry is valid,
|
|
61
|
-
so you can attach before implementing handlers. Coding agents should use
|
|
62
|
-
`attach --daemon` (or `npm run attach:daemon`) so the command returns after
|
|
53
|
+
Run `inject` before `attach`. `npx foldspace observe` reads the test window,
|
|
54
|
+
read-only, and works while `attach` runs. `attach` loads `dist/index.js` into
|
|
55
|
+
the page over CDP; an empty registry is valid, so attach before any handler
|
|
56
|
+
exists. Run it as `npx foldspace attach --daemon`: it returns after
|
|
63
57
|
registration instead of occupying the terminal.
|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
The attach mode is read off the page:
|
|
66
60
|
|
|
67
61
|
- Default swap: the page already has the configured product and agent.
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
70
|
-
an SDK without the configured agent.
|
|
62
|
+
- `npx foldspace attach --bootstrap --daemon`: the page has no Foldspace SDK.
|
|
63
|
+
- `npx foldspace attach --replace --daemon`: the page has a different product
|
|
64
|
+
or agent, or an SDK without the configured agent.
|
|
71
65
|
|
|
72
66
|
Run `npx foldspace help attach` for full requirements and safety options. Coding
|
|
73
67
|
agents can read the versioned contract with `npx foldspace help --json`.
|
|
74
68
|
|
|
75
69
|
## Add an action
|
|
76
70
|
|
|
77
|
-
1.
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
1. Once the human has chosen the experience, observe what it needs in the
|
|
72
|
+
test window (`npx foldspace observe screen`, then `read`) and capture a
|
|
73
|
+
real 200 for the API the action will call.
|
|
74
|
+
2. Create the action as a draft with the Foldspace tools (`create_action`).
|
|
80
75
|
3. Copy `agent/actions/_example.ts` to `agent/actions/<action_key>.ts`.
|
|
81
76
|
4. Implement the handler from that observed request and response.
|
|
82
77
|
5. Import and register the handler in `agent/actions/index.ts`.
|
|
83
|
-
6. Type-check
|
|
84
|
-
|
|
78
|
+
6. Type-check and rebuild; if attach is running, `npx foldspace attach --stop`,
|
|
79
|
+
then `npx foldspace attach --daemon` with the flags it ran with. Ask the human
|
|
80
|
+
before publishing - the agent cannot call the action until it is published.
|
|
81
|
+
On a yes, publish it and prove it with
|
|
82
|
+
`npx foldspace ask "<question>" --retry 2`; on a not yet, it stays a draft:
|
|
83
|
+
say so in one line.
|
|
85
84
|
|
|
86
85
|
Action keys and parameter names must exactly match the action schema.
|
|
@@ -17,9 +17,9 @@ export const example_action = {
|
|
|
17
17
|
|
|
18
18
|
const result = await apiFetch<{ items?: Item[] }>("/__observe_me");
|
|
19
19
|
if (!result.ok) {
|
|
20
|
-
// Pass `reason` through so `render` can call renderFailure
|
|
21
|
-
// session, a 404 and a throttled call are three different
|
|
22
|
-
return {
|
|
20
|
+
// Pass `reason` through so `render` can call renderFailure - an expired
|
|
21
|
+
// session, a 404 and a throttled call are three different card states.
|
|
22
|
+
return { success: false, error: result.error, reason: result.reason };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const items = result.data.items ?? [];
|
|
@@ -30,6 +30,6 @@ export const example_action = {
|
|
|
30
30
|
|
|
31
31
|
// Zero matches is an answer, not an error. Return the query so the empty
|
|
32
32
|
// state can name what was searched for.
|
|
33
|
-
return {
|
|
33
|
+
return { success: true, query, matches };
|
|
34
34
|
},
|
|
35
35
|
};
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Registration here is the switch: the SDK transmits these to the server, and
|
|
4
4
|
// the server excludes any active action it does not receive. Comment an entry
|
|
5
|
-
// out and the
|
|
5
|
+
// out and the agent can no longer see it - no unpublishing required.
|
|
6
6
|
//
|
|
7
|
-
// LOAD_MODE "injected"
|
|
7
|
+
// LOAD_MODE "injected" puts the registry on window for attach to swap. "embedded"
|
|
8
8
|
// skips that assignment because the host page already loads Foldspace.
|
|
9
9
|
|
|
10
10
|
import { LOAD_MODE } from "../constants";
|
|
@@ -13,8 +13,9 @@ export const API_BASE = "";
|
|
|
13
13
|
* `{ kind: "localStorage", name: "access_token" }` or
|
|
14
14
|
* `{ kind: "cookie", name: "session" }`.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* see
|
|
16
|
+
* Auth that is not Bearer (a custom header, or cookies with no token) needs
|
|
17
|
+
* its own `apiFetch` - see "Custom auth" in `agent/utils.ts`. Do not guess
|
|
18
|
+
* this value.
|
|
18
19
|
*/
|
|
19
20
|
export const AUTH_SOURCE:
|
|
20
21
|
| { kind: "localStorage" | "cookie"; name: string }
|
|
@@ -22,7 +23,7 @@ export const AUTH_SOURCE:
|
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* "injected" assigns `window.__FOLDSPACE_REMOTE_ACTIONS__` so attach can swap
|
|
25
|
-
* the bundle. "embedded" does not
|
|
26
|
+
* the bundle. "embedded" does not - the page already registers handlers
|
|
26
27
|
* through its own SDK snippet.
|
|
27
28
|
*/
|
|
28
29
|
export const LOAD_MODE: "injected" | "embedded" = "injected";
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* ## What `export *` gives you
|
|
12
12
|
*
|
|
13
13
|
* HTTP (customer API, from the signed-in page):
|
|
14
|
-
* - `apiFetch`
|
|
15
|
-
*
|
|
14
|
+
* - `apiFetch` - JSON + `Authorization: Bearer`. For any other sign-in, see
|
|
15
|
+
* "Custom auth" below.
|
|
16
16
|
* - `apiFetchBinary` — same auth, raw bytes (file download).
|
|
17
17
|
* - `publicFetch` — no token; marketing / public origin only.
|
|
18
18
|
* - `getAuthToken` / `parseJwt` / `redact` — session read and safe logging.
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
* Foldspace SDK (thin wrappers; the SDK is the source of truth):
|
|
22
22
|
* - `getAgent` — overlay handle for `searchInList` and similar.
|
|
23
23
|
* - `rankBy` — substring first, then SDK fuzzy match. Not for domain ranking.
|
|
24
|
-
* - `armAllInstances`
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* - `armAllInstances` - never in agent code: attach handles the page, and
|
|
25
|
+
* test mode is `npx foldspace attach --test-mode`, only for an agent that
|
|
26
|
+
* already has real users. Anything in `agent/` ships to production.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
28
|
+
* Card states (unstyled; replace in `agent/views/` when brand matters):
|
|
29
29
|
* - `renderLoading` / `renderEmpty` / `renderError` / `renderFatal`
|
|
30
30
|
* - `renderFailure` — picks one of those from `ApiFailure.reason`, so an expired
|
|
31
31
|
* session, a 404 and a throttled call each draw the right state.
|
|
@@ -36,20 +36,17 @@
|
|
|
36
36
|
* Leave `API_BASE` and `AUTH_SOURCE` empty in `constants.ts` until you capture
|
|
37
37
|
* a real XHR. Empty config makes `apiFetch` return `{ ok: false }` instead of
|
|
38
38
|
* guessing a host. Errors are returned, never thrown; show `result.error` in
|
|
39
|
-
* the
|
|
39
|
+
* the card and log `result.detail` only.
|
|
40
40
|
*
|
|
41
|
-
* ##
|
|
41
|
+
* ## Custom auth (not Bearer)
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* export { getAgent, rankBy, renderEmpty, renderError, renderFatal, renderLoading };
|
|
51
|
-
* // Do not `export *` if that would re-export the default apiFetch.
|
|
52
|
-
* ```
|
|
43
|
+
* Read `node_modules/@foldspace_npm/harness/recipes/swap-the-login-method/README.md`
|
|
44
|
+
* and copy over this file the variant it names for what the app's requests
|
|
45
|
+
* carry: `agent/utils.ts` for a custom header, `variants/utils.cookies.ts` for
|
|
46
|
+
* cookies with no token. Each defines its own `apiFetch`, keeps the harness's
|
|
47
|
+
* failure `reason`s through `httpFailure` / `networkFailure`, and re-exports
|
|
48
|
+
* the other helpers by name - keep it that way: an `export *` would also bring
|
|
49
|
+
* in helpers such as `apiFetchBinary` that still use the default sign-in.
|
|
53
50
|
*/
|
|
54
51
|
|
|
55
52
|
import { configure } from "@foldspace_npm/harness/runtime";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Written by foldspace init
|
|
2
|
+
"$comment": "Written by foldspace init: the commands a build runs, allowed up front so the coding agent is not stopped one denial at a time. init and upgrade are left out on purpose, so they ask, like anything not listed. foldspace deploy is Foldspace-internal and never runs in a build.",
|
|
3
3
|
"permissions": {
|
|
4
4
|
"allow": [
|
|
5
5
|
"Bash(npx foldspace help:*)",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"Bash(npx foldspace observe:*)",
|
|
11
11
|
"Bash(npx foldspace ask:*)",
|
|
12
12
|
"Bash(npx foldspace badge:*)",
|
|
13
|
+
"Bash(npx foldspace published:*)",
|
|
13
14
|
"Bash(npm install:*)",
|
|
14
15
|
"Bash(npm run build:*)",
|
|
15
16
|
"Bash(npm run typecheck:*)",
|