@foldspace_npm/harness 0.1.30 → 0.1.32
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 +42 -11
- package/README.md +25 -2
- package/bin/ask.mjs +16 -145
- package/bin/attach.mjs +2 -0
- package/bin/cli.mjs +22 -0
- package/bin/inject.mjs +2 -14
- package/bin/record.mjs +412 -0
- package/package.json +1 -1
- package/src/attach-daemon.mjs +18 -0
- package/src/badge-auto.mjs +38 -1
- package/src/cdp-client.mjs +7 -1
- package/src/chrome-binary.mjs +28 -0
- package/src/cli-help.mjs +1 -0
- package/src/cli-registry.mjs +53 -3
- package/src/init.mjs +170 -16
- package/src/panel-driver.mjs +283 -0
- package/src/session-events.mjs +1 -0
- package/src/upgrade.mjs +87 -1
- package/src/video/README.md +29 -0
- package/src/video/assets/foldspace-mark.svg +27 -0
- package/src/video/assets/logo-dark.svg +68 -0
- package/src/video/assets/logo-light.svg +56 -0
- package/src/video/brand.mjs +46 -0
- package/src/video/compose.mjs +476 -0
- package/src/video/composer/composer.mjs +554 -0
- package/src/video/composer/index.html +13 -0
- package/src/video/fonts/OFL.txt +93 -0
- package/src/video/fonts/Poppins-ExtraBold.woff2 +0 -0
- package/src/video/fonts/Poppins-Regular.woff2 +0 -0
- package/src/video/fonts/Poppins-SemiBold.woff2 +0 -0
- package/src/video/recorder.mjs +331 -0
- package/src/video/scenes.mjs +253 -0
- package/src/video/timeline.mjs +726 -0
- package/src/video/vendor/mp4-muxer.LICENSE +21 -0
- package/src/video/vendor/mp4-muxer.min.mjs +5 -0
- package/templates/agent-starter/AGENTS.md +24 -0
- package/templates/agent-starter/README.md +5 -0
- package/templates/agent-starter/claude-settings.json +1 -0
- package/templates/agent-starter/gitignore +3 -0
- package/templates/agent-starter/skills/share-a-video/SKILL.md +235 -0
package/CLAUDE.md
CHANGED
|
@@ -29,9 +29,9 @@ session ends at something they can **see on their own page** - not at
|
|
|
29
29
|
|
|
30
30
|
| Result | Do |
|
|
31
31
|
|---|---|
|
|
32
|
-
| The tool does not exist | The Foldspace MCP is not installed in this editor. Install it yourself, the one way that fits *this* region and editor. **EU** - any editor: the MCP block on <https://foldspace.ai/eu/agents.md>, never the plugin. **US** - Claude Code with git on the machine (`command -v git`): `claude plugin marketplace add https://github.com/foldspace-ai/plugins` (already added, or added from another source, is fine), then `claude plugin install foldspace-agent-builder@foldspace-plugins`; without git, or any other editor: the MCP block on <https://foldspace.ai/agents.md>. Either way it needs sign-in: start it before your turn ends, as **Connect Foldspace tools** on that page says. **The plugin** loads when your turn ends: call `list_agents` again on the human's next message. **A server added from the MCP block** (`claude mcp add`) never reaches a chat that is already running - not on the next message, not through `/mcp`: once it is signed in, the human opens a new chat in this folder and repeats their request. `claude mcp get` saying Connected proves only the setting; the check is `list_agents` in the chat you are in |
|
|
33
|
-
| It exists but fails on auth | Installed, not signed in. Only the human can approve sign-in. Tell them exactly where - Claude Code: `/mcp` → **foldspace-eu** (**EU**) or **foldspace** (**US**; listed as `plugin:foldspace-agent-builder:foldspace` when the plugin installed it) → finish in the browser - then check again. If `/mcp` does not list that server, it was added after this chat started: the human opens a new chat in this folder, as above |
|
|
34
|
-
| It returns agents |
|
|
32
|
+
| The tool does not exist | The Foldspace MCP is not installed in this editor. Install it yourself, the one way that fits *this* region and editor. **EU** - any editor: the MCP block on <https://foldspace.ai/eu/agents.md>, never the plugin. **US** - Claude Code with git on the machine (`command -v git`): `claude plugin marketplace add https://github.com/foldspace-ai/plugins` (already added, or added from another source, is fine), then `claude plugin install foldspace-agent-builder@foldspace-plugins`; without git, or any other editor: the MCP block on <https://foldspace.ai/agents.md>. Either way it needs sign-in: start it before your turn ends, as **Connect Foldspace tools** on that page says. **The plugin** loads when your turn ends: call `list_agents` again on the human's next message. **A server added from the MCP block** (`claude mcp add`) never reaches a chat that is already running - not on the next message, not through `/mcp`: once it is signed in, the human opens a new chat in this folder and repeats their request. `claude mcp get` saying Connected proves only the setting; the check is `list_agents` in the chat you are in. **Codex**: `codex mcp add foldspace --url https://api.foldspace.ai/mcp` (**EU**: `foldspace-eu` and `https://api.eu.foldspace.ai/mcp`) adds it and starts sign-in in one step; a server added during a thread never reaches that thread, so once it is signed in the work continues in a new thread in this folder: start it yourself if Codex gives you a tool for it - the human's request word for word, plus the one paragraph the playbook's Codex block gives - and say so in one line; otherwise the human starts it and repeats their request. **Cursor**: merge the server into `~/.cursor/mcp.json` yourself, keeping every other server; it reaches this chat on the human's next message, needing sign-in. **Any editor**: never read a saved sign-in or credentials file, and never reach the Foldspace MCP any other way (curl, a script, a client you write). If its tools are not in this chat, the fix is a new chat |
|
|
33
|
+
| It exists but fails on auth | Installed, not signed in. Only the human can approve sign-in. Tell them exactly where - Claude Code: `/mcp` → **foldspace-eu** (**EU**) or **foldspace** (**US**; listed as `plugin:foldspace-agent-builder:foldspace` when the plugin installed it) → finish in the browser - then check again. If `/mcp` does not list that server, it was added after this chat started: the human opens a new chat in this folder, as above. Codex: run `codex mcp login foldspace` (`foldspace-eu` for **EU**) - if the browser does not open, give the human the link it prints - then a new thread. Cursor: call the server's own sign-in tool, `mcp_auth`, yourself - the human finishes in the browser - then `list_agents` in this same chat |
|
|
34
|
+
| It returns agents | Work on the agent whose Agent Key this project has (`agentApiName` in `foldspace.dev.json`). Name it in one line - name, Agent Key and product - and carry on; the human objects if it is wrong. Skip that line when the setup playbook (agents.md) already named it in this chat. When no agent has that key, this account cannot see this project's agent: tell the human which Agent Key the project is for, and that they type `/mcp`, pick that server, and sign in as the account that has it; then check again. Never work on another agent from this folder - one the human wants instead gets a folder of its own (agents.md, from **Confirm the signed-in account**) |
|
|
35
35
|
|
|
36
36
|
3. **Orient** — `get_agent_settings`, `list_actions`, `list_task_agents`,
|
|
37
37
|
`list_navigation_routes`. Record current versions before changing anything;
|
|
@@ -39,17 +39,26 @@ session ends at something they can **see on their own page** - not at
|
|
|
39
39
|
seeded at sign-up are often a specification someone already wrote.
|
|
40
40
|
4. **Read `docs/app-profile.md`.** It is what is already known about this app,
|
|
41
41
|
with how each fact was established.
|
|
42
|
-
5. **Does the agent already have code saved in Foldspace?** If
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
5. **Does the agent already have code saved in Foldspace?** If the setup
|
|
43
|
+
playbook (agents.md) ran in this chat, it already asked: use its answer in
|
|
44
|
+
the list below, and do not tell the human again what setup told them. If
|
|
45
|
+
setup also set this folder up and found a code project, the project's
|
|
46
|
+
`agent/` is already here - do not replace it again. Otherwise, if your
|
|
47
|
+
Foldspace tools include `repo_read`, call it with command `status` now -
|
|
48
|
+
before the first build, because the first build is what the test window
|
|
49
|
+
shows them.
|
|
45
50
|
- **A code project.** Call `repo_read` with command `instructions` and
|
|
46
51
|
follow what it returns. The project's `agent/` is the copy Foldspace
|
|
47
52
|
builds and publishes, and it may hold work someone else saved. Before you
|
|
48
53
|
build, replace this folder's `agent/` with it (`repo_read` commands `tree`
|
|
49
|
-
and `file
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
and `file`, with the `ref` the instructions name - the default can be
|
|
55
|
+
older than what someone saved): delete what is there rather than copying
|
|
56
|
+
over the top, or their page shows the template instead of their agent.
|
|
57
|
+
This folder's `agent/` may hold work a session never saved, so copy it to
|
|
58
|
+
`.foldspace-dev/agent-kept/` first. Put back the action files the project
|
|
59
|
+
lacks; where a file differs, the project's wins. If any differed, tell
|
|
60
|
+
the human in one line which actions had changes on this computer that
|
|
61
|
+
were never saved.
|
|
53
62
|
- **`NO_REPO`.** Nothing is saved yet. Follow the next step `status` gives:
|
|
54
63
|
it creates the code project in about a minute and changes nothing anyone
|
|
55
64
|
sees - say you are setting up their project, no need to ask. Get the agent
|
|
@@ -73,6 +82,12 @@ session ends at something they can **see on their own page** - not at
|
|
|
73
82
|
- **No `repo_read` tool.** This folder is the only copy, so there is no code
|
|
74
83
|
project.
|
|
75
84
|
|
|
85
|
+
With a code project, a published action (step 3, `isPublished: true`)
|
|
86
|
+
whose handler `agent/actions/index.ts` does not register has its code
|
|
87
|
+
somewhere else - on someone's computer, or in their product's own code.
|
|
88
|
+
When you ask to publish for their users, name it and say it may stop
|
|
89
|
+
working once this project is published, because its code is not in it.
|
|
90
|
+
|
|
76
91
|
Before an unfamiliar harness operation, `npx foldspace help --json` is the
|
|
77
92
|
current CLI contract — risk, prerequisites, effects, next step.
|
|
78
93
|
|
|
@@ -153,6 +168,10 @@ steps in **Done, and what you say at the end**:
|
|
|
153
168
|
Behavior instructions (Agent Studio → **Style & Behavior** → **Behavior &
|
|
154
169
|
Responses**). No Foldspace tool writes those, so it is theirs to add: on a
|
|
155
170
|
yes, give them the line to paste and where it goes.
|
|
171
|
+
6. **A short video of it to share** - the `share-a-video` skill, in
|
|
172
|
+
`.claude/skills/share-a-video/SKILL.md` (any other editor: read
|
|
173
|
+
`node_modules/@foldspace_npm/harness/templates/agent-starter/skills/share-a-video/SKILL.md`).
|
|
174
|
+
Ask it as its own question next to the choice - a choice holds four options.
|
|
156
175
|
|
|
157
176
|
Not before the first experience: nothing about it needs any of these, and it
|
|
158
177
|
is silent work before the human has seen anything.
|
|
@@ -162,7 +181,7 @@ Uploads and extraction are a later session.
|
|
|
162
181
|
### Ask as a choice, and ask almost nothing
|
|
163
182
|
|
|
164
183
|
Every question to the human is an **enumerated choice with the recommended
|
|
165
|
-
option first** (in Claude Code, `AskUserQuestion
|
|
184
|
+
option first** (in Claude Code, `AskUserQuestion`; elsewhere, the editor's own question tool), asked once - not repeated in your message. A first session asks only
|
|
166
185
|
this: which agent (only if several), the app's hostname, which experience
|
|
167
186
|
first - and a yes wherever this manual asks for one, such as agreeing the
|
|
168
187
|
experience (below), upgrading, publishing an action, task agent or
|
|
@@ -794,6 +813,18 @@ import is right.
|
|
|
794
813
|
`Bash(npx foldspace observe:*)` - and meanwhile hand that step to them: for
|
|
795
814
|
a chat turn, that is them typing the question into the panel. `init`,
|
|
796
815
|
`upgrade` and `deploy` ask on purpose: never give an allow rule for them.
|
|
816
|
+
- **A sandbox error is not a refusal.** Some editors (Codex) run your commands
|
|
817
|
+
in a sandbox: no network, and no writes outside this folder, unless the
|
|
818
|
+
human approves that one command. There `npm`, `npx foldspace`, `inject` and
|
|
819
|
+
anything that reaches the test window fail with errors that look like
|
|
820
|
+
something else - a host that cannot be resolved, `EPERM` in npm's cache,
|
|
821
|
+
`listen EPERM`, Chrome quitting, or *the test window is closed*. Ask to run
|
|
822
|
+
that same command outside the sandbox through the editor's own approval,
|
|
823
|
+
with one plain line on why; that approval is not a retry. Never move npm's
|
|
824
|
+
cache, change file ownership, or have the human run the command instead.
|
|
825
|
+
An approval can expire when the human sends a new message, so after one,
|
|
826
|
+
the same errors mean ask again - not that the window closed. If the human
|
|
827
|
+
declines, that is a refusal: the rule above applies.
|
|
797
828
|
- **Never create an account or a key** - not in Foldspace, not in the
|
|
798
829
|
customer's product, not anywhere. If one is missing, say which and where it
|
|
799
830
|
goes. **Never widen your own access**: if something is refused for a site or
|
package/README.md
CHANGED
|
@@ -125,8 +125,16 @@ project loads (`sdkUrl`): each region has its own build, pointing at that
|
|
|
125
125
|
region's servers. A project with no `region` is US. The values per region live
|
|
126
126
|
in `src/region.mjs`.
|
|
127
127
|
|
|
128
|
-
For safety, `init`
|
|
129
|
-
|
|
128
|
+
For safety, `init` sets up a new folder, or fills one that holds nothing but
|
|
129
|
+
what the system or an editor put there (`.DS_Store`, `.localized`, `Icon\r`,
|
|
130
|
+
`._*`, `Thumbs.db`, `desktop.ini`, `.mcp.json`, and the folders `.git/`,
|
|
131
|
+
`.vscode/`, `.cursor/`, `.idea/`, and `.claude/` without `settings.json`). It
|
|
132
|
+
leaves those as they are and refuses anything else, including an existing
|
|
133
|
+
Foldspace project. It never fills the home directory or its Desktop, Documents
|
|
134
|
+
or Downloads, the disk's root, or the folder Claude Code Desktop makes for a
|
|
135
|
+
session started with no folder (under `scratch-workspaces`), or Codex's folder
|
|
136
|
+
for chats with no project (`~/Documents/Codex`) and each chat's folder in it,
|
|
137
|
+
even when they are empty. It never overwrites a file and does not initialize Git. On a TTY it can offer
|
|
130
138
|
`npm install --ignore-scripts` after scaffolding. After creation:
|
|
131
139
|
|
|
132
140
|
```bash
|
|
@@ -314,6 +322,21 @@ Without attach, `ask` still reports the reply, the card text and a
|
|
|
314
322
|
screenshot; `handler.execute` and `handler.render` read "not tracked here
|
|
315
323
|
(published code)".
|
|
316
324
|
|
|
325
|
+
### Make a short video to share
|
|
326
|
+
|
|
327
|
+
`npx foldspace record` shows the agent's key use cases, from
|
|
328
|
+
`video/scenes.json`: an opening card meets the product's agent, then each
|
|
329
|
+
question is asked in the test window, typed in a new conversation, with a
|
|
330
|
+
caption once it is answered - the Foldspace features it shows as pills, the
|
|
331
|
+
question, and one large line on what the user gets, its payoff highlighted.
|
|
332
|
+
About 40 s, in Foldspace's colours with the official logo:
|
|
333
|
+
`video/<product>-<date>.mp4` and a poster. It records with Chrome's own screencast and encodes in a hidden Google
|
|
334
|
+
Chrome - no ffmpeg, no screen-recording permission, nothing uploaded. The
|
|
335
|
+
video shows real data from the signed-in account; sharing it is the human's
|
|
336
|
+
call, and `video/.gitignore` keeps it out of git. The `share-a-video` skill,
|
|
337
|
+
written into each project's `.claude/skills/`, is the how-to. It runs for a
|
|
338
|
+
few minutes; stopped half-way, it puts the window back as it was.
|
|
339
|
+
|
|
317
340
|
### Add the harness to an existing project
|
|
318
341
|
|
|
319
342
|
```bash
|
package/bin/ask.mjs
CHANGED
|
@@ -25,7 +25,6 @@ import path from "node:path";
|
|
|
25
25
|
import {
|
|
26
26
|
createSettleTracker,
|
|
27
27
|
numberFlag,
|
|
28
|
-
replyAfter,
|
|
29
28
|
shouldRetry,
|
|
30
29
|
squash,
|
|
31
30
|
summarizeHandlerLog,
|
|
@@ -35,9 +34,15 @@ import { ATTACH_DAEMON_LOG_FILE, liveDaemon } from "../src/attach-daemon.mjs";
|
|
|
35
34
|
import { CdpPage, parseArgs, printJson, sleep } from "../src/cdp-client.mjs";
|
|
36
35
|
import { recordEvent } from "../src/session-events.mjs";
|
|
37
36
|
import { withWorkingBadge } from "../src/badge-auto.mjs";
|
|
37
|
+
import {
|
|
38
|
+
PANEL_DOC,
|
|
39
|
+
openComposer,
|
|
40
|
+
readPanel,
|
|
41
|
+
sendQuestion,
|
|
42
|
+
waitForReply,
|
|
43
|
+
} from "../src/panel-driver.mjs";
|
|
38
44
|
import { resolveProjectDir } from "../src/upgrade.mjs";
|
|
39
45
|
|
|
40
|
-
const PANEL_ID = "eucera-agent-iframe";
|
|
41
46
|
const COMPOSER_WAIT_S = 30;
|
|
42
47
|
const POLL_MS = 1500;
|
|
43
48
|
|
|
@@ -81,147 +86,20 @@ function logSince(offset) {
|
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
// Shared by every page snippet: the panel document, or null while it loads.
|
|
85
|
-
const PANEL_DOC = `
|
|
86
|
-
const frame = document.getElementById(${JSON.stringify(PANEL_ID)});
|
|
87
|
-
let doc = null;
|
|
88
|
-
try { doc = frame && frame.contentDocument; } catch {}
|
|
89
|
-
`;
|
|
90
|
-
|
|
91
|
-
async function panelState() {
|
|
92
|
-
return page.evaluate(`${PANEL_DOC}
|
|
93
|
-
if (!doc) return { frame: !!frame, composer: false };
|
|
94
|
-
const box = doc.querySelector("textarea");
|
|
95
|
-
const rect = frame.getBoundingClientRect();
|
|
96
|
-
return { frame: true, composer: !!box, visible: rect.width > 50 && rect.height > 50 };`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// open() is the SDK's public "show the panel" call, the same thing the
|
|
100
|
-
// launcher button does. The handle comes from the bootstrap when attach made
|
|
101
|
-
// one, else from the page's own SDK.
|
|
102
|
-
async function openPanel(agentApiName) {
|
|
103
|
-
return page.evaluate(`
|
|
104
|
-
const wanted = ${JSON.stringify(agentApiName || "")};
|
|
105
|
-
const namespace = typeof window.__FOLD_SPACE__ === "string" && window.__FOLD_SPACE__
|
|
106
|
-
? window.__FOLD_SPACE__ : "foldspace";
|
|
107
|
-
const sdk = window[namespace];
|
|
108
|
-
let agent = window.__FOLDSPACE_AGENT__ || null;
|
|
109
|
-
if (!agent && typeof sdk?.agentIds === "function") {
|
|
110
|
-
const ids = sdk.agentIds().filter((id) => typeof id === "string" && id.indexOf("-") > 0);
|
|
111
|
-
const id = ids.find((item) => item.slice(item.indexOf("-") + 1) === wanted) || ids[0];
|
|
112
|
-
if (id) {
|
|
113
|
-
const cut = id.indexOf("-");
|
|
114
|
-
agent = sdk.agent({ apiName: id.slice(cut + 1), mode: id.slice(0, cut).toUpperCase() });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (typeof agent?.open !== "function") return { sdk: !!sdk, opened: false };
|
|
118
|
-
try { agent.open(); return { sdk: true, opened: true }; }
|
|
119
|
-
catch (error) { return { sdk: true, opened: false, error: String(error?.message || error) }; }`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function readPanel(cardsBefore) {
|
|
123
|
-
return page.evaluate(`${PANEL_DOC}
|
|
124
|
-
if (!doc || !doc.body) return { panel: "", cards: [], cardFrames: 0, newCardFrames: 0 };
|
|
125
|
-
const squash = (text) => (text || "").replace(/\\s+/g, " ").trim();
|
|
126
|
-
const frames = Array.from(doc.querySelectorAll("iframe"));
|
|
127
|
-
const cards = [];
|
|
128
|
-
let painted = 0;
|
|
129
|
-
// Cards from earlier turns are still in the panel; only the new ones count.
|
|
130
|
-
for (const card of frames.slice(${Number(cardsBefore) || 0})) {
|
|
131
|
-
const rect = card.getBoundingClientRect();
|
|
132
|
-
if (rect.width > 20 && rect.height > 20) painted += 1;
|
|
133
|
-
try {
|
|
134
|
-
const body = card.contentDocument && card.contentDocument.body;
|
|
135
|
-
if (body) cards.push(squash(body.innerText || body.textContent));
|
|
136
|
-
} catch {}
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
panel: squash(doc.body.innerText || doc.body.textContent),
|
|
140
|
-
cards: cards.filter(Boolean),
|
|
141
|
-
cardFrames: frames.length,
|
|
142
|
-
newCardFrames: painted,
|
|
143
|
-
};`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Real key events through the browser's input pipeline, like a person at the
|
|
147
|
-
// keyboard. Frameworks that ignore synthetic DOM events still hear these.
|
|
148
|
-
async function typeLikeAPerson(text) {
|
|
149
|
-
const focused = await page.evaluate(`${PANEL_DOC}
|
|
150
|
-
const box = doc && doc.querySelector("textarea");
|
|
151
|
-
if (!box) return false;
|
|
152
|
-
window.focus(); frame.contentWindow.focus(); box.focus();
|
|
153
|
-
return doc.activeElement === box;`);
|
|
154
|
-
if (!focused) return false;
|
|
155
|
-
await page.send("Input.insertText", { text });
|
|
156
|
-
const typed = await page.evaluate(`${PANEL_DOC}
|
|
157
|
-
const box = doc && doc.querySelector("textarea");
|
|
158
|
-
return box ? box.value : null;`);
|
|
159
|
-
if (squash(typed) !== text) return false;
|
|
160
|
-
const enter = {
|
|
161
|
-
key: "Enter",
|
|
162
|
-
code: "Enter",
|
|
163
|
-
windowsVirtualKeyCode: 13,
|
|
164
|
-
nativeVirtualKeyCode: 13,
|
|
165
|
-
};
|
|
166
|
-
await page.send("Input.dispatchKeyEvent", { type: "rawKeyDown", ...enter });
|
|
167
|
-
await page.send("Input.dispatchKeyEvent", { type: "char", ...enter, text: "\r" });
|
|
168
|
-
await page.send("Input.dispatchKeyEvent", { type: "keyUp", ...enter });
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// A background or unfocused tab can swallow CDP input. Same gesture, delivered
|
|
173
|
-
// as DOM events on the composer; still only typing, still nothing injected.
|
|
174
|
-
async function typeWithDomEvents(text) {
|
|
175
|
-
return page.evaluate(`${PANEL_DOC}
|
|
176
|
-
const box = doc && doc.querySelector("textarea");
|
|
177
|
-
if (!box) return false;
|
|
178
|
-
const view = frame.contentWindow;
|
|
179
|
-
const setValue = Object.getOwnPropertyDescriptor(view.HTMLTextAreaElement.prototype, "value").set;
|
|
180
|
-
box.focus();
|
|
181
|
-
setValue.call(box, ${JSON.stringify(text)});
|
|
182
|
-
box.dispatchEvent(new view.Event("input", { bubbles: true }));
|
|
183
|
-
for (const type of ["keydown", "keypress", "keyup"]) {
|
|
184
|
-
box.dispatchEvent(new view.KeyboardEvent(type, {
|
|
185
|
-
key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true, cancelable: true,
|
|
186
|
-
}));
|
|
187
|
-
}
|
|
188
|
-
return true;`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function composerValue() {
|
|
192
|
-
return page
|
|
193
|
-
.evaluate(`${PANEL_DOC}
|
|
194
|
-
const box = doc && doc.querySelector("textarea");
|
|
195
|
-
return box ? box.value : "";`)
|
|
196
|
-
.catch(() => "");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
89
|
async function askOnce() {
|
|
200
|
-
const before = await readPanel(0);
|
|
90
|
+
const before = await readPanel(page, 0);
|
|
201
91
|
const cardsBefore = before.cardFrames;
|
|
202
92
|
const logOffset = logSize();
|
|
203
93
|
|
|
204
|
-
|
|
205
|
-
let sent = await typeLikeAPerson(question).catch(() => false);
|
|
206
|
-
await sleep(700);
|
|
207
|
-
// Still sitting in the composer means Enter never landed.
|
|
208
|
-
if (!sent || squash(await composerValue()) === question) {
|
|
209
|
-
typedBy = "dom-events";
|
|
210
|
-
sent = await typeWithDomEvents(question).catch(() => false);
|
|
211
|
-
}
|
|
94
|
+
const { sent, typedBy } = await sendQuestion(page, question);
|
|
212
95
|
if (!sent) return { sent: false };
|
|
213
96
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
await sleep(POLL_MS);
|
|
221
|
-
seen = await readPanel(cardsBefore).catch(() => seen);
|
|
222
|
-
reply = replyAfter(seen.panel, question);
|
|
223
|
-
settled = tracker.observe({ reply, cards: seen.cards }).settled;
|
|
224
|
-
}
|
|
97
|
+
const { settled, reply, seen } = await waitForReply(page, question, {
|
|
98
|
+
cardsBefore,
|
|
99
|
+
timeoutS,
|
|
100
|
+
pollMs: POLL_MS,
|
|
101
|
+
tracker: createSettleTracker(),
|
|
102
|
+
});
|
|
225
103
|
// The render line trails the card by a beat; let attach flush it.
|
|
226
104
|
await sleep(400);
|
|
227
105
|
return {
|
|
@@ -284,14 +162,7 @@ async function main() {
|
|
|
284
162
|
|
|
285
163
|
// The composer does not exist until the panel has opened, and one open() is
|
|
286
164
|
// not always enough, so it is retried inside the poll.
|
|
287
|
-
|
|
288
|
-
let opened = { sdk: false, opened: false };
|
|
289
|
-
for (let tick = 0; tick < COMPOSER_WAIT_S && !(state.composer && state.visible); tick++) {
|
|
290
|
-
opened = await openPanel(page.info.target?.agentApiName).catch(() => opened);
|
|
291
|
-
await sleep(1000);
|
|
292
|
-
state = await panelState().catch(() => state);
|
|
293
|
-
if (tick === 5 && !state.frame && !opened.sdk) break;
|
|
294
|
-
}
|
|
165
|
+
const { state } = await openComposer(page, page.info.target?.agentApiName, { waitS: COMPOSER_WAIT_S });
|
|
295
166
|
if (!state.composer) {
|
|
296
167
|
finish(
|
|
297
168
|
{
|
package/bin/attach.mjs
CHANGED
|
@@ -303,6 +303,8 @@ const badgeSrc = `(() => {
|
|
|
303
303
|
// one. See the playbook's test-mode rule.
|
|
304
304
|
const testModeScript = (enable) => `(() => {
|
|
305
305
|
if (window.top !== window.self) return;
|
|
306
|
+
// What this attach set, for \`foldspace record\` to read (it refuses test mode).
|
|
307
|
+
window.__FOLDSPACE_TEST_MODE__ = ${enable};
|
|
306
308
|
const AGENT = ${JSON.stringify(agentApiName)};
|
|
307
309
|
const WANT = ${enable};
|
|
308
310
|
const WORD = ${JSON.stringify(enable ? "ON" : "OFF")};
|
package/bin/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { runInit } from "../src/init.mjs";
|
|
|
8
8
|
import { isWriteFlag } from "../src/observe-core.mjs";
|
|
9
9
|
import {
|
|
10
10
|
checkForUpdate,
|
|
11
|
+
ensureProjectSkills,
|
|
11
12
|
gateForCommand,
|
|
12
13
|
nagIfOutdated,
|
|
13
14
|
resolveProjectDir,
|
|
@@ -41,12 +42,28 @@ function fail(message) {
|
|
|
41
42
|
process.exitCode = 1;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// A stop sent to this wrapper alone (a timeout, `kill <pid>`) goes on to the
|
|
46
|
+
// command: otherwise the wrapper dies and the command runs on as an orphan,
|
|
47
|
+
// still driving the test window. Ctrl-C reaches both; each command takes a
|
|
48
|
+
// second stop as the same one.
|
|
49
|
+
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
50
|
+
|
|
44
51
|
function runScript(scriptName, args) {
|
|
45
52
|
const child = spawn(process.execPath, [path.join(here, scriptName), ...args], {
|
|
46
53
|
stdio: "inherit",
|
|
47
54
|
env: process.env,
|
|
48
55
|
});
|
|
56
|
+
const forward = (signal) => {
|
|
57
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
58
|
+
try {
|
|
59
|
+
child.kill(signal);
|
|
60
|
+
} catch {
|
|
61
|
+
// already gone
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
for (const signal of FORWARDED_SIGNALS) process.on(signal, forward);
|
|
49
65
|
child.on("exit", (code, signal) => {
|
|
66
|
+
for (const name of FORWARDED_SIGNALS) process.removeListener(name, forward);
|
|
50
67
|
if (signal) {
|
|
51
68
|
process.kill(process.pid, signal);
|
|
52
69
|
return;
|
|
@@ -152,6 +169,11 @@ function normalizeCommandArgs(command, args) {
|
|
|
152
169
|
|
|
153
170
|
const [command, ...args] = process.argv.slice(2);
|
|
154
171
|
|
|
172
|
+
// The harness's skills, when the project lacks them: the first upgrade to a
|
|
173
|
+
// version that ships one runs the older harness, which cannot write it. Only
|
|
174
|
+
// missing files, and only in a Foldspace project; silent.
|
|
175
|
+
if (command !== "init") ensureProjectSkills(resolveProjectDir());
|
|
176
|
+
|
|
155
177
|
if (!command || command === "--help" || command === "-h") {
|
|
156
178
|
printHelp(null, args.includes("--json"));
|
|
157
179
|
} else if (command === "--version" || command === "-v") {
|
package/bin/inject.mjs
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
import { recordEvent } from "../src/session-events.mjs";
|
|
15
15
|
import fs from "fs";
|
|
16
16
|
import path from "path";
|
|
17
|
-
import os from "os";
|
|
18
17
|
import { spawn } from "child_process";
|
|
19
18
|
import {
|
|
20
19
|
ownershipErrorMessage,
|
|
21
20
|
waitForOwnedCdp,
|
|
22
21
|
writeLaunchSentinel,
|
|
23
22
|
} from "../src/cdp-ownership.mjs";
|
|
23
|
+
import { findChrome } from "../src/chrome-binary.mjs";
|
|
24
24
|
|
|
25
25
|
// Resolve the CONSUMING repo, not this package. FOLDSPACE_PROJECT_DIR lets a
|
|
26
26
|
// hosted builder point the harness at a workspace it controls.
|
|
@@ -165,19 +165,7 @@ fs.writeFileSync(
|
|
|
165
165
|
}, null, 2),
|
|
166
166
|
);
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
const candidates =
|
|
170
|
-
os.platform() === "darwin"
|
|
171
|
-
? [
|
|
172
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
173
|
-
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
174
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
175
|
-
]
|
|
176
|
-
: ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium"];
|
|
177
|
-
return candidates.find((p) => fs.existsSync(p));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const chrome = process.env.CHROME_PATH || findChrome();
|
|
168
|
+
const chrome = findChrome();
|
|
181
169
|
if (!chrome) {
|
|
182
170
|
console.error("\nChrome not found. Set CHROME_PATH=/path/to/chrome and retry.");
|
|
183
171
|
process.exit(1);
|