humanish 0.94.0 → 0.95.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 +8 -0
- package/dist/comms-connections.d.ts +50 -0
- package/dist/comms-connections.js +123 -0
- package/dist/comms-connections.js.map +1 -0
- package/dist/doctor-lab.d.ts +7 -0
- package/dist/doctor-lab.js +15 -7
- package/dist/doctor-lab.js.map +1 -1
- package/dist/key-resolution.d.ts +1 -1
- package/dist/key-resolution.js +6 -4
- package/dist/key-resolution.js.map +1 -1
- package/dist/lab-config.js +9 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/lab-summary.d.ts +2 -2
- package/dist/lab-summary.js +11 -7
- package/dist/lab-summary.js.map +1 -1
- package/dist/program.d.ts +2 -0
- package/dist/program.js +136 -75
- package/dist/program.js.map +1 -1
- package/dist/secret-prompt.d.ts +2 -0
- package/dist/secret-prompt.js +36 -0
- package/dist/secret-prompt.js.map +1 -0
- package/dist/tui-app.js +121 -121
- package/dist/tui-contract.d.ts +13 -1
- package/dist/tui-contract.js.map +1 -1
- package/docs/contracts/schemas.md +33 -5
- package/docs/goals/current.md +5 -5
- package/docs/ramp/README.md +9 -3
- package/docs/release/0.95.0-connections-setup.md +38 -0
- package/package.json +3 -2
- package/skills/humanish/SKILL.md +28 -7
package/dist/program.js
CHANGED
|
@@ -10,6 +10,8 @@ import { Command, Option } from "commander";
|
|
|
10
10
|
import { startCodexAppServerUi } from "./codex-app-server-ui.js";
|
|
11
11
|
import { loadEnvFile } from "./env-file.js";
|
|
12
12
|
import { discoverProviderKeys, listUserKeys, resolveKeyName, setUserKey, unsetUserKey, userKeyStorePath } from "./key-resolution.js";
|
|
13
|
+
import { COMMS_PROVIDERS, readCommsSetup, saveCommsConnection } from "./comms-connections.js";
|
|
14
|
+
import { promptSecret } from "./secret-prompt.js";
|
|
13
15
|
import { redactText } from "./redaction.js";
|
|
14
16
|
import { draftFeedback, listFeedback, renderIssueMarkdown, renderIssueUrl, verifyFeedback } from "./feedback.js";
|
|
15
17
|
import { runInit } from "./init.js";
|
|
@@ -461,6 +463,7 @@ function registerDoctorCommand(parent, io) {
|
|
|
461
463
|
});
|
|
462
464
|
}
|
|
463
465
|
const defaultTuiRuntime = {
|
|
466
|
+
promptSecret,
|
|
464
467
|
stdin: process.stdin,
|
|
465
468
|
stdout: process.stdout,
|
|
466
469
|
env: process.env,
|
|
@@ -560,10 +563,14 @@ function registerTuiCommand(parent, io) {
|
|
|
560
563
|
.description("Open the interactive terminal surface for browsing labs and runs (humans only).")
|
|
561
564
|
.summary("Human terminal for labs and runs; refuses detected agent sessions and non-TTY input/output. Agents: humanish lab list --json, humanish lab inspect <lab> --json, humanish runs --json.")
|
|
562
565
|
.option("--cwd <path>", "Target project directory.", ".")
|
|
566
|
+
.option("--env-file <path>", "Load a local env file for this terminal session and its runs without printing values.")
|
|
563
567
|
.option("--force", "Open it anyway in a session that looks like an agent's.")
|
|
564
568
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
565
569
|
.action(async (options, command) => {
|
|
566
570
|
const { stdin, stdout } = tuiRuntime;
|
|
571
|
+
// Production uses process.env, including SDKs used by existing cleanup actions. Tests inject
|
|
572
|
+
// an isolated host context. Values stay behind the capability closures, never in view data.
|
|
573
|
+
const sessionEnv = tuiRuntime.env;
|
|
567
574
|
// An agent runner, even with a real terminal. `codex exec` allocates a PTY for the commands
|
|
568
575
|
// it runs, so the TTY check below passes and the surface used to open: a study watched an
|
|
569
576
|
// agent navigate the labs list and start a run it did not mean to start
|
|
@@ -623,43 +630,89 @@ function registerTuiCommand(parent, io) {
|
|
|
623
630
|
});
|
|
624
631
|
return;
|
|
625
632
|
}
|
|
633
|
+
const discoveredKeys = new Set();
|
|
634
|
+
if (!await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io, env: sessionEnv,
|
|
635
|
+
onDiscovered: names => names.forEach(name => discoveredKeys.add(name)) }))
|
|
636
|
+
return;
|
|
637
|
+
// Probe stored credentials afresh. Discovery fills must not become permanent env overrides
|
|
638
|
+
// when a person replaces a stored key during this terminal session.
|
|
639
|
+
const connectionEnv = () => {
|
|
640
|
+
const env = { ...sessionEnv };
|
|
641
|
+
for (const name of discoveredKeys)
|
|
642
|
+
delete env[name];
|
|
643
|
+
return env;
|
|
644
|
+
};
|
|
626
645
|
const observerSession = createTuiObserverSession(resolve(options.cwd));
|
|
627
|
-
let exitCode;
|
|
646
|
+
let exitCode = 0;
|
|
647
|
+
let connectionNotice;
|
|
628
648
|
try {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
649
|
+
for (;;) {
|
|
650
|
+
const outcome = await loaded.startTui({
|
|
651
|
+
...(connectionNotice === undefined ? {} : { initialScreen: "connections", connectionNotice }),
|
|
652
|
+
cwd: resolve(options.cwd),
|
|
653
|
+
version: { cli: CLI_VERSION },
|
|
654
|
+
capabilities: {
|
|
655
|
+
comms: {
|
|
656
|
+
read: () => readCommsSetup(resolve(options.cwd), connectionEnv()),
|
|
657
|
+
save: () => saveCommsConnection(resolve(options.cwd))
|
|
658
|
+
},
|
|
659
|
+
// One cache for the life of the surface: it refreshes on a cadence, and re-walking every
|
|
660
|
+
// run tree each tick is the cost this index exists to avoid.
|
|
661
|
+
readRunIndex: (target, readOptions) => readRunIndex(target, { ...readOptions, cache: runIndexCache }),
|
|
662
|
+
listLabs: listLabManifests,
|
|
663
|
+
startRun: launchOptions => launchRun({ ...launchOptions, env: sessionEnv }),
|
|
664
|
+
readLaunchLog: readLaunchLogTail,
|
|
665
|
+
readRunDetail,
|
|
666
|
+
readLabSummary: (target, lab, readOptions) => readLabSummary(target, lab, { ...readOptions, env: sessionEnv }),
|
|
667
|
+
readProjectState,
|
|
668
|
+
openObserver: (target, observerPath) => observerSession.open(target, observerPath),
|
|
669
|
+
reclaimRun: (target, runId) => reclaimRunSandboxes(target, runId),
|
|
670
|
+
stopRun,
|
|
671
|
+
initProject: async (target) => {
|
|
672
|
+
const result = await runInit({ cwd: target, yes: true });
|
|
673
|
+
return result.ok
|
|
674
|
+
? {
|
|
675
|
+
schema: TUI_ACTION_SCHEMA,
|
|
676
|
+
ok: true,
|
|
677
|
+
message: `set up humanish here — ${result.changes.filter((change) => change.action !== "skip").length} files written`
|
|
678
|
+
}
|
|
679
|
+
: {
|
|
680
|
+
schema: TUI_ACTION_SCHEMA,
|
|
681
|
+
ok: false,
|
|
682
|
+
message: result.error?.message ?? "humanish init could not set this directory up"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
stdin,
|
|
687
|
+
stdout
|
|
688
|
+
});
|
|
689
|
+
if (typeof outcome === "number") {
|
|
690
|
+
exitCode = outcome;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
if (outcome.action !== "agentmail-key") {
|
|
694
|
+
exitCode = 1;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
// startTui has unmounted: only the host reads the credential, then remounts the view.
|
|
698
|
+
const value = await tuiRuntime.promptSecret("AgentMail API key", stdin, stdout);
|
|
699
|
+
if (value === null) {
|
|
700
|
+
connectionNotice = "Key entry cancelled. Nothing was changed.";
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
setUserKey("AGENTMAIL_API_KEY", value, sessionEnv);
|
|
705
|
+
// Refresh only a value filled implicitly by discovery; explicit env/file wins.
|
|
706
|
+
if (discoveredKeys.has("AGENTMAIL_API_KEY"))
|
|
707
|
+
delete sessionEnv.AGENTMAIL_API_KEY;
|
|
708
|
+
const saved = await saveCommsConnection(resolve(options.cwd));
|
|
709
|
+
connectionNotice = saved.ok ? "Key stored. Project connection saved."
|
|
710
|
+
: `Key stored for your user. ${saved.message}`;
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
connectionNotice = "Could not store the key. Use a single non-empty line and check key-store permissions.";
|
|
714
|
+
}
|
|
715
|
+
}
|
|
663
716
|
}
|
|
664
717
|
finally {
|
|
665
718
|
await observerSession.close();
|
|
@@ -681,7 +734,7 @@ function formatKeysHuman(result) {
|
|
|
681
734
|
return `${lines.join("\n")}\n`;
|
|
682
735
|
}
|
|
683
736
|
/** Read one secret line: from a piped stdin when --stdin, else a hidden TTY prompt. */
|
|
684
|
-
async function readSecretValue(useStdin, promptLabel
|
|
737
|
+
async function readSecretValue(useStdin, promptLabel) {
|
|
685
738
|
if (useStdin || !process.stdin.isTTY) {
|
|
686
739
|
const chunks = [];
|
|
687
740
|
for await (const chunk of process.stdin)
|
|
@@ -689,38 +742,7 @@ async function readSecretValue(useStdin, promptLabel, io) {
|
|
|
689
742
|
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
690
743
|
return text.length > 0 ? text : null;
|
|
691
744
|
}
|
|
692
|
-
|
|
693
|
-
return await new Promise((resolve) => {
|
|
694
|
-
const stdin = process.stdin;
|
|
695
|
-
let value = "";
|
|
696
|
-
stdin.setRawMode?.(true);
|
|
697
|
-
stdin.resume();
|
|
698
|
-
stdin.setEncoding("utf8");
|
|
699
|
-
const onData = (key) => {
|
|
700
|
-
if (key === "\u0003") {
|
|
701
|
-
cleanup();
|
|
702
|
-
resolve(null);
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
if (key === "\r" || key === "\n") {
|
|
706
|
-
cleanup();
|
|
707
|
-
io.writeErr("\n");
|
|
708
|
-
resolve(value.trim().length > 0 ? value.trim() : null);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (key === "\u007f" || key === "\b") {
|
|
712
|
-
value = value.slice(0, -1);
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
value += key;
|
|
716
|
-
};
|
|
717
|
-
const cleanup = () => {
|
|
718
|
-
stdin.off("data", onData);
|
|
719
|
-
stdin.setRawMode?.(false);
|
|
720
|
-
stdin.pause();
|
|
721
|
-
};
|
|
722
|
-
stdin.on("data", onData);
|
|
723
|
-
});
|
|
745
|
+
return promptSecret(promptLabel, process.stdin, process.stderr);
|
|
724
746
|
}
|
|
725
747
|
function registerKeysCommand(parent, io) {
|
|
726
748
|
const keys = parent
|
|
@@ -729,7 +751,7 @@ function registerKeysCommand(parent, io) {
|
|
|
729
751
|
.summary("Manage the user-level provider key store.");
|
|
730
752
|
keys
|
|
731
753
|
.command("set")
|
|
732
|
-
.argument("<vendor-or-name>", "A vendor alias (openai, e2b, anthropic, github) or a raw ENV_NAME.")
|
|
754
|
+
.argument("<vendor-or-name>", "A vendor alias (openai, e2b, anthropic, github, agentmail) or a raw ENV_NAME.")
|
|
733
755
|
.description("Store one provider key in the user store (0600), prompted with hidden input.")
|
|
734
756
|
.option("--stdin", "Read the value from stdin instead of prompting (for agents/pipes).")
|
|
735
757
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
@@ -739,13 +761,13 @@ function registerKeysCommand(parent, io) {
|
|
|
739
761
|
if (name === null) {
|
|
740
762
|
const result = {
|
|
741
763
|
schema: KEYS_RESULT_SCHEMA, ok: false, action: "set", store: storePath, names: [],
|
|
742
|
-
message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors: openai, e2b, anthropic, github.`
|
|
764
|
+
message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors: openai, e2b, anthropic, github, agentmail.`
|
|
743
765
|
};
|
|
744
766
|
writeResult(command, io, result, formatKeysHuman);
|
|
745
767
|
io.setExitCode(2);
|
|
746
768
|
return;
|
|
747
769
|
}
|
|
748
|
-
const value = await readSecretValue(options.stdin === true, `Value for ${name}
|
|
770
|
+
const value = await readSecretValue(options.stdin === true, `Value for ${name}`);
|
|
749
771
|
if (value === null) {
|
|
750
772
|
const result = {
|
|
751
773
|
schema: KEYS_RESULT_SCHEMA, ok: false, action: "set", store: storePath, names: [name],
|
|
@@ -1140,8 +1162,42 @@ function registerRunsCommand(parent, io) {
|
|
|
1140
1162
|
function registerCommsCommands(parent, io) {
|
|
1141
1163
|
const comms = parent
|
|
1142
1164
|
.command("comms")
|
|
1143
|
-
.description("
|
|
1165
|
+
.description("Email capture and communication connection setup.")
|
|
1144
1166
|
.summary("Off-app comms surfaces.");
|
|
1167
|
+
comms.command("providers")
|
|
1168
|
+
.description("List installed communication provider capabilities. No network requests.")
|
|
1169
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1170
|
+
.action((_options, command) => {
|
|
1171
|
+
const result = { schema: "humanish.comms-providers.v1", ok: true, providers: COMMS_PROVIDERS };
|
|
1172
|
+
writeResult(command, io, result, () => COMMS_PROVIDERS.map(provider => `${provider.label}: ${provider.limitation}\nKey: ${provider.keyEnv}\nSetup: ${provider.setupUrl}\n`).join("\n"));
|
|
1173
|
+
});
|
|
1174
|
+
const connections = comms.command("connections").description("Manage project-local non-secret connection profiles. Setup does not enable email receiving.");
|
|
1175
|
+
connections.command("list")
|
|
1176
|
+
.description("Show saved connections and local credential status; does not authenticate with a provider.")
|
|
1177
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
1178
|
+
.option("--env-file <path>", "Load credentials for local status without printing values.")
|
|
1179
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1180
|
+
.action(async (options, command) => {
|
|
1181
|
+
if (!await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io }))
|
|
1182
|
+
return;
|
|
1183
|
+
const result = await readCommsSetup(resolve(options.cwd), process.env);
|
|
1184
|
+
writeResult(command, io, result, value => `${value.message}\nAgentMail key: ${value.credential.present ? "present" : "missing"}\n${value.connections.map(connection => `${connection.name}: ${connection.provider} (${connection.apiKeyEnv})\n`).join("")}`);
|
|
1185
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
1186
|
+
});
|
|
1187
|
+
connections.command("add")
|
|
1188
|
+
.argument("[name]", "Project connection name.", "agentmail")
|
|
1189
|
+
.description("Save an AgentMail connection profile. Does not write a key, alter a lab or contact the provider.")
|
|
1190
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
1191
|
+
.option("--provider <id>", "Installed provider id.", "agentmail")
|
|
1192
|
+
.option("--api-key-env <name>", "Environment variable NAME, never its value.", "AGENTMAIL_API_KEY")
|
|
1193
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1194
|
+
.action(async (name, options, command) => {
|
|
1195
|
+
const result = { schema: "humanish.comms-connection-result.v1", ...(options.provider === "agentmail"
|
|
1196
|
+
? await saveCommsConnection(resolve(options.cwd), name, options.apiKeyEnv)
|
|
1197
|
+
: { ok: false, message: "Only AgentMail connection setup is currently available." }) };
|
|
1198
|
+
writeResult(command, io, result, value => `${value.message}\n`);
|
|
1199
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
1200
|
+
});
|
|
1145
1201
|
comms
|
|
1146
1202
|
.command("catch")
|
|
1147
1203
|
.description("Run the email catch on THIS host so humanish can study an app it does not provision (#328). Your app posts its email sends here; the persona opens /inbox; humanish drains GET /deliveries and writes digest-only evidence. Point your lab's comms.email.external.catchBaseUrl at this server.")
|
|
@@ -3232,24 +3288,29 @@ async function renderAndMaybeFollowObserver(args) {
|
|
|
3232
3288
|
}
|
|
3233
3289
|
}
|
|
3234
3290
|
async function applyEnvFileOption(args) {
|
|
3291
|
+
const env = args.env ?? process.env;
|
|
3235
3292
|
if (args.envFile) {
|
|
3236
|
-
const
|
|
3293
|
+
const stagedEnv = { ...env };
|
|
3294
|
+
const result = await loadEnvFile(args.cwd, args.envFile, stagedEnv);
|
|
3237
3295
|
if (!result.ok) {
|
|
3238
3296
|
writeResult(args.command, args.io, result, formatEnvFileHuman);
|
|
3239
3297
|
args.io.setExitCode(2);
|
|
3240
3298
|
return false;
|
|
3241
3299
|
}
|
|
3300
|
+
for (const name of result.loaded)
|
|
3301
|
+
env[name] = stagedEnv[name];
|
|
3242
3302
|
}
|
|
3243
3303
|
// Provider-key discovery (#436): fill still-missing keys from the documented project
|
|
3244
3304
|
// overlay, the owning vendors' native stores, and the humanish user store — fill-only
|
|
3245
3305
|
// (an explicit --env-file or process env always wins), each fill announced by name and
|
|
3246
3306
|
// source on stderr, never by value. HUMANISH_STRICT_KEYS=1 restores env-only behavior.
|
|
3247
3307
|
try {
|
|
3248
|
-
await keyDiscoveryFn({
|
|
3308
|
+
const discovered = await keyDiscoveryFn({
|
|
3249
3309
|
cwd: args.cwd,
|
|
3250
|
-
env
|
|
3310
|
+
env,
|
|
3251
3311
|
announce: (line) => args.io.writeErr(`${line}\n`)
|
|
3252
3312
|
});
|
|
3313
|
+
args.onDiscovered?.(discovered.map(fill => fill.name));
|
|
3253
3314
|
}
|
|
3254
3315
|
catch {
|
|
3255
3316
|
// Discovery must never break a command; a rung that fails to read is a miss, not an error.
|