@instafy/cli 0.4.0 → 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 +2 -0
- package/dist/bind/bin/instafy-bind.mjs +56 -8
- package/dist/bind/src/server.mjs +4 -0
- package/dist/bind/src/starters/compare/app.js +2 -0
- package/dist/cli.js +948 -355
- package/package.json +1 -1
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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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)
|
|
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`);
|
package/dist/bind/src/server.mjs
CHANGED
|
@@ -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
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* unknown is never listed as under $160; it is listed apart, under "Price unknown".
|
|
16
16
|
* items: [{ref: "option:a", name, why (one short line), badge?: "Previous model", note?, facts: {key: fact},
|
|
17
17
|
* store: {name, url, readable}, more?: [{label, fact}], sources?: [{name, url}]}]
|
|
18
|
+
* every candidate a page you read named, also one nothing could be read for: its cells say
|
|
19
|
+
* "Unknown" (a table of unknowns). Empty only when no page named any: it reads "No options yet".
|
|
18
20
|
* unchecked: ["what was not checked", …] listed once in the footer
|
|
19
21
|
* fact: {"v": "$145", "src": "Example Store", "url": "https://…", "date": "2026-10-17", "asof"?: "2026-08-05", "est": false}
|
|
20
22
|
* Each value cites its page and day ("checked 17 Oct on maker.com ↗"), and no cell is blank: a source you read that
|