@instafy/cli 0.4.1 → 0.5.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 CHANGED
@@ -59,6 +59,8 @@ Run Instafy spaces locally and connect them back to Instafy Studio — from any
59
59
  - `instafy web read <url…> --json` — read up to 6 public pages over plain HTTP and get their text, links and structured data with provenance, wrapped as untrusted page content. robots.txt (including AI-agent user agents), the web-research skill's `no-agents` link-only sites (built in, plus the space's additions) and translators', caches' and archives' copies of them, private-address checks at every redirect, stop rules and per-turn caps are enforced; a `no-reuse` site is read and marked `noReuse`; see [docs/CLI.md](https://github.com/instafy-dev/instafy/blob/main/docs/CLI.md#reading-web-pages-instafy-web-read).
60
60
  - `instafy web search "<query>" --json` — find links to read, in Octo's turns only, with the space's own search key (the web-research skill's providers: Brave, then Kagi). Results are untrusted leads: a link-only site's result keeps only its link and site name, keys go only to their provider's host, nothing is cached, and the room keeps only the query and the count; see [docs/CLI.md](https://github.com/instafy-dev/instafy/blob/main/docs/CLI.md#searching-for-links-instafy-web-search).
61
61
  - `instafy web link <site> [name=value…] --json` — the link the person opens in their own tab for a `no-agents` site (guided mode): the site's search template from the web-research skill's list, filled with checked, URL-encoded values, or its home page and what to type there when it bars deep links. Reads nothing.
62
+ - `instafy web budget --json` — inspect remaining fresh-page reads shared by the current task and its helpers. Cached pages are free.
63
+ - `instafy web settings --interactive` — space managers configure total and per-site page allowances for new tasks; running tasks retain their original budget. Use `--max-page-loads` and `--max-page-loads-per-site` for scripts.
62
64
  - `instafy secrets list` — list space secret metadata (names/descriptions, never values).
63
65
  - `instafy secrets get <name-or-id>` — inspect one space secret metadata entry.
64
66
  - `instafy secrets put <name> --value ...` — create/update a space secret.
@@ -48,7 +48,8 @@ serve Serve the site (or every subfolder with a bound.json) with the binder
48
48
  given). --agent-conversation <id> uses an existing conversation instead. --agent-budget caps Octo
49
49
  turns per site per hour (repairs included). --dry-run prints those prompts instead.
50
50
  --detach: start the binder in the background (its own session, output to --log, default
51
- <tmp>/instafy-bind-<port>.log), wait until it answers, print where it serves and exit. A server
51
+ <tmp>/instafy-bind-<port>.log), wait until it answers as itself, print where it serves and exit. A
52
+ binder of this folder already serving on the port is kept; another server there is refused. A server
52
53
  started in an agent turn without it stops when the turn ends.
53
54
  In a folder of an Instafy space, the binder saves the data files it writes for clicks (records,
54
55
  data/version.json): it commits them to the space's checkout a few seconds after the last write, and
@@ -119,6 +120,15 @@ function detachedServeArgs(argv) {
119
120
  return out;
120
121
  }
121
122
 
