@foldspace_npm/harness 0.1.33 → 0.1.35
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 +180 -50
- package/README.md +30 -2
- package/bin/inject.mjs +2 -18
- package/bin/observe.mjs +71 -12
- package/bin/record.mjs +2 -0
- package/bin/sync.mjs +48 -0
- package/package.json +1 -1
- package/recipes/INDEX.md +1 -1
- package/recipes/bottom-bar/README.md +24 -11
- package/recipes/bottom-bar/agent/bottomBar.ts +76 -38
- package/recipes/bottom-bar/fixtures/configuration.sent.json +13 -12
- package/recipes/swap-the-login-method/README.md +1 -1
- package/recipes/swap-the-login-method/agent/utils.ts +8 -7
- package/recipes/swap-the-login-method/variants/utils.cookies.ts +8 -7
- package/src/chrome-binary.mjs +33 -0
- package/src/cli-registry.mjs +53 -5
- package/src/customer-subdomain.mjs +149 -0
- package/src/lint/index.mjs +23 -3
- package/src/lint/rules.mjs +46 -2
- package/src/lint/safety.mjs +785 -0
- package/src/observe-core.mjs +23 -1
- package/src/runtime/VERSION +1 -1
- package/src/runtime/config.ts +15 -2
- package/src/runtime/http.ts +40 -3
- package/src/runtime/index.ts +1 -0
- package/src/sync-core.mjs +374 -0
- package/src/upgrade.mjs +36 -7
- package/src/video/compose.mjs +12 -3
- package/src/video/recorder.mjs +15 -7
- package/templates/agent-starter/agent/constants.ts +15 -1
- package/templates/agent-starter/agent/utils.ts +3 -0
- package/templates/agent-starter/claude-settings.json +3 -1
- package/templates/agent-starter/skills/review-experience/SKILL.md +135 -0
- package/templates/agent-starter/skills/share-a-video/SKILL.md +9 -2
package/bin/record.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
checkRecordingMatches,
|
|
38
38
|
closeAllHeadless,
|
|
39
39
|
loadRecording,
|
|
40
|
+
wideWindowNote,
|
|
40
41
|
needsChrome,
|
|
41
42
|
probeEncoder,
|
|
42
43
|
renderInChrome,
|
|
@@ -337,6 +338,7 @@ async function compose(config, recordDir, chromePath) {
|
|
|
337
338
|
note: `${Math.round(plan.fit.totalS)} s, longer than the ${TIMING.maxTotalS} s it should be at most, with the waits already shortened: drop a scene, or pick questions with shorter answers.`,
|
|
338
339
|
}
|
|
339
340
|
: {}),
|
|
341
|
+
...(wideWindowNote(recording) ? { wideWindow: wideWindowNote(recording) } : {}),
|
|
340
342
|
sceneSeconds: plan.clips.filter((clip) => clip.kind === "scene").map((clip) => Math.round(clip.durationS * 10) / 10),
|
|
341
343
|
lastFrames: lastFrames(recording),
|
|
342
344
|
};
|
package/bin/sync.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* foldspace sync <check|apply|saved> [options]
|
|
4
|
+
*
|
|
5
|
+
* Keeps this folder's agent/ up to date with the agent's saved code, which
|
|
6
|
+
* several builders can work on. The coding agent reads the saved code with the
|
|
7
|
+
* Foldspace tools and passes the heads it read; this command remembers what
|
|
8
|
+
* this folder last pulled (.foldspace-dev/saved-code/) and merges.
|
|
9
|
+
*
|
|
10
|
+
* check --develop <sha> [--line <sha>]
|
|
11
|
+
* apply
|
|
12
|
+
* saved --head <commit_sha> --paths <a,b,...>
|
|
13
|
+
*
|
|
14
|
+
* Saving and publishing follow the code project's own instructions.
|
|
15
|
+
*/
|
|
16
|
+
import { resolveProjectDir } from "../src/upgrade.mjs";
|
|
17
|
+
import { syncApply, syncCheck, syncSaved } from "../src/sync-core.mjs";
|
|
18
|
+
|
|
19
|
+
const VALUE_FLAGS = new Set(["--develop", "--line", "--head", "--paths"]);
|
|
20
|
+
|
|
21
|
+
function parse(argv) {
|
|
22
|
+
const flags = {};
|
|
23
|
+
const rest = [];
|
|
24
|
+
for (let index = 0; index < argv.length; index++) {
|
|
25
|
+
const arg = argv[index];
|
|
26
|
+
if (VALUE_FLAGS.has(arg)) flags[arg.slice(2)] = argv[++index];
|
|
27
|
+
else if (arg.startsWith("--")) throw new Error(`unknown option '${arg}'`);
|
|
28
|
+
else rest.push(arg);
|
|
29
|
+
}
|
|
30
|
+
return { flags, rest };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const projectDir = resolveProjectDir();
|
|
35
|
+
const { flags, rest } = parse(process.argv.slice(2));
|
|
36
|
+
const [verb, extra] = rest;
|
|
37
|
+
if (extra) throw new Error(`unexpected argument '${extra}'`);
|
|
38
|
+
const allowed = { check: ["develop", "line"], apply: [], saved: ["head", "paths"] }[verb];
|
|
39
|
+
if (!allowed) throw new Error("usage: foldspace sync <check|apply|saved> - see foldspace help sync");
|
|
40
|
+
const stray = Object.keys(flags).find((name) => !allowed.includes(name));
|
|
41
|
+
if (stray) throw new Error(`option '--${stray}' does not go with sync ${verb}`);
|
|
42
|
+
const report =
|
|
43
|
+
verb === "check" ? syncCheck(projectDir, flags) : verb === "apply" ? syncApply(projectDir) : syncSaved(projectDir, flags);
|
|
44
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`sync: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
package/package.json
CHANGED
package/recipes/INDEX.md
CHANGED
|
@@ -8,7 +8,7 @@ Read this whole table, then open the recipe closest to the outcome.
|
|
|
8
8
|
| [`find-by-name`](find-by-name/) | **L2** | The agent turns a name the user said into the id the next action needs; a count when no name is given; every tie when the name is ambiguous; near-misses when nothing matches | A data action. **No card** | 3 production builds |
|
|
9
9
|
| [`lookup-then-view`](lookup-then-view/) | **L2** | Show one account the user names - **start here**. One clear match: its card, no question. Several or none: the **pick-one** list - search, pages, one click - then the card: who it is, the money, open deals, people, open cases. The card action's **required** `accountId`, described as coming from the lookup, makes the agent call the lookup first. The app already hands over the id → `account-overview` | Two actions - an input card (`find_account`) and a view card (`show_account_overview`) - with their Agent Studio definitions word for word | 1 production build |
|
|
10
10
|
| [`pick-from-a-list`](pick-from-a-list/) | **L2** | A clickable list, shown **only** when the user has to choose — with "none of these" and cancel | An input card: the agent waits for the user's answer | 1 production build |
|
|
11
|
-
| [`bottom-bar`](bottom-bar/) | **L0** | The agent's resting entry point is a bar at the foot of the page whose starters change with the screen the user is on
|
|
11
|
+
| [`bottom-bar`](bottom-bar/) | **L0** | The agent's resting entry point is a bar at the foot of the page (Foldspace turns it on) whose starters - at most 3 - change with the screen the user is on, set from code | Page set-up, runs once at start-up | 2 production builds |
|
|
12
12
|
| [`swap-the-login-method`](swap-the-login-method/) | any | `apiFetch` for an app that authenticates with a custom header, or with cookies, instead of a bearer token | A replacement `agent/utils.ts` | 2 production builds (a third uses the default bearer token) |
|
|
13
13
|
| [`account-overview`](account-overview/) | **L2** | One account on a card, when the app already hands over its id: identity, pipeline total, contacts, cases, meetings. The agent gets what the card shows. An account the user names → `lookup-then-view` | A view card: no `callback`, no `awaitUserInput` | 1 production build |
|
|
14
14
|
| [`upload-contacts`](upload-contacts/) | **L4** | A CSV is previewed, then written only after the user chooses Import | An input card: the agent waits for the user's answer | 1 production build |
|
|
@@ -5,17 +5,19 @@ page that shows a few starters and opens the agent when one is clicked. Its
|
|
|
5
5
|
starters change with the screen the user is on, so an agent with two actions
|
|
6
6
|
still looks useful everywhere.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
product's own source, one from
|
|
8
|
+
**Foldspace turns the bar on** from its own settings - this code does not.
|
|
9
|
+
What the code does is pick each page's starters.
|
|
10
|
+
**Proven by 2 production builds** (one in the product's own source, one from
|
|
11
|
+
the agent project behind a feature flag).
|
|
11
12
|
|
|
12
13
|
## Adapt it
|
|
13
14
|
|
|
14
15
|
| In `agent/bottomBar.ts` | Change |
|
|
15
16
|
|---|---|
|
|
16
|
-
| `PAGE_STARTERS` |
|
|
17
|
-
| `
|
|
18
|
-
|
|
17
|
+
| `PAGE_STARTERS` | The product's main-menu screens (`npx foldspace observe menu`: path prefixes) and, for each, **at most 3 starters** the agent can answer today. A starter for something the agent cannot do is worse than none: leave that page out |
|
|
18
|
+
| `ACTION` or `KNOWLEDGE` | `ACTION` when clicking it runs one of the agent's actions - the bar shows the action icon. `KNOWLEDGE` when the answer comes from what the agent knows (its knowledge base) |
|
|
19
|
+
|
|
20
|
+
A page that is not listed shows Agent Studio's starters (`setConversationStarters(null)`).
|
|
19
21
|
|
|
20
22
|
Then call it from the bundle's entry point:
|
|
21
23
|
|
|
@@ -30,14 +32,25 @@ installBottomBar();
|
|
|
30
32
|
- **`setConversationStarters` replaces the whole set for the page** — both
|
|
31
33
|
groups. `null` restores Agent Studio's. So always pass the full list you
|
|
32
34
|
want shown.
|
|
35
|
+
- **It also sets the agent panel's opening screen**, not only the bar. One
|
|
36
|
+
customer's list had 7 there. At most 3 per page, both groups together:
|
|
37
|
+
`npx foldspace lint` refuses more. `maxVisibleStarters` caps the bar only.
|
|
38
|
+
- **Route changes in a single-page app don't reload**, so the starters must be
|
|
39
|
+
re-applied on navigation. The recipe uses the browser's own page-change
|
|
40
|
+
event (`navigation`'s `currententrychange`); where a browser has no
|
|
41
|
+
Navigation API (⚠️ some Safari and Firefox versions, not tried live) it
|
|
42
|
+
wraps `pushState` / `replaceState` and listens to `popstate`. It brings the
|
|
43
|
+
bar back to full only when the starters change; a navigation that keeps the
|
|
44
|
+
same starters leaves a bar that has folded to compact as it is.
|
|
45
|
+
- **An older SDK may lack these calls.** The recipe then logs once in the
|
|
46
|
+
console and leaves Agent Studio's starters.
|
|
47
|
+
- **No `bottomBarSettings`.** Send one only if the human asks for a setting
|
|
48
|
+
that differs from the default (the fields are on the Bottom Bar page), and
|
|
49
|
+
send `enabled: true` in the same group. ⚠️ Whether a partial group merges
|
|
50
|
+
with Foldspace's own settings is not verified.
|
|
33
51
|
- **In embedded mode the bar needs `openEmbeddedCallback`**, or the bar does
|
|
34
52
|
not render at all. This recipe is for the overlay agent; see the docs for
|
|
35
53
|
embedded.
|
|
36
|
-
- **Route changes in a single-page app don't reload**, so the starters must be
|
|
37
|
-
re-applied on navigation - this recipe wraps `pushState` / `replaceState`
|
|
38
|
-
and listens to `popstate`. It brings the bar back to full only when the
|
|
39
|
-
starters change; a navigation that keeps the same starters leaves a bar
|
|
40
|
-
that has folded to compact (after `idleTimeoutMs`) as it is.
|
|
41
54
|
- **Dark mode**: the SDK's default colours do not follow a dark theme yet
|
|
42
55
|
(PLG-5891, in the backlog); a product in dark mode may need `theme` /
|
|
43
56
|
`darkModeSettings` set.
|
|
@@ -1,52 +1,64 @@
|
|
|
1
1
|
// The bottom bar: the agent's resting entry point at the foot of the page,
|
|
2
|
-
//
|
|
3
|
-
// Agent Studio has no UI for it today, and this is how customers set it.
|
|
2
|
+
// showing starters that fit the screen the user is on.
|
|
4
3
|
// https://docs.foldspace.ai/customize/bottom-bar/
|
|
5
4
|
// https://docs.foldspace.ai/customize/conversation-starters/
|
|
6
5
|
//
|
|
6
|
+
// Foldspace turns the bar on from its own settings, so this code never sends
|
|
7
|
+
// `bottomBarSettings`: it only picks each page's starters.
|
|
8
|
+
//
|
|
7
9
|
// Product-neutral: the paths and starters below are placeholders. Replace them
|
|
8
|
-
// with this product's
|
|
10
|
+
// with this product's main-menu screens (`npx foldspace observe menu`) and,
|
|
11
|
+
// for each, starters the agent can answer today.
|
|
9
12
|
|
|
10
13
|
import { getAgent } from "./utils";
|
|
11
14
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The most starters one page shows, both groups together. The same call sets
|
|
17
|
+
* the agent panel's opening screen, and a long list there buries the one
|
|
18
|
+
* that matters. `npx foldspace lint` refuses more.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_STARTERS_PER_PAGE = 3;
|
|
17
21
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Starters per screen. Paths are matched as prefixes, first match wins.
|
|
24
|
+
* - `ACTION`: clicking it runs one of the agent's actions (the bar shows the
|
|
25
|
+
* action icon), e.g. "Show my open invoices".
|
|
26
|
+
* - `KNOWLEDGE`: a question answered from what the agent knows (its knowledge
|
|
27
|
+
* base), e.g. "How do I add a teammate?".
|
|
28
|
+
* A page that is not listed shows Agent Studio's starters.
|
|
29
|
+
*/
|
|
30
|
+
export const PAGE_STARTERS: Array<{ match: RegExp; KNOWLEDGE: string[]; ACTION: string[] }> = [
|
|
31
|
+
{ match: /^\/dashboard/, KNOWLEDGE: [], ACTION: ["<Something an action shows about this screen>"] },
|
|
32
|
+
{ match: /^\/<records>/, KNOWLEDGE: ["<A how-to question about this screen>"], ACTION: ["Find <a record> by name"] },
|
|
33
|
+
];
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
// `false`, and the bar then never renders) and `maxVisibleStarters: 3` (the
|
|
23
|
-
// docs default 5). Change any of them only if the human asks: every field is
|
|
24
|
-
// documented on the Bottom Bar page.
|
|
25
|
-
export const BOTTOM_BAR_SETTINGS = {
|
|
26
|
-
enabled: true,
|
|
27
|
-
maxVisibleStarters: 3,
|
|
28
|
-
reopenFrequency: "EVERY_LOAD",
|
|
29
|
-
starterClickBehavior: "OPEN_AGENT",
|
|
30
|
-
idleTimeoutMs: 5000,
|
|
31
|
-
initialBehaviorMode: "FULL",
|
|
32
|
-
} as const;
|
|
35
|
+
export type PageStarters = (typeof PAGE_STARTERS)[number];
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
+
/** This page's starters, or null for a page that keeps Agent Studio's. */
|
|
38
|
+
export function startersForPath(pathname: string): PageStarters | null {
|
|
39
|
+
return PAGE_STARTERS.find((entry) => entry.match.test(pathname)) ?? null;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// On window, not in module scope: a bundle evaluated twice on one page must
|
|
40
43
|
// not install two route listeners.
|
|
41
44
|
const INSTALLED_FLAG = "__foldspace_bottom_bar__";
|
|
42
45
|
|
|
46
|
+
const warned = new Set<string>();
|
|
47
|
+
function warnOnce(method: string): void {
|
|
48
|
+
if (warned.has(method)) return;
|
|
49
|
+
warned.add(method);
|
|
50
|
+
console.warn(
|
|
51
|
+
`[foldspace] This page's Foldspace SDK has no ${method}(), so the bottom bar keeps Agent Studio's starters. Load a newer SDK to get page starters.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
56
|
+
* Keep the bar's starters matched to the page. Call once from the bundle's
|
|
57
|
+
* entry point; safe to call again.
|
|
46
58
|
*
|
|
47
59
|
* `setConversationStarters` REPLACES the whole set for this page view (both
|
|
48
|
-
* groups)
|
|
49
|
-
*
|
|
60
|
+
* groups) and also sets the panel's opening screen; `null` restores what
|
|
61
|
+
* Agent Studio has.
|
|
50
62
|
*/
|
|
51
63
|
export function installBottomBar(): void {
|
|
52
64
|
const foldspace = (window as any).foldspace;
|
|
@@ -57,7 +69,12 @@ export function installBottomBar(): void {
|
|
|
57
69
|
foldspace("when", "ready", () => {
|
|
58
70
|
const agent = getAgent();
|
|
59
71
|
if (!agent) return;
|
|
60
|
-
|
|
72
|
+
if (typeof agent.setConversationStarters !== "function") {
|
|
73
|
+
warnOnce("setConversationStarters");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Agent Studio's starters are what the page starts with.
|
|
77
|
+
let current: PageStarters | null = null;
|
|
61
78
|
|
|
62
79
|
// On a navigation, the bar is brought back to full only when its starters
|
|
63
80
|
// changed, so the new ones are seen. Same starters: nothing is sent, and a
|
|
@@ -66,19 +83,40 @@ export function installBottomBar(): void {
|
|
|
66
83
|
const next = startersForPath(window.location.pathname);
|
|
67
84
|
if (next === current) return;
|
|
68
85
|
current = next;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
if (next) {
|
|
87
|
+
agent.setConversationStarters(
|
|
88
|
+
{
|
|
89
|
+
KNOWLEDGE: next.KNOWLEDGE.map((title) => ({ title })),
|
|
90
|
+
ACTION: next.ACTION.map((title) => ({ title })),
|
|
91
|
+
},
|
|
92
|
+
// The group shown first. Not documented: which group the bar itself
|
|
93
|
+
// shows when a page has both - check it on the page.
|
|
94
|
+
next.ACTION.length ? "ACTION" : "KNOWLEDGE",
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
agent.setConversationStarters(null);
|
|
98
|
+
}
|
|
99
|
+
if (!reopen) return;
|
|
100
|
+
if (typeof agent.openBottomBar === "function") agent.openBottomBar({ mode: "FULL" });
|
|
101
|
+
else warnOnce("openBottomBar");
|
|
74
102
|
};
|
|
75
103
|
|
|
76
|
-
// One whole settings group per call, as the docs do.
|
|
77
|
-
agent.setConfiguration?.({ bottomBarSettings: BOTTOM_BAR_SETTINGS });
|
|
78
104
|
apply(false);
|
|
79
105
|
|
|
80
|
-
// Single-page apps change the
|
|
106
|
+
// Single-page apps change the address without a page load: re-apply on
|
|
107
|
+
// each navigation. The browser's own page-change event where it has one:
|
|
108
|
+
// `currententrychange` fires once the address has changed - router links,
|
|
109
|
+
// pushState, replaceState, back and forward. (`navigate` fires before the
|
|
110
|
+
// address changes, so the page it would read is the old one.)
|
|
81
111
|
const onRouteChange = () => apply(true);
|
|
112
|
+
const navigation = (window as any).navigation;
|
|
113
|
+
if (navigation && typeof navigation.addEventListener === "function") {
|
|
114
|
+
navigation.addEventListener("currententrychange", onRouteChange);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Browsers without the Navigation API (some Safari and Firefox versions;
|
|
118
|
+
// not tried live): listen for back/forward and wrap pushState /
|
|
119
|
+
// replaceState.
|
|
82
120
|
window.addEventListener("popstate", onRouteChange);
|
|
83
121
|
const history = window.history as any;
|
|
84
122
|
for (const method of ["pushState", "replaceState"] as const) {
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"note": "This recipe makes no network calls. What it sends to the SDK on
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"reopenFrequency": "EVERY_LOAD",
|
|
8
|
-
"starterClickBehavior": "OPEN_AGENT",
|
|
9
|
-
"idleTimeoutMs": 5000,
|
|
10
|
-
"initialBehaviorMode": "FULL"
|
|
2
|
+
"note": "This recipe makes no network calls and sends no bottomBarSettings (Foldspace turns the bar on). What it sends to the SDK on each page:",
|
|
3
|
+
"/dashboard": {
|
|
4
|
+
"setConversationStarters": {
|
|
5
|
+
"starters": { "KNOWLEDGE": [], "ACTION": [{ "title": "<Something an action shows about this screen>" }] },
|
|
6
|
+
"defaultStarterType": "ACTION"
|
|
11
7
|
}
|
|
12
8
|
},
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
9
|
+
"/<records>/42": {
|
|
10
|
+
"setConversationStarters": {
|
|
11
|
+
"starters": { "KNOWLEDGE": [{ "title": "<A how-to question about this screen>" }], "ACTION": [{ "title": "Find <a record> by name" }] },
|
|
12
|
+
"defaultStarterType": "ACTION"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"a page with no starters of its own, after one that had them": {
|
|
16
|
+
"setConversationStarters": { "starters": null }
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -31,7 +31,7 @@ says). If you observed neither, leave the user anonymous.
|
|
|
31
31
|
|---|---|
|
|
32
32
|
| `SESSION_STORAGE_KEY`, `SESSION_HEADER` | The key and header name you **observed** |
|
|
33
33
|
| `EXTRA_HEADERS` in the cookies variant | What you **observed** the app sending besides the cookies; most apps send none |
|
|
34
|
-
| `API_BASE` in `constants.ts` | For a same-origin app, the
|
|
34
|
+
| `API_BASE` in `constants.ts` | For a same-origin app, `"/api"` (the path), so each user's page calls its own host - on a product with a subdomain per customer, never the address you observed (the manual's **Each customer on their own subdomain**). Empty stays a failure on purpose, so a guessed host cannot look like success. Both files build their URLs with `apiBaseUrl()`, never `API_BASE` directly |
|
|
35
35
|
|
|
36
36
|
## What those builds learned the hard way
|
|
37
37
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Only the transport is replaced. What a 401, a 404 or a 429 means still comes
|
|
5
5
|
// from the harness, so actions and renderFailure behave exactly the same.
|
|
6
6
|
|
|
7
|
-
import { configure, httpFailure, networkFailure, type ApiResult } from "@foldspace_npm/harness/runtime";
|
|
7
|
+
import { apiBaseUrl, configure, httpFailure, networkFailure, type ApiResult } from "@foldspace_npm/harness/runtime";
|
|
8
8
|
import { AGENT_API_NAME, API_BASE } from "./constants";
|
|
9
9
|
|
|
10
10
|
configure({ agentApiName: AGENT_API_NAME, apiBase: API_BASE });
|
|
@@ -27,13 +27,13 @@ export async function apiFetch<T = unknown>(
|
|
|
27
27
|
// Empty API_BASE is a failure on purpose: a guessed host cannot look like
|
|
28
28
|
// success. `error` is drawn on the card and read by the agent - it says what
|
|
29
29
|
// is true; the builder's hint goes in `detail`, which only the console sees.
|
|
30
|
-
if (!
|
|
30
|
+
if (!apiBaseUrl()) {
|
|
31
31
|
return {
|
|
32
32
|
ok: false,
|
|
33
33
|
status: 0,
|
|
34
34
|
reason: "config",
|
|
35
35
|
error: "This is not set up yet.",
|
|
36
|
-
detail: "API_BASE is not set
|
|
36
|
+
detail: "API_BASE is not set, or names a {subdomain} this page does not have.",
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
const session = readSession();
|
|
@@ -42,7 +42,7 @@ export async function apiFetch<T = unknown>(
|
|
|
42
42
|
const controller = new AbortController();
|
|
43
43
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
44
44
|
try {
|
|
45
|
-
const res = await fetch(`${
|
|
45
|
+
const res = await fetch(`${apiBaseUrl()}${path}`, {
|
|
46
46
|
...init,
|
|
47
47
|
signal: controller.signal,
|
|
48
48
|
headers: { "Content-Type": "application/json", [SESSION_HEADER]: session, ...(init.headers ?? {}) },
|
|
@@ -69,13 +69,13 @@ export async function apiFetchBinary(
|
|
|
69
69
|
init: RequestInit = {},
|
|
70
70
|
timeoutMs = 15_000,
|
|
71
71
|
): Promise<ApiResult<Uint8Array>> {
|
|
72
|
-
if (!
|
|
72
|
+
if (!apiBaseUrl()) {
|
|
73
73
|
return {
|
|
74
74
|
ok: false,
|
|
75
75
|
status: 0,
|
|
76
76
|
reason: "config",
|
|
77
77
|
error: "This is not set up yet.",
|
|
78
|
-
detail: "API_BASE is not set
|
|
78
|
+
detail: "API_BASE is not set, or names a {subdomain} this page does not have.",
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
const session = readSession();
|
|
@@ -84,7 +84,7 @@ export async function apiFetchBinary(
|
|
|
84
84
|
const controller = new AbortController();
|
|
85
85
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
86
86
|
try {
|
|
87
|
-
const res = await fetch(`${
|
|
87
|
+
const res = await fetch(`${apiBaseUrl()}${path}`, {
|
|
88
88
|
...init,
|
|
89
89
|
signal: controller.signal,
|
|
90
90
|
headers: { [SESSION_HEADER]: session, ...(init.headers ?? {}) },
|
|
@@ -103,6 +103,7 @@ export async function apiFetchBinary(
|
|
|
103
103
|
// the harness's returns a config failure here, because this file sets no
|
|
104
104
|
// `authSource`. For the same reason getAuthToken returns null (see the README).
|
|
105
105
|
export {
|
|
106
|
+
apiBaseUrl,
|
|
106
107
|
getAgent,
|
|
107
108
|
armAllInstances,
|
|
108
109
|
rankBy,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// There is nothing to check before the request, so "signed out" is only known
|
|
5
5
|
// from the 401. It comes back as `signed_out` like any other.
|
|
6
6
|
|
|
7
|
-
import { configure, httpFailure, networkFailure, type ApiResult } from "@foldspace_npm/harness/runtime";
|
|
7
|
+
import { apiBaseUrl, configure, httpFailure, networkFailure, type ApiResult } from "@foldspace_npm/harness/runtime";
|
|
8
8
|
import { AGENT_API_NAME, API_BASE } from "./constants";
|
|
9
9
|
|
|
10
10
|
configure({ agentApiName: AGENT_API_NAME, apiBase: API_BASE });
|
|
@@ -24,20 +24,20 @@ export async function apiFetch<T = unknown>(
|
|
|
24
24
|
// Empty API_BASE is a failure on purpose: a guessed host cannot look like
|
|
25
25
|
// success. `error` is drawn on the card and read by the agent - it says what
|
|
26
26
|
// is true; the builder's hint goes in `detail`, which only the console sees.
|
|
27
|
-
if (!
|
|
27
|
+
if (!apiBaseUrl()) {
|
|
28
28
|
return {
|
|
29
29
|
ok: false,
|
|
30
30
|
status: 0,
|
|
31
31
|
reason: "config",
|
|
32
32
|
error: "This is not set up yet.",
|
|
33
|
-
detail: "API_BASE is not set
|
|
33
|
+
detail: "API_BASE is not set, or names a {subdomain} this page does not have.",
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const controller = new AbortController();
|
|
38
38
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
39
|
try {
|
|
40
|
-
const res = await fetch(`${
|
|
40
|
+
const res = await fetch(`${apiBaseUrl()}${path}`, {
|
|
41
41
|
...init,
|
|
42
42
|
credentials: "include",
|
|
43
43
|
signal: controller.signal,
|
|
@@ -65,20 +65,20 @@ export async function apiFetchBinary(
|
|
|
65
65
|
init: RequestInit = {},
|
|
66
66
|
timeoutMs = 15_000,
|
|
67
67
|
): Promise<ApiResult<Uint8Array>> {
|
|
68
|
-
if (!
|
|
68
|
+
if (!apiBaseUrl()) {
|
|
69
69
|
return {
|
|
70
70
|
ok: false,
|
|
71
71
|
status: 0,
|
|
72
72
|
reason: "config",
|
|
73
73
|
error: "This is not set up yet.",
|
|
74
|
-
detail: "API_BASE is not set
|
|
74
|
+
detail: "API_BASE is not set, or names a {subdomain} this page does not have.",
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const controller = new AbortController();
|
|
79
79
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
80
80
|
try {
|
|
81
|
-
const res = await fetch(`${
|
|
81
|
+
const res = await fetch(`${apiBaseUrl()}${path}`, {
|
|
82
82
|
...init,
|
|
83
83
|
credentials: "include",
|
|
84
84
|
signal: controller.signal,
|
|
@@ -98,6 +98,7 @@ export async function apiFetchBinary(
|
|
|
98
98
|
// the harness's returns a config failure here, because this file sets no
|
|
99
99
|
// `authSource`. For the same reason getAuthToken returns null (see the README).
|
|
100
100
|
export {
|
|
101
|
+
apiBaseUrl,
|
|
101
102
|
getAgent,
|
|
102
103
|
armAllInstances,
|
|
103
104
|
rankBy,
|
package/src/chrome-binary.mjs
CHANGED
|
@@ -26,3 +26,36 @@ export function chromeCandidates(platform = os.platform(), env = process.env) {
|
|
|
26
26
|
export function findChrome({ env = process.env, platform = os.platform(), exists = fs.existsSync } = {}) {
|
|
27
27
|
return env.CHROME_PATH || chromeCandidates(platform, env).find((candidate) => exists(candidate)) || null;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
// Chrome slows a page whose window is hidden, covered or minimized - timers
|
|
31
|
+
// throttled, the renderer moved to the background - and the build in it stalls.
|
|
32
|
+
// These keep the test window working while the builder looks at another app
|
|
33
|
+
// or another macOS user's session (⚠️ not tried live with a user switch).
|
|
34
|
+
// The hidden Chrome that makes the video uses them too.
|
|
35
|
+
export const KEEP_RUNNING_ARGS = Object.freeze([
|
|
36
|
+
"--disable-background-timer-throttling",
|
|
37
|
+
"--disable-renderer-backgrounding",
|
|
38
|
+
"--disable-backgrounding-occluded-windows",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** How `inject` starts the test window. */
|
|
42
|
+
export function testWindowArgs({ profileDir, debugPort, themeDir, startUrl }) {
|
|
43
|
+
return [
|
|
44
|
+
`--user-data-dir=${profileDir}`,
|
|
45
|
+
`--remote-debugging-port=${debugPort}`,
|
|
46
|
+
// The theme only paints the isolated window Foldspace blue. SDK and action
|
|
47
|
+
// code are loaded later by attach over CDP.
|
|
48
|
+
`--load-extension=${themeDir}`,
|
|
49
|
+
`--disable-extensions-except=${themeDir}`,
|
|
50
|
+
// Chrome 136+ dropped --load-extension. Keep the legacy switch for older
|
|
51
|
+
// builds, and enable the CDP Extensions domain so we can load it at runtime.
|
|
52
|
+
"--disable-features=DisableLoadExtensionCommandLineSwitch",
|
|
53
|
+
"--enable-unsafe-extension-debugging",
|
|
54
|
+
"--no-first-run",
|
|
55
|
+
"--no-default-browser-check",
|
|
56
|
+
"--test-type",
|
|
57
|
+
"--remote-allow-origins=*",
|
|
58
|
+
...KEEP_RUNNING_ARGS,
|
|
59
|
+
startUrl,
|
|
60
|
+
];
|
|
61
|
+
}
|
package/src/cli-registry.mjs
CHANGED
|
@@ -76,7 +76,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
76
76
|
name: "upgrade",
|
|
77
77
|
entry: null,
|
|
78
78
|
group: "start",
|
|
79
|
-
summary: "Check for a newer harness and
|
|
79
|
+
summary: "Check for a newer harness and update the version this project uses",
|
|
80
80
|
usage:
|
|
81
81
|
"foldspace upgrade [--check | --yes] [--refresh-instructions]",
|
|
82
82
|
risk: "local-write",
|
|
@@ -91,7 +91,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
91
91
|
flag("--check", "Compare versions and print JSON; never install"),
|
|
92
92
|
flag(
|
|
93
93
|
"--yes",
|
|
94
|
-
"Install the latest version after the human has agreed",
|
|
94
|
+
"Install the latest version: within the same line (update.autoUpgrade) without asking; across a line, after the human has agreed",
|
|
95
95
|
),
|
|
96
96
|
flag(
|
|
97
97
|
"--refresh-instructions",
|
|
@@ -111,11 +111,56 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
111
111
|
"Any foldspace command writes a harness skill the project is missing, so the first upgrade from a harness older than the skill still gets it",
|
|
112
112
|
],
|
|
113
113
|
next: [
|
|
114
|
-
"If
|
|
114
|
+
"If update.autoUpgrade (a newer version in the same line, such as 0.1.x): run npx foldspace upgrade --yes without asking, and tell the human in one plain line",
|
|
115
|
+
"If outdated across a line (0.1 to 0.2): ask the human, then run npx foldspace upgrade --yes",
|
|
115
116
|
"Run npm run build after an upgrade",
|
|
116
117
|
"If attach is running: `npx foldspace attach --stop`, then `npx foldspace attach --daemon` with the flags it ran with, to load the rebuild",
|
|
117
118
|
],
|
|
118
119
|
}),
|
|
120
|
+
Object.freeze({
|
|
121
|
+
name: "sync",
|
|
122
|
+
entry: "sync.mjs",
|
|
123
|
+
group: "start",
|
|
124
|
+
summary: "Keep agent/ up to date with the agent's saved code: what to pull, then merge it in",
|
|
125
|
+
usage:
|
|
126
|
+
"foldspace sync <check|apply|saved> [--develop <sha>] [--line <sha>] [--head <sha>] [--paths <a,b>]",
|
|
127
|
+
risk: "local-write",
|
|
128
|
+
environment: "node",
|
|
129
|
+
environmentVariables: ["FOLDSPACE_PROJECT_DIR"],
|
|
130
|
+
capabilities: ["project.sync"],
|
|
131
|
+
positionals: [
|
|
132
|
+
Object.freeze({
|
|
133
|
+
name: "verb",
|
|
134
|
+
required: true,
|
|
135
|
+
description:
|
|
136
|
+
"check: compare the heads you just read with what this folder last pulled, and say what to pull | apply: merge what you pulled into agent/ | saved: remember a save this folder made",
|
|
137
|
+
}),
|
|
138
|
+
],
|
|
139
|
+
options: [
|
|
140
|
+
value("--develop", "sha", "check: develop_head from repo_read status"),
|
|
141
|
+
value("--line", "sha", "check: the head of foldspace/<Agent Key>, the line of work saves go to (the sha repo_read tree prints for that ref); leave it out while that line does not exist"),
|
|
142
|
+
value("--head", "sha", "saved: the commit_sha the save returned"),
|
|
143
|
+
value("--paths", "a,b", "saved: the committed_paths the save returned, comma-separated"),
|
|
144
|
+
],
|
|
145
|
+
prerequisites: [
|
|
146
|
+
"foldspace.dev.json",
|
|
147
|
+
"A code project (repo_read status), read with the Foldspace tools: this command never reaches Foldspace itself",
|
|
148
|
+
],
|
|
149
|
+
effects: [
|
|
150
|
+
"Writes only under .foldspace-dev/ (saved-code/ and agent-kept/) and, for apply, agent/",
|
|
151
|
+
"Changes nothing about saving or publishing: those follow repo_read command instructions",
|
|
152
|
+
"check names each ref to pull and the folder to write it into (pull[].into); it pulls nothing itself",
|
|
153
|
+
"apply copies agent/ to .foldspace-dev/agent-kept/<time>/ first, then merges file by file against what this folder last pulled: a file only one side changed takes that side; one both changed takes the saved version and is listed in conflicts",
|
|
154
|
+
"On a first pull (nothing remembered), the saved code wins every difference, and of the files it lacks only agent/actions/, agent/api/ and agent/views/ go back",
|
|
155
|
+
"agent/runtime/ is the build's: differences there are never conflicts",
|
|
156
|
+
],
|
|
157
|
+
next: [
|
|
158
|
+
"check with pull: read agent/ at each ref into its into folder (repo_read tree and file), then npx foldspace sync apply",
|
|
159
|
+
"check up to date: save as repo_read command instructions says, with base_sha = baseSha",
|
|
160
|
+
"apply: follow its next - conflicts mean putting this session's change back on top; then npm run build",
|
|
161
|
+
"After each save: npx foldspace sync saved with the commit_sha and committed_paths it returned",
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
119
164
|
Object.freeze({
|
|
120
165
|
name: "build",
|
|
121
166
|
entry: "build-cli.mjs",
|
|
@@ -326,6 +371,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
326
371
|
],
|
|
327
372
|
options: [
|
|
328
373
|
value("--click", "label", "screen: reach the screen by a navigation label, as a person would"),
|
|
374
|
+
flag("--human-said-yes", "screen --click: the human said yes in chat to clicking this one plain button; a label that reads like an action is still refused"),
|
|
329
375
|
value("--goto", "path", "screen: a path on this host, or a full https address on a host this project names"),
|
|
330
376
|
value("--match", "words", "screen: rank the requests seen by these words"),
|
|
331
377
|
value("--wait", "ms", "screen/auth: how long to keep listening"),
|
|
@@ -577,6 +623,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
577
623
|
export const CAPABILITY_CATALOGUE = Object.freeze([
|
|
578
624
|
["project.scaffold", "Create a new configured project on local disk"],
|
|
579
625
|
["project.upgrade", "Update the harness version the consuming project uses"],
|
|
626
|
+
["project.sync", "Merge the agent's saved code, read with the Foldspace tools, into agent/"],
|
|
580
627
|
["artifact.build", "Build the portable dist/index.js action artifact"],
|
|
581
628
|
["browser.launch", "Launch an isolated local Chrome profile"],
|
|
582
629
|
["browser.cdp", "Connect to local Chrome through CDP"],
|
|
@@ -656,9 +703,10 @@ export function createCliRegistry({ packageName, packageVersion, env = process.e
|
|
|
656
703
|
"The harness bin alias is equivalent to foldspace.",
|
|
657
704
|
"attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
|
|
658
705
|
"Coding agents run attach --daemon; foreground attach is for someone watching the terminal.",
|
|
659
|
-
"If update.
|
|
660
|
-
"If update.broken is true, this version was withdrawn and build and attach refuse to run.
|
|
706
|
+
"If update.autoUpgrade is true (a newer version in the same line), run npx foldspace upgrade --yes without asking and tell the human in one plain line; if update.outdated is true across a line, ask the human first.",
|
|
707
|
+
"If update.broken is true, this version was withdrawn and build and attach refuse to run. Within the same line, upgrade without asking; across a line, ask the human to agree to the upgrade, and set FOLDSPACE_SKIP_UPDATE_CHECK=1 only if they decline.",
|
|
661
708
|
"observe looks at the app, and ask tests the agent through its panel; both work while attach runs.",
|
|
709
|
+
"With a code project, pull the saved code with `npx foldspace sync` at session start, before each new experience and before each save or publish (manual: \"Several builders, one agent\"); saving and publishing follow repo_read command instructions.",
|
|
662
710
|
"`foldspace deploy` is for Foldspace staff only: never run or offer it - it is not the `deploy` tool. With a code project, putting the agent's code in front of their users is yours, as the manual's \"Done, and what you say at the end\" says: save with `repo_write`; publish to dev with the `deploy` tool (say you are doing it, then do it - unless its dev preview comes back with no warnings, which means this agent loads dev for its users: then ask first, as for production); check dev with `npx foldspace attach --stop`, `npx foldspace published dev` and `npx foldspace ask \"<question>\" --retry 2`; and publish to production only once that check passed and the human says yes. The dev check and production need a site that loads the agent by itself: after `attach --bootstrap` or `--replace`, publish to dev only, keep attach running, and the rest is a step the Foldspace team does with the human. If `published` says this window cannot show dev, say so in one sentence, attach again, and stop after dev. Without a code project, or when a Foldspace tool refuses (except a save refused because someone saved after you read: redo it as `repo_read` command `instructions` says), the Foldspace team does it with the human (https://foldspace.ai/demo, support@foldspace.ai).",
|
|
663
711
|
],
|
|
664
712
|
};
|