@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/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
- const outputPath = relativePath === "gitignore" ? ".gitignore" : relativePath;
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
 
@@ -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
- /** `--goto` accepts a path on the page's own origin and nothing else. */
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("\\")) {
@@ -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
- return host instanceof HTMLElement ? host : host.container;
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 |