@ouro.bot/cli 0.1.0-alpha.450 → 0.1.0-alpha.452

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.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.452",
6
+ "changes": [
7
+ "`ouro doctor` now checks the canonical bundle-local daemon log directory for each agent instead of the obsolete `~/.ouro-cli/logs` path.",
8
+ "Disk health output now names the owning agent for daemon log-size checks, so humans and agents can connect the warning directly to the right bundle and `ouro logs prune` target.",
9
+ "Doctor coverage now locks the bundle-local log path, missing-log-dir warning, and per-agent log-size thresholds."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.451",
14
+ "changes": [
15
+ "Agent Mail is now a first-class sense: agent identity, prompt truth, status/doctor surfaces, friend channel capabilities, and the base tool registry all understand `mail` alongside CLI, Teams, and BlueBubbles.",
16
+ "`ouro connect mail --agent <agent>` provisions a vault-coupled Mailroom identity with private mail keys stored in the agent vault, a non-secret bundle registry, a canonical `@ouro.bot` mailbox, and delegated source aliases such as HEY shadow imbox addresses.",
17
+ "The Mailroom substrate can accept SMTP, screen unknown recipients, import MBOX exports, store encrypted message/raw payloads in file or Azure Blob backends, and expose bounded read/audit tools for agents to read mail as mail."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.450",
6
22
  "changes": [
@@ -79,6 +79,9 @@ const provider_models_1 = require("../provider-models");
79
79
  const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
80
80
  const update_checker_1 = require("../versioning/update-checker");
81
81
  const sync_1 = require("../sync");
82
+ const core_1 = require("../../mailroom/core");
83
+ const file_store_1 = require("../../mailroom/file-store");
84
+ const mbox_import_1 = require("../../mailroom/mbox-import");
82
85
  const cli_parse_1 = require("./cli-parse");
83
86
  const cli_parse_2 = require("./cli-parse");
84
87
  const cli_help_1 = require("./cli-help");
@@ -323,6 +326,7 @@ function agentResolutionFailureMode(command) {
323
326
  case "vault.config.set":
324
327
  case "vault.config.status":
325
328
  case "connect":
329
+ case "mail.import-mbox":
326
330
  case "auth.run":
327
331
  case "auth.verify":
328
332
  case "auth.switch":
@@ -2030,6 +2034,8 @@ function enableAgentSense(agent, sense, deps) {
2030
2034
  ...senses,
2031
2035
  cli: senses.cli ?? { enabled: true },
2032
2036
  teams: senses.teams ?? { enabled: false },
2037
+ bluebubbles: senses.bluebubbles ?? { enabled: false },
2038
+ mail: senses.mail ?? { enabled: false },
2033
2039
  [sense]: { ...existing, enabled: true },
2034
2040
  };
2035
2041
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
@@ -2044,6 +2050,7 @@ function readConnectBaySenseFlags(agent, deps) {
2044
2050
  return {
2045
2051
  teamsEnabled: parsed.senses?.teams?.enabled === true,
2046
2052
  blueBubblesEnabled: parsed.senses?.bluebubbles?.enabled === true,
2053
+ mailEnabled: parsed.senses?.mail?.enabled === true,
2047
2054
  };
2048
2055
  }
2049
2056
  async function buildConnectMenu(agent, deps, onProgress) {
@@ -2073,7 +2080,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
2073
2080
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
2074
2081
  onProgress?.("loading this machine's settings");
2075
2082
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
2076
- const { teamsEnabled, blueBubblesEnabled } = readConnectBaySenseFlags(agent, deps);
2083
+ const { teamsEnabled, blueBubblesEnabled, mailEnabled } = readConnectBaySenseFlags(agent, deps);
2077
2084
  const perplexityApiKey = runtimeConfig.ok
2078
2085
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
2079
2086
  : null;
@@ -2150,6 +2157,18 @@ async function buildConnectMenu(agent, deps, onProgress) {
2150
2157
  ? "attached"
2151
2158
  : "not attached"
2152
2159
  : machineRuntimeReadStatus(machineRuntime);
2160
+ const mailroomConfig = runtimeConfig.ok && runtimeConfig.config.mailroom && typeof runtimeConfig.config.mailroom === "object" && !Array.isArray(runtimeConfig.config.mailroom)
2161
+ ? runtimeConfig.config.mailroom
2162
+ : {};
2163
+ const mailPrivateKeys = mailroomConfig.privateKeys;
2164
+ const hasMailPrivateKeys = !!mailPrivateKeys && typeof mailPrivateKeys === "object" && !Array.isArray(mailPrivateKeys) && Object.values(mailPrivateKeys).some((value) => typeof value === "string" && value.trim().length > 0);
2165
+ const mailStatus = runtimeConfig.ok
2166
+ ? hasRuntimeConfigValue(runtimeConfig.config, "mailroom.mailboxAddress")
2167
+ && hasMailPrivateKeys
2168
+ && mailEnabled
2169
+ ? "ready"
2170
+ : "missing"
2171
+ : runtimeConfigReadStatus(runtimeConfig);
2153
2172
  const entries = [
2154
2173
  {
2155
2174
  option: "1",
@@ -2215,6 +2234,19 @@ async function buildConnectMenu(agent, deps, onProgress) {
2215
2234
  status: blueBubblesStatus,
2216
2235
  }) ? `ouro connect bluebubbles --agent ${agent}` : undefined,
2217
2236
  },
2237
+ {
2238
+ option: "6",
2239
+ name: "Agent Mail",
2240
+ section: "Portable",
2241
+ status: mailStatus,
2242
+ description: "Vault-coupled Mailroom mailbox and delegated source aliases.",
2243
+ nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
2244
+ option: "6",
2245
+ name: "Agent Mail",
2246
+ section: "Portable",
2247
+ status: mailStatus,
2248
+ }) ? `ouro connect mail --agent ${agent}` : undefined,
2249
+ },
2218
2250
  ];
2219
2251
  const isTTY = connectMenuIsTTY(deps);
2220
2252
  return (0, connect_bay_1.renderConnectBay)(entries, {
@@ -2649,6 +2681,185 @@ async function executeConnectBlueBubbles(agent, deps) {
2649
2681
  ],
2650
2682
  });
2651
2683
  }
2684
+ async function executeConnectMail(agent, deps) {
2685
+ if (agent === "SerpentGuide") {
2686
+ throw new Error("SerpentGuide has no persistent runtime credentials. Connect mail on the hatchling agent instead.");
2687
+ }
2688
+ const promptInput = requirePromptInput(deps, "Agent Mail setup");
2689
+ writeConnectorIntro(deps, {
2690
+ title: "Connect Agent Mail",
2691
+ subtitle: `${agent} gets a private Mailroom identity and delegated source lane.`,
2692
+ summary: "Provision a vault-coupled mailbox, source alias, encrypted local store, and Mail sense.",
2693
+ sections: [
2694
+ {
2695
+ title: "Unlocks",
2696
+ lines: [
2697
+ "Private agent mail at the agent's canonical @ouro.bot address.",
2698
+ "HEY/source aliases that stay labeled as delegated human mail.",
2699
+ "Bounded read tools with access logging.",
2700
+ ],
2701
+ },
2702
+ {
2703
+ title: "What you need",
2704
+ lines: [
2705
+ "Optional owner email for the first delegated source alias.",
2706
+ "No mail password is printed or pasted; Ouro generates mail keys and stores private keys in the agent vault.",
2707
+ ],
2708
+ },
2709
+ {
2710
+ title: "Where it lives",
2711
+ lines: [
2712
+ `${agent}'s vault runtime/config item for private keys and Mailroom coordinates.`,
2713
+ `${agent}'s bundle state for non-secret registry and local encrypted mail cache.`,
2714
+ ],
2715
+ },
2716
+ ],
2717
+ fallbackLines: [
2718
+ `Connect Agent Mail for ${agent}`,
2719
+ "Ouro generates mail keys and stores private keys in the agent vault.",
2720
+ "The registry and encrypted local store live under the agent bundle state.",
2721
+ ],
2722
+ });
2723
+ const ownerEmail = (await promptInput("Delegated owner email for first source alias [blank to skip]: ")).trim();
2724
+ const source = ownerEmail
2725
+ ? ((await promptInput("Delegated source label [hey]: ")).trim() || "hey")
2726
+ : "";
2727
+ const agentRoot = providerCliAgentRoot({ agent }, deps);
2728
+ const mailStateDir = path.join(agentRoot, "state", "mailroom");
2729
+ const registryPath = path.join(mailStateDir, "registry.json");
2730
+ const storePath = mailStateDir;
2731
+ const progress = createHumanCommandProgress(deps, "connect mail");
2732
+ let stored;
2733
+ let mailboxAddress = `${agent.toLowerCase()}@ouro.bot`;
2734
+ let sourceAlias = null;
2735
+ try {
2736
+ progress.startPhase("provisioning Mailroom identity");
2737
+ const provisioned = (0, core_1.provisionMailboxRegistry)({
2738
+ agentId: agent,
2739
+ ownerEmail: ownerEmail || undefined,
2740
+ source: source || undefined,
2741
+ });
2742
+ mailboxAddress = provisioned.registry.mailboxes[0].canonicalAddress;
2743
+ sourceAlias = provisioned.registry.sourceGrants[0]?.aliasAddress ?? null;
2744
+ fs.mkdirSync(mailStateDir, { recursive: true });
2745
+ fs.writeFileSync(registryPath, `${JSON.stringify(provisioned.registry, null, 2)}\n`, "utf-8");
2746
+ progress.updateDetail("checking existing runtime config");
2747
+ const current = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
2748
+ if (!current.ok && current.reason !== "missing") {
2749
+ progress.end();
2750
+ throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
2751
+ }
2752
+ const nextConfig = {
2753
+ ...(current.ok ? current.config : {}),
2754
+ mailroom: {
2755
+ mailboxAddress,
2756
+ registryPath,
2757
+ storePath,
2758
+ privateKeys: provisioned.keys,
2759
+ },
2760
+ };
2761
+ progress.updateDetail("storing private mail keys in vault");
2762
+ stored = await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(agent, nextConfig, providerCliNow(deps));
2763
+ progress.updateDetail("enabling Mail sense in agent.json");
2764
+ enableAgentSense(agent, "mail", deps);
2765
+ progress.completePhase("provisioning Mailroom identity", "mail ready");
2766
+ progress.end();
2767
+ }
2768
+ catch (error) {
2769
+ progress.end();
2770
+ throw error;
2771
+ }
2772
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
2773
+ return writeCapabilityOutcome(deps, {
2774
+ subtitle: `${agent}'s Mail sense is configured.`,
2775
+ summary: `Agent Mail is ready for ${agent}.`,
2776
+ whatChanged: [
2777
+ `Mailbox: ${mailboxAddress}`,
2778
+ ...(sourceAlias ? [`Delegated alias: ${sourceAlias}`] : []),
2779
+ `Registry: ${registryPath}`,
2780
+ `Encrypted store: ${storePath}`,
2781
+ `Stored: ${stored.itemPath}`,
2782
+ "agent.json: senses.mail.enabled = true",
2783
+ "private mail keys were not printed",
2784
+ ...(syncSummary ? [syncSummary] : []),
2785
+ ],
2786
+ nextMoves: [
2787
+ "Run ouro up so the daemon picks up the Mail sense.",
2788
+ sourceAlias
2789
+ ? `Use ${sourceAlias} for HEY forwarding or Extensions after the SMTP ingress proof is accepted.`
2790
+ : "Add delegated source aliases when you are ready to mirror human mail.",
2791
+ ],
2792
+ fallbackLines: [
2793
+ `Agent Mail connected for ${agent}`,
2794
+ `mailbox: ${mailboxAddress}`,
2795
+ ...(sourceAlias ? [`delegated alias: ${sourceAlias}`] : []),
2796
+ `registry: ${registryPath}`,
2797
+ `encrypted store: ${storePath}`,
2798
+ `stored: ${stored.itemPath}`,
2799
+ "agent.json: senses.mail.enabled = true",
2800
+ "private mail keys were not printed",
2801
+ "",
2802
+ "Next: run `ouro up` so the daemon picks up the Mail sense.",
2803
+ ...(syncSummary ? [syncSummary] : []),
2804
+ ],
2805
+ });
2806
+ }
2807
+ function stringField(record, key) {
2808
+ const value = record[key];
2809
+ return typeof value === "string" ? value.trim() : "";
2810
+ }
2811
+ async function executeMailImportMbox(command, deps) {
2812
+ const progress = createHumanCommandProgress(deps, "mail import");
2813
+ try {
2814
+ progress.startPhase("reading Mailroom config");
2815
+ const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
2816
+ if (!runtime.ok) {
2817
+ progress.end();
2818
+ throw new Error(`cannot read Mailroom config from ${runtime.itemPath}: ${runtime.error}`);
2819
+ }
2820
+ const mailroom = runtime.config.mailroom;
2821
+ if (!mailroom || typeof mailroom !== "object" || Array.isArray(mailroom)) {
2822
+ progress.end();
2823
+ throw new Error(`missing mailroom config for ${command.agent}; run 'ouro connect mail --agent ${command.agent}' first`);
2824
+ }
2825
+ const registryPath = stringField(mailroom, "registryPath");
2826
+ const storePath = stringField(mailroom, "storePath");
2827
+ if (!registryPath || !storePath) {
2828
+ progress.end();
2829
+ throw new Error(`mailroom config for ${command.agent} is missing registryPath/storePath; run 'ouro connect mail --agent ${command.agent}' again`);
2830
+ }
2831
+ progress.updateDetail("reading registry and MBOX");
2832
+ const registry = JSON.parse(fs.readFileSync(registryPath, "utf-8"));
2833
+ const filePath = path.resolve(command.filePath);
2834
+ const rawMbox = fs.readFileSync(filePath);
2835
+ progress.updateDetail("importing delegated mail");
2836
+ const result = await (0, mbox_import_1.importMboxToStore)({
2837
+ registry,
2838
+ store: new file_store_1.FileMailroomStore({ rootDir: storePath }),
2839
+ agentId: command.agent,
2840
+ rawMbox,
2841
+ ownerEmail: command.ownerEmail,
2842
+ source: command.source,
2843
+ });
2844
+ progress.completePhase("reading Mailroom config", "imported");
2845
+ progress.end();
2846
+ const message = [
2847
+ `Imported MBOX for ${command.agent}`,
2848
+ `file: ${filePath}`,
2849
+ `grant: ${result.sourceGrant.grantId} (${result.sourceGrant.source}; ${result.sourceGrant.ownerEmail})`,
2850
+ `scanned: ${result.scanned}`,
2851
+ `imported: ${result.imported}`,
2852
+ `duplicates: ${result.duplicates}`,
2853
+ "body reads remain explicit through mail_recent/mail_search/mail_thread and are access-logged.",
2854
+ ].join("\n");
2855
+ deps.writeStdout(message);
2856
+ return message;
2857
+ }
2858
+ catch (error) {
2859
+ progress.end();
2860
+ throw error;
2861
+ }
2862
+ }
2652
2863
  async function executeConnectProviders(agent, deps) {
2653
2864
  const promptInput = deps.promptInput;
2654
2865
  if (!promptInput) {
@@ -2690,6 +2901,8 @@ function connectMenuTarget(answer) {
2690
2901
  return "teams";
2691
2902
  if (normalized === "5" || normalized === "bluebubbles" || normalized === "imessage" || normalized === "messages")
2692
2903
  return "bluebubbles";
2904
+ if (normalized === "6" || normalized === "mail" || normalized === "email" || normalized === "mailroom")
2905
+ return "mail";
2693
2906
  return "cancel";
2694
2907
  }
2695
2908
  async function executeConnect(command, deps) {
@@ -2703,6 +2916,8 @@ async function executeConnect(command, deps) {
2703
2916
  return executeConnectTeams(command.agent, deps);
2704
2917
  if (command.target === "bluebubbles")
2705
2918
  return executeConnectBlueBubbles(command.agent, deps);
2919
+ if (command.target === "mail")
2920
+ return executeConnectMail(command.agent, deps);
2706
2921
  const progress = createHumanCommandProgress(deps, "connect");
2707
2922
  let menu;
2708
2923
  try {
@@ -2721,6 +2936,7 @@ async function executeConnect(command, deps) {
2721
2936
  `Run: ouro connect embeddings --agent ${command.agent}`,
2722
2937
  `Run: ouro connect teams --agent ${command.agent}`,
2723
2938
  `Run: ouro connect bluebubbles --agent ${command.agent}`,
2939
+ `Run: ouro connect mail --agent ${command.agent}`,
2724
2940
  ].join("\n");
2725
2941
  deps.writeStdout(message);
2726
2942
  return message;
@@ -2736,6 +2952,8 @@ async function executeConnect(command, deps) {
2736
2952
  return executeConnectTeams(command.agent, deps);
2737
2953
  if (answer === "bluebubbles")
2738
2954
  return executeConnectBlueBubbles(command.agent, deps);
2955
+ if (answer === "mail")
2956
+ return executeConnectMail(command.agent, deps);
2739
2957
  const message = "connect cancelled.";
2740
2958
  deps.writeStdout(message);
2741
2959
  return message;
@@ -4056,6 +4274,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4056
4274
  if (command.kind === "connect") {
4057
4275
  return executeConnect(command, deps);
4058
4276
  }
4277
+ if (command.kind === "mail.import-mbox") {
4278
+ return executeMailImportMbox(command, deps);
4279
+ }
4059
4280
  if (command.kind === "daemon.up") {
4060
4281
  // ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
4061
4282
  /* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
@@ -166,9 +166,16 @@ exports.COMMAND_REGISTRY = {
166
166
  connect: {
167
167
  category: "Auth",
168
168
  description: "Set up providers, portable integrations, and local senses from one guided screen",
169
- usage: "ouro connect [providers|perplexity|embeddings|teams|bluebubbles] [--agent <name>]",
169
+ usage: "ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>]",
170
170
  example: "ouro connect",
171
- subcommands: ["providers", "perplexity", "embeddings", "teams", "bluebubbles"],
171
+ subcommands: ["providers", "perplexity", "embeddings", "teams", "bluebubbles", "mail"],
172
+ },
173
+ mail: {
174
+ category: "Auth",
175
+ description: "Import delegated mail into the agent Mailroom substrate",
176
+ usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
177
+ example: "ouro mail import-mbox --file ~/Downloads/hey.mbox --owner-email ari@mendelow.me --source hey --agent slugger",
178
+ subcommands: ["import-mbox"],
172
179
  },
173
180
  use: {
174
181
  category: "Auth",
@@ -297,6 +304,16 @@ const SUBCOMMAND_HELP = {
297
304
  usage: "ouro connect bluebubbles [--agent <name>]",
298
305
  example: "ouro connect bluebubbles",
299
306
  },
307
+ "connect mail": {
308
+ description: "Provision portable Agent Mail / Mailroom access and enable the Mail sense",
309
+ usage: "ouro connect mail [--agent <name>]",
310
+ example: "ouro connect mail",
311
+ },
312
+ "mail import-mbox": {
313
+ description: "Import a HEY or other MBOX export into an existing delegated Mailroom source grant",
314
+ usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
315
+ example: "ouro mail import-mbox --file ~/Downloads/hey.mbox --owner-email ari@mendelow.me --source hey --agent slugger",
316
+ },
300
317
  "provider refresh": {
301
318
  description: "Reload this agent's provider credentials from its vault into daemon memory",
302
319
  usage: "ouro provider refresh [--agent <name>]",
@@ -83,7 +83,8 @@ function usage() {
83
83
  " ouro config model [--agent <name>] <model-name>",
84
84
  " ouro config models [--agent <name>]",
85
85
  " ouro auth [--agent <name>] [--provider <provider>]",
86
- " ouro connect [providers|perplexity|embeddings|teams|bluebubbles] [--agent <name>]",
86
+ " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>]",
87
+ " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
87
88
  " ouro auth verify [--agent <name>] [--provider <provider>]",
88
89
  " ouro auth switch [--agent <name>] --provider <provider>",
89
90
  " ouro vault create [--agent <name>] --email <email> [--server <url>] [--store <store>]",
@@ -600,15 +601,53 @@ function normalizeConnectTarget(value) {
600
601
  return "teams";
601
602
  if (value === "bluebubbles" || value === "imessage" || value === "messages")
602
603
  return "bluebubbles";
603
- throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles] [--agent <name>]");
604
+ if (value === "mail" || value === "email" || value === "mailroom")
605
+ return "mail";
606
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>]");
604
607
  }
605
608
  function parseConnectCommand(args) {
606
609
  const { agent, rest } = extractAgentFlag(args);
607
610
  if (rest.length > 1)
608
- throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles] [--agent <name>]");
611
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>]");
609
612
  const target = normalizeConnectTarget(rest[0]);
610
613
  return { kind: "connect", ...(agent ? { agent } : {}), ...(target ? { target } : {}) };
611
614
  }
615
+ function parseMailCommand(args) {
616
+ const [sub, ...subArgs] = args;
617
+ if (sub !== "import-mbox") {
618
+ throw new Error("Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]");
619
+ }
620
+ const { agent, rest } = extractAgentFlag(subArgs);
621
+ let filePath;
622
+ let ownerEmail;
623
+ let source;
624
+ for (let i = 0; i < rest.length; i += 1) {
625
+ const token = rest[i];
626
+ if (token === "--file" && rest[i + 1]) {
627
+ filePath = rest[++i];
628
+ continue;
629
+ }
630
+ if (token === "--owner-email" && rest[i + 1]) {
631
+ ownerEmail = rest[++i];
632
+ continue;
633
+ }
634
+ if (token === "--source" && rest[i + 1]) {
635
+ source = rest[++i];
636
+ continue;
637
+ }
638
+ throw new Error("Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]");
639
+ }
640
+ if (!filePath) {
641
+ throw new Error("Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]");
642
+ }
643
+ return {
644
+ kind: "mail.import-mbox",
645
+ ...(agent ? { agent } : {}),
646
+ filePath,
647
+ ...(ownerEmail ? { ownerEmail } : {}),
648
+ ...(source ? { source } : {}),
649
+ };
650
+ }
612
651
  function parseProviderUseCommand(args) {
613
652
  const { agent, rest: afterAgent } = extractAgentFlag(args);
614
653
  const { facing, rest: afterFacing } = extractFacingFlag(afterAgent);
@@ -1078,6 +1117,8 @@ function parseOuroCommand(args) {
1078
1117
  }
1079
1118
  if (head === "provider")
1080
1119
  return parseProviderCommand(args.slice(1));
1120
+ if (head === "mail")
1121
+ return parseMailCommand(args.slice(1));
1081
1122
  if (head === "logs") {
1082
1123
  if (second === "prune")
1083
1124
  return { kind: "daemon.logs.prune" };
@@ -181,7 +181,7 @@ async function checkSenses(deps) {
181
181
  continue;
182
182
  }
183
183
  const senses = config.senses;
184
- const senseNames = ["cli", "teams", "bluebubbles"];
184
+ const senseNames = ["cli", "teams", "bluebubbles", "mail"];
185
185
  for (const sense of senseNames) {
186
186
  if (!(sense in senses))
187
187
  continue;
@@ -329,12 +329,7 @@ function checkSecurity(deps) {
329
329
  }
330
330
  function checkDisk(deps) {
331
331
  const checks = [];
332
- // Check daemon logs directory
333
- const logsDir = `${deps.homedir}/.ouro-cli/logs`;
334
- if (!deps.existsSync(logsDir)) {
335
- checks.push({ label: "daemon logs dir", status: "warn", detail: `${logsDir} not found` });
336
- }
337
- else {
332
+ const addLogSizeCheck = (labelPrefix, logsDir) => {
338
333
  let totalSize = 0;
339
334
  try {
340
335
  const files = deps.readdirSync(logsDir);
@@ -353,13 +348,26 @@ function checkDisk(deps) {
353
348
  }
354
349
  const sizeMB = totalSize / (1024 * 1024);
355
350
  if (sizeMB > 500) {
356
- checks.push({ label: "daemon log size", status: "fail", detail: `${sizeMB.toFixed(1)}MB — exceeds 500MB limit` });
351
+ checks.push({ label: `${labelPrefix} daemon log size`, status: "fail", detail: `${sizeMB.toFixed(1)}MB — exceeds 500MB limit` });
357
352
  }
358
353
  else if (sizeMB > 100) {
359
- checks.push({ label: "daemon log size", status: "warn", detail: `${sizeMB.toFixed(1)}MB — consider pruning with \`ouro logs prune\`` });
354
+ checks.push({ label: `${labelPrefix} daemon log size`, status: "warn", detail: `${sizeMB.toFixed(1)}MB — consider pruning with \`ouro logs prune\`` });
355
+ }
356
+ else {
357
+ checks.push({ label: `${labelPrefix} daemon log size`, status: "pass", detail: `${sizeMB.toFixed(1)}MB` });
358
+ }
359
+ };
360
+ const agents = discoverAgents(deps);
361
+ if (agents.length === 0) {
362
+ checks.push({ label: "daemon logs dir", status: "warn", detail: "no agent bundles found for bundle-local logs" });
363
+ }
364
+ for (const agentDir of agents) {
365
+ const logsDir = `${deps.bundlesRoot}/${agentDir}/state/daemon/logs`;
366
+ if (!deps.existsSync(logsDir)) {
367
+ checks.push({ label: `${agentDir} daemon logs dir`, status: "warn", detail: `${logsDir} not found` });
360
368
  }
361
369
  else {
362
- checks.push({ label: "daemon log size", status: "pass", detail: `${sizeMB.toFixed(1)}MB` });
370
+ addLogSizeCheck(agentDir, logsDir);
363
371
  }
364
372
  }
365
373
  // Check AgentBundles root
@@ -52,6 +52,7 @@ function defaultSenses() {
52
52
  cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
53
53
  teams: { ...identity_1.DEFAULT_AGENT_SENSES.teams },
54
54
  bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
55
+ mail: { ...identity_1.DEFAULT_AGENT_SENSES.mail },
55
56
  };
56
57
  }
57
58
  function readAgentSenses(agentJsonPath) {
@@ -77,7 +78,7 @@ function readAgentSenses(agentJsonPath) {
77
78
  if (!rawSenses || typeof rawSenses !== "object" || Array.isArray(rawSenses)) {
78
79
  return defaults;
79
80
  }
80
- for (const sense of ["cli", "teams", "bluebubbles"]) {
81
+ for (const sense of ["cli", "teams", "bluebubbles", "mail"]) {
81
82
  const rawSense = rawSenses[sense];
82
83
  if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
83
84
  continue;
@@ -117,6 +118,7 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
117
118
  cli: { configured: true, detail: "local interactive terminal" },
118
119
  teams: { configured: false, detail: "not enabled in agent.json" },
119
120
  bluebubbles: { configured: false, detail: "not enabled in agent.json" },
121
+ mail: { configured: false, detail: "not enabled in agent.json" },
120
122
  };
121
123
  const payload = runtimeConfig.ok ? runtimeConfig.config : {};
122
124
  const unavailableDetail = runtimeConfigUnavailableDetail(agent, runtimeConfig);
@@ -125,6 +127,7 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
125
127
  const machinePayload = machineRuntimeConfig.ok ? machineRuntimeConfig.config : {};
126
128
  const bluebubbles = machinePayload.bluebubbles;
127
129
  const bluebubblesChannel = machinePayload.bluebubblesChannel;
130
+ const mailroom = payload.mailroom;
128
131
  if (senses.teams.enabled) {
129
132
  const missing = [];
130
133
  if (!textField(teams, "clientId"))
@@ -166,12 +169,33 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
166
169
  : runtimeConfigUnavailableDetail(agent, machineRuntimeConfig),
167
170
  };
168
171
  }
172
+ if (senses.mail.enabled) {
173
+ const privateKeys = mailroom?.privateKeys;
174
+ const hasPrivateKeys = !!privateKeys && typeof privateKeys === "object" && !Array.isArray(privateKeys) && Object.values(privateKeys).some((value) => typeof value === "string" && value.trim().length > 0);
175
+ const mailboxAddress = textField(mailroom, "mailboxAddress");
176
+ const missing = [];
177
+ if (!mailboxAddress)
178
+ missing.push("mailroom.mailboxAddress");
179
+ if (!hasPrivateKeys)
180
+ missing.push("mailroom.privateKeys");
181
+ base.mail = missing.length === 0
182
+ ? { configured: true, detail: mailboxAddress }
183
+ : {
184
+ configured: false,
185
+ detail: runtimeConfig.ok
186
+ ? `missing ${missing.join("/")}`
187
+ : unavailableDetail,
188
+ };
189
+ }
169
190
  return base;
170
191
  }
171
192
  function senseRepairHint(agent, sense) {
172
193
  if (sense === "teams") {
173
194
  return `Run 'ouro vault config set --agent ${agent} --key teams.clientId', teams.clientSecret, and teams.tenantId; then run 'ouro up' again.`;
174
195
  }
196
+ if (sense === "mail") {
197
+ return `Run 'ouro connect mail --agent ${agent}' to provision Mailroom access; then run 'ouro up' again.`;
198
+ }
175
199
  return `Run 'ouro connect bluebubbles --agent ${agent}' to attach BlueBubbles on this machine; then run 'ouro up' again.`;
176
200
  }
177
201
  function currentMachineId() {
@@ -182,7 +206,7 @@ function parseSenseSnapshotName(name) {
182
206
  if (parts.length !== 2)
183
207
  return null;
184
208
  const [agent, sense] = parts;
185
- if (sense !== "teams" && sense !== "bluebubbles")
209
+ if (sense !== "teams" && sense !== "bluebubbles" && sense !== "mail")
186
210
  return null;
187
211
  return { agent, sense };
188
212
  }
@@ -345,6 +369,9 @@ class DaemonSenseManager {
345
369
  optional: context.facts.bluebubbles.optional,
346
370
  ...blueBubblesRuntimeFacts,
347
371
  },
372
+ mail: {
373
+ configured: context.facts.mail.configured,
374
+ },
348
375
  };
349
376
  const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
350
377
  return inventory.map((entry) => ({
@@ -134,12 +134,14 @@ exports.DEFAULT_AGENT_SENSES = {
134
134
  cli: { enabled: true },
135
135
  teams: { enabled: false },
136
136
  bluebubbles: { enabled: false },
137
+ mail: { enabled: false },
137
138
  };
138
139
  function normalizeSenses(value, configFile) {
139
140
  const defaults = {
140
141
  cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
141
142
  teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
142
143
  bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
144
+ mail: { ...exports.DEFAULT_AGENT_SENSES.mail },
143
145
  };
144
146
  if (value === undefined) {
145
147
  return defaults;
@@ -155,7 +157,7 @@ function normalizeSenses(value, configFile) {
155
157
  throw new Error(`agent.json at ${configFile} must include senses as an object when present.`);
156
158
  }
157
159
  const raw = value;
158
- const senseNames = ["cli", "teams", "bluebubbles"];
160
+ const senseNames = ["cli", "teams", "bluebubbles", "mail"];
159
161
  for (const senseName of senseNames) {
160
162
  const rawSense = raw[senseName];
161
163
  if (rawSense === undefined) {
@@ -197,6 +199,7 @@ function buildDefaultAgentTemplate(_agentName) {
197
199
  cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
198
200
  teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
199
201
  bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
202
+ mail: { ...exports.DEFAULT_AGENT_SENSES.mail },
200
203
  },
201
204
  phrases: {
202
205
  thinking: [...exports.DEFAULT_AGENT_PHRASES.thinking],
@@ -56,6 +56,8 @@ function createOutlookHttpReadHooks(options) {
56
56
  readAgentSelfFix: options.readAgentSelfFix ?? ((agentName) => (0, outlook_read_1.readSelfFixView)(agentRoot(agentName))),
57
57
  readAgentNoteDecisions: options.readAgentNoteDecisions ?? ((agentName) => (0, outlook_read_1.readNoteDecisionView)(agentRoot(agentName))),
58
58
  readAgentHabits: options.readAgentHabits ?? ((agentName) => (0, outlook_read_1.readHabitView)(agentRoot(agentName))),
59
+ readAgentMail: options.readAgentMail ?? ((agentName) => (0, outlook_read_1.readMailView)(agentName)),
60
+ readAgentMailMessage: options.readAgentMailMessage ?? ((agentName, messageId) => (0, outlook_read_1.readMailMessageView)(agentName, messageId)),
59
61
  readDaemonHealth: options.readDaemonHealth ?? (() => (0, outlook_read_1.readDaemonHealthDeep)(options.healthPath)),
60
62
  readLogs: options.readLogs ?? (() => (0, outlook_read_1.readLogView)(options.logPath ?? null)),
61
63
  readDeskPrefs: (agentName) => (0, outlook_read_1.readDeskPrefs)(agentRoot(agentName)),
@@ -82,10 +82,12 @@ function createOutlookHttpRequestHandler(options) {
82
82
  }
83
83
  const agentMatch = /^\/api\/agents\/([^/]+)(?:\/(.+))?$/.exec(pathname);
84
84
  if (agentMatch) {
85
- handleAgentRoute(request, response, {
85
+ void handleAgentRoute(request, response, {
86
86
  agent: decodeURIComponent(agentMatch[1]),
87
87
  surface: agentMatch[2] ?? null,
88
88
  options,
89
+ }).catch((error) => {
90
+ (0, outlook_http_response_1.writeJson)(response, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
89
91
  });
90
92
  return;
91
93
  }
@@ -97,7 +99,7 @@ function createOutlookHttpRequestHandler(options) {
97
99
  (0, outlook_http_response_1.writeJson)(response, 404, { ok: false, error: `not found: ${pathname}` });
98
100
  };
99
101
  }
100
- function handleAgentRoute(request, response, context) {
102
+ async function handleAgentRoute(request, response, context) {
101
103
  const { agent, surface, options } = context;
102
104
  if (!surface) {
103
105
  const view = options.readAgentView?.(agent);
@@ -190,6 +192,16 @@ function handleAgentRoute(request, response, context) {
190
192
  (0, outlook_http_response_1.writeJson)(response, 200, options.hooks.readAgentHabits(agent));
191
193
  return;
192
194
  }
195
+ if (surface === "mail") {
196
+ (0, outlook_http_response_1.writeJson)(response, 200, await options.hooks.readAgentMail(agent));
197
+ return;
198
+ }
199
+ const mailMessageMatch = /^mail\/([^/]+)$/.exec(surface);
200
+ if (mailMessageMatch) {
201
+ const messageId = decodeURIComponent(mailMessageMatch[1]);
202
+ (0, outlook_http_response_1.writeJson)(response, 200, await options.hooks.readAgentMailMessage(agent, messageId));
203
+ return;
204
+ }
193
205
  if (surface === "inner-transcript") {
194
206
  const transcript = options.hooks.readAgentTranscript(agent, "self", "inner", "dialog");
195
207
  (0, outlook_http_response_1.writeJson)(response, 200, transcript ?? { messageCount: 0, messages: [] });