humanish 0.56.0 → 0.58.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.
@@ -0,0 +1,337 @@
1
+ // A computer-use brain that is already installed on the operator's machine.
2
+ //
3
+ // WHY: the fastest thing humanish can do for someone new is show them a persona driving a real
4
+ // desktop. Today that needs a provider API key before anything happens, and "go make an API key"
5
+ // is where most people stop. But a developer trying humanish very often ALREADY has a coding agent
6
+ // signed in — Codex on a ChatGPT plan, Claude Code on a Max plan — and those CLIs will look at a
7
+ // screenshot and answer with the next action. Measured, not assumed: with every OPENAI_* variable
8
+ // explicitly unset, `codex exec --image` returned a correct click on the Applications menu of a
9
+ // real desktop screenshot, and `claude -p` independently agreed within three pixels.
10
+ //
11
+ // WHAT THIS IS NOT: a way to avoid paying. Subscription usage consumes the operator's own plan,
12
+ // which is why the cost line for these runs says "not priced" rather than $0 — $0 would be a lie.
13
+ // It is also not marketed as free API access, and it fails closed on a rate limit rather than
14
+ // hammering a plan that was sold for interactive coding.
15
+ //
16
+ // WHERE IT IS SAFE, and this inverts the intuitive reading: the local agent only DECIDES. humanish
17
+ // executes the action inside the E2B sandbox, so nothing the persona chooses ever runs on the
18
+ // operator's machine. The same trick on the TERMINAL lane would be the opposite — it would move
19
+ // code execution out of the sandbox and onto a real disk — which is why this is a computer-use
20
+ // provider and nothing else. Even so, these are coding agents with their own shell and file tools,
21
+ // so each one is spawned tool-restricted, in a scratch directory, with a per-turn timeout.
22
+ import { spawn } from "node:child_process";
23
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
24
+ import { tmpdir } from "node:os";
25
+ import path from "node:path";
26
+ export const LOCAL_AGENTS = [
27
+ { id: "codex", bin: "codex", label: "Codex", credentialPath: ".codex/auth.json" },
28
+ { id: "claude", bin: "claude", label: "Claude Code", credentialPath: ".claude/.credentials.json" }
29
+ ];
30
+ /** The action vocabulary the local agent is asked to answer in — a strict subset of CuaAction. */
31
+ const ACTION_KINDS = ["click", "double_click", "type", "keypress", "scroll", "wait", "done"];
32
+ /**
33
+ * OpenAI structured outputs run in STRICT mode: every property must appear in `required`, so
34
+ * "optional" is expressed as a nullable type. Getting this wrong is a 400 before any thinking
35
+ * happens, which is how this shape was arrived at.
36
+ */
37
+ export function localAgentTurnSchema() {
38
+ return {
39
+ type: "object",
40
+ additionalProperties: false,
41
+ required: ["reasoning", "done", "message", "actions"],
42
+ properties: {
43
+ reasoning: { type: "string" },
44
+ done: { type: "boolean" },
45
+ message: { type: ["string", "null"] },
46
+ actions: {
47
+ type: "array",
48
+ items: {
49
+ type: "object",
50
+ additionalProperties: false,
51
+ required: ["kind", "x", "y", "text", "keys", "ms"],
52
+ properties: {
53
+ kind: { type: "string", enum: [...ACTION_KINDS] },
54
+ x: { type: ["integer", "null"] },
55
+ y: { type: ["integer", "null"] },
56
+ text: { type: ["string", "null"] },
57
+ keys: { type: ["array", "null"], items: { type: "string" } },
58
+ ms: { type: ["integer", "null"] }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ };
64
+ }
65
+ /**
66
+ * Map the agent's answer onto the harness action vocabulary. Anything unrecognized is DROPPED
67
+ * rather than guessed at: a coordinate we invented would be recorded as the participant's choice.
68
+ */
69
+ export function toCuaActions(raw) {
70
+ const actions = [];
71
+ for (const item of raw) {
72
+ const x = typeof item.x === "number" ? Math.round(item.x) : undefined;
73
+ const y = typeof item.y === "number" ? Math.round(item.y) : undefined;
74
+ switch (item.kind) {
75
+ case "click":
76
+ if (x !== undefined && y !== undefined)
77
+ actions.push({ kind: "click", x, y });
78
+ break;
79
+ case "double_click":
80
+ if (x !== undefined && y !== undefined)
81
+ actions.push({ kind: "double_click", x, y });
82
+ break;
83
+ case "type":
84
+ if (typeof item.text === "string" && item.text.length > 0)
85
+ actions.push({ kind: "type", text: item.text });
86
+ break;
87
+ case "keypress":
88
+ if (Array.isArray(item.keys) && item.keys.length > 0)
89
+ actions.push({ kind: "keypress", keys: [...item.keys] });
90
+ break;
91
+ case "scroll":
92
+ if (x !== undefined && y !== undefined) {
93
+ actions.push({ kind: "scroll", x, y, dx: 0, dy: typeof item.ms === "number" ? item.ms : 300 });
94
+ }
95
+ break;
96
+ case "wait":
97
+ actions.push({ kind: "wait", ...(typeof item.ms === "number" ? { ms: item.ms } : {}) });
98
+ break;
99
+ default:
100
+ break; // "done" carries no action; unknown kinds are dropped on purpose
101
+ }
102
+ }
103
+ return actions;
104
+ }
105
+ /**
106
+ * Pull the JSON object out of whatever the CLI printed. Codex writes clean JSON to
107
+ * `--output-last-message`; Claude Code wraps it in a ```json fence. Both are handled here rather
108
+ * than in two places, and a response with no object at all is a turn error, never an empty turn —
109
+ * an empty turn would read to the loop as "the participant chose to do nothing".
110
+ */
111
+ export function parseAgentJson(text) {
112
+ // ORDER MATTERS, and a test caught it: Claude Code's envelope is valid JSON whose `result`
113
+ // STRING contains a ```json fence. Stripping fences first reached inside that string and
114
+ // mangled the envelope. So: parse what we were given, and only go fence-hunting if it is not
115
+ // already JSON.
116
+ const attempts = [text.trim()];
117
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
118
+ if (fenced?.[1] !== undefined)
119
+ attempts.push(fenced[1].trim());
120
+ const bare = text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1);
121
+ if (text.indexOf("{") >= 0 && bare.length > 1)
122
+ attempts.push(bare);
123
+ for (const attempt of attempts) {
124
+ if (attempt.length === 0)
125
+ continue;
126
+ try {
127
+ const parsed = JSON.parse(attempt);
128
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
129
+ return parsed;
130
+ }
131
+ }
132
+ catch {
133
+ // try the next shape
134
+ }
135
+ }
136
+ throw new Error("the local agent did not return a JSON object");
137
+ }
138
+ const defaultSpawn = async (bin, args, options) => await new Promise((resolve) => {
139
+ const child = spawn(bin, [...args], { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
140
+ let stdout = "";
141
+ let stderr = "";
142
+ const timer = setTimeout(() => child.kill("SIGKILL"), options.timeoutMs);
143
+ // Stopping a run must stop the thinking too: a local agent mid-turn can hold a terminal for
144
+ // minutes, and a Stop that leaves it running is not a stop.
145
+ const onAbort = () => { child.kill("SIGKILL"); };
146
+ options.signal?.addEventListener("abort", onAbort, { once: true });
147
+ const cleanup = () => {
148
+ clearTimeout(timer);
149
+ options.signal?.removeEventListener("abort", onAbort);
150
+ };
151
+ child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
152
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
153
+ child.on("error", (error) => {
154
+ cleanup();
155
+ resolve({ code: null, stdout, stderr: `${stderr}${String(error)}` });
156
+ });
157
+ child.on("close", (code) => {
158
+ cleanup();
159
+ resolve({ code, stdout, stderr });
160
+ });
161
+ });
162
+ export const LOCAL_AGENT_CAPABILITIES = {
163
+ headless: true,
164
+ structuredTrace: true,
165
+ lanes: ["computer-use"],
166
+ producesScreenshots: true,
167
+ // The operator brings the model by being signed into it already; humanish never sees a key.
168
+ byoModel: true,
169
+ preGrantableApprovals: false,
170
+ inProcessTools: false,
171
+ license: "open"
172
+ };
173
+ function promptFor(request, screenshotPath, agent) {
174
+ const hint = request.contextHint === undefined ? "" : `\n\nNote from the harness: ${request.contextHint}`;
175
+ // Claude Code has no --output-schema, so the shape is stated in the prompt for both; codex gets
176
+ // it enforced as well. Saying it twice costs nothing and keeps one prompt for both adapters.
177
+ const shape = '{"reasoning":string,"done":boolean,"message":string|null,'
178
+ + '"actions":[{"kind":"click|double_click|type|keypress|scroll|wait|done",'
179
+ + '"x":int|null,"y":int|null,"text":string|null,"keys":[string]|null,"ms":int|null}]}';
180
+ const readFile = agent === "claude" ? `Read the image file ${screenshotPath}. ` : "";
181
+ return [
182
+ request.instructions,
183
+ "",
184
+ `${readFile}That image is the CURRENT SCREEN. You are the participant: decide what to do next, `
185
+ + "as this person would. Coordinates are pixels from the top-left of the screenshot.",
186
+ "Return between one and three actions. Set done=true ONLY when the task is finished or you are "
187
+ + "giving up, and put your closing words in message.",
188
+ `Reply with ONLY a JSON object of this shape: ${shape}`,
189
+ hint
190
+ ].join("\n");
191
+ }
192
+ /**
193
+ * A CuaProvider backed by a coding agent that is already signed in on this machine.
194
+ *
195
+ * Deliberately NOT a new lane: the loop, the executor, the trace, the affordance record and the
196
+ * Observer are all unchanged, because the only thing that differs is where the next action comes
197
+ * from. That is also why it is honest to compare a local-agent run against an API run.
198
+ */
199
+ export function createLocalAgentProvider(options) {
200
+ const spawnFn = options.spawnFn ?? defaultSpawn;
201
+ const timeoutMs = options.timeoutMs ?? 180_000;
202
+ const effort = options.reasoningEffort ?? "low";
203
+ const descriptor = LOCAL_AGENTS.find((candidate) => candidate.id === options.agent);
204
+ if (descriptor === undefined) {
205
+ throw new Error(`unknown local agent "${options.agent}"`);
206
+ }
207
+ return {
208
+ id: `local-agent-${descriptor.id}`,
209
+ version: options.model ?? `${descriptor.bin} (local, operator-authenticated)`,
210
+ modelSettings: { reasoningEffort: effort },
211
+ capabilities: LOCAL_AGENT_CAPABILITIES,
212
+ // It reasons over pixels, so the loop must hand it a frame or fail closed.
213
+ requiresFrame: true,
214
+ async nextTurn(request, signal) {
215
+ const frame = request.observation.screenshot;
216
+ if (frame === undefined) {
217
+ throw new Error("the local-agent provider needs a screenshot and this observation has none");
218
+ }
219
+ const work = await mkdtemp(path.join(options.workRoot ?? tmpdir(), "humanish-local-agent-"));
220
+ try {
221
+ const screenshotPath = path.join(work, "screen.png");
222
+ await writeFile(screenshotPath, frame);
223
+ const prompt = promptFor(request, screenshotPath, descriptor.id);
224
+ let args;
225
+ if (descriptor.id === "codex") {
226
+ const schemaPath = path.join(work, "turn-schema.json");
227
+ await writeFile(schemaPath, JSON.stringify(localAgentTurnSchema()), "utf8");
228
+ args = [
229
+ "exec",
230
+ "--image", screenshotPath,
231
+ "--output-schema", schemaPath,
232
+ "--output-last-message", path.join(work, "turn.json"),
233
+ "--skip-git-repo-check",
234
+ // The agent's OWN shell tools stay read-only: it is here to look at a picture, and a
235
+ // coding agent that decides to go exploring is exploring the operator's disk.
236
+ "--sandbox", "read-only",
237
+ "-c", `model_reasoning_effort=${effort}`,
238
+ ...(options.model === undefined ? [] : ["--model", options.model]),
239
+ prompt
240
+ ];
241
+ }
242
+ else {
243
+ args = [
244
+ "-p",
245
+ "--output-format", "json",
246
+ // Read is the only tool it needs — the screenshot — and the only one it gets.
247
+ "--allowedTools", "Read",
248
+ ...(options.model === undefined ? [] : ["--model", options.model]),
249
+ prompt
250
+ ];
251
+ }
252
+ const result = await spawnFn(descriptor.bin, args, { cwd: work, timeoutMs, ...(signal === undefined ? {} : { signal }) });
253
+ if (result.code !== 0) {
254
+ // Fail loud with the CLI's own words. A rate-limited plan says so here, and that is a
255
+ // sentence the operator can act on, unlike "turn failed".
256
+ const detail = (result.stderr || result.stdout).trim().slice(-400);
257
+ throw new Error(`${descriptor.label} exited ${result.code ?? "on a signal"}: ${detail}`);
258
+ }
259
+ // Codex writes the structured answer to a file; Claude Code returns an envelope on stdout
260
+ // whose `result` field holds the text.
261
+ let payload = result.stdout;
262
+ if (descriptor.id === "codex") {
263
+ const { readFile: read } = await import("node:fs/promises");
264
+ payload = await read(path.join(work, "turn.json"), "utf8");
265
+ }
266
+ else {
267
+ const envelope = parseAgentJson(result.stdout);
268
+ payload = typeof envelope.result === "string" ? envelope.result : result.stdout;
269
+ }
270
+ const turn = parseAgentJson(payload);
271
+ const actions = toCuaActions(Array.isArray(turn.actions) ? turn.actions : []);
272
+ const done = turn.done === true || (actions.length === 0 && typeof turn.message === "string");
273
+ return {
274
+ actions,
275
+ pendingSafetyChecks: [],
276
+ done,
277
+ ...(typeof turn.reasoning === "string" && turn.reasoning.length > 0 ? { reasoning: turn.reasoning } : {}),
278
+ ...(typeof turn.message === "string" && turn.message.length > 0 ? { message: turn.message } : {})
279
+ // No `usage`: a subscription CLI does not report tokens we can price, and inventing a
280
+ // number here is what would make the run's cost line a lie.
281
+ };
282
+ }
283
+ finally {
284
+ await rm(work, { recursive: true, force: true }).catch(() => undefined);
285
+ }
286
+ }
287
+ };
288
+ }
289
+ /**
290
+ * Which coding agents are installed and signed in on this machine.
291
+ *
292
+ * This is the whole point of the feature at the surface: someone new does not have to go and make
293
+ * an API key if the thing that can drive the study is already on their laptop. `doctor` says so,
294
+ * and says it as a capability rather than a gate — a machine with no local agent is not broken,
295
+ * it just needs a key.
296
+ */
297
+ export async function detectLocalAgents(options = {}) {
298
+ const home = options.home ?? process.env.HOME ?? "";
299
+ const which = options.which ?? (async (bin) => {
300
+ const found = await defaultSpawn("sh", ["-lc", `command -v ${bin} 2>/dev/null || true`], {
301
+ cwd: home || ".",
302
+ timeoutMs: 10_000
303
+ });
304
+ const resolved = found.stdout.trim().split("\n")[0]?.trim();
305
+ return resolved !== undefined && resolved.length > 0 ? resolved : undefined;
306
+ });
307
+ const exists = options.exists ?? (async (file) => {
308
+ const { access } = await import("node:fs/promises");
309
+ return await access(file).then(() => true).catch(() => false);
310
+ });
311
+ const found = [];
312
+ for (const descriptor of LOCAL_AGENTS) {
313
+ const binPath = await which(descriptor.bin);
314
+ if (binPath === undefined)
315
+ continue;
316
+ found.push({
317
+ ...descriptor,
318
+ binPath,
319
+ credentialsPresent: home.length > 0 && (await exists(path.join(home, descriptor.credentialPath)))
320
+ });
321
+ }
322
+ return found;
323
+ }
324
+ /** One line for `doctor`, in the register the other rows use. */
325
+ export function localAgentDoctorMessage(found) {
326
+ if (found.length === 0) {
327
+ return "no local coding agent found — a live run needs a provider API key (`humanish keys set openai`)";
328
+ }
329
+ const ready = found.filter((agent) => agent.credentialsPresent);
330
+ if (ready.length === 0) {
331
+ const names = found.map((agent) => agent.label).join(", ");
332
+ return `${names} installed but not signed in — sign in, or use a provider API key`;
333
+ }
334
+ const names = ready.map((agent) => agent.label).join(", ");
335
+ return `${names} signed in — a live run can use ${ready.length === 1 ? "it" : "one"} instead of a provider API key (actors[0].type: local-agent)`;
336
+ }
337
+ //# sourceMappingURL=local-agent-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-agent-cli.js","sourceRoot":"","sources":["../src/local-agent-cli.ts"],"names":[],"mappings":"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"}
package/dist/program.d.ts CHANGED
@@ -37,6 +37,8 @@ export interface TuiRuntime {
37
37
  stdin: NodeJS.ReadStream;
38
38
  stdout: NodeJS.WriteStream;
39
39
  nodeVersion: string;
40
+ /** Injected so a test can pose as an agent session without touching the real process env. */
41
+ env: NodeJS.ProcessEnv;
40
42
  /** Resolves the bundle, or null when it is not present. */
41
43
  loadTui(bundle: URL): Promise<TuiModule | null>;
42
44
  }
package/dist/program.js CHANGED
@@ -31,6 +31,8 @@ import { openObserverArtifact, stopRun } from "./tui-actions.js";
31
31
  import { readRunDetail } from "./run-detail.js";
32
32
  import { launchRun, readLaunchLogTail } from "./tui-launch.js";
33
33
  import { TUI_MIN_NODE_MAJOR, nodeSupportsTui, tuiBundleUrl } from "./tui-contract.js";
34
+ import { forTerminal } from "./terminal-encoding.js";
35
+ import { detectAgentSession } from "./agent-session.js";
34
36
  import { runCommsCatchHost } from "./comms-catch-host.js";
35
37
  import { DEFAULT_SANDBOX_CATCH_PORT } from "./comms-sandbox-catch.js";
36
38
  export const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
@@ -43,9 +45,14 @@ const CLI_VERSION = readCliVersion();
43
45
  // Shared so the ~20 leaf commands that declare their own --json flag cannot drift
44
46
  // from each other in wording.
45
47
  const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response.";
48
+ // Transcode ONLY for a terminal. A pipe carries bytes to another program — mangling those would
49
+ // corrupt a JSON payload for a reader that handles UTF-8 perfectly well — while a TTY carries them
50
+ // to a font, through a locale that may not decode them. See src/terminal-encoding.ts for what a
51
+ // participant actually read back off the screen.
52
+ const forStream = (stream, text) => stream.isTTY === true ? forTerminal(text) : text;
46
53
  const defaultIo = {
47
- writeOut: (text) => process.stdout.write(text),
48
- writeErr: (text) => process.stderr.write(text),
54
+ writeOut: (text) => process.stdout.write(forStream(process.stdout, text)),
55
+ writeErr: (text) => process.stderr.write(forStream(process.stderr, text)),
49
56
  setExitCode: (code) => {
50
57
  process.exitCode = code;
51
58
  }
@@ -291,6 +298,7 @@ function registerDoctorCommand(parent, io) {
291
298
  const defaultTuiRuntime = {
292
299
  stdin: process.stdin,
293
300
  stdout: process.stdout,
301
+ env: process.env,
294
302
  nodeVersion: process.version,
295
303
  loadTui: async (bundle) => {
296
304
  if (!existsSync(bundle))
@@ -325,9 +333,31 @@ function registerTuiCommand(parent, io) {
325
333
  .description("Open the interactive terminal surface for browsing labs and runs (humans only).")
326
334
  .summary("Open the interactive terminal surface.")
327
335
  .option("--cwd <path>", "Target project directory.", ".")
336
+ .option("--force", "Open it anyway in a session that looks like an agent's.")
328
337
  .option("--json", JSON_OPTION_DESCRIPTION)
329
338
  .action(async (options, command) => {
330
339
  const { stdin, stdout } = tuiRuntime;
340
+ // An agent runner, even with a real terminal. `codex exec` allocates a PTY for the commands
341
+ // it runs, so the TTY check below passes and the surface used to open: a study watched an
342
+ // agent navigate the labs list and start a run it did not mean to start
343
+ // (labs/handed-a-human-surface.yaml). A TTY says a terminal exists, not that anyone is
344
+ // reading it. `--force` is the escape for the person who really is at this keyboard —
345
+ // capturing frames from inside an agent session is exactly that case.
346
+ const agent = options.force === true ? undefined : detectAgentSession(tuiRuntime.env);
347
+ if (agent !== undefined) {
348
+ refuseTui(command, io, {
349
+ schema: TUI_RESULT_SCHEMA,
350
+ ok: false,
351
+ error: {
352
+ code: "HUMANISH_TUI_AGENT_SESSION",
353
+ message: `humanish tui is a surface for a person, and ${agent.marker} says this session belongs to ${agent.runner}. `
354
+ + "It renders frames of escape codes into a transcript, and its keys can start runs. "
355
+ + "`humanish runs --json` lists runs, `humanish lab list --json` lists the studies in this project, "
356
+ + "and `humanish lab run <lab> --json` starts one. If you are a person at this keyboard, add --force."
357
+ }
358
+ });
359
+ return;
360
+ }
331
361
  if (stdin.isTTY !== true || stdout.isTTY !== true) {
332
362
  refuseTui(command, io, {
333
363
  schema: TUI_RESULT_SCHEMA,
@@ -2853,7 +2883,10 @@ function formatDoctorHuman(result) {
2853
2883
  return [
2854
2884
  `humanish doctor ${result.ok ? "ok" : "needs setup"}`,
2855
2885
  `cwd: ${result.cwd}`,
2856
- ...result.checks.map((check) => `- ${check.ok ? "ok" : "missing"} ${check.name}: ${check.message}`)
2886
+ // "missing" is a VERDICT, and a row that never ran has none. A participant reading doctor on a
2887
+ // fresh desktop got `- missing package.json: package.json is present and safe to read`, which
2888
+ // contradicts itself in eleven words (labs/tui-self-study.yaml).
2889
+ ...result.checks.map((check) => `- ${check.ok ? "ok" : check.checked === false ? "not checked" : "missing"} ${check.name}: ${check.message}`)
2857
2890
  ].join("\n") + "\n";
2858
2891
  }
2859
2892
  function formatRunHuman(result) {