@instafy/cli 0.2.4 → 0.3.1

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
@@ -41,8 +41,8 @@ Run Instafy spaces locally and connect them back to Instafy Studio — from any
41
41
  - Like an ordinary Studio chat, the CLI marks the turn as potentially workspace-writing. Controller and member permissions still apply, and read-only questions do not require a file change.
42
42
  - `instafy bind serve <site>` — serve a bound site: a website whose clicks become typed intents, run by a handler script, with records streamed back to every viewer. "Ask Octo" clicks are handed to Studio and sent as the clicker's own chat message, so the binder needs no login. Runs the binder bundled with the CLI (`packages/instafy-bind`, copied into `dist/bind` at build time; `INSTAFY_BIND_BIN` overrides it). See `docs/Bound-Sites.md`.
43
43
  - `instafy agents list` — list available top-level agents.
44
- - `instafy agents settings` — inspect the linked space's shared multi-agent limits. Add `--interactive` to edit them, or use `--max-agents-per-plan <n>` and/or `--max-concurrent-agents <n>`.
45
- - Plans default to at most 64 team workers (range 1–256); at most 8 team workers run concurrently across the space (range 1–64), subject to runtime capacity. Lead/root jobs and native provider subagents are outside the concurrent team-worker count. Changing settings requires permission to manage the space; runtime job tokens cannot change them.
44
+ - `instafy agents settings` — inspect the linked space's shared multi-agent limits. Add `--interactive` to edit them, or use `--max-agents-per-plan <n>`, `--max-concurrent-agents <n>`, and/or `--max-native-subagents <n>`.
45
+ - Plans default to at most 64 team workers (range 1–256); at most 8 team workers run concurrently across the space (range 1–64), subject to runtime capacity. Lead/root jobs and native helpers are outside the concurrent team-worker count. Native helpers have a separate limit of 3 children per parent (range 1–64), excluding the parent itself; this setting requires a supporting controller. Changing settings requires permission to manage the space; runtime job tokens cannot change them.
46
46
  - `instafy agents context list --query "<topic>"` — inspect optional compact scoped context-card hints for cross-agent coordination.
47
47
  - `instafy agents context put --agent @octo "<summary>"` — save/update a compact context card for the current conversation when `INSTAFY_CONVERSATION_ID` is set.
48
48
  - `instafy automations create --name "Octo check-in" --mode space_review --schedule-kind hourly --interval-hours 24` — explicitly opt in to private, quiet space reviews; no custom prompt is needed.
@@ -56,6 +56,7 @@ Run Instafy spaces locally and connect them back to Instafy Studio — from any
56
56
  - `instafy tunnel logs <tunnelId> --follow` — tail tunnel logs.
57
57
  - `instafy tunnel stop <tunnelId>` — stop + revoke a tunnel.
