@foldspace_npm/harness 0.1.0
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/README.md +156 -0
- package/bin/attach.mjs +399 -0
- package/bin/build-cli.mjs +13 -0
- package/bin/build.ts +82 -0
- package/bin/buildExtension.mjs +90 -0
- package/bin/cli.mjs +33 -0
- package/bin/deploy.mjs +220 -0
- package/bin/inject.mjs +524 -0
- package/bin/packageExtension.mjs +29 -0
- package/package.json +23 -0
- package/src/init.mjs +301 -0
- package/src/transports/README.md +24 -0
- package/templates/agent-starter/CLAUDE.md +90 -0
- package/templates/agent-starter/README.md +54 -0
- package/templates/agent-starter/agent/actions/_example.ts +18 -0
- package/templates/agent-starter/agent/actions/index.ts +10 -0
- package/templates/agent-starter/agent/api/.gitkeep +1 -0
- package/templates/agent-starter/agent/constants.ts +3 -0
- package/templates/agent-starter/agent/utils.ts +15 -0
- package/templates/agent-starter/foldspace.dev.json +17 -0
- package/templates/agent-starter/gitignore +6 -0
- package/templates/agent-starter/package.json +17 -0
- package/templates/agent-starter/tsconfig.json +17 -0
package/src/init.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const defaultTemplateRoot = path.join(packageRoot, "templates", "agent-starter");
|
|
7
|
+
const allowedFlags = new Set(["name", "product-id", "agent-api-name", "domain"]);
|
|
8
|
+
const tokenPattern = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
9
|
+
|
|
10
|
+
export const initUsage =
|
|
11
|
+
"foldspace init <directory> --product-id <id> --agent-api-name <name> --domain <host> [--name <display-name>]";
|
|
12
|
+
|
|
13
|
+
function assertSupportedNode() {
|
|
14
|
+
const major = Number.parseInt(process.versions.node.split(".", 1)[0], 10);
|
|
15
|
+
if (!Number.isInteger(major) || major < 20) {
|
|
16
|
+
throw new Error(`Node 20 or newer is required; found ${process.versions.node}.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readFlag(argv, index) {
|
|
21
|
+
const argument = argv[index];
|
|
22
|
+
const equalsIndex = argument.indexOf("=");
|
|
23
|
+
const name = argument.slice(2, equalsIndex === -1 ? undefined : equalsIndex);
|
|
24
|
+
|
|
25
|
+
if (!allowedFlags.has(name)) {
|
|
26
|
+
throw new Error(`Unknown option: --${name}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (equalsIndex !== -1) {
|
|
30
|
+
const value = argument.slice(equalsIndex + 1);
|
|
31
|
+
if (!value) {
|
|
32
|
+
throw new Error(`Missing value for --${name}`);
|
|
33
|
+
}
|
|
34
|
+
return { name, value, consumed: 1 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const value = argv[index + 1];
|
|
38
|
+
if (!value || value.startsWith("--")) {
|
|
39
|
+
throw new Error(`Missing value for --${name}`);
|
|
40
|
+
}
|
|
41
|
+
return { name, value, consumed: 2 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseInitArgs(argv) {
|
|
45
|
+
const positional = [];
|
|
46
|
+
const flags = {};
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < argv.length; ) {
|
|
49
|
+
const argument = argv[index];
|
|
50
|
+
if (argument.startsWith("--")) {
|
|
51
|
+
const parsed = readFlag(argv, index);
|
|
52
|
+
if (flags[parsed.name] !== undefined) {
|
|
53
|
+
throw new Error(`Option provided more than once: --${parsed.name}`);
|
|
54
|
+
}
|
|
55
|
+
flags[parsed.name] = parsed.value;
|
|
56
|
+
index += parsed.consumed;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
positional.push(argument);
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (positional.length !== 1) {
|
|
65
|
+
throw new Error(`Usage: ${initUsage}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const required of ["product-id", "agent-api-name", "domain"]) {
|
|
69
|
+
if (!flags[required]) {
|
|
70
|
+
throw new Error(`Missing required option: --${required}\nUsage: ${initUsage}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
targetDir: path.resolve(positional[0]),
|
|
76
|
+
displayName: flags.name || path.basename(path.resolve(positional[0])),
|
|
77
|
+
productId: flags["product-id"],
|
|
78
|
+
agentApiName: flags["agent-api-name"],
|
|
79
|
+
domain: flags.domain,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toPackageName(value) {
|
|
84
|
+
return value
|
|
85
|
+
.trim()
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
88
|
+
.replace(/^-+|-+$/g, "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateIdentifier(value, label) {
|
|
92
|
+
const normalized = value.trim();
|
|
93
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(normalized)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${label} must start with a letter or number and contain only letters, numbers, hyphens, or underscores.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateProductId(value) {
|
|
102
|
+
const productId = validateIdentifier(value, "Product ID");
|
|
103
|
+
if (/^EU-/i.test(productId)) {
|
|
104
|
+
throw new Error("Product ID must be the bare product ID, not an EU-… SDK key.");
|
|
105
|
+
}
|
|
106
|
+
return productId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeTarget(value) {
|
|
110
|
+
const raw = value.trim();
|
|
111
|
+
if (!raw || raw.includes("*")) {
|
|
112
|
+
throw new Error("Domain must be a concrete hostname without a wildcard.");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let url;
|
|
116
|
+
try {
|
|
117
|
+
url = new URL(raw.includes("://") ? raw : `https://${raw}`);
|
|
118
|
+
} catch {
|
|
119
|
+
throw new Error(`Invalid domain: ${value}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
!["http:", "https:"].includes(url.protocol) ||
|
|
124
|
+
url.username ||
|
|
125
|
+
url.password ||
|
|
126
|
+
url.port ||
|
|
127
|
+
(url.pathname !== "/" && url.pathname !== "") ||
|
|
128
|
+
url.search ||
|
|
129
|
+
url.hash
|
|
130
|
+
) {
|
|
131
|
+
throw new Error("Domain must contain only an HTTP(S) hostname, without credentials, a port, path, query, or fragment.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const domain = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
135
|
+
if (!domain || domain.includes("..")) {
|
|
136
|
+
throw new Error(`Invalid domain: ${value}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
domain,
|
|
141
|
+
startUrl: `${url.protocol}//${domain}/`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readHarnessVersion(root) {
|
|
146
|
+
const manifestPath = path.join(root, "package.json");
|
|
147
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
148
|
+
if (typeof manifest.version !== "string" || !manifest.version) {
|
|
149
|
+
throw new Error(`Harness package version is missing from ${manifestPath}`);
|
|
150
|
+
}
|
|
151
|
+
return manifest.version;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createTemplateValues(config, harnessVersion) {
|
|
155
|
+
const displayName = config.displayName.trim();
|
|
156
|
+
if (!displayName || /[\r\n]/.test(displayName) || displayName.includes("{{")) {
|
|
157
|
+
throw new Error("Display name must be a non-empty single line and cannot contain '{{'.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const packageName = toPackageName(displayName);
|
|
161
|
+
if (!packageName) {
|
|
162
|
+
throw new Error("Display name must contain at least one letter or number.");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const productId = validateProductId(config.productId);
|
|
166
|
+
const agentApiName = validateIdentifier(config.agentApiName, "Agent API name");
|
|
167
|
+
const target = normalizeTarget(config.domain);
|
|
168
|
+
const hosts =
|
|
169
|
+
target.domain === "localhost" || /^\d+(?:\.\d+){3}$/.test(target.domain)
|
|
170
|
+
? [target.domain]
|
|
171
|
+
: [target.domain, `*.${target.domain}`];
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
DISPLAY_NAME: displayName,
|
|
175
|
+
PACKAGE_NAME: packageName,
|
|
176
|
+
PACKAGE_DESCRIPTION_JSON: JSON.stringify(`Foldspace browser actions for ${displayName}.`),
|
|
177
|
+
PRODUCT_ID_JSON: JSON.stringify(productId),
|
|
178
|
+
AGENT_API_NAME_JSON: JSON.stringify(agentApiName),
|
|
179
|
+
APP_DOMAIN_JSON: JSON.stringify(target.domain),
|
|
180
|
+
START_URL_JSON: JSON.stringify(target.startUrl),
|
|
181
|
+
HOSTS_JSON: JSON.stringify(hosts),
|
|
182
|
+
HARNESS_VERSION: harnessVersion,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderTemplate(content, values, sourcePath) {
|
|
187
|
+
const rendered = content.replace(tokenPattern, (_match, key) => {
|
|
188
|
+
if (!(key in values)) {
|
|
189
|
+
throw new Error(`Missing value for {{${key}}} in ${sourcePath}`);
|
|
190
|
+
}
|
|
191
|
+
return values[key];
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const unresolved = rendered.match(tokenPattern);
|
|
195
|
+
if (unresolved) {
|
|
196
|
+
throw new Error(`Unresolved template token ${unresolved[0]} in ${sourcePath}`);
|
|
197
|
+
}
|
|
198
|
+
return rendered;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function assertInside(root, candidate) {
|
|
202
|
+
const relative = path.relative(root, candidate);
|
|
203
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
204
|
+
throw new Error(`Template path escapes the project root: ${candidate}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function copyTemplate(templateRoot, targetRoot, values) {
|
|
209
|
+
const walk = (relativeDir) => {
|
|
210
|
+
const sourceDir = path.join(templateRoot, relativeDir);
|
|
211
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
212
|
+
const relativePath = path.join(relativeDir, entry.name);
|
|
213
|
+
const sourcePath = path.join(templateRoot, relativePath);
|
|
214
|
+
// npm excludes .gitignore files from package tarballs, so the bundled
|
|
215
|
+
// template uses a publish-safe name and restores it during generation.
|
|
216
|
+
const outputPath = relativePath === "gitignore" ? ".gitignore" : relativePath;
|
|
217
|
+
const targetPath = path.join(targetRoot, outputPath);
|
|
218
|
+
assertInside(targetRoot, targetPath);
|
|
219
|
+
|
|
220
|
+
if (entry.isSymbolicLink()) {
|
|
221
|
+
throw new Error(`Template symlinks are not supported: ${sourcePath}`);
|
|
222
|
+
}
|
|
223
|
+
if (entry.isDirectory()) {
|
|
224
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
225
|
+
walk(relativePath);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (!entry.isFile()) {
|
|
229
|
+
throw new Error(`Unsupported template entry: ${sourcePath}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
233
|
+
const content = fs.readFileSync(sourcePath, "utf8");
|
|
234
|
+
fs.writeFileSync(targetPath, renderTemplate(content, values, sourcePath), "utf8");
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
walk("");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validateGeneratedProject(projectDir) {
|
|
242
|
+
for (const relativePath of ["package.json", "foldspace.dev.json"]) {
|
|
243
|
+
const filePath = path.join(projectDir, relativePath);
|
|
244
|
+
JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function scaffoldProject(config, options = {}) {
|
|
249
|
+
const targetDir = config.targetDir;
|
|
250
|
+
if (fs.existsSync(targetDir)) {
|
|
251
|
+
throw new Error(`Refusing to overwrite existing path: ${targetDir}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const templateRoot = options.templateRoot || defaultTemplateRoot;
|
|
255
|
+
if (!fs.existsSync(templateRoot)) {
|
|
256
|
+
throw new Error(`Bundled project template not found: ${templateRoot}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const harnessVersion = options.harnessVersion || readHarnessVersion(options.packageRoot || packageRoot);
|
|
260
|
+
const values = createTemplateValues(config, harnessVersion);
|
|
261
|
+
const parentDir = path.dirname(targetDir);
|
|
262
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
263
|
+
const temporaryDir = fs.mkdtempSync(path.join(parentDir, `.${path.basename(targetDir)}.foldspace-init-`));
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
copyTemplate(templateRoot, temporaryDir, values);
|
|
267
|
+
validateGeneratedProject(temporaryDir);
|
|
268
|
+
if (fs.existsSync(targetDir)) {
|
|
269
|
+
throw new Error(`Target path appeared while initializing: ${targetDir}`);
|
|
270
|
+
}
|
|
271
|
+
fs.renameSync(temporaryDir, targetDir);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
targetDir,
|
|
279
|
+
packageName: values.PACKAGE_NAME,
|
|
280
|
+
harnessVersion,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function runInit(argv, options = {}) {
|
|
285
|
+
assertSupportedNode();
|
|
286
|
+
const config = parseInitArgs(argv);
|
|
287
|
+
const result = scaffoldProject(config, options);
|
|
288
|
+
const log = options.log || console.log;
|
|
289
|
+
|
|
290
|
+
log(`Created Foldspace project at ${result.targetDir}`);
|
|
291
|
+
log(`Using @foldspace_npm/harness ${result.harnessVersion}`);
|
|
292
|
+
log("");
|
|
293
|
+
log("Next steps:");
|
|
294
|
+
log(` cd ${result.targetDir}`);
|
|
295
|
+
log(" npm install --ignore-scripts");
|
|
296
|
+
log(" npm run build");
|
|
297
|
+
log(" npm run inject");
|
|
298
|
+
log(" npm run attach");
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Transports
|
|
2
|
+
|
|
3
|
+
The harness does the same three jobs regardless of where the browser is:
|
|
4
|
+
|
|
5
|
+
put the bundle in the page · run code in the page · look at the result
|
|
6
|
+
|
|
7
|
+
Track A (`local`) does them over CDP against a browser on this machine.
|
|
8
|
+
Track B (`extension`) will do them over a websocket to a Web Store extension in
|
|
9
|
+
the user's own Chrome.
|
|
10
|
+
|
|
11
|
+
`agent/`, `fixtures/` and the build are identical in both — only this layer
|
|
12
|
+
differs. Keep it that way: "export to repo" from a hosted build is a copy only
|
|
13
|
+
while the two tracks share an identical `agent/` directory.
|
|
14
|
+
|
|
15
|
+
| Verb | local (CDP) | extension (planned) |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `loadBundle` | `addScriptToEvaluateOnNewDocument` / `Fetch.fulfillRequest` | `<script src>` at the deployed CDN path |
|
|
18
|
+
| `evaluate` | `Runtime.evaluate` | content-script message |
|
|
19
|
+
| `screenshot` | `Page.captureScreenshot` | `tabs.captureVisibleTab` |
|
|
20
|
+
| `relaxCSP` | `Page.setBypassCSP` | `declarativeNetRequest` header rules |
|
|
21
|
+
| `navigate` | `Page.navigate` | `tabs.update` |
|
|
22
|
+
|
|
23
|
+
`local` is implemented today as `bin/inject.mjs` + `bin/attach.mjs`.
|
|
24
|
+
`extension` is not built; see `architecture-hosted.md`.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Foldspace browser actions
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This project contains Foldspace action handlers. The customer website, SDK
|
|
6
|
+
snippet, `identify()` call, and layout are out of scope unless the user asks for
|
|
7
|
+
changes there.
|
|
8
|
+
|
|
9
|
+
## Before building
|
|
10
|
+
|
|
11
|
+
1. Connect Product MCP and verify that `list_agents` works.
|
|
12
|
+
2. Inspect the selected agent with `get_agent_settings`, `list_actions`, and
|
|
13
|
+
`list_task_agents`.
|
|
14
|
+
3. Use `discover_actions` to identify candidate experiences, then let the user
|
|
15
|
+
choose what to build.
|
|
16
|
+
|
|
17
|
+
## Build workflow
|
|
18
|
+
|
|
19
|
+
An experience can require several reusable actions. Separate lookup actions
|
|
20
|
+
from actions that read or mutate a selected resource.
|
|
21
|
+
|
|
22
|
+
1. Agree on the experience and how its actions compose.
|
|
23
|
+
2. Create the action metadata as a draft.
|
|
24
|
+
3. Ask before publishing when the agent has real users; publishing is a live
|
|
25
|
+
product change.
|
|
26
|
+
4. Observe the real target workflow and capture its browser requests.
|
|
27
|
+
5. Generate the typed handler skeleton from the published action schema.
|
|
28
|
+
6. Implement the handler using the observed request and response shapes.
|
|
29
|
+
7. Register the handler in `agent/actions/index.ts`.
|
|
30
|
+
8. Complete the verification gates below.
|
|
31
|
+
|
|
32
|
+
## API rules
|
|
33
|
+
|
|
34
|
+
Actions execute in the user's signed-in browser session.
|
|
35
|
+
|
|
36
|
+
- Use the same internal APIs as the product page, with
|
|
37
|
+
`credentials: "include"`.
|
|
38
|
+
- Never guess endpoints or schemas. Capture at least one real request and
|
|
39
|
+
response before implementing a parser.
|
|
40
|
+
- Verify that the user is signed in before observing a workflow.
|
|
41
|
+
- Do not substitute a public developer API when the browser session is missing;
|
|
42
|
+
ask the user to sign in.
|
|
43
|
+
- Validate parameters and return sanitized errors.
|
|
44
|
+
|
|
45
|
+
## Local harness loop
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run dev
|
|
49
|
+
npm run inject
|
|
50
|
+
npm run attach
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`inject` launches an isolated Chrome profile and records its debug port.
|
|
54
|
+
`attach` loads the SDK and the local `dist/index.js` bundle. Use
|
|
55
|
+
`npm run attach -- --bootstrap` when the target page does not already embed the
|
|
56
|
+
Foldspace SDK.
|
|
57
|
+
|
|
58
|
+
The attach log must report `actions attached: N` before treating the page as
|
|
59
|
+
running local action code.
|
|
60
|
+
|
|
61
|
+
## Verification gates
|
|
62
|
+
|
|
63
|
+
Do not report success without all six:
|
|
64
|
+
|
|
65
|
+
1. TypeScript compiles with `npx tsc --noEmit -p tsconfig.json`.
|
|
66
|
+
2. The expected handler appears in `dist/index.js`.
|
|
67
|
+
3. The browser reports the expected number of attached actions.
|
|
68
|
+
4. The action behaves correctly against the real target workflow.
|
|
69
|
+
5. Existing neighbouring action fixtures still pass when fixtures exist.
|
|
70
|
+
6. Browser evidence came from the live target, not from hand-authored examples.
|
|
71
|
+
|
|
72
|
+
## Layout
|
|
73
|
+
|
|
74
|
+
- `agent/actions/` — one handler per action, registered in `index.ts`
|
|
75
|
+
- `agent/api/` — one HTTP helper per endpoint
|
|
76
|
+
- `agent/constants.ts` — agent, product, and domain identifiers
|
|
77
|
+
- `agent/utils.ts` — Foldspace agent lookup
|
|
78
|
+
- `foldspace.dev.json` — local harness target configuration
|
|
79
|
+
|
|
80
|
+
Do not introduce another bundler or bundle format.
|
|
81
|
+
|
|
82
|
+
## Safety
|
|
83
|
+
|
|
84
|
+
- Do not commit secrets, cookies, HAR files, browser storage, or
|
|
85
|
+
`.foldspace-dev/`.
|
|
86
|
+
- Local handler changes are not cloud publication.
|
|
87
|
+
- Creating or publishing Foldspace resources requires explicit approval.
|
|
88
|
+
- Action keys and parameter names must match the published schema.
|
|
89
|
+
- Search Foldspace documentation before asserting unfamiliar platform
|
|
90
|
+
behaviour.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# {{DISPLAY_NAME}}
|
|
2
|
+
|
|
3
|
+
This project contains Foldspace browser action handlers. The handlers are built
|
|
4
|
+
locally and loaded into the configured product page by `@foldspace_npm/harness`.
|
|
5
|
+
The customer website and its Foldspace SDK installation are separate.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
`foldspace init` generated the product, agent, and domain settings in:
|
|
10
|
+
|
|
11
|
+
- `agent/constants.ts`
|
|
12
|
+
- `foldspace.dev.json`
|
|
13
|
+
|
|
14
|
+
Install dependencies without running dependency lifecycle scripts, then build:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --ignore-scripts
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Project layout
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
agent/
|
|
25
|
+
actions/ one file per action; register actions in index.ts
|
|
26
|
+
api/ one file per HTTP helper
|
|
27
|
+
constants.ts product, agent, and domain identifiers
|
|
28
|
+
utils.ts Foldspace agent lookup
|
|
29
|
+
foldspace.dev.json
|
|
30
|
+
CLAUDE.md
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run build # create dist/index.js
|
|
37
|
+
npm run dev # rebuild dist/index.js on changes
|
|
38
|
+
npm run inject # launch the dedicated Chrome profile
|
|
39
|
+
npm run attach # load the SDK and local actions over CDP
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Run `inject` before `attach`. Sign in to the product in the Chrome window that
|
|
43
|
+
`inject` opens. For a product that does not already embed the Foldspace SDK, run
|
|
44
|
+
`npm run attach -- --bootstrap`.
|
|
45
|
+
|
|
46
|
+
## Add an action
|
|
47
|
+
|
|
48
|
+
1. Define and publish the action in Agent Studio or through Product MCP.
|
|
49
|
+
2. Copy `agent/actions/_example.ts` to `agent/actions/<action_key>.ts`.
|
|
50
|
+
3. Implement the handler from observed browser API evidence.
|
|
51
|
+
4. Import and register the handler in `agent/actions/index.ts`.
|
|
52
|
+
5. Type-check, rebuild, attach, and verify it in the target product.
|
|
53
|
+
|
|
54
|
+
Action keys and parameter names must exactly match the published action schema.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copy this file to <action_key>.ts and register it in index.ts.
|
|
2
|
+
// Do not register _example — it is not a real Agent Studio action.
|
|
3
|
+
|
|
4
|
+
export const example_action = {
|
|
5
|
+
execute: async (params: { message?: string }) => {
|
|
6
|
+
const message = typeof params?.message === "string" ? params.message.trim() : "";
|
|
7
|
+
if (!message) {
|
|
8
|
+
return { ok: false, error: "message is required" };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
return { ok: true, echo: message };
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const detail = error instanceof Error ? error.message : "unknown error";
|
|
15
|
+
return { ok: false, error: detail };
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Entry point for the agent actions. One file per action in this directory.
|
|
2
|
+
//
|
|
3
|
+
// Registration here is the switch: the SDK transmits these to the server, and
|
|
4
|
+
// the server excludes any active action it does not receive. Comment an entry
|
|
5
|
+
// out and the copilot can no longer see it — no unpublishing required.
|
|
6
|
+
|
|
7
|
+
const actions = {};
|
|
8
|
+
(window as any).__FOLDSPACE_REMOTE_ACTIONS__ = actions;
|
|
9
|
+
|
|
10
|
+
export default actions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# API helpers belong in this directory.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AGENT_API_NAME } from "./constants";
|
|
2
|
+
|
|
3
|
+
let agent: any | null = null;
|
|
4
|
+
|
|
5
|
+
export function getAgent(): any | null {
|
|
6
|
+
if (agent) {
|
|
7
|
+
return agent;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
agent = (window as any).foldspace?.agent({
|
|
11
|
+
apiName: AGENT_API_NAME,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return agent;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Generated by foldspace init. Use the bare product ID, not the EU-… SDK key.",
|
|
3
|
+
"defaultTarget": "app",
|
|
4
|
+
"sdkUrl": "https://script.eucerahive.io/web/sdk/foldspace.js",
|
|
5
|
+
"localActionsUrl": "http://localhost:3007/dist/index.js",
|
|
6
|
+
"targets": {
|
|
7
|
+
"app": {
|
|
8
|
+
"name": "App",
|
|
9
|
+
"startUrl": {{START_URL_JSON}},
|
|
10
|
+
"productId": {{PRODUCT_ID_JSON}},
|
|
11
|
+
"agentApiName": {{AGENT_API_NAME_JSON}},
|
|
12
|
+
"hosts": {{HOSTS_JSON}},
|
|
13
|
+
"mode": "OVERLAY",
|
|
14
|
+
"loadLocally": true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": {{PACKAGE_DESCRIPTION_JSON}},
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "foldspace-build --watch",
|
|
9
|
+
"build": "foldspace-build",
|
|
10
|
+
"inject": "foldspace-inject",
|
|
11
|
+
"attach": "foldspace-attach"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@foldspace_npm/harness": "{{HARNESS_VERSION}}",
|
|
15
|
+
"typescript": "^5.3.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": false,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": ".",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|