123
+ /** The folder's real path, or the path itself when it cannot be resolved. */
124
+ function realOr(dir) {
125
+ try {
126
+ return fs.realpathSync(path.resolve(String(dir)));
127
+ } catch {
128
+ return path.resolve(String(dir));
129
+ }
130
+ }
131
+
122
132
  async function serveDetached(dir, flags) {
123
133
  const port = flags.port === undefined ? 4180 : Number(flags.port);
124
134
  if (!Number.isInteger(port) || port < 1 || port > 65535) fail("--detach needs a fixed --port");
@@ -132,16 +142,39 @@ async function serveDetached(dir, flags) {
132
142
  fs.closeSync(fd);
133
143
  child.unref();
134
144
  const host = flags.host || "127.0.0.1";
145
+ const folder = realOr(dir);
146
+ // A gallery answers under each site's folder: ask the first one.
147
+ let prefix = "";
148
+ try {
149
+ prefix = discoverSites(dir, { plain: true })[0]?.prefix ?? "";
150
+ } catch {
151
+ // the binder says why in its log
152
+ }
153
+ const running = () => child.exitCode === null && child.signalCode === null;
135
154
  const deadline = Date.now() + 20_000;
136
155
  let up = false;
137
- while (Date.now() < deadline) {
138
- if (child.exitCode !== null) break;
156
+ // Whether something else answered on the port: another site's server, or another binder.
157
+ let other = false;
158
+ // A binder of this same folder that was already serving on the port (an earlier --detach): it is kept.
159
+ let earlier = null;
160
+ while (Date.now() < deadline && running()) {
139
161
  try {
140
- const response = await fetch(`http://${host.includes(":") ? `[${host}]` : host}:${port}/__bind/health`);
141
- if (response.status < 500) {
142
- up = true;
143
- break;
162
+ const response = await fetch(`http://${host.includes(":") ? `[${host}]` : host}:${port}${prefix}/__bind/health`, { signal: AbortSignal.timeout(2_000) });
163
+ // Only this site's own binder counts: the folder it serves and the process it runs in. The child started here
164
+ // while it still runs, or a binder of this same folder that already held the port, whose answer is never taken
165
+ // for the child's. Any other answer on the port is another server, never this site being served.
166
+ const binder = (await response.json().catch(() => null))?.binder;
167
+ if (response.ok && binder && realOr(binder.root) === folder && Number.isInteger(binder.pid)) {
168
+ if (binder.pid === child.pid && running()) {
169
+ up = true;
170
+ break;
171
+ }
172
+ if (binder.pid !== child.pid) {
173
+ earlier = binder.pid;
174
+ break;
175
+ }
144
176
  }
177
+ other = true;
145
178
  } catch {
146
179
  // not listening yet
147
180
  }
@@ -154,7 +187,22 @@ async function serveDetached(dir, flags) {
154
187
  return "";
155
188
  }
156
189
  };
157
- if (!up) fail(`the background binder did not start (log ${logFile}):\n${tail()}`);
190
+ if (!up) {
191
+ // A binder that never answered as itself is not left running in the background.
192
+ if (running()) {
193
+ try {
194
+ process.kill(child.pid, "SIGTERM");
195
+ } catch {
196
+ // already gone
197
+ }
198
+ }
199
+ if (earlier !== null) {
200
+ process.stdout.write(`instafy-bind already serves ${dir} on port ${port} in the background (pid ${earlier}); it keeps running after this turn.\n`);
201
+ return;
202
+ }
203
+ if (other) fail(`port ${port} is in use by another server, so ${dir} is not served there; pick another --port (log ${logFile}):\n${tail()}`);
204
+ fail(`the background binder did not start (log ${logFile}):\n${tail()}`);
205
+ }
158
206
  process.stdout.write(`instafy-bind serving ${dir} on port ${port} in the background (pid ${child.pid}; log ${logFile}); it keeps running after this turn.\n`);
159
207
  const shown = tail().split("\n").filter((line) => /show it in Studio|public link|agent executor|intents:/.test(line));
160
208
  for (const line of shown) process.stdout.write(`${line}\n`);
@@ -698,6 +698,10 @@ export function createBinder(options) {
698
698
  site: { title: site.title }, token: tokenNeeded(site, req),
699
699
  viewer: viewerOk(site, req, url), parentOrigins: parentOriginsFor(site, req),
700
700
  executors: { script: "ready", agent: agentState },
701
+ // Which folder this binder serves and its process, told only to this machine (a loopback socket and name, no
702
+ // proxy; a tunnel's client connects from here but keeps the viewer's host): `serve --detach` checks that the
703
+ // answer on the port is its own binder's, not another server's.
704
+ ...(adminLocal(req) ? { binder: { root, pid: process.pid } } : {}),
701
705
  });
702
706
  case "session": {
703
707
  // The client trades #k= for an HttpOnly cookie once, so EventSource, polling and the page's own fetches