@ouro.bot/cli 0.1.0-alpha.454 → 0.1.0-alpha.456

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.456",
6
+ "changes": [
7
+ "Ouro Outlook session transcripts now respect the reader's scroll position during live refresh: if a human has scrolled up to inspect older iMessage/Teams/CLI history, background transcript updates no longer yank the pane back to the bottom.",
8
+ "Open transcripts no longer insert a transient loading row over already-rendered messages during refresh, removing the visible stutter that made long iMessage transcripts feel unstable.",
9
+ "Inner-dialog transcript panes now use the same bottom-stickiness behavior, so loading earlier messages or jumping to landmarks does not immediately undo the user's navigation."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.455",
14
+ "changes": [
15
+ "Agent Mail now has a complete all-agent onboarding runbook around `ouro account ensure --agent <agent>`, including native `@ouro.bot` mailbox setup, optional delegated human-mail aliases, vault-coupled private keys, bundle-local encrypted Mailroom state, HEY MBOX import, live forwarding boundaries, and golden-path verification.",
16
+ "Screener decisions now persist family-authorized sender policies for discard and quarantine as well as allow decisions, so future mail from a discarded sender goes directly to the retained recovery drawer instead of repeatedly interrupting the agent.",
17
+ "The system prompt now points agents at `docs/agent-mail-setup.md` and distinguishes full work-substrate account setup from mail-only repair/provisioning, while docs contract coverage locks the native-vs-delegated trust model, recovery semantics, human-only DNS/HEY/MX gates, confirmed outbound sends, and Ouro Outlook audit expectations."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.454",
6
22
  "changes": [
@@ -326,6 +326,7 @@ function agentResolutionFailureMode(command) {
326
326
  case "vault.config.set":
327
327
  case "vault.config.status":
328
328
  case "connect":
329
+ case "account.ensure":
329
330
  case "mail.import-mbox":
330
331
  case "auth.run":
331
332
  case "auth.verify":
@@ -2681,11 +2682,93 @@ async function executeConnectBlueBubbles(agent, deps) {
2681
2682
  ],
2682
2683
  });
2683
2684
  }
2684
- async function executeConnectMail(agent, deps) {
2685
+ function isPlainRecord(value) {
2686
+ return !!value && typeof value === "object" && !Array.isArray(value);
2687
+ }
2688
+ function readMailroomRegistryFromDisk(registryPath) {
2689
+ if (!fs.existsSync(registryPath))
2690
+ return undefined;
2691
+ const parsed = JSON.parse(fs.readFileSync(registryPath, "utf-8"));
2692
+ if (!isPlainRecord(parsed) || parsed.schemaVersion !== 1 || !Array.isArray(parsed.mailboxes) || !Array.isArray(parsed.sourceGrants)) {
2693
+ throw new Error(`mailroom registry at ${registryPath} is not a valid Mailroom registry`);
2694
+ }
2695
+ return parsed;
2696
+ }
2697
+ function mailroomPrivateKeys(mailroom) {
2698
+ const keys = isPlainRecord(mailroom?.privateKeys) ? mailroom.privateKeys : {};
2699
+ return Object.fromEntries(Object.entries(keys).filter((entry) => typeof entry[1] === "string"));
2700
+ }
2701
+ async function promptDelegatedMailSource(deps) {
2702
+ const promptInput = requirePromptInput(deps, "Agent Mail setup");
2703
+ const ownerEmail = (await promptInput("Delegated owner email for first source alias [blank to skip]: ")).trim();
2704
+ const source = ownerEmail
2705
+ ? ((await promptInput("Delegated source label [hey]: ")).trim() || "hey")
2706
+ : "";
2707
+ return { ownerEmail, source };
2708
+ }
2709
+ async function ensureAgentMailroom(agent, input, deps, progressLabel) {
2685
2710
  if (agent === "SerpentGuide") {
2686
2711
  throw new Error("SerpentGuide has no persistent runtime credentials. Connect mail on the hatchling agent instead.");
2687
2712
  }
2688
- const promptInput = requirePromptInput(deps, "Agent Mail setup");
2713
+ const agentRoot = providerCliAgentRoot({ agent }, deps);
2714
+ const mailStateDir = path.join(agentRoot, "state", "mailroom");
2715
+ const progress = createHumanCommandProgress(deps, progressLabel);
2716
+ let stored;
2717
+ let mailboxAddress = `${agent.toLowerCase()}@ouro.bot`;
2718
+ let sourceAlias = null;
2719
+ let registryPath = path.join(mailStateDir, "registry.json");
2720
+ let storePath = mailStateDir;
2721
+ try {
2722
+ progress.startPhase("checking Mailroom runtime config");
2723
+ const current = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
2724
+ if (!current.ok && current.reason !== "missing") {
2725
+ throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
2726
+ }
2727
+ const currentConfig = current.ok ? current.config : {};
2728
+ const existingMailroom = isPlainRecord(currentConfig.mailroom) ? currentConfig.mailroom : undefined;
2729
+ registryPath = stringField(existingMailroom ?? {}, "registryPath") || registryPath;
2730
+ storePath = stringField(existingMailroom ?? {}, "storePath") || storePath;
2731
+ progress.updateDetail("ensuring Mailroom identity");
2732
+ const ensured = (0, core_1.ensureMailboxRegistry)({
2733
+ agentId: agent,
2734
+ registry: readMailroomRegistryFromDisk(registryPath),
2735
+ keys: mailroomPrivateKeys(existingMailroom),
2736
+ ownerEmail: input.ownerEmail || undefined,
2737
+ source: input.source || undefined,
2738
+ });
2739
+ mailboxAddress = ensured.mailboxAddress;
2740
+ sourceAlias = ensured.sourceAlias;
2741
+ fs.mkdirSync(path.dirname(registryPath), { recursive: true });
2742
+ fs.mkdirSync(storePath, { recursive: true });
2743
+ fs.writeFileSync(registryPath, `${JSON.stringify(ensured.registry, null, 2)}\n`, "utf-8");
2744
+ const nextConfig = {
2745
+ ...currentConfig,
2746
+ mailroom: {
2747
+ ...(existingMailroom ?? {}),
2748
+ mailboxAddress,
2749
+ registryPath,
2750
+ storePath,
2751
+ privateKeys: ensured.keys,
2752
+ },
2753
+ };
2754
+ progress.updateDetail("storing private mail keys in vault");
2755
+ stored = await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(agent, nextConfig, providerCliNow(deps));
2756
+ progress.updateDetail("enabling Mail sense in agent.json");
2757
+ enableAgentSense(agent, "mail", deps);
2758
+ progress.completePhase("checking Mailroom runtime config", "mail ready");
2759
+ progress.end();
2760
+ }
2761
+ catch (error) {
2762
+ progress.end();
2763
+ throw error;
2764
+ }
2765
+ /* v8 ignore next -- defensive: successful setup always stores before leaving the guarded block. @preserve */
2766
+ if (!stored)
2767
+ throw new Error("Mailroom setup did not store runtime credentials");
2768
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
2769
+ return { mailboxAddress, sourceAlias, registryPath, storePath, stored, syncSummary };
2770
+ }
2771
+ async function executeConnectMail(agent, deps) {
2689
2772
  writeConnectorIntro(deps, {
2690
2773
  title: "Connect Agent Mail",
2691
2774
  subtitle: `${agent} gets a private Mailroom identity and delegated source lane.`,
@@ -2720,87 +2803,96 @@ async function executeConnectMail(agent, deps) {
2720
2803
  "The registry and encrypted local store live under the agent bundle state.",
2721
2804
  ],
2722
2805
  });
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);
2806
+ const setup = await promptDelegatedMailSource(deps);
2807
+ const outcome = await ensureAgentMailroom(agent, setup, deps, "connect mail");
2773
2808
  return writeCapabilityOutcome(deps, {
2774
2809
  subtitle: `${agent}'s Mail sense is configured.`,
2775
2810
  summary: `Agent Mail is ready for ${agent}.`,
2776
2811
  whatChanged: [
2777
- `Mailbox: ${mailboxAddress}`,
2778
- ...(sourceAlias ? [`Delegated alias: ${sourceAlias}`] : []),
2779
- `Registry: ${registryPath}`,
2780
- `Encrypted store: ${storePath}`,
2781
- `Stored: ${stored.itemPath}`,
2812
+ `Mailbox: ${outcome.mailboxAddress}`,
2813
+ ...(outcome.sourceAlias ? [`Delegated alias: ${outcome.sourceAlias}`] : []),
2814
+ `Registry: ${outcome.registryPath}`,
2815
+ `Encrypted store: ${outcome.storePath}`,
2816
+ `Stored: ${outcome.stored.itemPath}`,
2782
2817
  "agent.json: senses.mail.enabled = true",
2783
2818
  "private mail keys were not printed",
2784
- ...(syncSummary ? [syncSummary] : []),
2819
+ ...(outcome.syncSummary ? [outcome.syncSummary] : []),
2785
2820
  ],
2786
2821
  nextMoves: [
2787
2822
  "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.`
2823
+ outcome.sourceAlias
2824
+ ? `Use ${outcome.sourceAlias} for HEY forwarding or Extensions after the SMTP ingress proof is accepted.`
2790
2825
  : "Add delegated source aliases when you are ready to mirror human mail.",
2791
2826
  ],
2792
2827
  fallbackLines: [
2793
2828
  `Agent Mail connected for ${agent}`,
2794
- `mailbox: ${mailboxAddress}`,
2795
- ...(sourceAlias ? [`delegated alias: ${sourceAlias}`] : []),
2796
- `registry: ${registryPath}`,
2797
- `encrypted store: ${storePath}`,
2798
- `stored: ${stored.itemPath}`,
2829
+ `mailbox: ${outcome.mailboxAddress}`,
2830
+ ...(outcome.sourceAlias ? [`delegated alias: ${outcome.sourceAlias}`] : []),
2831
+ `registry: ${outcome.registryPath}`,
2832
+ `encrypted store: ${outcome.storePath}`,
2833
+ `stored: ${outcome.stored.itemPath}`,
2799
2834
  "agent.json: senses.mail.enabled = true",
2800
2835
  "private mail keys were not printed",
2801
2836
  "",
2802
2837
  "Next: run `ouro up` so the daemon picks up the Mail sense.",
2803
- ...(syncSummary ? [syncSummary] : []),
2838
+ ...(outcome.syncSummary ? [outcome.syncSummary] : []),
2839
+ ],
2840
+ });
2841
+ }
2842
+ async function executeAccountEnsure(command, deps) {
2843
+ writeConnectorIntro(deps, {
2844
+ title: "Ensure Agent Account",
2845
+ subtitle: `${command.agent}'s Ouro work substrate gets its vault-backed coordinates.`,
2846
+ summary: "Prepare the portable runtime account shape that couples vault, Mailroom, and enabled senses.",
2847
+ sections: [
2848
+ {
2849
+ title: "Account substrate",
2850
+ lines: [
2851
+ "runtime/config remains the private vault item for portable integration secrets.",
2852
+ "Mailroom gets a private native mailbox and optional delegated source alias.",
2853
+ "agent.json records that the Mail sense is enabled.",
2854
+ ],
2855
+ },
2856
+ {
2857
+ title: "Human-only edges",
2858
+ lines: [
2859
+ "This command does not perform browser auth, HEY forwarding, DNS edits, MX cutover, or autonomous sending enablement.",
2860
+ ],
2861
+ },
2862
+ ],
2863
+ fallbackLines: [
2864
+ `Ensure Ouro work substrate for ${command.agent}`,
2865
+ "This creates or preserves runtime/config Mailroom coordinates and enables the Mail sense.",
2866
+ ],
2867
+ });
2868
+ const setup = await promptDelegatedMailSource(deps);
2869
+ const outcome = await ensureAgentMailroom(command.agent, setup, deps, "account ensure");
2870
+ return writeCapabilityOutcome(deps, {
2871
+ subtitle: `${command.agent}'s work substrate account is configured.`,
2872
+ summary: `Ouro work substrate is ready for ${command.agent}.`,
2873
+ whatChanged: [
2874
+ "Vault item: runtime/config",
2875
+ `Mailbox: ${outcome.mailboxAddress}`,
2876
+ ...(outcome.sourceAlias ? [`Delegated alias: ${outcome.sourceAlias}`] : []),
2877
+ `Registry: ${outcome.registryPath}`,
2878
+ `Encrypted store: ${outcome.storePath}`,
2879
+ "agent.json: senses.mail.enabled = true",
2880
+ ...(outcome.syncSummary ? [outcome.syncSummary] : []),
2881
+ ],
2882
+ nextMoves: [
2883
+ "Run ouro up so the daemon picks up the Mail sense.",
2884
+ "Import HEY MBOX mail only after the human has exported the file.",
2885
+ "Keep production MX and autonomous send disabled until the final explicit confirmation.",
2886
+ ],
2887
+ fallbackLines: [
2888
+ `Ouro work substrate ready for ${command.agent}`,
2889
+ "vault: runtime/config",
2890
+ `mailbox: ${outcome.mailboxAddress}`,
2891
+ ...(outcome.sourceAlias ? [`delegated alias: ${outcome.sourceAlias}`] : []),
2892
+ `registry: ${outcome.registryPath}`,
2893
+ `encrypted store: ${outcome.storePath}`,
2894
+ "agent.json: senses.mail.enabled = true",
2895
+ ...(outcome.syncSummary ? [outcome.syncSummary] : []),
2804
2896
  ],
2805
2897
  });
2806
2898
  }
@@ -4274,6 +4366,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4274
4366
  if (command.kind === "connect") {
4275
4367
  return executeConnect(command, deps);
4276
4368
  }
4369
+ if (command.kind === "account.ensure") {
4370
+ return executeAccountEnsure(command, deps);
4371
+ }
4277
4372
  if (command.kind === "mail.import-mbox") {
4278
4373
  return executeMailImportMbox(command, deps);
4279
4374
  }
@@ -163,6 +163,13 @@ exports.COMMAND_REGISTRY = {
163
163
  example: "ouro auth",
164
164
  subcommands: ["verify", "switch"],
165
165
  },
166
+ account: {
167
+ category: "Auth",
168
+ description: "Ensure the agent's vault-backed work substrate account, including Mailroom setup",
169
+ usage: "ouro account ensure [--agent <name>]",
170
+ example: "ouro account ensure --agent slugger",
171
+ subcommands: ["ensure"],
172
+ },
166
173
  connect: {
167
174
  category: "Auth",
168
175
  description: "Set up providers, portable integrations, and local senses from one guided screen",
@@ -309,6 +316,11 @@ const SUBCOMMAND_HELP = {
309
316
  usage: "ouro connect mail [--agent <name>]",
310
317
  example: "ouro connect mail",
311
318
  },
319
+ "account ensure": {
320
+ description: "Idempotently prepare an agent's vault-backed work substrate account and private Mailroom mailbox",
321
+ usage: "ouro account ensure [--agent <name>]",
322
+ example: "ouro account ensure --agent slugger",
323
+ },
312
324
  "mail import-mbox": {
313
325
  description: "Import a HEY or other MBOX export into an existing delegated Mailroom source grant",
314
326
  usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
@@ -83,6 +83,7 @@ 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 account ensure [--agent <name>]",
86
87
  " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>]",
87
88
  " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
88
89
  " ouro auth verify [--agent <name>] [--provider <provider>]",
@@ -648,6 +649,16 @@ function parseMailCommand(args) {
648
649
  ...(source ? { source } : {}),
649
650
  };
650
651
  }
652
+ function parseAccountCommand(args) {
653
+ const [sub, ...subArgs] = args;
654
+ if (sub !== "ensure") {
655
+ throw new Error("Usage: ouro account ensure [--agent <name>]");
656
+ }
657
+ const { agent, rest } = extractAgentFlag(subArgs);
658
+ if (rest.length > 0)
659
+ throw new Error("Usage: ouro account ensure [--agent <name>]");
660
+ return { kind: "account.ensure", ...(agent ? { agent } : {}) };
661
+ }
651
662
  function parseProviderUseCommand(args) {
652
663
  const { agent, rest: afterAgent } = extractAgentFlag(args);
653
664
  const { facing, rest: afterFacing } = extractFacingFlag(afterAgent);
@@ -1130,6 +1141,8 @@ function parseOuroCommand(args) {
1130
1141
  return parseHatchCommand(args.slice(1));
1131
1142
  if (head === "auth")
1132
1143
  return parseAuthCommand(args.slice(1));
1144
+ if (head === "account")
1145
+ return parseAccountCommand(args.slice(1));
1133
1146
  if (head === "connect")
1134
1147
  return parseConnectCommand(args.slice(1));
1135
1148
  if (head === "vault")
@@ -50,7 +50,7 @@ function defaultLoggingForProcess(processName) {
50
50
  sinks: ["ndjson"],
51
51
  };
52
52
  }
53
- if (processName === "bluebubbles") {
53
+ if (processName === "bluebubbles" || processName === "mail") {
54
54
  return {
55
55
  level: "warn",
56
56
  sinks: ["terminal", "ndjson"],
@@ -215,6 +215,13 @@ function runtimeInfoFor(status) {
215
215
  return { runtime: "running" };
216
216
  return { runtime: "error" };
217
217
  }
218
+ function managedSenseEntry(sense) {
219
+ if (sense === "teams")
220
+ return "senses/teams-entry.js";
221
+ if (sense === "bluebubbles")
222
+ return "senses/bluebubbles/entry.js";
223
+ return "senses/mail-entry.js";
224
+ }
218
225
  function blueBubblesRuntimeStateIsFresh(lastCheckedAt, now = Date.now()) {
219
226
  if (!lastCheckedAt) {
220
227
  return false;
@@ -281,12 +288,12 @@ class DaemonSenseManager {
281
288
  return [agent, { senses, facts }];
282
289
  }));
283
290
  const managedSenseAgents = [...this.contexts.entries()].flatMap(([agent, context]) => {
284
- return ["teams", "bluebubbles"]
291
+ return ["teams", "bluebubbles", "mail"]
285
292
  .filter((sense) => context.senses[sense].enabled)
286
293
  .map((sense) => ({
287
294
  name: `${agent}:${sense}`,
288
295
  agentArg: agent,
289
- entry: sense === "teams" ? "senses/teams-entry.js" : "senses/bluebubbles/entry.js",
296
+ entry: managedSenseEntry(sense),
290
297
  channel: sense,
291
298
  autoStart: true,
292
299
  }));
@@ -371,6 +378,7 @@ class DaemonSenseManager {
371
378
  },
372
379
  mail: {
373
380
  configured: context.facts.mail.configured,
381
+ ...(runtime.get(agent)?.mail ?? {}),
374
382
  },
375
383
  };
376
384
  const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
@@ -12,10 +12,17 @@ function emptyFolders() {
12
12
  return [
13
13
  { id: "imbox", label: "Imbox", count: 0 },
14
14
  { id: "screener", label: "Screener", count: 0 },
15
+ { id: "discarded", label: "Discarded", count: 0 },
16
+ { id: "quarantine", label: "Quarantine", count: 0 },
17
+ { id: "draft", label: "Drafts", count: 0 },
18
+ { id: "sent", label: "Sent", count: 0 },
15
19
  { id: "delegated", label: "Delegated", count: 0 },
16
20
  { id: "native", label: "Native", count: 0 },
17
21
  ];
18
22
  }
23
+ function emptyRecovery() {
24
+ return { discardedCount: 0, quarantineCount: 0 };
25
+ }
19
26
  function unavailableMailView(agentName, status, error) {
20
27
  return {
21
28
  status,
@@ -25,6 +32,9 @@ function unavailableMailView(agentName, status, error) {
25
32
  store: null,
26
33
  folders: emptyFolders(),
27
34
  messages: [],
35
+ screener: [],
36
+ outbound: [],
37
+ recovery: emptyRecovery(),
28
38
  accessLog: [],
29
39
  error,
30
40
  };
@@ -41,6 +51,16 @@ function unavailableMessageView(agentName, status, error) {
41
51
  };
42
52
  }
43
53
  function mailSummary(message) {
54
+ const provenance = {
55
+ placement: message.placement,
56
+ compartmentKind: message.compartmentKind,
57
+ ownerEmail: message.ownerEmail ?? null,
58
+ source: message.source ?? null,
59
+ recipient: message.recipient,
60
+ mailboxId: message.mailboxId,
61
+ grantId: message.grantId ?? null,
62
+ trustReason: message.trustReason,
63
+ };
44
64
  return {
45
65
  id: message.id,
46
66
  subject: message.private.subject,
@@ -57,12 +77,17 @@ function mailSummary(message) {
57
77
  recipient: message.recipient,
58
78
  attachmentCount: message.private.attachments.length,
59
79
  untrustedContentWarning: message.private.untrustedContentWarning,
80
+ provenance,
60
81
  };
61
82
  }
62
- function buildFolders(messages) {
83
+ function buildFolders(messages, outbound) {
63
84
  const folders = [
64
85
  { id: "imbox", label: "Imbox", count: messages.filter((message) => message.placement === "imbox").length },
65
86
  { id: "screener", label: "Screener", count: messages.filter((message) => message.placement === "screener").length },
87
+ { id: "discarded", label: "Discarded", count: messages.filter((message) => message.placement === "discarded").length },
88
+ { id: "quarantine", label: "Quarantine", count: messages.filter((message) => message.placement === "quarantine").length },
89
+ { id: "draft", label: "Drafts", count: outbound.filter((record) => record.status === "draft").length },
90
+ { id: "sent", label: "Sent", count: outbound.filter((record) => record.status === "sent").length },
66
91
  { id: "delegated", label: "Delegated", count: messages.filter((message) => message.compartmentKind === "delegated").length },
67
92
  { id: "native", label: "Native", count: messages.filter((message) => message.compartmentKind === "native").length },
68
93
  ];
@@ -77,6 +102,45 @@ function buildFolders(messages) {
77
102
  }
78
103
  return folders;
79
104
  }
105
+ function screenerCandidate(candidate) {
106
+ return {
107
+ id: candidate.id,
108
+ messageId: candidate.messageId,
109
+ senderEmail: candidate.senderEmail,
110
+ senderDisplay: candidate.senderDisplay,
111
+ recipient: candidate.recipient,
112
+ source: candidate.source ?? null,
113
+ ownerEmail: candidate.ownerEmail ?? null,
114
+ status: candidate.status,
115
+ placement: candidate.placement,
116
+ trustReason: candidate.trustReason,
117
+ firstSeenAt: candidate.firstSeenAt,
118
+ lastSeenAt: candidate.lastSeenAt,
119
+ messageCount: candidate.messageCount,
120
+ };
121
+ }
122
+ function outboundRecord(record) {
123
+ return {
124
+ id: record.id,
125
+ status: record.status,
126
+ from: record.from,
127
+ to: record.to,
128
+ cc: record.cc,
129
+ bcc: record.bcc,
130
+ subject: record.subject,
131
+ createdAt: record.createdAt,
132
+ updatedAt: record.updatedAt,
133
+ sentAt: record.sentAt ?? null,
134
+ transport: record.transport ?? null,
135
+ reason: record.reason,
136
+ };
137
+ }
138
+ function buildRecovery(messages) {
139
+ return {
140
+ discardedCount: messages.filter((message) => message.placement === "discarded").length,
141
+ quarantineCount: messages.filter((message) => message.placement === "quarantine").length,
142
+ };
143
+ }
80
144
  function accessEntries(entries) {
81
145
  return entries
82
146
  .slice()
@@ -113,6 +177,9 @@ async function readMailView(agentName) {
113
177
  const stored = await resolved.store.listMessages({ agentId: agentName, limit: OUTLOOK_MAIL_COUNT_LIMIT });
114
178
  const decrypted = (0, file_store_1.decryptMessages)(stored, resolved.config.privateKeys);
115
179
  const summaries = decrypted.map(mailSummary);
180
+ const screener = (await resolved.store.listScreenerCandidates({ agentId: agentName, status: "pending", limit: 100 }))
181
+ .map(screenerCandidate);
182
+ const outbound = (await resolved.store.listMailOutbound(agentName)).map(outboundRecord);
116
183
  await resolved.store.recordAccess({
117
184
  agentId: agentName,
118
185
  tool: "outlook_mail_list",
@@ -129,8 +196,11 @@ async function readMailView(agentName) {
129
196
  kind: resolved.storeKind,
130
197
  label: resolved.storeLabel,
131
198
  },
132
- folders: buildFolders(summaries),
199
+ folders: buildFolders(summaries, outbound),
133
200
  messages: summaries.slice(0, OUTLOOK_MAIL_LIST_LIMIT),
201
+ screener,
202
+ outbound,
203
+ recovery: buildRecovery(summaries),
134
204
  accessLog,
135
205
  error: null,
136
206
  };