58
58
  - `instafy preview check --port <port>` — load a local site in a headless browser and list its errors, failed requests and a blank page, with desktop and phone screenshots. Local only; see [docs/CLI.md](https://github.com/instafy-dev/instafy/blob/main/docs/CLI.md#previews) for exit codes and settings.
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), skill-listed link-only sites, private-address checks at every redirect, stop rules and per-turn caps are enforced; see [docs/CLI.md](https://github.com/instafy-dev/instafy/blob/main/docs/CLI.md#reading-web-pages-instafy-web-read).
59
60
  - `instafy secrets list` — list space secret metadata (names/descriptions, never values).
60
61
  - `instafy secrets get <name-or-id>` — inspect one space secret metadata entry.
61
62
  - `instafy secrets put <name> --value ...` — create/update a space secret.
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { credentialFreeEnv } from "./detached-env.mjs";
6
7
 
7
8
  export const DEFAULT_CONTROLLER_URL = "http://127.0.0.1:8788";
8
9
 
@@ -145,7 +146,6 @@ export function agentUsesOwnerConversation(config) {
145
146
 
146
147
  /** Credential names the binder itself reads as the owner's login (above). */
147
148
  const BINDER_LOGIN_ENV = ["INSTAFY_ACCESS_TOKEN", "SUPABASE_ACCESS_TOKEN"];
148
- const CREDENTIAL_ENV_RE = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE|(^|_)KEY$|_KEY_|API_?KEY)/;
149
149
 
150
150
  /**
151
151
  * Why --agent-executor is refused here, or null. It treats a click from this machine's loopback as the owner's own,
@@ -158,23 +158,12 @@ export function agentExecutorRefusal(env = process.env) {
158
158
  }
159
159
 
160
160
  /**
161
- * The environment of a binder started with `serve --detach`. It outlives the Octo turn that started it and every
162
- * process in a runtime can read its /proc/<pid>/environ, so it keeps no credential it does not need: no runtime job
163
- * token or other CONTROLLER_* secret, no SUPABASE_* service values, no credential-like names (space secrets) except
164
- * names passed with --allow-env, and the owner's login only with `keepLogin` (--agent-executor, the one mode that
161
+ * The environment of a binder started with `serve --detach`: `credentialFreeEnv` (no runtime job token or other
162
+ * CONTROLLER_* secret, no SUPABASE_* service values, no credential-like names such as space secrets), except names
163
+ * passed with --allow-env, and the owner's login only with `keepLogin` (--agent-executor, the one mode that
165
164
  * dispatches Octo turns itself). Everything else (PATH, HOME, the space and conversation ids, plain settings a
166
165
  * handler may declare in `env`) is kept.
167
166
  */
168
167
  export function detachedBinderEnv(env = process.env, allowEnv = [], { keepLogin = false } = {}) {
169
- const out = {};
170
- for (const [name, value] of Object.entries(env)) {
171
- if (value === undefined) continue;
172
- const keep =
173
- (keepLogin && BINDER_LOGIN_ENV.includes(name)) ||
174
- allowEnv.includes(name) ||
175
- name === "CONTROLLER_BASE_URL" ||
176
- (!/^(CONTROLLER|SUPABASE)_/.test(name) && !CREDENTIAL_ENV_RE.test(name));
177
- if (keep) out[name] = value;
178
- }
179
- return out;
168
+ return credentialFreeEnv(env, [...allowEnv, ...(keepLogin ? BINDER_LOGIN_ENV : [])]);
180
169
  }
@@ -0,0 +1,27 @@
1
+ // The environment of a process started to outlive the command (or Octo turn) that started it.
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ //
4
+ // Shared by `instafy bind serve --detach` and `instafy preview open --start` (the CLI loads this file from its bundled
5
+ // copy of the binder), so both strip the same credentials.
6
+
7
+ const CREDENTIAL_ENV_RE = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE|(^|_)KEY$|_KEY_|API_?KEY)/;
8
+
9
+ /**
10
+ * `env` without credentials a long-lived process does not need. Such a process outlives the job that started it, and
11
+ * every process in a runtime can read its /proc/<pid>/environ, so it keeps no runtime job token or other CONTROLLER_*
12
+ * value (except CONTROLLER_BASE_URL, which is not secret), no SUPABASE_* value and no credential-like name (space
13
+ * secrets, logins). Names in `keep` are kept anyway. Everything else (PATH, HOME, the space and conversation ids,
14
+ * plain settings) is kept.
15
+ */
16
+ export function credentialFreeEnv(env = process.env, keep = []) {
17
+ const out = {};
18
+ for (const [name, value] of Object.entries(env)) {
19
+ if (value === undefined) continue;
20
+ const kept =
21
+ keep.includes(name) ||
22
+ name === "CONTROLLER_BASE_URL" ||
23
+ (!/^(CONTROLLER|SUPABASE)_/.test(name) && !CREDENTIAL_ENV_RE.test(name));
24
+ if (kept) out[name] = value;
25
+ }
26
+ return out;
27
+ }
@@ -48,8 +48,10 @@ function isPrivateV6(ip) {
48
48
  if (allZeroUpTo(7) && g[7] === 1) return true; // ::1
49
49
  const embedded = () => `${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`;
50
50
  if (allZeroUpTo(5) && g[5] === 0xffff) return isPrivateV4(embedded()); // ::ffff:a.b.c.d
51
+ if (allZeroUpTo(4) && g[4] === 0xffff && g[5] === 0) return isPrivateV4(embedded()); // ::ffff:0:a.b.c.d (SIIT translated)
51
52
  if (allZeroUpTo(6)) return true; // ::a.b.c.d (deprecated compatible)
52
53
  if (g[0] === 0x64 && g[1] === 0xff9b && g.slice(2, 6).every((x) => x === 0)) return isPrivateV4(embedded()); // NAT64
54
+ if (g[0] === 0x64 && g[1] === 0xff9b && g[2] === 1) return true; // 64:ff9b:1::/48 local-use NAT64 (RFC 8215): a network's own translator
53
55
  if ((g[0] & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local
54
56
  if ((g[0] & 0xffc0) === 0xfe80) return true; // fe80::/10 link local
55
57
  if ((g[0] & 0xffc0) === 0xfec0) return true; // fec0::/10 site local
@@ -70,6 +72,16 @@ export function isPrivateAddress(ip) {
70
72
 
71
73
  const LOCAL_NAME_RE = /(^|\.)(localhost|local|internal|lan|home|intranet|localdomain)$/i;
72
74
 
75
+ /** True for a host name that names this machine or a local network (localhost, *.local, a single label, …). */
76
+ export function isLocalHostName(host) {
77
+ // A fully qualified name may end in a dot ("localhost.", "router.lan."); it names the same host.
78
+ // Trimmed with a loop, not /\.+$/, which backtracks quadratically on a long run of dots.
79
+ let end = host.length;
80
+ while (end > 0 && host.charCodeAt(end - 1) === 0x2e) end -= 1;
81
+ const name = host.slice(0, end);
82
+ return LOCAL_NAME_RE.test(name) || !name.includes(".");
83
+ }
84
+
73
85
  /**
74
86
  * Check that a URL points at a public http(s) host. Resolves DNS names.
75
87
  * Returns {ok: true, addresses} or {ok: false, reason}.
@@ -88,7 +100,7 @@ export async function checkPublicUrl(raw, { allowLocal = false, lookup = dns.pro
88
100
  if (net.isIP(host)) {
89
101
  return isPrivateAddress(host) ? { ok: false, reason: `${host} is a private or local address` } : { ok: true, addresses: [host] };
90
102
  }
91
- if (LOCAL_NAME_RE.test(host) || !host.includes(".")) return { ok: false, reason: `${host} is a local host name` };
103
+ if (isLocalHostName(host)) return { ok: false, reason: `${host} is a local host name` };
92
104
  let addresses;
93
105
  try {
94
106
  addresses = (await lookup(host, { all: true, verbatim: true })).map((entry) => entry.address);
@@ -5,7 +5,13 @@
5
5
  import { isUnavailable, pageFetch } from "./page-fetch.mjs";
6
6
 
7
7
  export const USER_AGENT = "Instafy-Agent/1.0 (+https://instafy.dev; reads one public page on a person's request)";
8
- export const AI_AGENTS = ["gptbot", "chatgpt-user", "oai-searchbot", "claudebot", "claude-user", "anthropic-ai", "perplexitybot", "ccbot", "google-extended"];
8
+ // The AI crawlers and the user-initiated AI fetchers (the closest match to a read on a person's request). A site that
9
+ // keeps any of them out of a path is not read there.
10
+ export const AI_AGENTS = [
11
+ "gptbot", "chatgpt-user", "oai-searchbot", "claudebot", "claude-user", "claude-searchbot", "anthropic-ai", "perplexitybot",
12
+ "perplexity-user", "ccbot", "google-extended", "applebot-extended", "meta-externalagent", "meta-externalfetcher",
13
+ "bytespider", "amazonbot", "cohere-ai", "mistralai-user", "duckassistbot",
14
+ ];
9
15
 
10
16
  /** robots.txt → [{ agents: [lowercase], rules: [{ allow, path }] }] */
11
17
  export function parseRobots(txt) {