humanish 0.141.0 → 0.142.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.
Files changed (97) hide show
  1. package/ARCHITECTURE.md +1 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +2 -0
  4. package/dist/analysis/admission.d.ts +8 -0
  5. package/dist/analysis/admission.js +15 -4
  6. package/dist/analysis/automatic-config.d.ts +5 -2
  7. package/dist/analysis/automatic-config.js +23 -8
  8. package/dist/analysis/automatic.js +13 -7
  9. package/dist/analysis/execute.d.ts +17 -6
  10. package/dist/analysis/execute.js +36 -18
  11. package/dist/analysis/gemini-wire.d.ts +8 -0
  12. package/dist/analysis/gemini-wire.js +165 -0
  13. package/dist/analysis/image-tokens.d.ts +4 -2
  14. package/dist/analysis/image-tokens.js +24 -3
  15. package/dist/analysis/job.d.ts +1 -0
  16. package/dist/analysis/job.js +1 -0
  17. package/dist/analysis/openai-wire.d.ts +5 -0
  18. package/dist/analysis/openai-wire.js +101 -0
  19. package/dist/analysis/provider.d.ts +15 -7
  20. package/dist/analysis/provider.js +21 -101
  21. package/dist/analysis/responses.d.ts +3 -0
  22. package/dist/analysis/responses.js +19 -0
  23. package/dist/analysis/service.js +11 -3
  24. package/dist/analysis/types.d.ts +8 -2
  25. package/dist/analysis/validation.d.ts +1 -0
  26. package/dist/analysis/validation.js +13 -12
  27. package/dist/cli/commands/analyze.js +9 -7
  28. package/dist/cli/commands/observe.js +26 -1
  29. package/dist/cli/commands/run-command.d.ts +1 -1
  30. package/dist/cli/commands/run-command.js +12 -0
  31. package/dist/cli/commands/study-remote.d.ts +13 -0
  32. package/dist/cli/commands/study-remote.js +125 -0
  33. package/dist/cli/commands/study-route-computer-use.js +26 -2
  34. package/dist/cli/commands/study-route-scripted.js +1 -0
  35. package/dist/cli/commands/study-route-shared-world.js +1 -0
  36. package/dist/cli/commands/study-route-terminal.js +1 -0
  37. package/dist/cli/commands/study-run.js +34 -0
  38. package/dist/cli/commands/watch.js +5 -0
  39. package/dist/cli/doctor.js +7 -2
  40. package/dist/cli/findings.d.ts +8 -1
  41. package/dist/cli/findings.js +38 -15
  42. package/dist/cli/io.d.ts +8 -0
  43. package/dist/cli/io.js +2 -1
  44. package/dist/keys/key-resolution.d.ts +10 -0
  45. package/dist/keys/key-resolution.js +33 -5
  46. package/dist/observer/listen.d.ts +7 -2
  47. package/dist/observer/listen.js +10 -4
  48. package/dist/observer/render.d.ts +2 -0
  49. package/dist/observer/render.js +1 -1
  50. package/dist/observer-app.html +1 -1
  51. package/dist/remote/launch.d.ts +53 -0
  52. package/dist/remote/launch.js +320 -0
  53. package/dist/remote/observer-proxy.d.ts +11 -0
  54. package/dist/remote/observer-proxy.js +66 -0
  55. package/dist/remote/plan.d.ts +35 -0
  56. package/dist/remote/plan.js +114 -0
  57. package/dist/remote/reattach.d.ts +17 -0
  58. package/dist/remote/reattach.js +81 -0
  59. package/dist/remote/runner.d.ts +68 -0
  60. package/dist/remote/runner.js +216 -0
  61. package/dist/routes/computer-use/e2b-desktop/start.js +1 -1
  62. package/dist/routes/computer-use/types.d.ts +2 -0
  63. package/dist/routes/scripted/types.d.ts +2 -0
  64. package/dist/routes/shared-world/types.d.ts +2 -0
  65. package/dist/routes/terminal/types.d.ts +2 -0
  66. package/dist/run/costs.d.ts +2 -1
  67. package/dist/run/pricing.d.ts +13 -1
  68. package/dist/run/pricing.js +50 -4
  69. package/dist/run/results.d.ts +1 -1
  70. package/dist/run/run-cost.d.ts +2 -1
  71. package/dist/run/run-cost.js +6 -1
  72. package/dist/run/run.d.ts +13 -0
  73. package/dist/run/run.js +18 -7
  74. package/dist/run/sandbox-creates.d.ts +10 -3
  75. package/dist/run/sandbox-creates.js +48 -6
  76. package/dist/run-study.d.ts +5 -0
  77. package/dist/study/doctor.js +14 -1
  78. package/dist/study/requirements.d.ts +1 -1
  79. package/dist/study/requirements.js +8 -6
  80. package/dist/study/route-inputs.js +4 -0
  81. package/dist/study/run-study-events.d.ts +1 -1
  82. package/dist/study/run-study-events.js +9 -2
  83. package/dist/substrates/e2b/base-module.d.ts +3 -0
  84. package/dist/substrates/e2b/base-module.js +40 -0
  85. package/dist/substrates/e2b/desktop-browser.d.ts +1 -1
  86. package/dist/substrates/e2b/desktop-browser.js +24 -6
  87. package/dist/substrates/e2b/sdk.d.ts +24 -0
  88. package/dist/substrates/e2b/sdk.js +5 -1
  89. package/dist/substrates/e2b/shell-only.d.ts +5 -0
  90. package/dist/substrates/e2b/shell-only.js +27 -0
  91. package/dist/substrates/modal/sandbox.js +27 -28
  92. package/dist/substrates/modal/sdk.d.ts +7 -0
  93. package/dist/tui-app.js +46 -46
  94. package/docs/contracts/study-analysis.md +51 -3
  95. package/docs/product/automatic-analysis.md +26 -2
  96. package/package.json +2 -2
  97. package/skills/humanish/SKILL.md +10 -0
