humanish 0.94.0 → 0.96.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 +10 -0
- package/dist/comms-agentmail.d.ts +25 -0
- package/dist/comms-agentmail.js +418 -0
- package/dist/comms-agentmail.js.map +1 -0
- package/dist/comms-connections.d.ts +52 -0
- package/dist/comms-connections.js +123 -0
- package/dist/comms-connections.js.map +1 -0
- package/dist/comms-lease-store.d.ts +90 -0
- package/dist/comms-lease-store.js +489 -0
- package/dist/comms-lease-store.js.map +1 -0
- package/dist/comms-receiving-evidence.d.ts +46 -0
- package/dist/comms-receiving-evidence.js +35 -0
- package/dist/comms-receiving-evidence.js.map +1 -0
- package/dist/comms-receiving-inbox.d.ts +16 -0
- package/dist/comms-receiving-inbox.js +410 -0
- package/dist/comms-receiving-inbox.js.map +1 -0
- package/dist/comms-receiving-runtime.d.ts +25 -0
- package/dist/comms-receiving-runtime.js +45 -0
- package/dist/comms-receiving-runtime.js.map +1 -0
- package/dist/comms-receiving-types.d.ts +98 -0
- package/dist/comms-receiving-types.js +2 -0
- package/dist/comms-receiving-types.js.map +1 -0
- package/dist/comms-receiving.d.ts +58 -0
- package/dist/comms-receiving.js +554 -0
- package/dist/comms-receiving.js.map +1 -0
- package/dist/comms-setup.d.ts +40 -0
- package/dist/comms-setup.js +121 -0
- package/dist/comms-setup.js.map +1 -0
- package/dist/concurrent-shared-world-lab.js +662 -624
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +4 -1
- package/dist/cua-actor-lab.js +98 -17
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/doctor-lab.d.ts +7 -0
- package/dist/doctor-lab.js +26 -7
- package/dist/doctor-lab.js.map +1 -1
- package/dist/e2b-terminal-lab.js +3 -0
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.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.d.ts +20 -3
- package/dist/lab-config.js +65 -3
- package/dist/lab-config.js.map +1 -1
- package/dist/lab-engine.js +8 -3
- package/dist/lab-engine.js.map +1 -1
- package/dist/lab-summary.d.ts +3 -2
- package/dist/lab-summary.js +17 -7
- package/dist/lab-summary.js.map +1 -1
- package/dist/observer-app.html +1 -1
- package/dist/observer-data.js +14 -1
- package/dist/observer-data.js.map +1 -1
- package/dist/oss-lab.d.ts +1 -1
- package/dist/oss-lab.js.map +1 -1
- package/dist/oss-meta-lab.d.ts +1 -1
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/program.d.ts +4 -0
- package/dist/program.js +231 -74
- package/dist/program.js.map +1 -1
- package/dist/run-narration-secrets.d.ts +5 -0
- package/dist/run-narration-secrets.js +68 -0
- package/dist/run-narration-secrets.js.map +1 -0
- package/dist/run.d.ts +7 -2
- package/dist/run.js +25 -3
- package/dist/run.js.map +1 -1
- package/dist/scripted-browser-lab.js +7 -0
- package/dist/scripted-browser-lab.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/shared-world-lab.js +4 -0
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/study-analysis-engine.js +30 -1
- package/dist/study-analysis-engine.js.map +1 -1
- package/dist/study-analysis-evidence.js +9 -1
- package/dist/study-analysis-evidence.js.map +1 -1
- package/dist/tui-app.js +135 -135
- package/dist/tui-contract.d.ts +26 -1
- package/dist/tui-contract.js.map +1 -1
- package/dist/tui-launch.d.ts +2 -0
- package/dist/tui-launch.js +9 -1
- package/dist/tui-launch.js.map +1 -1
- package/docs/architecture/comms-inbox.md +4 -0
- package/docs/architecture/real-email-receiving.md +135 -0
- package/docs/contracts/run-bundle.md +9 -1
- package/docs/contracts/schemas.md +50 -5
- package/docs/goals/current.md +5 -5
- package/docs/ramp/README.md +13 -3
- package/docs/release/0.95.0-connections-setup.md +38 -0
- package/docs/release/0.96.0-real-email-receiving.md +47 -0
- package/package.json +4 -2
- package/skills/humanish/SKILL.md +55 -14
package/dist/program.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type AutomaticAnalysisHooks } from "./automatic-analysis-completion.js";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { discoverProviderKeys } from "./key-resolution.js";
|
|
4
|
+
import { checkCommsConnection } from "./comms-setup.js";
|
|
5
|
+
import { promptSecret } from "./secret-prompt.js";
|
|
4
6
|
import { type TelemetryProperties } from "./telemetry.js";
|
|
5
7
|
import type { CuaActorLabResult } from "./cua-actor-lab.js";
|
|
6
8
|
import type { ObserverResult, ObserverServer } from "./observer.js";
|
|
@@ -39,6 +41,8 @@ export declare function createProgram(io?: Partial<CliIo> & {
|
|
|
39
41
|
* TTY, an old Node, or a missing bundle appear.
|
|
40
42
|
*/
|
|
41
43
|
export interface TuiRuntime {
|
|
44
|
+
checkComms: typeof checkCommsConnection;
|
|
45
|
+
promptSecret: typeof promptSecret;
|
|
42
46
|
stdin: NodeJS.ReadStream;
|
|
43
47
|
stdout: NodeJS.WriteStream;
|
|
44
48
|
nodeVersion: string;
|
package/dist/program.js
CHANGED
|
@@ -10,6 +10,11 @@ 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 { checkCommsConnection, configureCommsLab } from "./comms-setup.js";
|
|
15
|
+
import { inspectCommsRecovery, recoverCommsReceiving } from "./comms-receiving.js";
|
|
16
|
+
import { resolveReceivingConnection } from "./comms-receiving-runtime.js";
|
|
17
|
+
import { promptSecret } from "./secret-prompt.js";
|
|
13
18
|
import { redactText } from "./redaction.js";
|
|
14
19
|
import { draftFeedback, listFeedback, renderIssueMarkdown, renderIssueUrl, verifyFeedback } from "./feedback.js";
|
|
15
20
|
import { runInit } from "./init.js";
|
|
@@ -461,6 +466,8 @@ function registerDoctorCommand(parent, io) {
|
|
|
461
466
|
});
|
|
462
467
|
}
|
|
463
468
|
const defaultTuiRuntime = {
|
|
469
|
+
checkComms: checkCommsConnection,
|
|
470
|
+
promptSecret,
|
|
464
471
|
stdin: process.stdin,
|
|
465
472
|
stdout: process.stdout,
|
|
466
473
|
env: process.env,
|
|
@@ -560,10 +567,14 @@ function registerTuiCommand(parent, io) {
|
|
|
560
567
|
.description("Open the interactive terminal surface for browsing labs and runs (humans only).")
|
|
561
568
|
.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
569
|
.option("--cwd <path>", "Target project directory.", ".")
|
|
570
|
+
.option("--env-file <path>", "Load a local env file for this terminal session and its runs without printing values.")
|
|
563
571
|
.option("--force", "Open it anyway in a session that looks like an agent's.")
|
|
564
572
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
565
573
|
.action(async (options, command) => {
|
|
566
574
|
const { stdin, stdout } = tuiRuntime;
|
|
575
|
+
// Production uses process.env, including SDKs used by existing cleanup actions. Tests inject
|
|
576
|
+
// an isolated host context. Values stay behind the capability closures, never in view data.
|
|
577
|
+
const sessionEnv = tuiRuntime.env;
|
|
567
578
|
// An agent runner, even with a real terminal. `codex exec` allocates a PTY for the commands
|
|
568
579
|
// it runs, so the TTY check below passes and the surface used to open: a study watched an
|
|
569
580
|
// agent navigate the labs list and start a run it did not mean to start
|
|
@@ -623,43 +634,110 @@ function registerTuiCommand(parent, io) {
|
|
|
623
634
|
});
|
|
624
635
|
return;
|
|
625
636
|
}
|
|
637
|
+
const discoveredKeys = new Set();
|
|
638
|
+
if (!await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io, env: sessionEnv,
|
|
639
|
+
onDiscovered: names => names.forEach(name => discoveredKeys.add(name)) }))
|
|
640
|
+
return;
|
|
641
|
+
// Probe stored credentials afresh. Discovery fills must not become permanent env overrides
|
|
642
|
+
// when a person replaces a stored key during this terminal session.
|
|
643
|
+
const connectionEnv = () => {
|
|
644
|
+
const env = { ...sessionEnv };
|
|
645
|
+
for (const name of discoveredKeys)
|
|
646
|
+
delete env[name];
|
|
647
|
+
return env;
|
|
648
|
+
};
|
|
626
649
|
const observerSession = createTuiObserverSession(resolve(options.cwd));
|
|
627
|
-
let exitCode;
|
|
650
|
+
let exitCode = 0;
|
|
651
|
+
let connectionNotice;
|
|
652
|
+
let connectionCheck;
|
|
628
653
|
try {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
ok: true,
|
|
651
|
-
message: `set up humanish here — ${result.changes.filter((change) => change.action !== "skip").length} files written`
|
|
654
|
+
for (;;) {
|
|
655
|
+
const outcome = await loaded.startTui({
|
|
656
|
+
...(connectionNotice === undefined ? {} : { initialScreen: "connections", connectionNotice }),
|
|
657
|
+
cwd: resolve(options.cwd),
|
|
658
|
+
version: { cli: CLI_VERSION },
|
|
659
|
+
capabilities: {
|
|
660
|
+
comms: {
|
|
661
|
+
read: async () => ({ ...await readCommsSetup(resolve(options.cwd), connectionEnv()), ...(connectionCheck ? { authentication: connectionCheck } : {}) }),
|
|
662
|
+
save: () => saveCommsConnection(resolve(options.cwd)),
|
|
663
|
+
check: async () => { connectionCheck = await tuiRuntime.checkComms({ cwd: resolve(options.cwd), env: connectionEnv(), online: true }); return connectionCheck; },
|
|
664
|
+
labs: async () => (await listLabManifests(resolve(options.cwd))).labs.map(lab => ({ title: lab.title ?? lab.id, path: lab.path })),
|
|
665
|
+
configure: (lab, apply, planToken) => configureCommsLab({ cwd: resolve(options.cwd), lab, connection: "agentmail", apply, ...(planToken ? { planToken } : {}) }),
|
|
666
|
+
recovery: () => inspectCommsRecovery({ cwd: resolve(options.cwd) }),
|
|
667
|
+
recover: async (runId, connectionName) => {
|
|
668
|
+
try {
|
|
669
|
+
const { connection, adapter } = await resolveReceivingConnection(resolve(options.cwd), connectionName, connectionEnv());
|
|
670
|
+
return await recoverCommsReceiving({ cwd: resolve(options.cwd), runId, connectionName, apiKeyEnv: connection.apiKeyEnv, adapter });
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return { ok: false, message: "Could not recover email resources. Check the connection and retry." };
|
|
674
|
+
}
|
|
652
675
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
676
|
+
},
|
|
677
|
+
// One cache for the life of the surface: it refreshes on a cadence, and re-walking every
|
|
678
|
+
// run tree each tick is the cost this index exists to avoid.
|
|
679
|
+
readRunIndex: (target, readOptions) => readRunIndex(target, { ...readOptions, cache: runIndexCache }),
|
|
680
|
+
listLabs: listLabManifests,
|
|
681
|
+
startRun: launchOptions => launchRun({ ...launchOptions, env: sessionEnv }),
|
|
682
|
+
readLaunchLog: readLaunchLogTail,
|
|
683
|
+
readRunDetail,
|
|
684
|
+
readLabSummary: (target, lab, readOptions) => readLabSummary(target, lab, { ...readOptions, env: sessionEnv }),
|
|
685
|
+
readProjectState,
|
|
686
|
+
openObserver: (target, observerPath) => observerSession.open(target, observerPath),
|
|
687
|
+
reclaimRun: (target, runId) => reclaimRunSandboxes(target, runId),
|
|
688
|
+
stopRun,
|
|
689
|
+
initProject: async (target) => {
|
|
690
|
+
const result = await runInit({ cwd: target, yes: true });
|
|
691
|
+
return result.ok
|
|
692
|
+
? {
|
|
693
|
+
schema: TUI_ACTION_SCHEMA,
|
|
694
|
+
ok: true,
|
|
695
|
+
message: `set up humanish here — ${result.changes.filter((change) => change.action !== "skip").length} files written`
|
|
696
|
+
}
|
|
697
|
+
: {
|
|
698
|
+
schema: TUI_ACTION_SCHEMA,
|
|
699
|
+
ok: false,
|
|
700
|
+
message: result.error?.message ?? "humanish init could not set this directory up"
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
stdin,
|
|
705
|
+
stdout
|
|
706
|
+
});
|
|
707
|
+
if (typeof outcome === "number") {
|
|
708
|
+
exitCode = outcome;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
if (outcome.action !== "agentmail-key") {
|
|
712
|
+
exitCode = 1;
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
// startTui has unmounted: only the host reads the credential, then remounts the view.
|
|
716
|
+
const value = await tuiRuntime.promptSecret("AgentMail API key", stdin, stdout);
|
|
717
|
+
if (value === null) {
|
|
718
|
+
connectionNotice = "Key entry cancelled. Nothing was changed.";
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
setUserKey("AGENTMAIL_API_KEY", value, sessionEnv);
|
|
723
|
+
// Refresh only a value filled implicitly by discovery; explicit env/file wins.
|
|
724
|
+
if (discoveredKeys.has("AGENTMAIL_API_KEY"))
|
|
725
|
+
delete sessionEnv.AGENTMAIL_API_KEY;
|
|
726
|
+
const saved = await saveCommsConnection(resolve(options.cwd));
|
|
727
|
+
connectionNotice = saved.ok ? "Key stored. Project connection saved."
|
|
728
|
+
: `Key stored for your user. ${saved.message}`;
|
|
729
|
+
stdout.write("Checking AgentMail authentication…\n");
|
|
730
|
+
connectionCheck = await tuiRuntime.checkComms({ cwd: resolve(options.cwd), env: connectionEnv(), online: true });
|
|
731
|
+
connectionNotice = saved.ok
|
|
732
|
+
? connectionCheck.authenticated === true ? "Key stored. Authentication passed."
|
|
733
|
+
: connectionCheck.authenticated === false ? "Key stored. Authentication rejected; test it for details."
|
|
734
|
+
: "Key stored. Authentication unknown; test it to retry."
|
|
735
|
+
: `${connectionNotice} ${connectionCheck.message}`;
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
connectionNotice = "Could not store the key. Use a single non-empty line and check key-store permissions.";
|
|
739
|
+
}
|
|
740
|
+
}
|
|
663
741
|
}
|
|
664
742
|
finally {
|
|
665
743
|
await observerSession.close();
|
|
@@ -681,7 +759,7 @@ function formatKeysHuman(result) {
|
|
|
681
759
|
return `${lines.join("\n")}\n`;
|
|
682
760
|
}
|
|
683
761
|
/** Read one secret line: from a piped stdin when --stdin, else a hidden TTY prompt. */
|
|
684
|
-
async function readSecretValue(useStdin, promptLabel
|
|
762
|
+
async function readSecretValue(useStdin, promptLabel) {
|
|
685
763
|
if (useStdin || !process.stdin.isTTY) {
|
|
686
764
|
const chunks = [];
|
|
687
765
|
for await (const chunk of process.stdin)
|
|
@@ -689,38 +767,7 @@ async function readSecretValue(useStdin, promptLabel, io) {
|
|
|
689
767
|
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
690
768
|
return text.length > 0 ? text : null;
|
|
691
769
|
}
|
|
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
|
-
});
|
|
770
|
+
return promptSecret(promptLabel, process.stdin, process.stderr);
|
|
724
771
|
}
|
|
725
772
|
function registerKeysCommand(parent, io) {
|
|
726
773
|
const keys = parent
|
|
@@ -729,7 +776,7 @@ function registerKeysCommand(parent, io) {
|
|
|
729
776
|
.summary("Manage the user-level provider key store.");
|
|
730
777
|
keys
|
|
731
778
|
.command("set")
|
|
732
|
-
.argument("<vendor-or-name>", "A vendor alias (openai, e2b, anthropic, github) or a raw ENV_NAME.")
|
|
779
|
+
.argument("<vendor-or-name>", "A vendor alias (openai, e2b, anthropic, github, agentmail) or a raw ENV_NAME.")
|
|
733
780
|
.description("Store one provider key in the user store (0600), prompted with hidden input.")
|
|
734
781
|
.option("--stdin", "Read the value from stdin instead of prompting (for agents/pipes).")
|
|
735
782
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
@@ -739,13 +786,13 @@ function registerKeysCommand(parent, io) {
|
|
|
739
786
|
if (name === null) {
|
|
740
787
|
const result = {
|
|
741
788
|
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.`
|
|
789
|
+
message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors: openai, e2b, anthropic, github, agentmail.`
|
|
743
790
|
};
|
|
744
791
|
writeResult(command, io, result, formatKeysHuman);
|
|
745
792
|
io.setExitCode(2);
|
|
746
793
|
return;
|
|
747
794
|
}
|
|
748
|
-
const value = await readSecretValue(options.stdin === true, `Value for ${name}
|
|
795
|
+
const value = await readSecretValue(options.stdin === true, `Value for ${name}`);
|
|
749
796
|
if (value === null) {
|
|
750
797
|
const result = {
|
|
751
798
|
schema: KEYS_RESULT_SCHEMA, ok: false, action: "set", store: storePath, names: [name],
|
|
@@ -1140,8 +1187,113 @@ function registerRunsCommand(parent, io) {
|
|
|
1140
1187
|
function registerCommsCommands(parent, io) {
|
|
1141
1188
|
const comms = parent
|
|
1142
1189
|
.command("comms")
|
|
1143
|
-
.description("
|
|
1190
|
+
.description("Local email capture, real receiving connections, checks and cleanup recovery.")
|
|
1144
1191
|
.summary("Off-app comms surfaces.");
|
|
1192
|
+
comms.command("providers")
|
|
1193
|
+
.description("List installed communication provider capabilities. No network requests.")
|
|
1194
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1195
|
+
.action((_options, command) => {
|
|
1196
|
+
const result = { schema: "humanish.comms-providers.v1", ok: true, providers: COMMS_PROVIDERS };
|
|
1197
|
+
writeResult(command, io, result, () => COMMS_PROVIDERS.map(provider => `${provider.label}: ${provider.limitation}\nKey: ${provider.keyEnv}\nSetup: ${provider.setupUrl}\n`).join("\n"));
|
|
1198
|
+
});
|
|
1199
|
+
const connections = comms.command("connections").description("Manage project-local non-secret connection profiles. A lab explicitly selects its receiving connection.");
|
|
1200
|
+
connections.command("list")
|
|
1201
|
+
.description("Show saved connections and local credential status; does not authenticate with a provider.")
|
|
1202
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
1203
|
+
.option("--env-file <path>", "Load credentials for local status without printing values.")
|
|
1204
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1205
|
+
.action(async (options, command) => {
|
|
1206
|
+
if (!await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io }))
|
|
1207
|
+
return;
|
|
1208
|
+
const result = await readCommsSetup(resolve(options.cwd), process.env);
|
|
1209
|
+
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("")}`);
|
|
1210
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
1211
|
+
});
|
|
1212
|
+
connections.command("add")
|
|
1213
|
+
.argument("[name]", "Project connection name.", "agentmail")
|
|
1214
|
+
.description("Save an AgentMail connection profile. Does not write a key, alter a lab or contact the provider.")
|
|
1215
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
1216
|
+
.option("--provider <id>", "Installed provider id.", "agentmail")
|
|
1217
|
+
.option("--api-key-env <name>", "Environment variable NAME, never its value.", "AGENTMAIL_API_KEY")
|
|
1218
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1219
|
+
.action(async (name, options, command) => {
|
|
1220
|
+
const result = { schema: "humanish.comms-connection-result.v1", ...(options.provider === "agentmail"
|
|
1221
|
+
? await saveCommsConnection(resolve(options.cwd), name, options.apiKeyEnv)
|
|
1222
|
+
: { ok: false, message: "Only AgentMail connection setup is currently available." }) };
|
|
1223
|
+
writeResult(command, io, result, value => `${value.message}\n`);
|
|
1224
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
1225
|
+
});
|
|
1226
|
+
comms.command("check")
|
|
1227
|
+
.description("Check connection and credential presence; --online authenticates without creating inboxes.")
|
|
1228
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
1229
|
+
.option("--connection <name>", "Saved connection name.", "agentmail")
|
|
1230
|
+
.option("--lab <path>", "Check the connection selected by this exact lab.")
|
|
1231
|
+
.option("--online", "Make a read-only provider authentication request.")
|
|
1232
|
+
.option("--env-file <path>", "Load credentials without printing values.")
|
|
1233
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1234
|
+
.action(async (options, command) => {
|
|
1235
|
+
if (!await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io }))
|
|
1236
|
+
return;
|
|
1237
|
+
let connection = options.connection;
|
|
1238
|
+
if (options.lab) {
|
|
1239
|
+
const lab = await resolveLabManifest(options.cwd, options.lab);
|
|
1240
|
+
if (!lab.ok || lab.config.comms?.email?.kind !== "real") {
|
|
1241
|
+
const result = { ok: false, message: "This lab does not select a real email connection." };
|
|
1242
|
+
writeResult(command, io, result, value => `${value.message}\n`);
|
|
1243
|
+
io.setExitCode(2);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
connection = lab.config.comms.email.connection;
|
|
1247
|
+
}
|
|
1248
|
+
const result = await checkCommsConnection({ cwd: resolve(options.cwd), connection, env: process.env, online: options.online === true });
|
|
1249
|
+
writeResult(command, io, result, value => `${value.message}\n`);
|
|
1250
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
1251
|
+
});
|
|
1252
|
+
comms.command("configure")
|
|
1253
|
+
.description("Preview or save a local receiving-enabled copy of a supported lab. No provider requests.")
|
|
1254
|
+
.requiredOption("--lab <path>", "Exact source lab path or handle.")
|
|
1255
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
1256
|
+
.option("--connection <name>", "Saved connection name.", "agentmail")
|
|
1257
|
+
.option("--apply", "Save the local copy; original lab remains unchanged.")
|
|
1258
|
+
.option("--plan-token <digest>", "Require the source and destination to match a previous preview.")
|
|
1259
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1260
|
+
.action(async (options, command) => {
|
|
1261
|
+
const result = await configureCommsLab({ ...options, cwd: resolve(options.cwd) });
|
|
1262
|
+
writeResult(command, io, result, value => `${value.message}\n`);
|
|
1263
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
1264
|
+
});
|
|
1265
|
+
comms.command("recover")
|
|
1266
|
+
.description("Inspect interrupted email leases; --apply deletes only privately recorded resources owned by this project and account.")
|
|
1267
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
1268
|
+
.option("--run <id>", "One run to inspect or recover.")
|
|
1269
|
+
.option("--apply", "Recover the selected inactive run and verify mailbox deletion.")
|
|
1270
|
+
.option("--env-file <path>", "Load credentials without printing values.")
|
|
1271
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1272
|
+
.action(async (options, command) => {
|
|
1273
|
+
if (!await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io }))
|
|
1274
|
+
return;
|
|
1275
|
+
const cwd = resolve(options.cwd);
|
|
1276
|
+
try {
|
|
1277
|
+
const entries = (await inspectCommsRecovery({ cwd })).filter(entry => !options.run || entry.runId === options.run);
|
|
1278
|
+
if (!options.apply) {
|
|
1279
|
+
const result = { schema: "humanish.comms-recovery.v1", ok: true, entries };
|
|
1280
|
+
writeResult(command, io, result, value => value.entries.length ? value.entries.map(entry => `${entry.runId}: ${entry.unresolvedCount} unresolved; ${entry.activeOwner === null ? "unknown owner" : entry.activeOwner ? "active owner" : "inactive"}\n`).join("") : "No recoverable email leases in this project.\n");
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (!options.run || entries.length !== 1)
|
|
1284
|
+
throw new Error("selection");
|
|
1285
|
+
const entry = entries[0];
|
|
1286
|
+
const { connection, adapter } = await resolveReceivingConnection(cwd, entry.connectionName, process.env);
|
|
1287
|
+
const result = { schema: "humanish.comms-recovery-result.v1", ...await recoverCommsReceiving({ cwd, runId: options.run, connectionName: entry.connectionName, apiKeyEnv: connection.apiKeyEnv, adapter }) };
|
|
1288
|
+
writeResult(command, io, result, value => `${value.message}\n`);
|
|
1289
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
1290
|
+
}
|
|
1291
|
+
catch {
|
|
1292
|
+
const result = { schema: "humanish.comms-recovery-result.v1", ok: false, message: "Recovery could not complete. Select one recorded run with --run, check its connection, and retry. No unrecorded resources are eligible." };
|
|
1293
|
+
writeResult(command, io, result, value => `${value.message}\n`);
|
|
1294
|
+
io.setExitCode(2);
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1145
1297
|
comms
|
|
1146
1298
|
.command("catch")
|
|
1147
1299
|
.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 +3384,29 @@ async function renderAndMaybeFollowObserver(args) {
|
|
|
3232
3384
|
}
|
|
3233
3385
|
}
|
|
3234
3386
|
async function applyEnvFileOption(args) {
|
|
3387
|
+
const env = args.env ?? process.env;
|
|
3235
3388
|
if (args.envFile) {
|
|
3236
|
-
const
|
|
3389
|
+
const stagedEnv = { ...env };
|
|
3390
|
+
const result = await loadEnvFile(args.cwd, args.envFile, stagedEnv);
|
|
3237
3391
|
if (!result.ok) {
|
|
3238
3392
|
writeResult(args.command, args.io, result, formatEnvFileHuman);
|
|
3239
3393
|
args.io.setExitCode(2);
|
|
3240
3394
|
return false;
|
|
3241
3395
|
}
|
|
3396
|
+
for (const name of result.loaded)
|
|
3397
|
+
env[name] = stagedEnv[name];
|
|
3242
3398
|
}
|
|
3243
3399
|
// Provider-key discovery (#436): fill still-missing keys from the documented project
|
|
3244
3400
|
// overlay, the owning vendors' native stores, and the humanish user store — fill-only
|
|
3245
3401
|
// (an explicit --env-file or process env always wins), each fill announced by name and
|
|
3246
3402
|
// source on stderr, never by value. HUMANISH_STRICT_KEYS=1 restores env-only behavior.
|
|
3247
3403
|
try {
|
|
3248
|
-
await keyDiscoveryFn({
|
|
3404
|
+
const discovered = await keyDiscoveryFn({
|
|
3249
3405
|
cwd: args.cwd,
|
|
3250
|
-
env
|
|
3406
|
+
env,
|
|
3251
3407
|
announce: (line) => args.io.writeErr(`${line}\n`)
|
|
3252
3408
|
});
|
|
3409
|
+
args.onDiscovered?.(discovered.map(fill => fill.name));
|
|
3253
3410
|
}
|
|
3254
3411
|
catch {
|
|
3255
3412
|
// Discovery must never break a command; a rung that fails to read is a miss, not an error.
|