@foldspace_npm/harness 0.1.22 → 0.1.24
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 +113 -36
- package/bin/ask.mjs +345 -0
- package/bin/attach.mjs +29 -1
- package/bin/observe.mjs +42 -8
- package/package.json +1 -1
- package/recipes/INDEX.md +1 -0
- package/recipes/bottom-bar/recipe.json +5 -1
- package/recipes/several-hosts/README.md +35 -0
- package/recipes/several-hosts/agent/hosts.ts +146 -0
- package/recipes/several-hosts/fixtures/nav-record.json +5 -0
- package/recipes/several-hosts/recipe.json +10 -0
- package/recipes/who-is-the-user/README.md +4 -3
- package/src/ask-core.mjs +195 -0
- package/src/attach-daemon.mjs +27 -0
- package/src/cdp-client.mjs +6 -1
- package/src/cli-registry.mjs +30 -1
- package/src/init.mjs +8 -1
- package/src/observe-core.mjs +96 -1
- package/src/runtime/render.ts +8 -1
- package/templates/agent-starter/claude-settings.json +21 -0
- package/templates/agent-starter/docs/app-profile.md +10 -0
package/src/init.mjs
CHANGED
|
@@ -353,7 +353,14 @@ function copyTemplate(templateRoot, targetRoot, values) {
|
|
|
353
353
|
const sourcePath = path.join(templateRoot, relativePath);
|
|
354
354
|
// npm excludes .gitignore files from package tarballs, so the bundled
|
|
355
355
|
// template uses a publish-safe name and restores it during generation.
|
|
356
|
-
|
|
356
|
+
// The permission allowlist lives in .claude/, kept flat here for the
|
|
357
|
+
// same reason.
|
|
358
|
+
const outputPath =
|
|
359
|
+
relativePath === "gitignore"
|
|
360
|
+
? ".gitignore"
|
|
361
|
+
: relativePath === "claude-settings.json"
|
|
362
|
+
? path.join(".claude", "settings.json")
|
|
363
|
+
: relativePath;
|
|
357
364
|
const targetPath = path.join(targetRoot, outputPath);
|
|
358
365
|
assertInside(targetRoot, targetPath);
|
|
359
366
|
|
package/src/observe-core.mjs
CHANGED
|
@@ -43,6 +43,63 @@ export function isSameSite(url, pageUrl, hosts = []) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* One product often lives on several hosts (www., analyze., admin. ...), and a
|
|
48
|
+
* project only works on the hosts it names. Given addresses seen from a page,
|
|
49
|
+
* say which hosts look like the same product but are NOT covered, and the one
|
|
50
|
+
* pattern that would cover them. The site is an approximation (see siteOf), so
|
|
51
|
+
* this only ever SUGGESTS: widening `hosts` is the human's decision.
|
|
52
|
+
*/
|
|
53
|
+
export function uncoveredSiblings(urls, pageUrl, hosts = []) {
|
|
54
|
+
let pageHost;
|
|
55
|
+
try {
|
|
56
|
+
pageHost = new URL(pageUrl).hostname.toLowerCase();
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const site = siteOf(pageHost);
|
|
61
|
+
if (!site.includes(".") || /^[\d.]+$/.test(site)) return null;
|
|
62
|
+
const counts = new Map();
|
|
63
|
+
for (const url of urls) {
|
|
64
|
+
let host;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = new URL(url);
|
|
67
|
+
if (!/^https?:$/.test(parsed.protocol)) continue;
|
|
68
|
+
host = parsed.hostname.toLowerCase();
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (host === pageHost || siteOf(host) !== site || hostMatchesAny(host, hosts)) continue;
|
|
73
|
+
counts.set(host, (counts.get(host) || 0) + 1);
|
|
74
|
+
}
|
|
75
|
+
if (!counts.size) return null;
|
|
76
|
+
const found = [...counts].map(([host, seen]) => ({ host, seen })).sort((a, b) => b.seen - a.seen || a.host.localeCompare(b.host));
|
|
77
|
+
const pattern = `*.${site}`;
|
|
78
|
+
return {
|
|
79
|
+
hosts: found.slice(0, 12),
|
|
80
|
+
pattern,
|
|
81
|
+
hint:
|
|
82
|
+
`This product also lives on ${found.slice(0, 4).map((entry) => entry.host).join(", ")}` +
|
|
83
|
+
`${found.length > 4 ? ` and ${found.length - 4} more` : ""}, which this project does not cover. ` +
|
|
84
|
+
`To work there, add "${pattern}" to "hosts" in foldspace.dev.json - with the human's yes - then run inject and attach again.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether the page the window is on is one this project works on; if not, what would make it one. */
|
|
89
|
+
export function pageCoverage(pageUrl, hosts = []) {
|
|
90
|
+
let host;
|
|
91
|
+
try {
|
|
92
|
+
host = new URL(pageUrl).hostname.toLowerCase();
|
|
93
|
+
} catch {
|
|
94
|
+
return { covered: false, host: null, pattern: null };
|
|
95
|
+
}
|
|
96
|
+
if (!hosts.length || hostMatchesAny(host, hosts)) return { covered: true, host, pattern: null };
|
|
97
|
+
const shared = hosts
|
|
98
|
+
.map((pattern) => String(pattern).toLowerCase().replace(/^\*\./, ""))
|
|
99
|
+
.some((bare) => siteOf(bare) === siteOf(host));
|
|
100
|
+
return { covered: false, host, pattern: shared ? `*.${siteOf(host)}` : null };
|
|
101
|
+
}
|
|
102
|
+
|
|
46
103
|
// ------------------------------------------------------------ navigation
|
|
47
104
|
|
|
48
105
|
export const NAV_SCOPE_SELECTOR =
|
|
@@ -50,7 +107,45 @@ export const NAV_SCOPE_SELECTOR =
|
|
|
50
107
|
export const NAV_ITEM_SELECTOR =
|
|
51
108
|
"a[href], button, [role=menuitem], [role=tab], [role=link]";
|
|
52
109
|
|
|
53
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Where `--goto` may go: a path on the page's own origin, or a full https
|
|
112
|
+
* address on a host this project names. Nowhere else - the window carries the
|
|
113
|
+
* human's session. The path is screened either way.
|
|
114
|
+
*/
|
|
115
|
+
export function gotoDestination(input, pageUrl, hosts = []) {
|
|
116
|
+
const raw = String(input ?? "").trim();
|
|
117
|
+
if (!/^https?:\/\//i.test(raw)) return { ...sameOriginPath(raw, pageUrl), host: null };
|
|
118
|
+
let target;
|
|
119
|
+
try {
|
|
120
|
+
target = new URL(raw);
|
|
121
|
+
} catch {
|
|
122
|
+
throw new Error(`--goto: "${raw}" is not an address.`);
|
|
123
|
+
}
|
|
124
|
+
if (target.protocol !== "https:" && new URL(pageUrl).protocol === "https:") {
|
|
125
|
+
throw new Error("--goto must use https.");
|
|
126
|
+
}
|
|
127
|
+
if (target.username || target.password) throw new Error("--goto takes no credentials in the address.");
|
|
128
|
+
const page = new URL(pageUrl);
|
|
129
|
+
const sameOrigin = target.origin === page.origin;
|
|
130
|
+
if (!sameOrigin && !hostMatchesAny(target.hostname, hosts)) {
|
|
131
|
+
const coverage = pageCoverage(target.href, hosts);
|
|
132
|
+
throw new Error(
|
|
133
|
+
`--goto: ${target.hostname} is not one of this project's hosts (${hosts.join(", ") || "none named"}). ` +
|
|
134
|
+
(coverage.pattern
|
|
135
|
+
? `If it is part of the product, add "${coverage.pattern}" to "hosts" in foldspace.dev.json - with the human's yes - then run inject and attach again.`
|
|
136
|
+
: "observe only opens screens of the product this project is for."),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const refused = pathRefusal(target.pathname);
|
|
140
|
+
if (refused) throw new Error(`--goto: ${refused}`);
|
|
141
|
+
return {
|
|
142
|
+
url: target.href,
|
|
143
|
+
path: target.pathname + target.search + target.hash,
|
|
144
|
+
host: sameOrigin ? null : target.hostname,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `--goto` with a path: the page's own origin and nothing else. */
|
|
54
149
|
export function sameOriginPath(input, pageUrl) {
|
|
55
150
|
const raw = String(input ?? "").trim();
|
|
56
151
|
if (!raw.startsWith("/") || raw.startsWith("//") || raw.includes("\\")) {
|
package/src/runtime/render.ts
CHANGED
|
@@ -14,7 +14,14 @@ import type { ApiFailure } from "./http";
|
|
|
14
14
|
export type ViewHost = HTMLElement | { container: HTMLElement };
|
|
15
15
|
|
|
16
16
|
function containerOf(host: ViewHost): HTMLElement {
|
|
17
|
-
|
|
17
|
+
// Not `instanceof HTMLElement`: the host Foldspace passes belongs to the
|
|
18
|
+
// agent panel's frame, and an element from another frame is not an instance
|
|
19
|
+
// of THIS frame's HTMLElement. The check was false for every real host, so
|
|
20
|
+
// `host.container` (undefined) was used and every failure card crashed
|
|
21
|
+
// instead of drawing (seen on a live build, 2026-09-24).
|
|
22
|
+
const wrapped = (host as { container?: HTMLElement }).container;
|
|
23
|
+
if (wrapped && typeof wrapped.appendChild === "function") return wrapped;
|
|
24
|
+
return host as HTMLElement;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
function el(tag: string, className: string, text?: string): HTMLElement {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Written by foldspace init. The commands a build runs, allowed up front so the coding agent is not stopped one denial at a time mid-build. Named one by one on purpose: init, upgrade and deploy are not here and still ask. Everything else still asks.",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"allow": [
|
|
5
|
+
"Bash(npx foldspace help:*)",
|
|
6
|
+
"Bash(npx foldspace build:*)",
|
|
7
|
+
"Bash(npx foldspace lint:*)",
|
|
8
|
+
"Bash(npx foldspace inject:*)",
|
|
9
|
+
"Bash(npx foldspace attach:*)",
|
|
10
|
+
"Bash(npx foldspace observe:*)",
|
|
11
|
+
"Bash(npx foldspace ask:*)",
|
|
12
|
+
"Bash(npx foldspace badge:*)",
|
|
13
|
+
"Bash(npm install:*)",
|
|
14
|
+
"Bash(npm run build:*)",
|
|
15
|
+
"Bash(npm run typecheck:*)",
|
|
16
|
+
"Bash(npm run lint:*)",
|
|
17
|
+
"Bash(npm run inject:*)",
|
|
18
|
+
"Bash(npm run attach:*)"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -101,6 +101,16 @@ across.
|
|
|
101
101
|
|
|
102
102
|
Listed so the next person knows they exist **and knows they are unverified**.
|
|
103
103
|
|
|
104
|
+
## Workarounds
|
|
105
|
+
|
|
106
|
+
Anything built here because Foldspace - the SDK, the harness, the panel - did
|
|
107
|
+
not do it, or did not seem to. One row each, and the same note as a comment in
|
|
108
|
+
the code. "Assumed" means you did not find support for it; it may exist.
|
|
109
|
+
|
|
110
|
+
| What it does | What it works around | Where in the code | Verified or assumed | Goes when |
|
|
111
|
+
|---|---|---|---|---|
|
|
112
|
+
| | | | | |
|
|
113
|
+
|
|
104
114
|
## Failures observed
|
|
105
115
|
|
|
106
116
|
| Call | Result | What it means |
|