@foldspace_npm/harness 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +169 -49
- package/README.md +30 -2
- package/bin/inject.mjs +2 -18
- package/bin/observe.mjs +18 -0
- package/bin/record.mjs +2 -0
- package/bin/share.mjs +63 -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 +96 -5
- package/src/customer-subdomain.mjs +149 -0
- package/src/lint/index.mjs +23 -3
- package/src/lint/rules.mjs +46 -2
- package/src/lint/safety.mjs +785 -0
- package/src/observe-core.mjs +1 -1
- package/src/runtime/VERSION +1 -1
- package/src/runtime/config.ts +15 -2
- package/src/runtime/http.ts +40 -3
- package/src/runtime/index.ts +1 -0
- package/src/sync-core.mjs +374 -0
- package/src/upgrade.mjs +36 -7
- package/src/video/compose.mjs +12 -3
- package/src/video/recorder.mjs +15 -7
- package/src/video/share.mjs +284 -0
- 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 +47 -4
|
@@ -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",
|
|
@@ -490,6 +535,48 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
490
535
|
"To change a scene's capabilities or line: edit the scenes file, then `npx foldspace record --from <recordDir>`",
|
|
491
536
|
],
|
|
492
537
|
}),
|
|
538
|
+
Object.freeze({
|
|
539
|
+
name: "share",
|
|
540
|
+
entry: "share.mjs",
|
|
541
|
+
group: "share",
|
|
542
|
+
summary: "Size and check a video for a Foldspace share link, then upload it with the targets create_video_share returned",
|
|
543
|
+
usage: "foldspace share <inspect|upload> [<video.mp4>] [--poster <file.jpg>] [--targets '<json>']",
|
|
544
|
+
risk: "remote-upload",
|
|
545
|
+
environment: "node",
|
|
546
|
+
environmentVariables: ["FOLDSPACE_PROJECT_DIR"],
|
|
547
|
+
capabilities: ["video.upload"],
|
|
548
|
+
positionals: [
|
|
549
|
+
Object.freeze({
|
|
550
|
+
name: "action",
|
|
551
|
+
required: true,
|
|
552
|
+
description:
|
|
553
|
+
"inspect: size and check the MP4 and its poster here, uploading nothing | upload: send both to Foldspace's video storage with the targets create_video_share returned",
|
|
554
|
+
}),
|
|
555
|
+
Object.freeze({
|
|
556
|
+
name: "video",
|
|
557
|
+
required: false,
|
|
558
|
+
description: "The MP4; default the newest video/*.mp4 that `record` made",
|
|
559
|
+
}),
|
|
560
|
+
],
|
|
561
|
+
options: [
|
|
562
|
+
value("--poster", "file.jpg", "The JPEG poster", { default: "the MP4's name with .jpg" }),
|
|
563
|
+
value("--targets", "json", "upload only: the targets JSON exactly as the upload command from create_video_share has it"),
|
|
564
|
+
],
|
|
565
|
+
prerequisites: [
|
|
566
|
+
"An H.264 MP4 with its moov box first (what `record` makes) and a JPEG poster",
|
|
567
|
+
"upload: the human said yes to a public link, and the Foldspace MCP tool create_video_share returned the upload command (its targets last 30 minutes)",
|
|
568
|
+
],
|
|
569
|
+
effects: [
|
|
570
|
+
"inspect reads the two files and nothing else",
|
|
571
|
+
"upload sends the MP4 and the poster to Foldspace's video storage, and only there; it reads no Foldspace sign-in",
|
|
572
|
+
"Never prints the upload targets: each is a one-time credential for one file",
|
|
573
|
+
"Anyone with the link publish_video_share returns can watch the video, which shows real data from the account",
|
|
574
|
+
],
|
|
575
|
+
next: [
|
|
576
|
+
"After inspect, with the human's yes: create_video_share with a title, video_bytes and poster_bytes, then run its upload command",
|
|
577
|
+
"After upload: publish_video_share with the share_id; give the human the link",
|
|
578
|
+
],
|
|
579
|
+
}),
|
|
493
580
|
Object.freeze({
|
|
494
581
|
name: "deploy",
|
|
495
582
|
// Foldspace-internal: it uploads to Foldspace's own storage, which no
|
|
@@ -535,6 +622,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
535
622
|
export const CAPABILITY_CATALOGUE = Object.freeze([
|
|
536
623
|
["project.scaffold", "Create a new configured project on local disk"],
|
|
537
624
|
["project.upgrade", "Update the harness version the consuming project uses"],
|
|
625
|
+
["project.sync", "Merge the agent's saved code, read with the Foldspace tools, into agent/"],
|
|
538
626
|
["artifact.build", "Build the portable dist/index.js action artifact"],
|
|
539
627
|
["browser.launch", "Launch an isolated local Chrome profile"],
|
|
540
628
|
["browser.cdp", "Connect to local Chrome through CDP"],
|
|
@@ -542,12 +630,14 @@ export const CAPABILITY_CATALOGUE = Object.freeze([
|
|
|
542
630
|
["page.evaluate", "Inspect bounded SDK and registration state"],
|
|
543
631
|
["page.mutate", "Prepare and restore a matching development page"],
|
|
544
632
|
["artifact.publish", "Write the built artifact to remote storage"],
|
|
633
|
+
["video.upload", "Upload a video and its poster for a Foldspace share link, with targets the Foldspace MCP issued"],
|
|
545
634
|
]);
|
|
546
635
|
|
|
547
636
|
export const RISK_CATALOGUE = Object.freeze([
|
|
548
637
|
["local-write", "Writes only inside the consuming project"],
|
|
549
638
|
["browser-session", "Launches or temporarily mutates a local browser session"],
|
|
550
639
|
["remote-publish", "Writes the action artifact to remote storage"],
|
|
640
|
+
["remote-upload", "Uploads files the human chose to share to Foldspace's video storage"],
|
|
551
641
|
]);
|
|
552
642
|
|
|
553
643
|
export const ENVIRONMENT_CATALOGUE = Object.freeze([
|
|
@@ -612,9 +702,10 @@ export function createCliRegistry({ packageName, packageVersion, env = process.e
|
|
|
612
702
|
"The harness bin alias is equivalent to foldspace.",
|
|
613
703
|
"attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
|
|
614
704
|
"Coding agents run attach --daemon; foreground attach is for someone watching the terminal.",
|
|
615
|
-
"If update.
|
|
616
|
-
"If update.broken is true, this version was withdrawn and build and attach refuse to run.
|
|
705
|
+
"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.",
|
|
706
|
+
"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.",
|
|
617
707
|
"observe looks at the app, and ask tests the agent through its panel; both work while attach runs.",
|
|
708
|
+
"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.",
|
|
618
709
|
"`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).",
|
|
619
710
|
],
|
|
620
711
|
};
|