@@ -11,7 +11,7 @@
11
11
  // GH_TOKEN <- `gh auth token` (the gh CLI's credential chain);
12
12
  // 4. the humanish user-level store `$XDG_CONFIG_HOME/humanish/keys.env`, written only by
13
13
  // `humanish keys set` (0600, prompted or --stdin), the fallback for vendors that ship
14
- // no machine chain of their own (OpenAI, Anthropic).
14
+ // no machine chain of their own (OpenAI, Anthropic, Gemini).
15
15
  //
16
16
  // Every fill is announced by key name + source, never by value. The subject app's own
17
17
  // `.env`/`.env.local` are never read: those hold product credentials, a different class
@@ -34,7 +34,8 @@ import { OPENAI_EGRESS_PLACEHOLDER } from "../routes/terminal/runtime-auth.js";
34
34
  * full-file load). ANTHROPIC_API_KEY is not listed, and doctor does not report it: a Claude Code
35
35
  * participant runs on its own login and never receives it. CODEX_API_KEY is an alternative spelling
36
36
  * of the OpenAI key for terminal studies. Doctor reports AGENTMAIL_API_KEY only under `--study`, for
37
- * a study whose email connection names it, since a connection may name another variable.
37
+ * a study whose email connection names it, since a connection may name another variable, and
38
+ * GEMINI_API_KEY only for a study whose analysis runs on Gemini.
38
39
  */
