humanish 0.112.0 → 0.113.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/CHANGELOG.md +19 -0
- package/README.md +11 -0
- package/TELEMETRY.md +21 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +21 -1
- package/dist/actors/codex/restricted-participant-policy.js +20 -2
- package/dist/actors/codex/restricted-participant.d.ts +1 -1
- package/dist/actors/codex/restricted-participant.js +2 -1
- package/dist/actors/computer-use/loop/debrief.d.ts +12 -4
- package/dist/actors/computer-use/loop/debrief.js +57 -20
- package/dist/actors/computer-use/loop/ending.d.ts +5 -2
- package/dist/actors/computer-use/loop/impressions.d.ts +14 -0
- package/dist/actors/computer-use/loop/impressions.js +40 -0
- package/dist/actors/computer-use/loop/session.d.ts +3 -1
- package/dist/actors/computer-use/loop/session.js +2 -0
- package/dist/actors/computer-use/loop/trace.js +2 -0
- package/dist/actors/computer-use/loop/types.d.ts +8 -1
- package/dist/actors/computer-use/loop.d.ts +2 -1
- package/dist/actors/computer-use/loop.js +15 -3
- package/dist/actors/computer-use/openai-conversation.d.ts +9 -4
- package/dist/actors/computer-use/openai-conversation.js +53 -24
- package/dist/actors/computer-use/openai-provider.js +18 -12
- package/dist/actors/contract.d.ts +43 -3
- package/dist/actors/contract.js +13 -0
- package/dist/analysis/evidence-sources.d.ts +3 -1
- package/dist/analysis/evidence-sources.js +10 -1
- package/dist/analysis/evidence.js +2 -2
- package/dist/analysis/execute.d.ts +1 -1
- package/dist/analysis/execute.js +3 -1
- package/dist/cli/commands/runs.js +33 -10
- package/dist/cli/commands/setup.js +95 -6
- package/dist/cli/commands/study-format.js +7 -4
- package/dist/cli/commands/study.js +1 -1
- package/dist/cli/commands/tui.js +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +18 -0
- package/dist/cli/io.d.ts +2 -0
- package/dist/cli/program.js +35 -1
- package/dist/cli/secret-prompt.d.ts +5 -1
- package/dist/cli/secret-prompt.js +6 -2
- package/dist/cli/update-check-worker.d.ts +1 -0
- package/dist/cli/update-check-worker.js +5 -0
- package/dist/cli/update-check.d.ts +46 -0
- package/dist/cli/update-check.js +203 -0
- package/dist/feedback/draft.js +18 -0
- package/dist/keys/key-resolution.js +2 -0
- package/dist/keys/key-status.d.ts +37 -0
- package/dist/keys/key-status.js +68 -0
- package/dist/observer-app.html +4 -4
- package/dist/routes/computer-use/local-vm.js +9 -2
- package/dist/routes/computer-use/participant-desktop.d.ts +6 -0
- package/dist/routes/computer-use/participant-execution.js +9 -1
- package/dist/routes/computer-use/rerun-selection.js +23 -21
- package/dist/routes/computer-use/result.js +3 -2
- package/dist/routes/computer-use/self-report.js +7 -1
- package/dist/routes/computer-use/setup.js +14 -0
- package/dist/routes/computer-use/types.d.ts +4 -1
- package/dist/routes/shared-world/bundle-records.js +8 -7
- package/dist/routes/shared-world/bundle.js +1 -1
- package/dist/run/detail.js +4 -4
- package/dist/run/participant-caption.d.ts +2 -1
- package/dist/run/participant-caption.js +11 -1
- package/dist/run/participant-records.d.ts +3 -1
- package/dist/run/participant-records.js +6 -0
- package/dist/run-study.js +17 -1
- package/dist/study/discover.js +6 -1
- package/dist/study/doctor.d.ts +2 -0
- package/dist/study/doctor.js +45 -7
- package/dist/study/init-templates.js +10 -5
- package/dist/study/plan.js +3 -2
- package/dist/study/preflight-result.d.ts +1 -0
- package/dist/study/preflight-result.js +1 -1
- package/dist/study/preflight.d.ts +2 -0
- package/dist/study/preflight.js +54 -14
- package/dist/study/warnings.d.ts +5 -0
- package/dist/study/warnings.js +79 -2
- package/dist/substrates/local/capacity.d.ts +75 -0
- package/dist/substrates/local/capacity.js +143 -0
- package/dist/substrates/local/firecracker-desktop.d.ts +8 -0
- package/dist/substrates/local/firecracker-desktop.js +34 -3
- package/dist/substrates/local/runtime-host.d.ts +16 -3
- package/dist/substrates/local/runtime-host.js +49 -4
- package/dist/substrates/local/runtime.d.ts +10 -0
- package/dist/substrates/local/runtime.js +44 -5
- package/dist/tui-app.js +58 -58
- package/docs/architecture/actor-contract.md +20 -6
- package/docs/contracts/feedback.md +8 -1
- package/docs/contracts/schemas.md +57 -34
- package/docs/contracts/study-analysis.md +15 -0
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +21 -10
package/dist/cli/doctor.d.ts
CHANGED
package/dist/cli/doctor.js
CHANGED
|
@@ -11,6 +11,8 @@ import { studySetupChecks, studiesByRequiredKey, } from "../study/doctor.js";
|
|
|
11
11
|
import { prepareSelectedOutputDirectory, } from "../run/contained-output.js";
|
|
12
12
|
import { implicitProjectDirectoryExists, readImplicitProjectFile, validateCwd, } from "../run/project.js";
|
|
13
13
|
import { cli } from "./invocation.js";
|
|
14
|
+
import { recordedVersion } from "./update-check.js";
|
|
15
|
+
import { CLI_VERSION } from "./version.js";
|
|
14
16
|
const DOCTOR_SCHEMA = "humanish.doctor-result.v1";
|
|
15
17
|
/**
|
|
16
18
|
* Version 2.3.1 stopped holding a launched background command's event stream open. Version 2.3.2
|
|
@@ -94,6 +96,7 @@ export async function doctor(cwdInput, options = {}) {
|
|
|
94
96
|
...(await projectChecks(projectRoot)),
|
|
95
97
|
await desktopSdkCheck(setup),
|
|
96
98
|
terminalSurfaceCheck(),
|
|
99
|
+
...versionCheck(env),
|
|
97
100
|
...localAgentChecks(agents),
|
|
98
101
|
await claudeTranscriptCheck(env),
|
|
99
102
|
...keyChecks(probes, receivingKey, setup, keyUsers),
|
|
@@ -109,6 +112,7 @@ export async function doctor(cwdInput, options = {}) {
|
|
|
109
112
|
schema: DOCTOR_SCHEMA,
|
|
110
113
|
ok: checks.every((check) => check.ok),
|
|
111
114
|
cwd,
|
|
115
|
+
...(setup?.next === undefined ? {} : { next: setup.next }),
|
|
112
116
|
checks: withStatus(checks),
|
|
113
117
|
};
|
|
114
118
|
}
|
|
@@ -246,6 +250,20 @@ function terminalSurfaceCheck() {
|
|
|
246
250
|
}),
|
|
247
251
|
};
|
|
248
252
|
}
|
|
253
|
+
/** The latest version the update check recorded on this machine; no row before a check. */
|
|
254
|
+
function versionCheck(env) {
|
|
255
|
+
const recorded = recordedVersion(CLI_VERSION, env, env.HOME ?? homedir());
|
|
256
|
+
if (recorded === undefined)
|
|
257
|
+
return [];
|
|
258
|
+
return [
|
|
259
|
+
{
|
|
260
|
+
name: "humanish version",
|
|
261
|
+
ok: true,
|
|
262
|
+
...(recorded.newer ? { status: "note" } : {}),
|
|
263
|
+
message: recorded.message,
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
}
|
|
249
267
|
/** Probes the live-run keys, plus the receiving-email key a real-comms study names. */
|
|
250
268
|
async function probeDoctorKeys(cwd, env, study, keyDeps, dotenv) {
|
|
251
269
|
const keyNames = new Set(["OPENAI_API_KEY", "E2B_API_KEY", "GH_TOKEN", "CODEX_API_KEY"]);
|
package/dist/cli/io.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
2
|
import { discoverProviderKeys, type DotenvLoad } from "../keys/key-resolution.js";
|
|
3
3
|
import { type TelemetryProperties } from "./telemetry.js";
|
|
4
|
+
import type { UpdateCheckContext } from "./update-check.js";
|
|
4
5
|
import type { RunResult } from "../run/results.js";
|
|
5
6
|
export declare const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
|
|
6
7
|
export { CLI_VERSION } from "./version.js";
|
|
@@ -9,6 +10,7 @@ export interface CliIo {
|
|
|
9
10
|
writeErr(text: string): void;
|
|
10
11
|
setExitCode(code: number): void;
|
|
11
12
|
keyDiscovery?: typeof discoverProviderKeys;
|
|
13
|
+
updateCheck?: Partial<UpdateCheckContext>;
|
|
12
14
|
}
|
|
13
15
|
export declare const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response.";
|
|
14
16
|
export declare const CWD_OPTION_DESCRIPTION = "Project directory.";
|
package/dist/cli/program.js
CHANGED
|
@@ -2,10 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { formatOrientationHuman, readOrientation } from "./orientation.js";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { Command } from "commander";
|
|
5
|
+
import { Command, Help } from "commander";
|
|
6
6
|
import { redactText } from "../evidence/redaction.js";
|
|
7
7
|
import { PortInUseError } from "../observer/listen.js";
|
|
8
8
|
import { buildPayload, disabledByEnvironment, durationBucket, readTelemetryState, sendTelemetry, TELEMETRY_NOTICE, writeTelemetryState, isOwnCheckoutRun, } from "./telemetry.js";
|
|
9
|
+
import { checkForUpdate, startRefreshWorker } from "./update-check.js";
|
|
9
10
|
import { registerAnalyzeCommand } from "./commands/analyze.js";
|
|
10
11
|
import { registerCodexCommands } from "./commands/codex.js";
|
|
11
12
|
import { registerCommsCommands } from "./commands/comms.js";
|
|
@@ -95,6 +96,29 @@ async function recordCommandTelemetry(command, ok, durationMs, exitCode) {
|
|
|
95
96
|
// Metrics are our problem, never the operator's.
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* After a command, one line on stderr when a newer humanish is published. It reads only the cache;
|
|
101
|
+
* the registry request runs detached (see update-check.ts), so nothing here waits on the network.
|
|
102
|
+
*/
|
|
103
|
+
function noticeNewerVersion(command, io) {
|
|
104
|
+
try {
|
|
105
|
+
const notice = checkForUpdate({
|
|
106
|
+
installed: CLI_VERSION,
|
|
107
|
+
env: process.env,
|
|
108
|
+
terminal: process.stderr.isTTY === true,
|
|
109
|
+
json: wantsJson(command),
|
|
110
|
+
ownCheckout: isOwnCheckoutRun(process.cwd(), dirname(fileURLToPath(import.meta.url)), (p) => readFileSync(p, "utf8")),
|
|
111
|
+
now: Date.now(),
|
|
112
|
+
startRefresh: startRefreshWorker,
|
|
113
|
+
...io.updateCheck,
|
|
114
|
+
});
|
|
115
|
+
if (notice !== undefined)
|
|
116
|
+
io.writeErr(`${notice}\n`);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// An update notice is never a reason for a command to fail.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
98
122
|
/**
|
|
99
123
|
* HUMANISH_DEBUG_HANDLES=1: after a command's handler settles, name what is still keeping the
|
|
100
124
|
* process alive. A live terminal run wrote its result 64 s in and the CLI stayed up for
|
|
@@ -165,6 +189,7 @@ class HumanishCommand extends Command {
|
|
|
165
189
|
const noticed = announceTelemetryOnce(this, cliIo);
|
|
166
190
|
const finish = (ok) => {
|
|
167
191
|
void recordCommandTelemetry(this, ok, Date.now() - startedAt, lastExitCode);
|
|
192
|
+
noticeNewerVersion(this, cliIo);
|
|
168
193
|
reportActiveHandles(this, cliIo);
|
|
169
194
|
};
|
|
170
195
|
try {
|
|
@@ -326,6 +351,15 @@ export function createProgram(io = {}) {
|
|
|
326
351
|
// each subcommand when it is created.
|
|
327
352
|
.helpOption("-h, --help", "Show help for this command.")
|
|
328
353
|
.helpCommand("help [command]", "Show help for a command.")
|
|
354
|
+
// `help <command>` and `<command> help` keep working; a command's listing names only the
|
|
355
|
+
// commands that do something, so `help` is left out of every Commands section.
|
|
356
|
+
.configureHelp({
|
|
357
|
+
visibleCommands(command) {
|
|
358
|
+
return Help.prototype.visibleCommands
|
|
359
|
+
.call(this, command)
|
|
360
|
+
.filter((child) => child.name() !== "help");
|
|
361
|
+
},
|
|
362
|
+
})
|
|
329
363
|
.showHelpAfterError()
|
|
330
364
|
.option("--json", "Print machine-readable JSON responses where supported.")
|
|
331
365
|
.configureOutput({
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Host-owned entry: no secret crosses the TUI view contract or reaches a writable terminal.
|
|
3
|
+
* Resolves to the trimmed line, an empty string when Enter was pressed alone, or null when the
|
|
4
|
+
* person cancelled or there is no terminal.
|
|
5
|
+
*/
|
|
2
6
|
export declare function promptSecret(label: string, stdin: NodeJS.ReadStream, stdout: NodeJS.WriteStream): Promise<string | null>;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { Writable } from "node:stream";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Host-owned entry: no secret crosses the TUI view contract or reaches a writable terminal.
|
|
5
|
+
* Resolves to the trimmed line, an empty string when Enter was pressed alone, or null when the
|
|
6
|
+
* person cancelled or there is no terminal.
|
|
7
|
+
*/
|
|
4
8
|
export async function promptSecret(label, stdin, stdout) {
|
|
5
9
|
if (!stdin.isTTY || !stdout.isTTY)
|
|
6
10
|
return null;
|
|
@@ -27,7 +31,7 @@ export async function promptSecret(label, stdin, stdout) {
|
|
|
27
31
|
stdout.write("\n");
|
|
28
32
|
resolve(value);
|
|
29
33
|
};
|
|
30
|
-
reader.once("line", (line) => finish(line.trim()
|
|
34
|
+
reader.once("line", (line) => finish(line.trim()));
|
|
31
35
|
reader.once("SIGINT", () => finish(null));
|
|
32
36
|
reader.once("close", () => finish(null));
|
|
33
37
|
reader.once("error", () => finish(null));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface UpdateCheckContext {
|
|
2
|
+
/** The running CLI's version. */
|
|
3
|
+
installed: string;
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
/** Whether stderr is a terminal. */
|
|
6
|
+
terminal: boolean;
|
|
7
|
+
/** Whether the command prints a JSON document. */
|
|
8
|
+
json: boolean;
|
|
9
|
+
/** Whether this CLI runs from humanish's own source checkout. */
|
|
10
|
+
ownCheckout: boolean;
|
|
11
|
+
/** The clock, in epoch milliseconds. */
|
|
12
|
+
now: number;
|
|
13
|
+
home?: string;
|
|
14
|
+
/** Starts the background registry request that writes the cache. Never awaited. */
|
|
15
|
+
startRefresh: (cachePath: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The one line to print after a command when the cache names a newer humanish, at most once a day.
|
|
19
|
+
* Records that it was shown. Every failure inside is swallowed: an update notice is never a reason
|
|
20
|
+
* for a command to fail.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkForUpdate(context: UpdateCheckContext): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The version the last successful check recorded, compared with `installed`, for `doctor`. Reads the
|
|
25
|
+
* cache only; undefined until a check has succeeded on this machine.
|
|
26
|
+
*/
|
|
27
|
+
export declare function recordedVersion(installed: string, env: NodeJS.ProcessEnv, home?: string): {
|
|
28
|
+
newer: boolean;
|
|
29
|
+
message: string;
|
|
30
|
+
} | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Ask the npm registry for humanish's `latest` dist-tag and record it in the cache. The detached
|
|
33
|
+
* worker runs this. The request is a GET with an Accept header and nothing else: no machine id,
|
|
34
|
+
* installed version, command or path. A failed or malformed answer leaves the cache as it was.
|
|
35
|
+
*/
|
|
36
|
+
export declare function refreshUpdateCache(args: {
|
|
37
|
+
cachePath: string;
|
|
38
|
+
fetchFn?: (url: string, init: RequestInit) => Promise<Response>;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Start the worker that runs refreshUpdateCache, detached and unreferenced so this process exits
|
|
44
|
+
* without waiting for it. A source checkout has no built worker, and nothing starts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function startRefreshWorker(cachePath: string): void;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Tells a person when a newer humanish is published, as npm, gh and the Vercel CLI do. A command
|
|
2
|
+
// only reads a small cache and prints at most one line from it; the registry request runs in a
|
|
3
|
+
// detached process that writes the cache for a later command, so the check never delays or fails
|
|
4
|
+
// the command that started it.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { disabledByEnvironment } from "./telemetry.js";
|
|
11
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const REGISTRY_DIST_TAGS = "https://registry.npmjs.org/-/package/humanish/dist-tags";
|
|
13
|
+
/** Next to telemetry.json, under the same XDG rule the key store uses. */
|
|
14
|
+
function updateCachePath(env, home = homedir()) {
|
|
15
|
+
const declared = env.XDG_CONFIG_HOME?.trim();
|
|
16
|
+
const configHome = declared !== undefined && declared !== "" && path.isAbsolute(declared)
|
|
17
|
+
? declared
|
|
18
|
+
: path.join(home, ".config");
|
|
19
|
+
return path.join(configHome, "humanish", "update-check.json");
|
|
20
|
+
}
|
|
21
|
+
function readUpdateCache(cachePath) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
24
|
+
if (parsed === null || typeof parsed !== "object")
|
|
25
|
+
return {};
|
|
26
|
+
const record = parsed;
|
|
27
|
+
const text = (field) => typeof record[field] === "string" ? record[field] : undefined;
|
|
28
|
+
const cache = {};
|
|
29
|
+
const checkedAt = text("checkedAt");
|
|
30
|
+
const latest = text("latest");
|
|
31
|
+
const notifiedAt = text("notifiedAt");
|
|
32
|
+
if (checkedAt !== undefined)
|
|
33
|
+
cache.checkedAt = checkedAt;
|
|
34
|
+
if (latest !== undefined && parseVersion(latest) !== undefined)
|
|
35
|
+
cache.latest = latest;
|
|
36
|
+
if (notifiedAt !== undefined)
|
|
37
|
+
cache.notifiedAt = notifiedAt;
|
|
38
|
+
return cache;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function writeUpdateCache(cachePath, cache) {
|
|
45
|
+
mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
46
|
+
writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
|
|
47
|
+
}
|
|
48
|
+
function parseVersion(version) {
|
|
49
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
50
|
+
return match === null ? undefined : [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
51
|
+
}
|
|
52
|
+
/** True when `candidate` is a higher release than `installed`. */
|
|
53
|
+
function isNewer(candidate, installed) {
|
|
54
|
+
const a = parseVersion(candidate);
|
|
55
|
+
const b = parseVersion(installed);
|
|
56
|
+
if (a === undefined || b === undefined)
|
|
57
|
+
return false;
|
|
58
|
+
for (let index = 0; index < 3; index += 1)
|
|
59
|
+
if (a[index] !== b[index])
|
|
60
|
+
return a[index] > b[index];
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/** True when `timestamp` is missing, unreadable, or at least a day before `now`. */
|
|
64
|
+
function dayPassed(timestamp, now) {
|
|
65
|
+
const then = timestamp === undefined ? Number.NaN : Date.parse(timestamp);
|
|
66
|
+
return Number.isNaN(then) || now - then >= DAY_MS;
|
|
67
|
+
}
|
|
68
|
+
const truthy = (value) => value !== undefined &&
|
|
69
|
+
value.trim() !== "" &&
|
|
70
|
+
value.trim() !== "0" &&
|
|
71
|
+
value.trim().toLowerCase() !== "false";
|
|
72
|
+
/**
|
|
73
|
+
* Only a person at a terminal is told, and only when nothing has switched the check off. A pipe,
|
|
74
|
+
* CI and a JSON document have a program reading them. A study participant runs the version the
|
|
75
|
+
* study chose, and a notice would send it to another. The telemetry switches turn this off too:
|
|
76
|
+
* someone who opted out of telemetry expects no unprompted requests.
|
|
77
|
+
*/
|
|
78
|
+
function checkAllowed(context) {
|
|
79
|
+
const env = context.env;
|
|
80
|
+
return (context.terminal &&
|
|
81
|
+
!context.json &&
|
|
82
|
+
!context.ownCheckout &&
|
|
83
|
+
!truthy(env.CI) &&
|
|
84
|
+
!truthy(env.HUMANISH_NO_UPDATE_CHECK) &&
|
|
85
|
+
!truthy(env.HUMANISH_STUDY_PARTICIPANT) &&
|
|
86
|
+
!disabledByEnvironment(env));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The one line to print after a command when the cache names a newer humanish, at most once a day.
|
|
90
|
+
* Records that it was shown. Every failure inside is swallowed: an update notice is never a reason
|
|
91
|
+
* for a command to fail.
|
|
92
|
+
*/
|
|
93
|
+
export function checkForUpdate(context) {
|
|
94
|
+
try {
|
|
95
|
+
if (!checkAllowed(context))
|
|
96
|
+
return undefined;
|
|
97
|
+
const cachePath = updateCachePath(context.env, context.home);
|
|
98
|
+
const cache = readUpdateCache(cachePath);
|
|
99
|
+
const stamp = new Date(context.now).toISOString();
|
|
100
|
+
const next = { ...cache };
|
|
101
|
+
// Stamped before the request starts, so an offline machine asks once a day rather than on
|
|
102
|
+
// every command. The request writes `latest` when it succeeds.
|
|
103
|
+
const refresh = dayPassed(cache.checkedAt, context.now);
|
|
104
|
+
if (refresh)
|
|
105
|
+
next.checkedAt = stamp;
|
|
106
|
+
const latest = cache.latest;
|
|
107
|
+
const notice = latest !== undefined &&
|
|
108
|
+
isNewer(latest, context.installed) &&
|
|
109
|
+
dayPassed(cache.notifiedAt, context.now)
|
|
110
|
+
? `humanish ${context.installed} is out of date; the latest is ${latest}. Run npx humanish@latest to use it.`
|
|
111
|
+
: undefined;
|
|
112
|
+
if (notice !== undefined)
|
|
113
|
+
next.notifiedAt = stamp;
|
|
114
|
+
if (refresh || notice !== undefined)
|
|
115
|
+
writeUpdateCache(cachePath, next);
|
|
116
|
+
if (refresh)
|
|
117
|
+
context.startRefresh(cachePath);
|
|
118
|
+
return notice;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The version the last successful check recorded, compared with `installed`, for `doctor`. Reads the
|
|
126
|
+
* cache only; undefined until a check has succeeded on this machine.
|
|
127
|
+
*/
|
|
128
|
+
export function recordedVersion(installed, env, home) {
|
|
129
|
+
const cache = readUpdateCache(updateCachePath(env, home));
|
|
130
|
+
if (cache.latest === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const checked = cache.checkedAt === undefined ? "" : ` (checked ${cache.checkedAt.slice(0, 10)})`;
|
|
133
|
+
return isNewer(cache.latest, installed)
|
|
134
|
+
? {
|
|
135
|
+
newer: true,
|
|
136
|
+
message: `${installed} installed; ${cache.latest} is the latest${checked}. Run npx humanish@latest to use it.`,
|
|
137
|
+
}
|
|
138
|
+
: { newer: false, message: `${installed} installed, the latest published${checked}` };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Ask the npm registry for humanish's `latest` dist-tag and record it in the cache. The detached
|
|
142
|
+
* worker runs this. The request is a GET with an Accept header and nothing else: no machine id,
|
|
143
|
+
* installed version, command or path. A failed or malformed answer leaves the cache as it was.
|
|
144
|
+
*/
|
|
145
|
+
export async function refreshUpdateCache(args) {
|
|
146
|
+
try {
|
|
147
|
+
const fetchFn = args.fetchFn ?? globalThis.fetch;
|
|
148
|
+
const response = await fetchFn(REGISTRY_DIST_TAGS, {
|
|
149
|
+
headers: { accept: "application/json" },
|
|
150
|
+
signal: AbortSignal.timeout(args.timeoutMs ?? 3_000),
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok)
|
|
153
|
+
return;
|
|
154
|
+
const body = await response.json();
|
|
155
|
+
const latest = body !== null && typeof body === "object"
|
|
156
|
+
? body.latest
|
|
157
|
+
: undefined;
|
|
158
|
+
if (typeof latest !== "string" || parseVersion(latest) === undefined)
|
|
159
|
+
return;
|
|
160
|
+
const now = (args.now ?? Date.now)();
|
|
161
|
+
writeUpdateCache(args.cachePath, {
|
|
162
|
+
...readUpdateCache(args.cachePath),
|
|
163
|
+
checkedAt: new Date(now).toISOString(),
|
|
164
|
+
latest,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Offline, slow or refused: the next check is a day away either way.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** What the worker's environment keeps: how this machine reaches the network, and no credentials. */
|
|
172
|
+
const WORKER_ENV_NAMES = [
|
|
173
|
+
"HTTPS_PROXY",
|
|
174
|
+
"HTTP_PROXY",
|
|
175
|
+
"NO_PROXY",
|
|
176
|
+
"https_proxy",
|
|
177
|
+
"http_proxy",
|
|
178
|
+
"no_proxy",
|
|
179
|
+
"NODE_USE_ENV_PROXY",
|
|
180
|
+
"NODE_EXTRA_CA_CERTS",
|
|
181
|
+
"SYSTEMROOT",
|
|
182
|
+
];
|
|
183
|
+
/**
|
|
184
|
+
* Start the worker that runs refreshUpdateCache, detached and unreferenced so this process exits
|
|
185
|
+
* without waiting for it. A source checkout has no built worker, and nothing starts.
|
|
186
|
+
*/
|
|
187
|
+
export function startRefreshWorker(cachePath) {
|
|
188
|
+
const worker = fileURLToPath(new URL("./update-check-worker.js", import.meta.url));
|
|
189
|
+
if (!existsSync(worker))
|
|
190
|
+
return;
|
|
191
|
+
const env = Object.fromEntries(WORKER_ENV_NAMES.flatMap((name) => {
|
|
192
|
+
const value = process.env[name];
|
|
193
|
+
return value === undefined ? [] : [[name, value]];
|
|
194
|
+
}));
|
|
195
|
+
const child = spawn(process.execPath, [worker, cachePath], {
|
|
196
|
+
detached: true,
|
|
197
|
+
stdio: "ignore",
|
|
198
|
+
windowsHide: true,
|
|
199
|
+
env,
|
|
200
|
+
});
|
|
201
|
+
child.on("error", () => { });
|
|
202
|
+
child.unref();
|
|
203
|
+
}
|
package/dist/feedback/draft.js
CHANGED
|
@@ -161,6 +161,7 @@ export async function buildAnalysisDraft(context, options) {
|
|
|
161
161
|
`Impact: ${finding.impact}. ${finding.affectedStreamIds.length} affected / ${finding.exposedStreamIds.length} observed exposed participants. ${finding.exposureReason}`,
|
|
162
162
|
`Recovery: ${finding.recovery}. Confidence: ${finding.confidence}.`,
|
|
163
163
|
...finding.observations.map((item) => `${item.basis}: ${item.claim}${item.limitation ? ` Limitation: ${item.limitation}` : ""}`),
|
|
164
|
+
...citedImpressions(bundle, evidence).map(({ streamId, kind, text }) => `Participant ${streamId} said at the end (${kind.replaceAll("_", " ")}): ${text}`),
|
|
164
165
|
`Next check: ${finding.nextStep}`,
|
|
165
166
|
correction
|
|
166
167
|
? `Human review: ${correction.status}. ${correction.reason}`
|
|
@@ -206,6 +207,23 @@ export async function buildAnalysisDraft(context, options) {
|
|
|
206
207
|
],
|
|
207
208
|
};
|
|
208
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* The participant impressions among a finding's cited evidence. Only an impression whose text the
|
|
212
|
+
* cited entry carries whole is quoted, so the draft repeats nothing the analysis share check did
|
|
213
|
+
* not read.
|
|
214
|
+
*/
|
|
215
|
+
function citedImpressions(bundle, evidence) {
|
|
216
|
+
return evidence.flatMap((item) => {
|
|
217
|
+
const impressions = bundle.streams.find((stream) => stream.id === item.streamId)?.actor
|
|
218
|
+
?.impressions;
|
|
219
|
+
const impression = impressions?.status === "collected"
|
|
220
|
+
? impressions.items.find((entry) => entry.messageId === item.eventId)
|
|
221
|
+
: undefined;
|
|
222
|
+
return impression === undefined || !item.text.includes(impression.text)
|
|
223
|
+
? []
|
|
224
|
+
: [{ streamId: item.streamId, kind: impression.kind, text: impression.text }];
|
|
225
|
+
});
|
|
226
|
+
}
|
|
209
227
|
export function isUsableFeedbackCandidate(candidate) {
|
|
210
228
|
if (!isRecord(candidate) ||
|
|
211
229
|
candidate.schema !== "humanish.feedback-candidate.v1" ||
|
|
@@ -245,6 +245,8 @@ export function missingKeyHint(name) {
|
|
|
245
245
|
return `run \`${cli("keys set openai")}\``;
|
|
246
246
|
case "ANTHROPIC_API_KEY":
|
|
247
247
|
return `run \`${cli("keys set anthropic")}\``;
|
|
248
|
+
case "AGENTMAIL_API_KEY":
|
|
249
|
+
return `run \`${cli("keys set agentmail")}\``;
|
|
248
250
|
default:
|
|
249
251
|
return `run \`${cli(`keys set ${name}`)}\``;
|
|
250
252
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type KeyResolutionDeps, type KeySourceProbe } from "./key-resolution.js";
|
|
2
|
+
interface KeyStatusRow extends KeySourceProbe {
|
|
3
|
+
/** What humanish uses the key for, in a few words. */
|
|
4
|
+
use: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function keyStatus(args: {
|
|
7
|
+
cwd: string;
|
|
8
|
+
env: NodeJS.ProcessEnv;
|
|
9
|
+
deps?: KeyResolutionDeps;
|
|
10
|
+
}): Promise<KeyStatusRow[]>;
|
|
11
|
+
/** Names in the user store that the status does not list, such as ANTHROPIC_API_KEY. */
|
|
12
|
+
export declare function otherStoredKeys(env: NodeJS.ProcessEnv, deps?: KeyResolutionDeps): string[];
|
|
13
|
+
export declare function formatKeyStatus(rows: readonly KeyStatusRow[], otherStored?: readonly string[]): string;
|
|
14
|
+
interface MissingKeysOutcome {
|
|
15
|
+
stored: string[];
|
|
16
|
+
/** Keys left unset because the person pressed Enter without a value. */
|
|
17
|
+
skipped: string[];
|
|
18
|
+
/** Keys whose value the store refused, with the reason. */
|
|
19
|
+
rejected?: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** The key whose prompt was cancelled. No later key was asked for. */
|
|
24
|
+
stoppedAt?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `humanish keys set` with no key named: ask for each missing key in turn and store each answer in
|
|
28
|
+
* the user store. An empty answer skips that key; a cancelled prompt (null) stops the walk and
|
|
29
|
+
* keeps what was already stored.
|
|
30
|
+
*/
|
|
31
|
+
export declare function askForMissingKeys(args: {
|
|
32
|
+
rows: readonly KeyStatusRow[];
|
|
33
|
+
env: NodeJS.ProcessEnv;
|
|
34
|
+
deps?: KeyResolutionDeps;
|
|
35
|
+
prompt: (label: string) => Promise<string | null>;
|
|
36
|
+
}): Promise<MissingKeysOutcome>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// `humanish keys`: which provider keys are set and where each comes from, through the same chain a
|
|
2
|
+
// live command resolves. Never a value.
|
|
3
|
+
import { cli } from "../cli/invocation.js";
|
|
4
|
+
import { listUserKeys, probeKeySources, setUserKey, } from "./key-resolution.js";
|
|
5
|
+
/**
|
|
6
|
+
* The keys a person sets for humanish, in the order the status lists them and `keys set` asks for
|
|
7
|
+
* them. ANTHROPIC_API_KEY is left out: a Claude Code participant runs on its own login and never
|
|
8
|
+
* receives it. CODEX_API_KEY is an alternative spelling of the OpenAI key for terminal studies.
|
|
9
|
+
*/
|
|
10
|
+
const STATUS_KEYS = [
|
|
11
|
+
{ name: "E2B_API_KEY", use: "hosted desktops" },
|
|
12
|
+
{ name: "OPENAI_API_KEY", use: "participant model and analysis" },
|
|
13
|
+
{ name: "GH_TOKEN", use: "private repository subjects" },
|
|
14
|
+
{ name: "AGENTMAIL_API_KEY", use: "email in studies" },
|
|
15
|
+
];
|
|
16
|
+
export async function keyStatus(args) {
|
|
17
|
+
const probes = await probeKeySources(STATUS_KEYS.map((key) => key.name), {
|
|
18
|
+
cwd: args.cwd,
|
|
19
|
+
env: args.env,
|
|
20
|
+
...(args.deps === undefined ? {} : { deps: args.deps }),
|
|
21
|
+
});
|
|
22
|
+
return probes.map((probe, index) => ({ ...probe, use: STATUS_KEYS[index].use }));
|
|
23
|
+
}
|
|
24
|
+
/** Names in the user store that the status does not list, such as ANTHROPIC_API_KEY. */
|
|
25
|
+
export function otherStoredKeys(env, deps = {}) {
|
|
26
|
+
const listed = new Set(STATUS_KEYS.map((key) => key.name));
|
|
27
|
+
return listUserKeys(env, deps).filter((name) => !listed.has(name));
|
|
28
|
+
}
|
|
29
|
+
export function formatKeyStatus(rows, otherStored = []) {
|
|
30
|
+
const lines = rows.map((row) => `${row.name} (${row.use}): ${row.source === null ? `missing; ${row.hint}` : `set, from ${row.source}`}`);
|
|
31
|
+
if (otherStored.length > 0)
|
|
32
|
+
lines.push(`Also in the user store: ${otherStored.join(", ")}. humanish does not use ${otherStored.length === 1 ? "it" : "them"} today.`);
|
|
33
|
+
const missing = rows.some((row) => row.source === null);
|
|
34
|
+
lines.push("", missing
|
|
35
|
+
? `Values are never printed. \`${cli("keys set")}\` asks for each missing key in turn.`
|
|
36
|
+
: "Values are never printed.");
|
|
37
|
+
return `${lines.join("\n")}\n`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `humanish keys set` with no key named: ask for each missing key in turn and store each answer in
|
|
41
|
+
* the user store. An empty answer skips that key; a cancelled prompt (null) stops the walk and
|
|
42
|
+
* keeps what was already stored.
|
|
43
|
+
*/
|
|
44
|
+
export async function askForMissingKeys(args) {
|
|
45
|
+
const outcome = { stored: [], skipped: [] };
|
|
46
|
+
for (const row of args.rows) {
|
|
47
|
+
if (row.source !== null)
|
|
48
|
+
continue;
|
|
49
|
+
const value = await args.prompt(`${row.name} for ${row.use}`);
|
|
50
|
+
if (value === null)
|
|
51
|
+
return { ...outcome, stoppedAt: row.name };
|
|
52
|
+
if (value === "") {
|
|
53
|
+
outcome.skipped.push(row.name);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
setUserKey(row.name, value, args.env, args.deps);
|
|
58
|
+
outcome.stored.push(row.name);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
(outcome.rejected ??= []).push({
|
|
62
|
+
name: row.name,
|
|
63
|
+
message: error instanceof Error ? error.message : String(error),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return outcome;
|
|
68
|
+
}
|