39
40
  const PROVIDER_KEYS = [
40
41
  {
@@ -69,9 +70,33 @@ const PROVIDER_KEYS = [
69
70
  { name: "GITHUB_TOKEN", spellingOf: "GH_TOKEN" },
70
71
  { name: "CODEX_API_KEY", doctor: true },
71
72
  { name: "AGENTMAIL_API_KEY", vendor: "agentmail", listed: { order: 4, use: "email in studies" } },
73
+ { name: "GEMINI_API_KEY", vendor: "gemini", listed: { order: 5, use: "analysis on Gemini" } },
74
+ // Google's client libraries also read GOOGLE_API_KEY, and prefer it when both are set. humanish
75
+ // reads GEMINI_API_KEY first: a GOOGLE_API_KEY exported for Maps or Firebase must not take the
76
+ // place of the key `humanish keys set gemini` stored.
77
+ { name: "GOOGLE_API_KEY", spellingOf: "GEMINI_API_KEY" },
72
78
  ];
73
79
  const PROVIDER_KEY_SET = new Set(PROVIDER_KEYS.map((key) => key.name));
74
80
  const providerKey = (name) => PROVIDER_KEYS.find((key) => key.name === name);
81
+ /**
82
+ * Every env name of the key `name` is a spelling of, in the order humanish reads them: the key's
83
+ * own name, then its second spellings. Discovery treats them as one key.
84
+ */
85
+ function keyNames(name) {
86
+ const own = providerKey(name)?.spellingOf ?? name;
87
+ return [own, ...PROVIDER_KEYS.filter((key) => key.spellingOf === own).map((key) => key.name)];
88
+ }
89
+ /** The key each API analyst reads, under its first spelling. */
90
+ const ANALYSIS_KEYS = { openai: "OPENAI_API_KEY", gemini: "GEMINI_API_KEY" };
91
+ /** The env names an API analyst's key may have, in the order they are read. */
92
+ export const analysisKeyNames = (vendor) => keyNames(ANALYSIS_KEYS[vendor]);
93
+ /** An API analyst's key from `env`: the first of its names with a non-blank value, else "". */
94
+ export function analysisApiKey(vendor, env) {
95
+ for (const name of analysisKeyNames(vendor))
96
+ if (env[name]?.trim())
97
+ return env[name];
98
+ return "";
99
+ }
75
100
  /** The vendor aliases `humanish keys set` accepts. A raw ENV_NAME is also accepted. */
76
101
  export const KEY_VENDORS = PROVIDER_KEYS.flatMap((key) => key.vendor === undefined ? [] : [key.vendor]);
77
102
  /** The keys `humanish keys` lists, in its order, each with what humanish uses it for. */
@@ -212,8 +237,11 @@ export async function discoverProviderKeys(args) {
212
237
  const ignored = [];
213
238
  // Fill-only means presence wins, including an explicitly-set empty value: an operator who
214
239
  // exported OPENAI_API_KEY="" said "off", and a rung that "fixes" that has overridden them.
215
- // Only a truly-unset name is fillable.
216
- const fillable = (name) => env[name] === undefined;
240
+ // Only a truly-unset name is fillable, and only while no spelling of its key was set before
241
+ // discovery: a repo overlay's GOOGLE_API_KEY must not stand beside, and outrank, an exported
242
+ // GEMINI_API_KEY. Spellings that discovery itself fills may stand together.
243
+ const explicit = new Set(Object.keys(env).filter((name) => env[name] !== undefined));
244
+ const fillable = (name) => env[name] === undefined && keyNames(name).every((spelling) => !explicit.has(spelling));
217
245
  const fill = (name, value, source) => {
218
246
  env[name] = value;
219
247
  fills.push({ name, source });
@@ -259,7 +287,7 @@ export async function discoverProviderKeys(args) {
259
287
  // Rung 3c: the gh CLI's credential chain. Only consulted when neither GitHub env name is
260
288
  // present; fills GH_TOKEN (the name humanish reads first). gh runs without any provider key in
261
289
  // its environment, so the keys the rungs above just filled stay in this process.
262
- if (fillable("GH_TOKEN") && fillable("GITHUB_TOKEN")) {
290
+ if (keyNames("GH_TOKEN").every((name) => env[name] === undefined)) {
263
291
  const exec = deps.execText ?? defaultExecText;
264
292
  const token = await exec("gh", ["auth", "token"], 3_000, withoutProviderKeys(env));
265
293
  if (token !== null)
@@ -12,5 +12,10 @@ export declare class PortInUseError extends Error {
12
12
  * that cannot decide says "other", which is the answer that suggests the safer action.
13
13
  */
14
14
  export declare function probePortHolder(port: number, fetchFn?: typeof fetch): Promise<PortHolder>;
15
- /** Bind on loopback only; resolve the port that was actually bound; name a taken port. */
16
- export declare function listenOnLoopback(server: Server, port: number, probe?: (port: number) => Promise<PortHolder>): Promise<number>;
15
+ /**
16
+ * Bind on loopback; resolve the port that was actually bound; name a taken port. `allInterfaces`
17
+ * binds every interface instead, for the Observer of a remote run's runner sandbox: Modal's proxy
18
+ * cannot reach a loopback-only server, and only the provider's authenticated proxy reaches the
19
+ * sandbox's ports.
20
+ */
21
+ export declare function listenOnLoopback(server: Server, port: number, probe?: (port: number) => Promise<PortHolder>, allInterfaces?: boolean): Promise<number>;
@@ -61,8 +61,13 @@ async function probeRoutes(port, fetchFn) {
61
61
  }
62
62
  return "other";
63
63
  }
64
- /** Bind on loopback only; resolve the port that was actually bound; name a taken port. */
65
- export function listenOnLoopback(server, port, probe = probePortHolder) {
64
+ /**
65
+ * Bind on loopback; resolve the port that was actually bound; name a taken port. `allInterfaces`
66
+ * binds every interface instead, for the Observer of a remote run's runner sandbox: Modal's proxy
67
+ * cannot reach a loopback-only server, and only the provider's authenticated proxy reaches the
68
+ * sandbox's ports.
69
+ */
70
+ export function listenOnLoopback(server, port, probe = probePortHolder, allInterfaces = false) {
66
71
  return new Promise((resolve, reject) => {
67
72
  const onError = (error) => {
68
73
  if (error.code === "EADDRINUSE") {
@@ -72,8 +77,9 @@ export function listenOnLoopback(server, port, probe = probePortHolder) {
72
77
  reject(error);
73
78
  };
74
79
  server.once("error", onError);
75
- // Never beyond loopback; exposure only ever happens through a tunnel or proxy forwarding here.
76
- server.listen(port, "127.0.0.1", () => {
80
+ // Otherwise never beyond loopback; exposure only ever happens through a tunnel or proxy
81
+ // forwarding here.
82
+ server.listen(port, allInterfaces ? "0.0.0.0" : "127.0.0.1", () => {
77
83
  server.off("error", onError);
78
84
  const address = server.address();
79
85
  if (!address || typeof address === "string") {
@@ -31,6 +31,8 @@ export interface ObserverServeOptions {
31
31
  /** Restrict history and evidence routes to the selected run. Exposed viewers always do this. */
32
32
  scope?: "run" | "library";
33
33
  exposed?: boolean;
34
+ /** Bind every interface: only a remote run's runner sandbox, behind its provider's proxy. */
35
+ allInterfaces?: boolean;
34
36
  }
35
37
  export interface ObserverServer {
36
38
  url: string;
@@ -293,7 +293,7 @@ export async function serveObserver(result, options = {}) {
293
293
  const server = createServer((request, response) => {
294
294
  void handleRequest(request, response);
295
295
  });
296
- port = await listenOnLoopback(server, options.port ?? 0);
296
+ port = await listenOnLoopback(server, options.port ?? 0, undefined, options.allInterfaces === true);
297
297
  if (exposed) {
298
298
  hostAllowlist.add(`127.0.0.1:${port}`);
299
299
  hostAllowlist.add(`localhost:${port}`);