diff --git a/eval/feature-manifest.ts b/eval/feature-manifest.ts index 1ee4a66c..2f56cc47 100644 --- a/eval/feature-manifest.ts +++ b/eval/feature-manifest.ts @@ -490,6 +490,34 @@ export const FEATURES: FeatureSpec[] = [ "agent-created loops carry source='agent' + the owner's user_id (auditable + per-owner scoped)", ], }, + { + id: "managed-loop-override", + summary: + "Consumer Loops audit + control surface (MobileApi.ListLoops/SetLoopEnabled). The hosted user owns none of the instance's `system`-owned background loops, so ListLoops also surfaces a curated managed set (auto-dream -> 'Brain consolidation', style-analyze -> 'Writing style learning') under friendly labels. Toggling one writes a per-user app.userLoop..enabled config override (setLoopUserEnabled) instead of mutating the shared system row; cron-engine honors it at fire time as an AND-gate (isLoopUserDisabled).", + trigger: { kind: "turn" }, + entry: [ + "curateConsumerLoops", + "isLoopUserDisabled", + "setLoopUserEnabled", + "userLoopOverrideKey", + ], + effects: [ + { + claim: "toggling a managed loop persists app.userLoop..enabled in the config table", + sql: { + query: "SELECT count(*) FROM config WHERE key LIKE 'app.userLoop.%'", + expect: "nonzero", + }, + notExercised: true, + }, + ], + invariants: [ + "managed loops display friendly labels but toggle/delete key off the real job name", + "AND-gate: a managed loop fires only if its system row is enabled AND the user has not opted out", + "the shared system cron_jobs row is never mutated per-user (per-customer DB scoping)", + "infra plumbing (wiki/graph/magic-docs/delta-sync) + the proactive family are hidden from the consumer surface", + ], + }, // ── Wired runtime helpers (dormant-prone) ── { @@ -794,20 +822,27 @@ export const FEATURES: FeatureSpec[] = [ }, { id: "draft-edit-learning", - summary: "Capture user edits to drafts as corrections that update the user model.", + summary: + "Capture user edits to drafts as corrections that update the user model (approveWithEdit -> captureDraftEdit -> updateUserModel). Only an actual edit (edited != original) is captured; a plain approve is not.", trigger: { kind: "turn" }, entry: ["approveWithEdit"], effects: [ { - claim: "edits land as user_model corrections", + // A correction is stored as a fact whose key is `correction_` and + // whose value is { text: corrected, original } (see updateUserModel). + claim: "edits land as user_model corrections (category='fact', key LIKE 'correction_%')", sql: { - query: "SELECT count(*) FROM user_model WHERE category='correction'", + query: + "SELECT count(*) FROM user_model WHERE category = 'fact' AND key LIKE 'correction_%'", expect: "nonzero", }, notExercised: true, }, ], - invariants: ["per-owner scoped"], + invariants: [ + "per-owner scoped", + "only an actual edit is learned (plain approve writes no correction)", + ], }, { id: "shadow-observer", @@ -877,6 +912,105 @@ export const FEATURES: FeatureSpec[] = [ }, ], }, + { + id: "consumer-advanced-surface", + summary: + "Hosted Advanced curation (MobileApi.ListSkills/ToggleSkill/ListPlugins). ListSkills filters the full skill catalog to consumer-facing skills (operator-curated external Google skills + an allowlist of bundled consumer skills like pdf/xlsx/weather), under friendly labels, with each skill's persisted on/off folded in (skill..enabled). ToggleSkill resolves the friendly label back to the raw name (resolveSkillName) before persisting. ListPlugins returns a curated read-only built-in tool set instead of the developer marketplace plugins.", + trigger: { kind: "turn" }, + entry: ["curateConsumerSkills", "resolveSkillName", "isConsumerSkill"], + effects: [ + { + claim: "toggling a skill persists skill..enabled in the config table", + sql: { + query: "SELECT count(*) FROM config WHERE key LIKE 'skill.%.enabled'", + expect: "nonzero", + }, + notExercised: true, + }, + ], + invariants: [ + "consumer skills = external (operator-curated) + an allowlist of bundled consumer skills; dev/internal/channel skills are hidden", + "skills display friendly labels but the toggle round-trips back to the raw skill name", + "ListPlugins surfaces the curated built-in tool set (read-only), not the developer marketplace plugins", + ], + }, + { + id: "scheduled-tasks", + summary: + "Consumer Tasks surface (MobileApi.ListTasks/UpdateTask/DeleteTask). ListTasks returns the user's own scheduled cron_jobs (one-off 'at' reminders + recurring jobs created via schedule_task/loop_create), owner-scoped by user_id so the instance's system-owned background loops never appear. UpdateTask reschedules/renames/edits the instruction/enables; DeleteTask removes one. Both assert ownership before mutating.", + trigger: { kind: "turn" }, + entry: ["curateConsumerTasks", "toConsumerTask"], + effects: [ + { + claim: "the user's scheduled tasks are stored as owner-scoped cron_jobs", + sql: { + query: "SELECT count(*) FROM cron_jobs WHERE source IN ('agent','user')", + expect: "nonzero", + }, + notExercised: true, + }, + ], + invariants: [ + "Tasks are owner-scoped (user_id); managed/system loops never appear on this surface", + "UpdateTask/DeleteTask assert job.userId === the resolved owner before mutating", + "schedule_task stamps source='agent' (a user-owned task, not infra)", + ], + }, + { + id: "brain-overview", + summary: + "Consumer Brain page (MobileApi.GetBrain). Composes the read model from real per-user memory: the knowledge graph (kg_nodes/kg_edges via getProjection) drives the map + entities, and the accumulated user_model drives the recently-learned facts feed. Owner-scoped via TenantContext.", + trigger: { kind: "turn" }, + entry: ["getBrainOverview", "getProjection"], + effects: [ + { + claim: "the brain map reads nodes from the per-user knowledge graph", + sql: { query: "SELECT count(*) FROM kg_nodes", expect: "nonzero" }, + notExercised: true, + }, + ], + invariants: [ + "owner-scoped: nodes/edges/facts filtered by user_id", + "facts come from user_model (confidence binned to 0..3); entities/edges from kg_nodes/kg_edges", + ], + }, + { + id: "inbox-overview", + summary: + "Consumer Inbox (MobileApi.GetInbox). Two owner-scoped sections: the agent's drafted replies awaiting approval (draft_messages: pending=needs-you, approved/sent=handled) and the CATE agent-to-agent inbound queue (cate_inbound, best-effort). Draft actions reuse ApproveDraft/RejectDraft; CATE via ActOnInboxItem.", + trigger: { kind: "turn" }, + entry: ["getInboxOverview"], + effects: [ + { + claim: "pending drafts awaiting approval are owner-scoped in draft_messages", + sql: { query: "SELECT count(*) FROM draft_messages", expect: "nonzero" }, + notExercised: true, + }, + ], + invariants: [ + "owner-scoped by user_id", + "drafts + CATE merged read-only; mutations via existing draft/inbox RPCs", + ], + }, + { + id: "today-overview", + summary: + "Consumer Today brief (MobileApi.GetToday). Composes today's Google Calendar events (live via gapiFetch, best-effort -- empty when Google isn't connected), pending commitments, and the user's scheduled tasks. briefingEnabled=false (proactive autonomy off) tells the client to show the enable-briefing deep link.", + trigger: { kind: "turn" }, + entry: ["getTodayOverview"], + effects: [ + { + claim: "commitments feeding the brief are owner-scoped", + sql: { query: "SELECT count(*) FROM commitments", expect: "nonzero" }, + notExercised: true, + }, + ], + invariants: [ + "owner-scoped", + "calendar is best-effort (empty when Google isn't connected)", + "gated on app.inboxAutonomy != 'off'", + ], + }, // ── Consent-aware drafting ── { diff --git a/proto/nomos.proto b/proto/nomos.proto index 109a34e9..271e5639 100644 --- a/proto/nomos.proto +++ b/proto/nomos.proto @@ -164,6 +164,7 @@ service MobileApi { // Skills tab rpc ListSkills (Empty) returns (MSkillsResponse); rpc ToggleSkill (MSkillToggleRequest) returns (MSkillToggleResponse); + rpc ListPlugins (Empty) returns (MPluginsResponse); // Earnings tab rpc GetEarnings (MEarningsRequest) returns (MEarningsResponse); @@ -178,6 +179,8 @@ service MobileApi { rpc UpdateConsent (MConsentRequest) returns (MAck); rpc UpdateTrustTier (MTrustTierRequest) returns (MAck); rpc UpdatePermission (MPermissionRequest) returns (MAck); + rpc UpdateAppSetting (MAppSettingRequest) returns (MAck); + rpc UpdateAgentIdentity (MAgentIdentityRequest) returns (MAck); rpc ListIntegrations (Empty) returns (MIntegrationsResponse); rpc StartConnectIntegration (MStartConnectRequest) returns (MStartConnectResponse); rpc ConnectGoogleAccount (MConnectGoogleRequest) returns (MAck); @@ -199,6 +202,23 @@ service MobileApi { rpc SetLoopEnabled (MSetLoopEnabledRequest) returns (MAck); rpc DeleteLoop (MLoopDeleteRequest) returns (MAck); + // Tasks tab (the user's scheduled tasks: one-off reminders + recurring jobs, + // editable: reschedule, rename, edit instruction, enable/disable, delete) + rpc ListTasks (Empty) returns (MTasksResponse); + rpc UpdateTask (MTaskUpdateRequest) returns (MAck); + rpc DeleteTask (MTaskDeleteRequest) returns (MAck); + + // Brain tab (the user's knowledge graph + learned facts, for the feed + map) + rpc GetBrain (Empty) returns (MBrainResponse); + + // Inbox tab (drafts to approve + CATE agent requests). Actions reuse + // ApproveDraft/RejectDraft (drafts) + ActOnInboxItem (CATE). + rpc GetInbox (Empty) returns (MGetInboxResponse); + + // Today tab (the daily brief: calendar + commitments + tasks, gated on the + // daily briefing being enabled). + rpc GetToday (Empty) returns (MTodayResponse); + // Studio (hosted-only feature). Blobs move via presigned PUT/GET, never gRPC. rpc StudioCreateAsset (MStudioCreateAssetRequest) returns (MStudioCreateAssetResponse); rpc StudioGetAssetUrl (MStudioAssetRef) returns (MStudioAssetUrlResponse); @@ -234,6 +254,109 @@ message MLoopDeleteRequest { string name = 1; } +// Tasks (the user's scheduled tasks) +message MTask { + string id = 1; + string name = 2; + string prompt = 3; // the instruction the task runs + string schedule = 4; // raw: "15m" | cron expr | ISO timestamp + string schedule_type = 5; // every | cron | at + string display_schedule = 6; // friendly, display-only + bool enabled = 7; + string source = 8; + string last_run = 9; // ISO-8601, empty if never run +} +message MTasksResponse { + repeated MTask tasks = 1; +} +// Full-state update (the client sends the edited task); empty name/schedule are +// ignored so a toggle-only call never blanks fields. +message MTaskUpdateRequest { + string id = 1; + string name = 2; + string prompt = 3; + string schedule = 4; + string schedule_type = 5; + bool enabled = 6; +} +message MTaskDeleteRequest { + string id = 1; +} + +// Brain (knowledge graph + learned facts) +message MBrainNode { + string id = 1; + string label = 2; + string kind = 3; // person | org | topic | decision | project | value | event | wiki | vault + string summary = 4; + int32 degree = 5; // connection count within the returned subgraph + double confidence = 6; +} +message MBrainEdge { + string src = 1; + string dst = 2; + string relation = 3; +} +message MBrainFact { + string text = 1; + string source = 2; + int32 confidence = 3; // 0..3 + string learned_at = 4; // ISO-8601 +} +message MBrainResponse { + repeated MBrainNode nodes = 1; + repeated MBrainEdge edges = 2; + repeated MBrainFact facts = 3; + int32 entity_count = 4; + int32 fact_count = 5; +} + +// Inbox (drafts + CATE agent requests) +message MInboxDraft { + string id = 1; + string recipient = 2; + string preview = 3; + string status = 4; // pending | approved | sent + string platform = 5; + string created_at = 6; +} +message MInboxCate { + string id = 1; + string from_label = 2; + string trust_tier = 3; + string subject = 4; + string bond_amount = 5; + string created_at = 6; +} +message MGetInboxResponse { + repeated MInboxDraft drafts = 1; + repeated MInboxCate cate = 2; + int32 blocked_count = 3; +} + +// Today (the daily brief) +message MTodayEvent { + string time = 1; + string title = 2; + string meta = 3; +} +message MTodayCommitment { + string id = 1; + string description = 2; + string due = 3; +} +message MTodayTask { + string id = 1; + string name = 2; + string schedule = 3; +} +message MTodayResponse { + bool briefing_enabled = 1; + repeated MTodayEvent events = 2; + repeated MTodayCommitment commitments = 3; + repeated MTodayTask tasks = 4; +} + message MAck { bool success = 1; string message = 2; @@ -372,6 +495,16 @@ message MSkillsResponse { repeated MSkill skills = 1; } +message MPlugin { + string name = 1; + string description = 2; + string marketplace = 3; +} + +message MPluginsResponse { + repeated MPlugin plugins = 1; +} + message MSkillToggleRequest { string name = 1; bool enabled = 2; @@ -478,6 +611,42 @@ message MSettingsResponse { repeated MTrustTier trust_tiers = 2; repeated MPermission permissions = 3; repeated MIntegration integrations = 4; + repeated MConsentEntry consent = 5; + MAgentIdentity identity = 6; + repeated MAppToggle app_toggles = 7; + MProactive proactive = 8; +} + +message MProactive { + string mode = 1; // off | passive | active (app.inboxAutonomy) + string briefing = 2; // cron for the daily briefing (app.briefingCron) +} + +message MConsentEntry { + string platform = 1; + string mode = 2; // always_ask | auto_approve | notify_only +} + +message MAgentIdentity { + string name = 1; + string voice = 2; // personality / voice & tone (the SOUL) + string avatar = 3; // chosen avatar (an emoji), empty = monogram fallback +} + +message MAppToggle { + string key = 1; // app. config key + bool enabled = 2; +} + +message MAppSettingRequest { + string key = 1; // app. config key (consumer-safe subset only) + string value = 2; // "true"/"false" for bools, raw string otherwise +} + +message MAgentIdentityRequest { + string name = 1; + string voice = 2; + string avatar = 3; } message MConsentRequest { diff --git a/src/cron/loop-overrides.ts b/src/cron/loop-overrides.ts new file mode 100644 index 00000000..17e89f01 --- /dev/null +++ b/src/cron/loop-overrides.ts @@ -0,0 +1,29 @@ +/** + * Per-user (per-customer DB) enable/disable override for a managed background + * loop. + * + * The instance's always-on loops (auto-dream, style-analyze, ...) live as a + * single cron_jobs row owned by the synthetic `system` tenant, so they must not + * be mutated per-user. Instead the consumer Loops UI toggles a config flag here, + * and cron-engine consults it at fire time as an AND-gate: a managed loop runs + * only if its system row is enabled AND the user has not opted out. Absent flag + * = enabled (default on). In a per-customer DB the config table is the customer's + * own, so this flag is effectively per-user. + */ + +import { getConfigValue, setConfigValue } from "../db/config.ts"; + +export function userLoopOverrideKey(name: string): string { + return `app.userLoop.${name}.enabled`; +} + +/** True when the user has explicitly turned this loop off. */ +export async function isLoopUserDisabled(name: string): Promise { + const v = await getConfigValue(userLoopOverrideKey(name)); + return v === false || v === "false"; +} + +/** Persist the user's on/off choice for a managed loop. */ +export async function setLoopUserEnabled(name: string, enabled: boolean): Promise { + await setConfigValue(userLoopOverrideKey(name), enabled); +} diff --git a/src/cron/loop-view.test.ts b/src/cron/loop-view.test.ts new file mode 100644 index 00000000..3c9f7d98 --- /dev/null +++ b/src/cron/loop-view.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect } from "vitest"; +import { curateConsumerLoops, prettifySchedule, MANAGED_LABEL_TO_NAME } from "./loop-view.ts"; +import type { CronJob } from "./types.ts"; + +function job(p: Partial): CronJob { + return { + id: p.id ?? p.name ?? "id", + userId: p.userId ?? "system", + name: p.name ?? "job", + schedule: p.schedule ?? "6h", + scheduleType: p.scheduleType ?? "every", + sessionTarget: "isolated", + deliveryMode: "none", + prompt: p.prompt ?? "", + enabled: p.enabled ?? true, + errorCount: p.errorCount ?? 0, + lastRun: p.lastRun, + createdAt: new Date(0), + source: p.source, + }; +} + +// Mirrors the live customer DB: 4 system loops + bundled examples (owned by +// "local", so absent from both partitions) + agent-created loops. +const SYSTEM = [ + job({ name: "auto-dream", schedule: "6h", source: "system" }), + job({ name: "style-analyze", schedule: "24h", source: "system" }), + job({ name: "graph-semantic", schedule: "6h", source: "system" }), + job({ name: "magic-docs-refresh", schedule: "1h", source: "system" }), + job({ name: "wiki-compile", schedule: "2h", source: "system" }), + job({ + name: "proactive:morning-briefing", + schedule: "0 8 * * *", + scheduleType: "cron", + source: "system", + }), +]; + +describe("curateConsumerLoops", () => { + it("surfaces only the managed system loops under friendly labels", () => { + const out = curateConsumerLoops(SYSTEM, new Set()); + const names = out.map((l) => l.name); + expect(names).toEqual(["Brain consolidation", "Writing style learning"]); + // Infra plumbing + proactive family are hidden. + for (const hidden of [ + "graph-semantic", + "magic-docs-refresh", + "wiki-compile", + "proactive:morning-briefing", + ]) { + expect(names).not.toContain(hidden); + } + }); + + it("marks managed loops source=managed so the client renders a toggle", () => { + const out = curateConsumerLoops(SYSTEM, new Set()); + expect(out.every((l) => l.source === "managed")).toBe(true); + }); + + it("folds the per-user opt-out into enabled without mutating the row", () => { + const on = curateConsumerLoops(SYSTEM, new Set()); + expect(on.find((l) => l.name === "Brain consolidation")?.enabled).toBe(true); + + const off = curateConsumerLoops(SYSTEM, new Set(["auto-dream"])); + expect(off.find((l) => l.name === "Brain consolidation")?.enabled).toBe(false); + // The other managed loop is unaffected. + expect(off.find((l) => l.name === "Writing style learning")?.enabled).toBe(true); + }); + + it("a disabled system row reads disabled even without an opt-out", () => { + const out = curateConsumerLoops( + [job({ name: "auto-dream", source: "system", enabled: false })], + new Set(), + ); + expect(out.find((l) => l.name === "Brain consolidation")?.enabled).toBe(false); + }); + + it("does not surface user/agent jobs (those are the Tasks surface)", () => { + const out = curateConsumerLoops( + [job({ name: "weekly-report", source: "agent", userId: "ba_user" })], + new Set(), + ); + // Only managed system loops by name are surfaced; an agent job is not managed. + expect(out).toHaveLength(0); + }); + + it("round-trips managed friendly labels back to real job names for toggling", () => { + expect(MANAGED_LABEL_TO_NAME.get("Brain consolidation")).toBe("auto-dream"); + expect(MANAGED_LABEL_TO_NAME.get("Writing style learning")).toBe("style-analyze"); + }); +}); + +describe("prettifySchedule", () => { + it("renders 'every' cadences", () => { + expect(prettifySchedule("6h", "every")).toBe("Every 6 hours"); + expect(prettifySchedule("1h", "every")).toBe("Hourly"); + expect(prettifySchedule("24h", "every")).toBe("Daily"); + expect(prettifySchedule("15m", "every")).toBe("Every 15 minutes"); + }); + + it("renders daily cron expressions", () => { + expect(prettifySchedule("0 8 * * *", "cron")).toBe("Daily at 8:00 AM"); + expect(prettifySchedule("30 17 * * *", "cron")).toBe("Daily at 5:30 PM"); + }); + + it("falls back to the raw string for unrecognized shapes", () => { + expect(prettifySchedule("0 9 * * 1", "cron")).toBe("0 9 * * 1"); + }); +}); diff --git a/src/cron/loop-view.ts b/src/cron/loop-view.ts new file mode 100644 index 00000000..bf71b7dd --- /dev/null +++ b/src/cron/loop-view.ts @@ -0,0 +1,73 @@ +/** + * Consumer Loops view model -- the pure shaping logic behind MobileApi.ListLoops. + * + * The instance runs its background loops as single rows owned by the synthetic + * `system` tenant, so a hosted user owns none of them and a naive per-user query + * returns nothing. The consumer Loops page is an audit + control surface, so we + * ALSO surface a curated set of the always-on "managed" loops under friendly + * labels. Pure + dependency-free so it is unit-testable in isolation. + */ + +import type { CronJob } from "./types.ts"; +import { prettifySchedule } from "./schedule-format.ts"; + +export { prettifySchedule }; + +export interface ConsumerLoop { + id: string; + name: string; + schedule: string; + enabled: boolean; + source: string; + errorCount: number; + lastRun: string; + prompt: string; +} + +/** system loop name -> friendly consumer label. Only these `system`-owned loops + * are surfaced; every other system loop (wiki/graph/magic-docs/delta-sync + the + * proactive family) is hidden from the consumer. */ +export const MANAGED_LOOPS: Record = { + "auto-dream": "Brain consolidation", + "style-analyze": "Writing style learning", +}; + +export const MANAGED_LABEL_TO_NAME = new Map( + Object.entries(MANAGED_LOOPS).map(([name, label]) => [label, name]), +); + +function toWire(j: CronJob, over: Partial = {}): ConsumerLoop { + return { + id: j.id, + name: over.name ?? j.name, + schedule: prettifySchedule(j.schedule, j.scheduleType), + enabled: over.enabled ?? j.enabled, + source: over.source ?? j.source ?? "user", + errorCount: j.errorCount, + lastRun: j.lastRun ? j.lastRun.toISOString() : "", + prompt: "", + }; +} + +/** + * Build the consumer Loops audit list: a curated, friendly-labeled managed set + * (auto-dream -> "Brain consolidation", style-analyze -> "Writing style + * learning") owned by the `system` tenant, with the per-user override folded into + * `enabled`. `optedOut` is the set of managed job NAMES the user has disabled. + * + * The user's / agent's own scheduled jobs are NOT shown here -- those are the + * Tasks surface (see cron/task-view.ts). Loops = the assistant's always-on + * background behaviors; Tasks = what you/the assistant scheduled. + */ +export function curateConsumerLoops(system: CronJob[], optedOut: Set): ConsumerLoop[] { + return system + .filter((j) => MANAGED_LOOPS[j.name]) + .map((j) => + toWire(j, { + name: MANAGED_LOOPS[j.name], + source: "managed", + enabled: j.enabled && !optedOut.has(j.name), + }), + ) + .sort((a, b) => a.name.localeCompare(b.name)); +} diff --git a/src/cron/schedule-format.ts b/src/cron/schedule-format.ts new file mode 100644 index 00000000..9e3acd8a --- /dev/null +++ b/src/cron/schedule-format.ts @@ -0,0 +1,58 @@ +/** + * Human-readable rendering of a cron_jobs schedule, shared by the consumer Loops + * and Tasks surfaces. The wire `schedule` stays the raw string (clients edit it + * and toggle/delete key off the id/name); this is display-only. + */ + +export function prettifySchedule(schedule: string, scheduleType: string): string { + const s = schedule.trim(); + + if (scheduleType === "every") { + const m = s.match(/^(\d+)\s*([smhd])$/i); + if (m) { + const n = Number(m[1]); + switch (m[2].toLowerCase()) { + case "h": + return n === 1 ? "Hourly" : n === 24 ? "Daily" : `Every ${n} hours`; + case "m": + return `Every ${n} minute${n === 1 ? "" : "s"}`; + case "d": + return n === 1 ? "Daily" : `Every ${n} days`; + case "s": + return `Every ${n} second${n === 1 ? "" : "s"}`; + } + } + return `Every ${s}`; + } + + if (scheduleType === "cron") { + const parts = s.split(/\s+/); + if (parts.length === 5) { + const [min, hour, dom, , dow] = parts; + if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === "*" && dow === "*") { + return `Daily at ${formatClock(Number(hour), min)}`; + } + } + return s; + } + + if (scheduleType === "at") { + const d = new Date(s); + if (!Number.isNaN(d.getTime())) { + return `Once, ${d.toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + })}`; + } + return s; + } + + return s; +} + +function formatClock(hour: number, min: string): string { + const h12 = hour % 12 === 0 ? 12 : hour % 12; + return `${h12}:${min.padStart(2, "0")} ${hour < 12 ? "AM" : "PM"}`; +} diff --git a/src/cron/task-view.test.ts b/src/cron/task-view.test.ts new file mode 100644 index 00000000..7f124875 --- /dev/null +++ b/src/cron/task-view.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { curateConsumerTasks, toConsumerTask } from "./task-view.ts"; +import { prettifySchedule } from "./schedule-format.ts"; +import type { CronJob } from "./types.ts"; + +function job(p: Partial): CronJob { + return { + id: p.id ?? p.name ?? "id", + userId: p.userId ?? "ba_user", + name: p.name ?? "task", + schedule: p.schedule ?? "15m", + scheduleType: p.scheduleType ?? "every", + sessionTarget: "isolated", + deliveryMode: "none", + prompt: p.prompt ?? "do the thing", + enabled: p.enabled ?? true, + errorCount: p.errorCount ?? 0, + lastRun: p.lastRun, + createdAt: new Date(0), + source: p.source ?? "agent", + }; +} + +describe("toConsumerTask", () => { + it("carries the raw schedule + type AND a friendly display string", () => { + const t = toConsumerTask(job({ name: "check-email", schedule: "15m", scheduleType: "every" })); + expect(t.schedule).toBe("15m"); + expect(t.scheduleType).toBe("every"); + expect(t.displaySchedule).toBe("Every 15 minutes"); + expect(t.prompt).toBe("do the thing"); + }); + + it("renders a one-off 'at' task time", () => { + const t = toConsumerTask(job({ schedule: "2026-06-13T17:00:00Z", scheduleType: "at" })); + expect(t.displaySchedule.startsWith("Once,")).toBe(true); + }); +}); + +describe("curateConsumerTasks", () => { + it("sorts enabled first, then alphabetical", () => { + const out = curateConsumerTasks([ + job({ name: "zebra", enabled: true }), + job({ name: "apple", enabled: false }), + job({ name: "mango", enabled: true }), + ]); + expect(out.map((t) => t.name)).toEqual(["mango", "zebra", "apple"]); + }); + + it("passes through every owned job (filtering is done by the per-user query)", () => { + const out = curateConsumerTasks([job({ name: "a" }), job({ name: "b" })]); + expect(out).toHaveLength(2); + }); +}); + +describe("prettifySchedule (shared)", () => { + it("handles every / cron / at", () => { + expect(prettifySchedule("1h", "every")).toBe("Hourly"); + expect(prettifySchedule("0 9 * * *", "cron")).toBe("Daily at 9:00 AM"); + expect(prettifySchedule("not-a-date", "at")).toBe("not-a-date"); + }); +}); diff --git a/src/cron/task-view.ts b/src/cron/task-view.ts new file mode 100644 index 00000000..b3a0261b --- /dev/null +++ b/src/cron/task-view.ts @@ -0,0 +1,48 @@ +/** + * Consumer Tasks view model -- the pure shaping behind MobileApi.ListTasks. + * + * A "task" is any cron_jobs row the user owns: one-off reminders ("at") and + * recurring jobs ("every"/"cron") that the user or the assistant scheduled on + * their behalf. The instance's always-on background loops are owned by the + * synthetic `system` tenant, so a per-user query (user_id = resolved owner) never + * includes them -- Loops and Tasks stay cleanly separate. + */ + +import type { CronJob } from "./types.ts"; +import { prettifySchedule } from "./schedule-format.ts"; + +export interface ConsumerTask { + id: string; + name: string; + prompt: string; + /** Raw schedule string (the client edits this). */ + schedule: string; + /** every | cron | at */ + scheduleType: string; + /** Friendly, display-only cadence (e.g. "Every 15 minutes", "Once, Jun 13 at 5:00 PM"). */ + displaySchedule: string; + enabled: boolean; + source: string; + lastRun: string; +} + +export function toConsumerTask(j: CronJob): ConsumerTask { + return { + id: j.id, + name: j.name, + prompt: j.prompt, + schedule: j.schedule, + scheduleType: j.scheduleType, + displaySchedule: prettifySchedule(j.schedule, j.scheduleType), + enabled: j.enabled, + source: j.source ?? "user", + lastRun: j.lastRun ? j.lastRun.toISOString() : "", + }; +} + +/** The user's scheduled tasks, enabled first then alphabetical. */ +export function curateConsumerTasks(jobs: CronJob[]): ConsumerTask[] { + return jobs + .map(toConsumerTask) + .sort((a, b) => Number(b.enabled) - Number(a.enabled) || a.name.localeCompare(b.name)); +} diff --git a/src/daemon/cron-engine.ts b/src/daemon/cron-engine.ts index 1adb4059..fbd80f89 100644 --- a/src/daemon/cron-engine.ts +++ b/src/daemon/cron-engine.ts @@ -12,6 +12,7 @@ import type { ChannelManager } from "./channel-manager.ts"; import type { AgentEvent } from "./types.ts"; import { createLogger } from "../lib/logger.ts"; import { stripHeartbeatToken } from "../auto-reply/heartbeat.ts"; +import { isLoopUserDisabled } from "../cron/loop-overrides.ts"; const log = createLogger("cron-engine"); @@ -66,6 +67,16 @@ export class CronEngine { } private async handleCronJob(job: CronJob): Promise { + // Per-user (per-customer DB) loop opt-out. The consumer Loops UI toggles a + // config override rather than mutating the shared `system` row, so honor it + // here: a managed loop the user turned off must actually stop firing. Absent + // flag = enabled (default on). Keyed generically by job name so any future + // override is covered without special-casing. + if (await isLoopUserDisabled(job.name)) { + log.info(`Skipping ${job.name}: disabled by user`); + return; + } + // Intercept delta-sync sentinel prompts -- route to ingest scheduler // instead of the agent message queue. if (job.prompt.startsWith("__delta_sync__:")) { diff --git a/src/daemon/inbox.ts b/src/daemon/inbox.ts new file mode 100644 index 00000000..321fae06 --- /dev/null +++ b/src/daemon/inbox.ts @@ -0,0 +1,125 @@ +/** + * Inbox overview -- the read model behind MobileApi.GetInbox. + * + * Two sections: (1) the agent's drafted replies awaiting the user's approval + * (`draft_messages`: pending = "needs you", approved/sent = "handled"), and + * (2) the CATE agent-to-agent inbound queue (`cate_inbound`, trust tiers + bonds). + * Owner-scoped. Draft actions reuse ApproveDraft/RejectDraft; CATE actions reuse + * ActOnInboxItem. + */ + +import type { TenantContext } from "../auth/tenant-context.ts"; +import { listPendingDrafts, type DraftRow } from "../db/drafts.ts"; +import { getKysely } from "../db/client.ts"; +import { sql } from "kysely"; + +export interface InboxDraft { + id: string; + recipient: string; + preview: string; + status: string; // pending | approved | sent + platform: string; + createdAt: string; +} +export interface InboxCate { + id: string; + fromLabel: string; + trustTier: string; + subject: string; + bondAmount: string; + createdAt: string; +} +export interface InboxOverview { + drafts: InboxDraft[]; + cate: InboxCate[]; + blockedCount: number; +} + +function draftRecipient(d: DraftRow): string { + // context is a jsonb column; the driver may hand it back as an object or as a + // raw JSON string -- handle both. + let c: Record = {}; + const raw: unknown = d.context; + if (typeof raw === "string") { + try { + c = JSON.parse(raw) as Record; + } catch { + c = {}; + } + } else if (raw && typeof raw === "object") { + c = raw as Record; + } + for (const key of ["contactName", "recipient", "to", "sender"]) { + const v = c[key]; + if (typeof v === "string" && v.trim()) return v; + } + return d.platform.charAt(0).toUpperCase() + d.platform.slice(1); +} + +function toInboxDraft(d: DraftRow): InboxDraft { + return { + id: d.id, + recipient: draftRecipient(d), + // Full draft text (the client truncates the list row; the edit sheet needs all of it). + preview: d.content, + status: d.status, + platform: d.platform, + createdAt: d.created_at.toISOString(), + }; +} + +export async function getInboxOverview(ctx: TenantContext): Promise { + const userId = ctx.userId; + const db = getKysely(); + + const pending = await listPendingDrafts(userId); + const handled = (await db + .selectFrom("draft_messages") + .selectAll() + .where("user_id", "=", userId) + .where("status", "in", ["approved", "sent"]) + .orderBy("created_at", "desc") + .limit(5) + .execute()) as unknown as DraftRow[]; + + const drafts = [...pending.map(toInboxDraft), ...handled.map(toInboxDraft)]; + + let cate: InboxCate[] = []; + let blockedCount = 0; + try { + const rows = await db.executeQuery( + sql<{ + id: string; + from_label: string | null; + trust_tier: string; + subject: string | null; + bond_amount: string | null; + created_at: Date; + }>` + SELECT id, from_label, trust_tier, subject, bond_amount::text AS bond_amount, created_at + FROM cate_inbound WHERE user_id = ${userId} AND status = 'pending' + ORDER BY created_at DESC LIMIT 50 + `.compile(db), + ); + cate = rows.rows.map((r) => ({ + id: r.id, + fromLabel: r.from_label ?? "", + trustTier: r.trust_tier, + subject: r.subject ?? "", + bondAmount: r.bond_amount ?? "", + createdAt: r.created_at.toISOString(), + })); + const blocked = await db.executeQuery( + sql<{ + count: string; + }>`SELECT COUNT(*)::text AS count FROM cate_inbound WHERE user_id = ${userId} AND status = 'denied'`.compile( + db, + ), + ); + blockedCount = Number(blocked.rows[0]?.count ?? 0); + } catch { + // pre-Phase-5b: no cate_inbound table -> drafts only. + } + + return { drafts, cate, blockedCount }; +} diff --git a/src/daemon/mobile-api.ts b/src/daemon/mobile-api.ts index 138e693d..027f6b6a 100644 --- a/src/daemon/mobile-api.ts +++ b/src/daemon/mobile-api.ts @@ -22,6 +22,8 @@ import type { DraftManager } from "./draft-manager.ts"; import type { AgentEvent, IncomingMessage } from "./types.ts"; import { getKysely } from "../db/client.ts"; import { listIntegrations } from "../db/integrations.ts"; +import { getConfigValue, setConfigValue } from "../db/config.ts"; +import { setConsentMode, listConsentModes, type ConsentMode } from "../db/consent-config.ts"; import { buildAuthUrl, exchangeCode, @@ -36,15 +38,34 @@ import { verifyOAuthState, } from "../auth/google-integration.ts"; import { loadSkills } from "../skills/loader.ts"; +import { + curateConsumerSkills, + resolveSkillName, + isConsumerSkill, + type ConsumerSkill, +} from "../skills/skill-view.ts"; +import { BUILTIN_TOOLS } from "../plugins/builtin-tools.ts"; import { getProjection, neighborhood, searchNodes } from "../memory/graph.ts"; import type { GraphNode, GraphEdge } from "../memory/graph.ts"; import { withAuthUnary, withAuthStream } from "../auth/grpc-interceptor.ts"; import { registerDevice, unregisterDevice } from "./push-notifications.ts"; import { vaultDelete, vaultList, vaultRead, vaultWrite } from "../memory/vault.ts"; import { CronStore } from "../cron/store.ts"; +import { isLoopUserDisabled, setLoopUserEnabled } from "../cron/loop-overrides.ts"; +import { + curateConsumerLoops, + MANAGED_LOOPS, + MANAGED_LABEL_TO_NAME, + type ConsumerLoop, +} from "../cron/loop-view.ts"; +import { curateConsumerTasks, type ConsumerTask } from "../cron/task-view.ts"; +import type { CronJobUpdate, ScheduleType } from "../cron/types.ts"; +import { getBrainOverview } from "../memory/brain.ts"; +import { getInboxOverview } from "./inbox.ts"; +import { getTodayOverview } from "./today.ts"; import { createLogger } from "../lib/logger.ts"; import type { TenantContext } from "../auth/tenant-context.ts"; -import { resolveMemoryUserId } from "../auth/tenant-context.ts"; +import { resolveMemoryUserId, systemTenant } from "../auth/tenant-context.ts"; import { buildStudioEngine } from "../sdk/studio-mcp.ts"; import { confirmAsset, @@ -99,6 +120,7 @@ export function buildMobileApiHandlers(deps: MobileApiDeps) { ), ListSkills: withAuthUnary("/nomos.MobileApi/ListSkills", () => handleListSkills()), ToggleSkill: withAuthUnary("/nomos.MobileApi/ToggleSkill", (call) => handleToggleSkill(call)), + ListPlugins: withAuthUnary("/nomos.MobileApi/ListPlugins", () => handleListPlugins()), GetEarnings: withAuthUnary("/nomos.MobileApi/GetEarnings", () => handleGetEarnings()), GetGraph: withAuthUnary("/nomos.MobileApi/GetGraph", (call, ctx) => handleGetGraph(call, ctx)), GetGraphNeighbors: withAuthUnary("/nomos.MobileApi/GetGraphNeighbors", (call, ctx) => @@ -117,6 +139,12 @@ export function buildMobileApiHandlers(deps: MobileApiDeps) { UpdatePermission: withAuthUnary("/nomos.MobileApi/UpdatePermission", (call) => handleUpdatePermission(call), ), + UpdateAppSetting: withAuthUnary("/nomos.MobileApi/UpdateAppSetting", (call) => + handleUpdateAppSetting(call), + ), + UpdateAgentIdentity: withAuthUnary("/nomos.MobileApi/UpdateAgentIdentity", (call) => + handleUpdateAgentIdentity(call), + ), ListIntegrations: withAuthUnary("/nomos.MobileApi/ListIntegrations", (_, ctx) => handleListIntegrations(ctx), ), @@ -158,6 +186,16 @@ export function buildMobileApiHandlers(deps: MobileApiDeps) { DeleteLoop: withAuthUnary("/nomos.MobileApi/DeleteLoop", (call, ctx) => handleDeleteLoop(call, ctx), ), + ListTasks: withAuthUnary("/nomos.MobileApi/ListTasks", (_, ctx) => handleListTasks(ctx)), + UpdateTask: withAuthUnary("/nomos.MobileApi/UpdateTask", (call, ctx) => + handleUpdateTask(call, ctx), + ), + DeleteTask: withAuthUnary("/nomos.MobileApi/DeleteTask", (call, ctx) => + handleDeleteTask(call, ctx), + ), + GetBrain: withAuthUnary("/nomos.MobileApi/GetBrain", (_, ctx) => handleGetBrain(ctx)), + GetInbox: withAuthUnary("/nomos.MobileApi/GetInbox", (_, ctx) => handleGetInbox(ctx)), + GetToday: withAuthUnary("/nomos.MobileApi/GetToday", (_, ctx) => handleGetToday(ctx)), // Studio (hosted-only feature) StudioCreateAsset: withAuthUnary("/nomos.MobileApi/StudioCreateAsset", (call, ctx) => @@ -553,43 +591,42 @@ async function handleActOnInboxItem( // ──────────── Skills ──────────── -async function handleListSkills(): Promise<{ - skills: Array<{ - name: string; - description: string; - source: string; - enabled: boolean; - certs: string[]; - price: string; - }>; -}> { - const skills = loadSkills().map((s) => ({ - name: s.name, - description: s.description ?? "", - source: s.source, - enabled: true, - certs: [] as string[], - price: "Free", - })); - return { skills }; +async function handleListSkills(): Promise<{ skills: ConsumerSkill[] }> { + const all = loadSkills(); + // Resolve each surfaced skill's persisted on/off (default on) before curating. + const enabled = new Map(); + for (const s of all) { + if (isConsumerSkill(s)) { + enabled.set(s.name, (await getConfigValue(`skill.${s.name}.enabled`)) ?? true); + } + } + return { skills: curateConsumerSkills(all, (name) => enabled.get(name) ?? true) }; } async function handleToggleSkill( call: grpc.ServerUnaryCall, ): Promise<{ success: boolean; message: string }> { - const db = getKysely(); - const key = `skill.${(call.request as any).name}.enabled`; - await db - .insertInto("config") - .values({ key, value: JSON.stringify((call.request as any).enabled) }) - .onConflict((oc) => - oc.column("key").doUpdateSet({ - value: JSON.stringify((call.request as any).enabled), - updated_at: new Date(), - }), - ) - .execute(); - return { success: true, message: (call.request as any).enabled ? "enabled" : "disabled" }; + const req = call.request as { name?: string; enabled?: boolean }; + if (!req.name) return { success: false, message: "missing_name" }; + // The client sends the friendly label; resolve it back to the raw skill name. + const name = resolveSkillName(loadSkills(), req.name); + await setConfigValue(`skill.${name}.enabled`, Boolean(req.enabled)); + return { success: true, message: req.enabled ? "enabled" : "disabled" }; +} + +/** Read-only list of the assistant's out-of-the-box capabilities. The Claude + * marketplace plugins are all developer tools, so consumers see this curated + * built-in set instead (marketplace install is a later iteration). */ +async function handleListPlugins(): Promise<{ + plugins: Array<{ name: string; description: string; marketplace: string }>; +}> { + return { + plugins: BUILTIN_TOOLS.map((t) => ({ + name: t.name, + description: t.description, + marketplace: "built-in", + })), + }; } // ──────────── Earnings (stub) ──────────── @@ -612,61 +649,175 @@ async function handleGetEarnings(): Promise<{ // ──────────── Settings ──────────── +// Catalog of consumer permissions + trust tiers. Labels/defaults are fixed; the +// actual on/off + tier mode are read back from the config table (where the +// UpdatePermission / UpdateTrustTier handlers persist them), so settings +// round-trip instead of returning literals. +const PERMISSION_DEFS = [ + { id: "p1", label: "Read emails", def: true }, + { id: "p2", label: "Draft replies", def: true }, + { id: "p3", label: "Send (with approval)", def: true }, + { id: "p4", label: "Send (auto)", def: false }, + { id: "p5", label: "Schedule meetings", def: true }, + { id: "p6", label: "Make purchases", def: false }, +] as const; + +const TRUST_TIER_DEFS = [ + { id: "friends", name: "Friends", description: "Always allowed", mode: "free", bondAmount: "" }, + { + id: "healthcare", + name: "Healthcare", + description: "Verified professionals", + mode: "free", + bondAmount: "", + }, + { + id: "brands", + name: "Brands", + description: "Min bid per impression", + mode: "bond", + bondAmount: "0.05", + }, + { + id: "unknown", + name: "Unknown", + description: "No identity = blocked", + mode: "blocked", + bondAmount: "", + }, +] as const; + async function handleGetSettings(ctx: TenantContext) { const integrations = await listIntegrationsForUser(ctx.userId); + + // Studio cloud-AI consent is stored separately (setCloudAIEnabled), so it's surfaced + // as its own permission row alongside the config-driven ones. + const permissions = [ + { id: "studio_cloud_ai", label: "Cloud AI photo edits", enabled: await isCloudAIEnabled() }, + ...(await Promise.all( + PERMISSION_DEFS.map(async (p) => ({ + id: p.id, + label: p.label, + enabled: (await getConfigValue(`permission.${p.id}`)) ?? p.def, + })), + )), + ]; + + const trustTiers = await Promise.all( + TRUST_TIER_DEFS.map(async (t) => { + const stored = await getConfigValue<{ mode?: string; bondAmount?: string }>( + `trust_tier.${t.id}`, + ); + return { + id: t.id, + name: t.name, + description: t.description, + mode: stored?.mode ?? t.mode, + bondAmount: stored?.bondAmount ?? t.bondAmount, + }; + }), + ); + + // Usage: real message count for this user; plan/name from config (consumer + // onboarding sets these; earned/saved stay 0 until CATE bond receipts land). + const messageCount = await countUserMessages(ctx.userId); + const profile = { + name: (await getConfigValue("profile.name")) ?? "", + plan: (await getConfigValue("profile.plan")) ?? "Free", + messageCount, + earnedCents: 0, + savedCents: 0, + }; + + // Per-platform consent modes (so the app's picker reflects current state). + const consentModes = await listConsentModes(); + const consent = Object.entries(consentModes).map(([platform, mode]) => ({ platform, mode })); + + // Agent identity (name + voice/tone), and the consumer behavior toggles. + const identity = { + name: (await getConfigValue("agent.name")) ?? "Nomos", + voice: (await getConfigValue("agent.soul")) ?? "", + avatar: (await getConfigValue("agent.avatar")) ?? "", + }; + const appToggles = await Promise.all( + CONSUMER_TOGGLE_KEYS.map(async (key) => ({ + key, + enabled: (await getConfigValue(key)) ?? true, + })), + ); + + // Proactive agency: autonomy level + daily-briefing schedule. + const proactive = { + mode: (await getConfigValue("app.inboxAutonomy")) ?? "passive", + briefing: (await getConfigValue("app.briefingCron")) ?? "", + }; + return { - profile: { - name: "Nomos", - plan: "Pro", - messageCount: 0, - earnedCents: 0, - savedCents: 0, - }, - trustTiers: [ - { - id: "friends", - name: "Friends", - description: "Always allowed", - mode: "free", - bondAmount: "", - }, - { - id: "healthcare", - name: "Healthcare", - description: "Verified professionals", - mode: "free", - bondAmount: "", - }, - { - id: "brands", - name: "Brands", - description: "Min bid per impression", - mode: "bond", - bondAmount: "0.05", - }, - { - id: "unknown", - name: "Unknown", - description: "No identity = blocked", - mode: "blocked", - bondAmount: "", - }, - ], - permissions: [ - // Studio cloud-AI consent: a real toggle (the iOS app surfaces it as its own - // "Cloud AI" row) plumbed through UpdatePermission → setCloudAIEnabled. - { id: "studio_cloud_ai", label: "Cloud AI photo edits", enabled: await isCloudAIEnabled() }, - { id: "p1", label: "Read emails", enabled: true }, - { id: "p2", label: "Draft replies", enabled: true }, - { id: "p3", label: "Send (with approval)", enabled: true }, - { id: "p4", label: "Send (auto)", enabled: false }, - { id: "p5", label: "Schedule meetings", enabled: true }, - { id: "p6", label: "Make purchases", enabled: false }, - ], + profile, + trustTiers, + permissions, integrations, + consent, + identity, + appToggles, + proactive, }; } +/** App-config keys the mobile app may read/flip (consumer-safe bool toggles). */ +const CONSUMER_TOGGLE_KEYS = [ + "app.adaptiveMemory", + "app.commitmentTracking", + "app.styleMatching", +] as const; + +/** Consumer-safe string app-config keys (proactive scheduling, DM policy). */ +const CONSUMER_STRING_KEYS = [ + "app.inboxAutonomy", + "app.briefingCron", + "app.defaultDmPolicy", +] as const; + +async function handleUpdateAppSetting( + call: grpc.ServerUnaryCall, +): Promise<{ success: boolean; message: string }> { + const req = call.request as { key?: string; value?: string }; + if (req.key && (CONSUMER_TOGGLE_KEYS as readonly string[]).includes(req.key)) { + await setConfigValue(req.key, req.value === "true"); + return { success: true, message: "ok" }; + } + if (req.key && (CONSUMER_STRING_KEYS as readonly string[]).includes(req.key)) { + await setConfigValue(req.key, req.value ?? ""); + return { success: true, message: "ok" }; + } + return { success: false, message: "key_not_allowed" }; +} + +async function handleUpdateAgentIdentity( + call: grpc.ServerUnaryCall, +): Promise<{ success: boolean; message: string }> { + const req = call.request as { name?: string; voice?: string; avatar?: string }; + if (req.name !== undefined) await setConfigValue("agent.name", req.name); + if (req.voice !== undefined) await setConfigValue("agent.soul", req.voice); + if (req.avatar !== undefined) await setConfigValue("agent.avatar", req.avatar); + return { success: true, message: "ok" }; +} + +/** Count transcript messages belonging to this user (best-effort; 0 on error). */ +async function countUserMessages(userId: string): Promise { + try { + const db = getKysely(); + const row = await db + .selectFrom("transcript_messages") + .select((eb) => eb.fn.countAll().as("n")) + .where("user_id", "=", userId) + .executeTakeFirst(); + return row ? Number(row.n) : 0; + } catch { + return 0; + } +} + async function setConfigKey(key: string, value: unknown): Promise { const db = getKysely(); await db @@ -684,8 +835,16 @@ async function setConfigKey(key: string, value: unknown): Promise { async function handleUpdateConsent( call: grpc.ServerUnaryCall, ): Promise<{ success: boolean; message: string }> { - await setConfigKey(`consent.${(call.request as any).platform}`, (call.request as any).mode); - return { success: true, message: "ok" }; + const req = call.request as { platform?: string; mode?: string }; + if (!req.platform) return { success: false, message: "missing_platform" }; + try { + // Writes `consent.mode.` (what DraftManager reads) and validates + // against always_ask / auto_approve / notify_only. + await setConsentMode(req.platform, req.mode as ConsentMode); + return { success: true, message: "ok" }; + } catch (err) { + return { success: false, message: err instanceof Error ? err.message : "invalid_mode" }; + } } async function handleUpdateTrustTier( @@ -886,38 +1045,30 @@ async function handleDeleteVaultNote( } // ──────────── Loops (autonomous recurring jobs) ──────────── -// Owner-scoped via the JWT-resolved user. System infra jobs are never mutable -// here (they belong to the daemon); these handlers run in the daemon process, so -// they emit cron:refresh directly to apply a change live. +// The instance runs its background loops under the synthetic `system` owner +// (see gateway.ts + proactive/scheduler.ts), so a hosted user owns none of them +// and a naive per-user query returns nothing. The consumer Loops page is an +// audit + control surface, so we ALSO surface a curated set of the always-on +// "managed" loops the assistant runs on the user's behalf -- under a friendly +// label, with a per-user enable/disable override. Those rows are permanently +// enabled, so the override (an AND-gate honored by cron-engine at fire time) can +// meaningfully turn them off without mutating the shared `system` row. Pure infra +// plumbing (wiki/graph/magic-docs/delta-sync) and the proactive family (gated by +// the Proactive setting) are hidden. User/agent-created loops show under their +// real name and toggle the row directly. + +async function handleListLoops(_ctx: TenantContext): Promise<{ loops: ConsumerLoop[] }> { + // Loops = the assistant's always-on background behaviors (owned by the `system` + // tenant). The user's own scheduled jobs live on the Tasks surface, not here. + const system = await new CronStore().listJobs({ userId: systemTenant().userId }); + + // Which managed loops the user has turned off (folded into `enabled`). + const optedOut = new Set(); + for (const j of system) { + if (MANAGED_LOOPS[j.name] && (await isLoopUserDisabled(j.name))) optedOut.add(j.name); + } -async function handleListLoops(ctx: TenantContext): Promise<{ - loops: Array<{ - id: string; - name: string; - schedule: string; - enabled: boolean; - source: string; - errorCount: number; - lastRun: string; - prompt: string; - }>; -}> { - const userId = resolveMemoryUserId(ctx.userId); - const jobs = await new CronStore().listJobs({ userId }); - return { - loops: jobs - .sort((a, b) => a.name.localeCompare(b.name)) - .map((j) => ({ - id: j.id, - name: j.name, - schedule: j.schedule, - enabled: j.enabled, - source: j.source ?? "system", - errorCount: j.errorCount, - lastRun: j.lastRun ? j.lastRun.toISOString() : "", - prompt: j.prompt, - })), - }; + return { loops: curateConsumerLoops(system, optedOut) }; } async function handleSetLoopEnabled( @@ -926,6 +1077,17 @@ async function handleSetLoopEnabled( ): Promise<{ success: boolean; message: string }> { const req = call.request as { name?: string; enabled?: boolean }; if (!req.name) return { success: false, message: "missing_name" }; + + // Managed loop: the client sends the friendly label. Persist a per-user + // override (honored by cron-engine at fire time) instead of mutating the + // shared `system` row. + const managedName = MANAGED_LABEL_TO_NAME.get(req.name); + if (managedName) { + await setLoopUserEnabled(managedName, Boolean(req.enabled)); + process.emit("cron:refresh" as never); + return { success: true, message: req.enabled ? "enabled" : "disabled" }; + } + const userId = resolveMemoryUserId(ctx.userId); const store = new CronStore(); const job = await store.getJobByName(req.name); @@ -952,6 +1114,96 @@ async function handleDeleteLoop( return { success: true, message: "deleted" }; } +// ──────────── Tasks (the user's scheduled tasks) ──────────── +// Owner-scoped by user_id: a per-user query never returns the instance's +// `system`-owned background loops, so Tasks and Loops stay cleanly separate. +// Update/Delete additionally assert ownership before mutating. + +async function handleListTasks(ctx: TenantContext): Promise<{ tasks: ConsumerTask[] }> { + const userId = resolveMemoryUserId(ctx.userId); + const jobs = await new CronStore().listJobs({ userId }); + return { tasks: curateConsumerTasks(jobs) }; +} + +async function handleUpdateTask( + call: grpc.ServerUnaryCall, + ctx: TenantContext, +): Promise<{ success: boolean; message: string }> { + const req = call.request as { + id?: string; + name?: string; + prompt?: string; + schedule?: string; + scheduleType?: string; + enabled?: boolean; + }; + if (!req.id) return { success: false, message: "missing_id" }; + const userId = resolveMemoryUserId(ctx.userId); + const store = new CronStore(); + const job = await store.getJob(req.id); + if (!job || job.userId !== userId) return { success: false, message: "task_not_found" }; + + // Full-state update from the editor; empty name/schedule are ignored so a + // toggle-only call (which still sends the whole proto) never blanks a field. + const updates: CronJobUpdate = { enabled: Boolean(req.enabled) }; + if (req.name?.trim()) updates.name = req.name.trim(); + if (req.prompt?.trim()) updates.prompt = req.prompt; + if (req.schedule?.trim()) { + updates.schedule = req.schedule.trim(); + updates.scheduleType = (req.scheduleType as ScheduleType) || job.scheduleType; + } + await store.updateJob(job.id, updates); + process.emit("cron:refresh" as never); + return { success: true, message: "updated" }; +} + +async function handleDeleteTask( + call: grpc.ServerUnaryCall, + ctx: TenantContext, +): Promise<{ success: boolean; message: string }> { + const req = call.request as { id?: string }; + if (!req.id) return { success: false, message: "missing_id" }; + const userId = resolveMemoryUserId(ctx.userId); + const store = new CronStore(); + const job = await store.getJob(req.id); + if (!job || job.userId !== userId) return { success: false, message: "task_not_found" }; + await store.deleteJob(job.id); + process.emit("cron:refresh" as never); + return { success: true, message: "deleted" }; +} + +// ──────────── Brain (knowledge graph + learned facts) ──────────── + +async function handleGetBrain(ctx: TenantContext): Promise<{ + nodes: Array<{ + id: string; + label: string; + kind: string; + summary: string; + degree: number; + confidence: number; + }>; + edges: Array<{ src: string; dst: string; relation: string }>; + facts: Array<{ text: string; source: string; confidence: number; learnedAt: string }>; + entityCount: number; + factCount: number; +}> { + // The graph + user_model are already owner-scoped by the TenantContext. + const overview = await getBrainOverview({ + orgId: ctx.orgId, + userId: resolveMemoryUserId(ctx.userId), + }); + return overview; +} + +async function handleGetInbox(ctx: TenantContext) { + return getInboxOverview({ orgId: ctx.orgId, userId: resolveMemoryUserId(ctx.userId) }); +} + +async function handleGetToday(ctx: TenantContext) { + return getTodayOverview({ orgId: ctx.orgId, userId: resolveMemoryUserId(ctx.userId) }); +} + // ──────────── Studio (hosted-only feature) ──────────── // Blobs move via presigned PUT/GET, never gRPC. Every handler is user_id-scoped // through the authenticated TenantContext. diff --git a/src/daemon/today.ts b/src/daemon/today.ts new file mode 100644 index 00000000..34c90711 --- /dev/null +++ b/src/daemon/today.ts @@ -0,0 +1,126 @@ +/** + * Today overview -- the read model behind MobileApi.GetToday. + * + * Composes the day's brief from: today's Google Calendar events (live, best-effort + * -- empty when Google isn't connected), pending commitments, and the user's + * scheduled tasks. Gated on the daily briefing: when proactive autonomy is off, + * `briefingEnabled` is false and the client shows a deep-link to enable it. + */ + +import type { TenantContext } from "../auth/tenant-context.ts"; +import { getConfigValue } from "../db/config.ts"; +import { getPendingCommitments } from "../proactive/commitment-tracker.ts"; +import { CronStore } from "../cron/store.ts"; +import { curateConsumerTasks } from "../cron/task-view.ts"; +import { gapiFetch } from "../sdk/google-rest-mcp.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("today"); +const CALENDAR_API = "https://www.googleapis.com/calendar/v3"; + +export interface TodayEvent { + time: string; + title: string; + meta: string; +} +export interface TodayCommitment { + id: string; + description: string; + due: string; +} +export interface TodayTask { + id: string; + name: string; + schedule: string; +} +export interface TodayOverview { + briefingEnabled: boolean; + events: TodayEvent[]; + commitments: TodayCommitment[]; + tasks: TodayTask[]; +} + +export async function getTodayOverview(ctx: TenantContext): Promise { + const userId = ctx.userId; + // The Today brief is the in-app face of the daily briefing; it's "on" whenever + // proactive autonomy is on (default passive). Off -> the client shows the CTA. + const mode = (await getConfigValue("app.inboxAutonomy")) ?? "passive"; + const briefingEnabled = mode !== "off"; + + const [events, commitmentRows, jobs] = await Promise.all([ + fetchTodayEvents(userId), + getPendingCommitments(userId).catch(() => []), + new CronStore().listJobs({ userId }), + ]); + + const commitments: TodayCommitment[] = commitmentRows.slice(0, 8).map((c) => ({ + id: c.id, + description: c.description, + due: c.deadline ? relativeDay(c.deadline) : "", + })); + + const tasks: TodayTask[] = curateConsumerTasks(jobs) + .filter((t) => t.enabled) + .slice(0, 6) + .map((t) => ({ id: t.id, name: t.name, schedule: t.displaySchedule })); + + return { briefingEnabled, events, commitments, tasks }; +} + +async function fetchTodayEvents(userId: string): Promise { + try { + const start = new Date(); + start.setHours(0, 0, 0, 0); + const end = new Date(); + end.setHours(23, 59, 59, 999); + const data = (await gapiFetch({ + userId, + method: "GET", + url: `${CALENDAR_API}/calendars/primary/events`, + query: { + timeMin: start.toISOString(), + timeMax: end.toISOString(), + singleEvents: true, + orderBy: "startTime", + maxResults: 12, + }, + })) as { + items?: Array<{ + summary?: string; + location?: string; + start?: { dateTime?: string; date?: string }; + }>; + }; + return (data.items ?? []).map((e) => ({ + time: formatEventTime(e.start), + title: e.summary ?? "(busy)", + meta: e.location ?? "", + })); + } catch (err) { + // No connected Google account / revoked token -> no calendar section. + log.debug({ err: err instanceof Error ? err.message : err }, "today: no calendar events"); + return []; + } +} + +function formatEventTime(start?: { dateTime?: string; date?: string }): string { + if (start?.date) return "All day"; + if (!start?.dateTime) return ""; + return new Date(start.dateTime).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + }); +} + +function relativeDay(deadline: Date): string { + const dayMs = 86_400_000; + const d0 = new Date(deadline); + d0.setHours(0, 0, 0, 0); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const diff = Math.round((d0.getTime() - today.getTime()) / dayMs); + if (diff < 0) return "Overdue"; + if (diff === 0) return "Today"; + if (diff === 1) return "Tomorrow"; + return new Date(deadline).toLocaleDateString("en-US", { month: "short", day: "numeric" }); +} diff --git a/src/gen/nomos_pb.ts b/src/gen/nomos_pb.ts index 0c4ec29b..53fd9cd8 100644 --- a/src/gen/nomos_pb.ts +++ b/src/gen/nomos_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file nomos.proto. */ export const file_nomos: GenFile /*@__PURE__*/ = fileDesc( - "Cgtub21vcy5wcm90bxIFbm9tb3MiBwoFRW1wdHkijgEKCExvb3BJbmZvEgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSEAoIc2NoZWR1bGUYAyABKAkSDwoHZW5hYmxlZBgEIAEoCBIOCgZzb3VyY2UYBSABKAkSEwoLZXJyb3JfY291bnQYBiABKAUSEAoIbGFzdF9ydW4YByABKAkSDgoGcHJvbXB0GAggASgJIioKCExvb3BMaXN0Eh4KBWxvb3BzGAEgAygLMg8ubm9tb3MuTG9vcEluZm8iNgoVU2V0TG9vcEVuYWJsZWRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHZW5hYmxlZBgCIAEoCCIhChFMb29wRGVsZXRlUmVxdWVzdBIMCgRuYW1lGAEgASgJIjYKEkxvb3BBY3Rpb25SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiMwoLQ2hhdFJlcXVlc3QSDwoHY29udGVudBgBIAEoCRITCgtzZXNzaW9uX2tleRgCIAEoCSIwCgpBZ2VudEV2ZW50EgwKBHR5cGUYASABKAkSFAoManNvbl9wYXlsb2FkGAIgASgJIjYKDkNvbW1hbmRSZXF1ZXN0Eg8KB2NvbW1hbmQYASABKAkSEwoLc2Vzc2lvbl9rZXkYAiABKAkiMwoPQ29tbWFuZFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJPCg5TdGF0dXNSZXNwb25zZRIPCgdydW5uaW5nGAEgASgIEhkKEWNvbm5lY3RlZF9jbGllbnRzGAIgASgFEhEKCXBsYXRmb3JtcxgDIAMoCSIlCg5TZXNzaW9uUmVxdWVzdBITCgtzZXNzaW9uX2tleRgBIAEoCSJVCg9TZXNzaW9uUmVzcG9uc2USCgoCaWQYASABKAkSEwoLc2Vzc2lvbl9rZXkYAiABKAkSDQoFbW9kZWwYAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoCSI3CgtTZXNzaW9uTGlzdBIoCghzZXNzaW9ucxgBIAMoCzIWLm5vbW9zLlNlc3Npb25SZXNwb25zZSIfCgtEcmFmdEFjdGlvbhIQCghkcmFmdF9pZBgBIAEoCSIxCg1EcmFmdFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSItCglEcmFmdExpc3QSIAoGZHJhZnRzGAEgAygLMhAubm9tb3MuRHJhZnRJdGVtInIKCURyYWZ0SXRlbRIKCgJpZBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCHBsYXRmb3JtGAMgASgJEhIKCmNoYW5uZWxfaWQYBCABKAkSDgoGc3RhdHVzGAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAkiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyKLAQoFTUxvb3ASCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghzY2hlZHVsZRgDIAEoCRIPCgdlbmFibGVkGAQgASgIEg4KBnNvdXJjZRgFIAEoCRITCgtlcnJvcl9jb3VudBgGIAEoBRIQCghsYXN0X3J1bhgHIAEoCRIOCgZwcm9tcHQYCCABKAkiLQoOTUxvb3BzUmVzcG9uc2USGwoFbG9vcHMYASADKAsyDC5ub21vcy5NTG9vcCI3ChZNU2V0TG9vcEVuYWJsZWRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHZW5hYmxlZBgCIAEoCCIiChJNTG9vcERlbGV0ZVJlcXVlc3QSDAoEbmFtZRgBIAEoCSIoCgRNQWNrEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSIjChFNVmF1bHRMaXN0UmVxdWVzdBIOCgZwcmVmaXgYASABKAkiRAoRTVZhdWx0Tm90ZVN1bW1hcnkSDAoEcGF0aBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgp1cGRhdGVkX2F0GAMgASgJIj0KEk1WYXVsdExpc3RSZXNwb25zZRInCgVub3RlcxgBIAMoCzIYLm5vbW9zLk1WYXVsdE5vdGVTdW1tYXJ5IiAKEE1WYXVsdEdldFJlcXVlc3QSDAoEcGF0aBgBIAEoCSJeCgpNVmF1bHROb3RlEgwKBHBhdGgYASABKAkSDQoFdGl0bGUYAiABKAkSDwoHY29udGVudBgDIAEoCRISCgp1cGRhdGVkX2F0GAQgASgJEg4KBmV4aXN0cxgFIAEoCCJCChJNVmF1bHRXcml0ZVJlcXVlc3QSDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEg0KBXRpdGxlGAMgASgJIiMKE01WYXVsdERlbGV0ZVJlcXVlc3QSDAoEcGF0aBgBIAEoCSI0CgxNQ2hhdFJlcXVlc3QSDwoHY29udGVudBgBIAEoCRITCgtzZXNzaW9uX2tleRgCIAEoCSIwCgpNQ2hhdEV2ZW50EgwKBHR5cGUYASABKAkSFAoManNvbl9wYXlsb2FkGAIgASgJIkwKE01HZXRNZXNzYWdlc1JlcXVlc3QSEwoLc2Vzc2lvbl9rZXkYASABKAkSDQoFbGltaXQYAiABKAUSEQoJYmVmb3JlX2lkGAMgASgJIkkKCE1NZXNzYWdlEgoKAmlkGAEgASgJEgwKBHJvbGUYAiABKAkSDwoHY29udGVudBgDIAEoCRISCgpjcmVhdGVkX2F0GAQgASgJIjkKFE1HZXRNZXNzYWdlc1Jlc3BvbnNlEiEKCG1lc3NhZ2VzGAEgAygLMg8ubm9tb3MuTU1lc3NhZ2UiIAoMTURyYWZ0QWN0aW9uEhAKCGRyYWZ0X2lkGAEgASgJIj0KFE1EcmFmdEFjdGlvbldpdGhFZGl0EhAKCGRyYWZ0X2lkGAEgASgJEhMKC2VkaXRlZF90ZXh0GAIgASgJIjgKFE1EcmFmdEFjdGlvblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSIuCg1NSW5ib3hSZXF1ZXN0Eg4KBnN0YXR1cxgBIAEoCRINCgVsaW1pdBgCIAEoBSKYAQoKTUluYm94SXRlbRIKCgJpZBgBIAEoCRISCgpmcm9tX2xhYmVsGAIgASgJEhIKCnRydXN0X3RpZXIYAyABKAkSDwoHc3ViamVjdBgEIAEoCRIMCgR0aW1lGAUgASgJEhMKC2JvbmRfYW1vdW50GAYgASgJEg4KBnVucmVhZBgHIAEoCBISCgpjcmVhdGVkX2F0GAggASgJIkkKDk1JbmJveFJlc3BvbnNlEiAKBWl0ZW1zGAEgAygLMhEubm9tb3MuTUluYm94SXRlbRIVCg1ibG9ja2VkX2NvdW50GAIgASgFIiQKEE1FbnZlbG9wZVJlcXVlc3QSEAoIaW5ib3hfaWQYASABKAkijQEKDU1DYXRlRW52ZWxvcGUSCwoDZGlkGAEgASgJEhIKCnRydXN0X3RpZXIYAiABKAkSDgoGaW50ZW50GAMgASgJEhUKDWNvbnNlbnRfZ3JhbnQYBCABKAkSDQoFc3RhbXAYBSABKAkSEwoLYm9uZF9hbW91bnQYBiABKAkSEAoIcmF3X2pzb24YByABKAkiNwoTTUluYm94QWN0aW9uUmVxdWVzdBIQCghpbmJveF9pZBgBIAEoCRIOCgZhY3Rpb24YAiABKAkiOAoUTUluYm94QWN0aW9uUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBIPCgdtZXNzYWdlGAIgASgJImoKBk1Ta2lsbBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg4KBnNvdXJjZRgDIAEoCRIPCgdlbmFibGVkGAQgASgIEg0KBWNlcnRzGAUgAygJEg0KBXByaWNlGAYgASgJIjAKD01Ta2lsbHNSZXNwb25zZRIdCgZza2lsbHMYASADKAsyDS5ub21vcy5NU2tpbGwiNAoTTVNraWxsVG9nZ2xlUmVxdWVzdBIMCgRuYW1lGAEgASgJEg8KB2VuYWJsZWQYAiABKAgiOAoUTVNraWxsVG9nZ2xlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBIPCgdtZXNzYWdlGAIgASgJIiIKEE1FYXJuaW5nc1JlcXVlc3QSDgoGcGVyaW9kGAEgASgJIooBChFNRWFybmluZ3NSZXNwb25zZRIZChF0aGlzX3BlcmlvZF9jZW50cxgBIAEoAxITCgtib25kc19jb3VudBgCIAEoAxIWCg5hdmdfYm9uZF9jZW50cxgDIAEoAxIXCg9hY2NlcHRfcmF0ZV9wY3QYBCABKAUSFAoMc2VyaWVzX2NlbnRzGAUgAygDIi0KDU1HcmFwaFJlcXVlc3QSDQoFa2luZHMYASADKAkSDQoFbGltaXQYAiABKAUiXgoWTUdyYXBoTmVpZ2hib3JzUmVxdWVzdBIPCgdub2RlX2lkGAEgASgJEg0KBWRlcHRoGAIgASgFEhEKCXJlbF90eXBlcxgDIAMoCRIRCglkaXJlY3Rpb24YBCABKAkiMwoTTUdyYXBoU2VhcmNoUmVxdWVzdBINCgVxdWVyeRgBIAEoCRINCgVsaW1pdBgCIAEoBSKXAQoKTUdyYXBoTm9kZRIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEgwKBG5hbWUYAyABKAkSDwoHYWxpYXNlcxgEIAMoCRIPCgdzdW1tYXJ5GAUgASgJEhIKCmNvbmZpZGVuY2UYBiABKAESFQoNZXh0ZXJuYWxfa2luZBgHIAEoCRIUCgxleHRlcm5hbF9yZWYYCCABKAkiaAoKTUdyYXBoRWRnZRIKCgJpZBgBIAEoCRIOCgZzcmNfaWQYAiABKAkSDgoGZHN0X2lkGAMgASgJEhAKCHJlbF90eXBlGAQgASgJEgwKBGZhY3QYBSABKAkSDgoGd2VpZ2h0GAYgASgBIlQKDk1HcmFwaFJlc3BvbnNlEiAKBW5vZGVzGAEgAygLMhEubm9tb3MuTUdyYXBoTm9kZRIgCgVlZGdlcxgCIAMoCzIRLm5vbW9zLk1HcmFwaEVkZ2UiXgoKTVRydXN0VGllchIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEgwKBG1vZGUYBCABKAkSEwoLYm9uZF9hbW91bnQYBSABKAkiOQoLTVBlcm1pc3Npb24SCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSDwoHZW5hYmxlZBgDIAEoCCKJAQoMTUludGVncmF0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEgwKBGljb24YAyABKAkSEQoJY29ubmVjdGVkGAQgASgIEhUKDWFjY291bnRfZW1haWwYBSABKAkSFAoMc2VuZF9lbmFibGVkGAYgASgIEhAKCHByb3ZpZGVyGAcgASgJImgKCE1Qcm9maWxlEgwKBG5hbWUYASABKAkSDAoEcGxhbhgCIAEoCRIVCg1tZXNzYWdlX2NvdW50GAMgASgDEhQKDGVhcm5lZF9jZW50cxgEIAEoAxITCgtzYXZlZF9jZW50cxgFIAEoAyKxAQoRTVNldHRpbmdzUmVzcG9uc2USIAoHcHJvZmlsZRgBIAEoCzIPLm5vbW9zLk1Qcm9maWxlEiYKC3RydXN0X3RpZXJzGAIgAygLMhEubm9tb3MuTVRydXN0VGllchInCgtwZXJtaXNzaW9ucxgDIAMoCzISLm5vbW9zLk1QZXJtaXNzaW9uEikKDGludGVncmF0aW9ucxgEIAMoCzITLm5vbW9zLk1JbnRlZ3JhdGlvbiIxCg9NQ29uc2VudFJlcXVlc3QSEAoIcGxhdGZvcm0YASABKAkSDAoEbW9kZRgCIAEoCSJCChFNVHJ1c3RUaWVyUmVxdWVzdBIKCgJpZBgBIAEoCRIMCgRtb2RlGAIgASgJEhMKC2JvbmRfYW1vdW50GAMgASgJIjEKEk1QZXJtaXNzaW9uUmVxdWVzdBIKCgJpZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIIkIKFU1JbnRlZ3JhdGlvbnNSZXNwb25zZRIpCgxpbnRlZ3JhdGlvbnMYASADKAsyEy5ub21vcy5NSW50ZWdyYXRpb24iKAoUTVN0YXJ0Q29ubmVjdFJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoVTVN0YXJ0Q29ubmVjdFJlc3BvbnNlEhEKCW9hdXRoX3VybBgBIAEoCSJDChJNRGlzY29ubmVjdFJlcXVlc3QSFgoOaW50ZWdyYXRpb25faWQYASABKAkSFQoNYWNjb3VudF9lbWFpbBgCIAEoCSI0ChVNQ29ubmVjdEdvb2dsZVJlcXVlc3QSDAoEY29kZRgBIAEoCRINCgVzdGF0ZRgCIAEoCSI/ChVNU2V0R29vZ2xlU2VuZFJlcXVlc3QSFQoNYWNjb3VudF9lbWFpbBgBIAEoCRIPCgdlbmFibGVkGAIgASgIIlEKD01EZXZpY2VSZWdpc3RlchIXCg9leHBvX3B1c2hfdG9rZW4YASABKAkSEAoIcGxhdGZvcm0YAiABKAkSEwoLYXBwX3ZlcnNpb24YAyABKAkiLAoRTURldmljZVVucmVnaXN0ZXISFwoPZXhwb19wdXNoX3Rva2VuGAEgASgJIuwBCg5EZXBvc2l0UmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIPCgd1c2VyX2lkGAIgASgJEhQKDGFjY2Vzc190b2tlbhgDIAEoCRIVCg1yZWZyZXNoX3Rva2VuGAQgASgJEhIKCmV4cGlyZXNfYXQYBSABKAMSDgoGc2NvcGVzGAYgASgJEjUKCG1ldGFkYXRhGAcgAygLMiMubm9tb3MuRGVwb3NpdFJlcXVlc3QuTWV0YWRhdGFFbnRyeRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiSwoPRGVwb3NpdFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCRIWCg5pbnRlZ3JhdGlvbl9pZBgDIAEoCSJtChlNU3R1ZGlvQ3JlYXRlQXNzZXRSZXF1ZXN0EgwKBG1pbWUYASABKAkSFAoMY29udGVudF9oYXNoGAIgASgJEg0KBXdpZHRoGAMgASgFEg4KBmhlaWdodBgEIAEoBRINCgVieXRlcxgFIAEoBSJqChpNU3R1ZGlvQ3JlYXRlQXNzZXRSZXNwb25zZRIQCghhc3NldF9pZBgBIAEoCRISCgp1cGxvYWRfdXJsGAIgASgJEhIKCm9iamVjdF9rZXkYAyABKAkSEgoKZXhwaXJlc19hdBgEIAEoAyI1Cg9NU3R1ZGlvQXNzZXRSZWYSEAoIYXNzZXRfaWQYASABKAkSEAoIb3JpZ2luYWwYAiABKAgiOgoXTVN0dWRpb0Fzc2V0VXJsUmVzcG9uc2USCwoDdXJsGAEgASgJEhIKCmV4cGlyZXNfYXQYAiABKAMinwEKEk1TdHVkaW9FZGl0UmVxdWVzdBIQCghhc3NldF9pZBgBIAEoCRIKCgJvcBgCIAEoCRITCgtwYXJhbXNfanNvbhgDIAEoCRIWCg5wYXJlbnRfZWRpdF9pZBgEIAEoCRIXCg9pZGVtcG90ZW5jeV9rZXkYBSABKAkSEAoIbWFza19rZXkYBiABKAkSEwoLaW5wdXRfaW1hZ2UYByABKAwiiQEKDE1TdHVkaW9FdmVudBIMCgRraW5kGAEgASgJEg8KB2VkaXRfaWQYAiABKAkSDgoGc3RhdHVzGAMgASgJEhMKC3ByZXZpZXdfa2V5GAQgASgJEhIKCm91dHB1dF9rZXkYBSABKAkSEAoIY29zdF91c2QYBiABKAESDwoHbWVzc2FnZRgHIAEoCSKcAQoLTVN0dWRpb0VkaXQSCgoCaWQYASABKAkSCgoCb3AYAiABKAkSDgoGc3RhdHVzGAMgASgJEhMKC3ByZXZpZXdfa2V5GAQgASgJEhIKCm91dHB1dF9rZXkYBSABKAkSEAoIY29zdF91c2QYBiABKAESFgoOcGFyZW50X2VkaXRfaWQYByABKAkSEgoKY3JlYXRlZF9hdBgIIAEoCSJRChZNU3R1ZGlvSGlzdG9yeVJlc3BvbnNlEiEKBWVkaXRzGAEgAygLMhIubm9tb3MuTVN0dWRpb0VkaXQSFAoMaGVhZF9lZGl0X2lkGAIgASgJIjcKFU1TdHVkaW9JZGVudGl0eVJlcG9ydBIPCgdlZGl0X2lkGAEgASgJEg0KBXNjb3JlGAIgASgBIikKGE1TdHVkaW9MaXN0QXNzZXRzUmVxdWVzdBINCgVsaW1pdBgBIAEoBSKcAQoTTVN0dWRpb0Fzc2V0U3VtbWFyeRIQCghhc3NldF9pZBgBIAEoCRITCgtwcmV2aWV3X3VybBgCIAEoCRISCgp1cGRhdGVkX2F0GAMgASgDEhEKCWZpbmFsaXplZBgEIAEoCBISCgplZGl0X2NvdW50GAUgASgFEg8KB2hlYWRfb3AYBiABKAkSEgoKZXhwaXJlc19hdBgHIAEoAyJHChlNU3R1ZGlvTGlzdEFzc2V0c1Jlc3BvbnNlEioKBmFzc2V0cxgBIAMoCzIaLm5vbW9zLk1TdHVkaW9Bc3NldFN1bW1hcnkiMgoRTVN0dWRpb1N1Z2dlc3Rpb24SDQoFbGFiZWwYASABKAkSDgoGcHJvbXB0GAIgASgJIksKGk1TdHVkaW9TdWdnZXN0aW9uc1Jlc3BvbnNlEi0KC3N1Z2dlc3Rpb25zGAEgAygLMhgubm9tb3MuTVN0dWRpb1N1Z2dlc3Rpb24yngUKCk5vbW9zQWdlbnQSLwoEQ2hhdBISLm5vbW9zLkNoYXRSZXF1ZXN0GhEubm9tb3MuQWdlbnRFdmVudDABEjgKB0NvbW1hbmQSFS5ub21vcy5Db21tYW5kUmVxdWVzdBoWLm5vbW9zLkNvbW1hbmRSZXNwb25zZRIwCglHZXRTdGF0dXMSDC5ub21vcy5FbXB0eRoVLm5vbW9zLlN0YXR1c1Jlc3BvbnNlEjAKDExpc3RTZXNzaW9ucxIMLm5vbW9zLkVtcHR5GhIubm9tb3MuU2Vzc2lvbkxpc3QSOwoKR2V0U2Vzc2lvbhIVLm5vbW9zLlNlc3Npb25SZXF1ZXN0GhYubm9tb3MuU2Vzc2lvblJlc3BvbnNlEiwKCkxpc3REcmFmdHMSDC5ub21vcy5FbXB0eRoQLm5vbW9zLkRyYWZ0TGlzdBI4CgxBcHByb3ZlRHJhZnQSEi5ub21vcy5EcmFmdEFjdGlvbhoULm5vbW9zLkRyYWZ0UmVzcG9uc2USNwoLUmVqZWN0RHJhZnQSEi5ub21vcy5EcmFmdEFjdGlvbhoULm5vbW9zLkRyYWZ0UmVzcG9uc2USKgoJTGlzdExvb3BzEgwubm9tb3MuRW1wdHkaDy5ub21vcy5Mb29wTGlzdBJJCg5TZXRMb29wRW5hYmxlZBIcLm5vbW9zLlNldExvb3BFbmFibGVkUmVxdWVzdBoZLm5vbW9zLkxvb3BBY3Rpb25SZXNwb25zZRJBCgpEZWxldGVMb29wEhgubm9tb3MuTG9vcERlbGV0ZVJlcXVlc3QaGS5ub21vcy5Mb29wQWN0aW9uUmVzcG9uc2USKQoEUGluZxIMLm5vbW9zLkVtcHR5GhMubm9tb3MuUG9uZ1Jlc3BvbnNlMokUCglNb2JpbGVBcGkSMAoEQ2hhdBITLm5vbW9zLk1DaGF0UmVxdWVzdBoRLm5vbW9zLk1DaGF0RXZlbnQwARJGCgtHZXRNZXNzYWdlcxIaLm5vbW9zLk1HZXRNZXNzYWdlc1JlcXVlc3QaGy5ub21vcy5NR2V0TWVzc2FnZXNSZXNwb25zZRJACgxBcHByb3ZlRHJhZnQSEy5ub21vcy5NRHJhZnRBY3Rpb24aGy5ub21vcy5NRHJhZnRBY3Rpb25SZXNwb25zZRI/CgtSZWplY3REcmFmdBITLm5vbW9zLk1EcmFmdEFjdGlvbhobLm5vbW9zLk1EcmFmdEFjdGlvblJlc3BvbnNlElAKFEFwcHJvdmVEcmFmdFdpdGhFZGl0Ehsubm9tb3MuTURyYWZ0QWN0aW9uV2l0aEVkaXQaGy5ub21vcy5NRHJhZnRBY3Rpb25SZXNwb25zZRI4CglMaXN0SW5ib3gSFC5ub21vcy5NSW5ib3hSZXF1ZXN0GhUubm9tb3MuTUluYm94UmVzcG9uc2USQAoPR2V0Q2F0ZUVudmVsb3BlEhcubm9tb3MuTUVudmVsb3BlUmVxdWVzdBoULm5vbW9zLk1DYXRlRW52ZWxvcGUSSQoOQWN0T25JbmJveEl0ZW0SGi5ub21vcy5NSW5ib3hBY3Rpb25SZXF1ZXN0Ghsubm9tb3MuTUluYm94QWN0aW9uUmVzcG9uc2USMgoKTGlzdFNraWxscxIMLm5vbW9zLkVtcHR5GhYubm9tb3MuTVNraWxsc1Jlc3BvbnNlEkYKC1RvZ2dsZVNraWxsEhoubm9tb3MuTVNraWxsVG9nZ2xlUmVxdWVzdBobLm5vbW9zLk1Ta2lsbFRvZ2dsZVJlc3BvbnNlEkAKC0dldEVhcm5pbmdzEhcubm9tb3MuTUVhcm5pbmdzUmVxdWVzdBoYLm5vbW9zLk1FYXJuaW5nc1Jlc3BvbnNlEjcKCEdldEdyYXBoEhQubm9tb3MuTUdyYXBoUmVxdWVzdBoVLm5vbW9zLk1HcmFwaFJlc3BvbnNlEkkKEUdldEdyYXBoTmVpZ2hib3JzEh0ubm9tb3MuTUdyYXBoTmVpZ2hib3JzUmVxdWVzdBoVLm5vbW9zLk1HcmFwaFJlc3BvbnNlEkAKC1NlYXJjaEdyYXBoEhoubm9tb3MuTUdyYXBoU2VhcmNoUmVxdWVzdBoVLm5vbW9zLk1HcmFwaFJlc3BvbnNlEjUKC0dldFNldHRpbmdzEgwubm9tb3MuRW1wdHkaGC5ub21vcy5NU2V0dGluZ3NSZXNwb25zZRI0Cg1VcGRhdGVDb25zZW50EhYubm9tb3MuTUNvbnNlbnRSZXF1ZXN0Ggsubm9tb3MuTUFjaxI4Cg9VcGRhdGVUcnVzdFRpZXISGC5ub21vcy5NVHJ1c3RUaWVyUmVxdWVzdBoLLm5vbW9zLk1BY2sSOgoQVXBkYXRlUGVybWlzc2lvbhIZLm5vbW9zLk1QZXJtaXNzaW9uUmVxdWVzdBoLLm5vbW9zLk1BY2sSPgoQTGlzdEludGVncmF0aW9ucxIMLm5vbW9zLkVtcHR5Ghwubm9tb3MuTUludGVncmF0aW9uc1Jlc3BvbnNlElQKF1N0YXJ0Q29ubmVjdEludGVncmF0aW9uEhsubm9tb3MuTVN0YXJ0Q29ubmVjdFJlcXVlc3QaHC5ub21vcy5NU3RhcnRDb25uZWN0UmVzcG9uc2USQQoUQ29ubmVjdEdvb2dsZUFjY291bnQSHC5ub21vcy5NQ29ubmVjdEdvb2dsZVJlcXVlc3QaCy5ub21vcy5NQWNrEjoKDVNldEdvb2dsZVNlbmQSHC5ub21vcy5NU2V0R29vZ2xlU2VuZFJlcXVlc3QaCy5ub21vcy5NQWNrEj8KFURpc2Nvbm5lY3RJbnRlZ3JhdGlvbhIZLm5vbW9zLk1EaXNjb25uZWN0UmVxdWVzdBoLLm5vbW9zLk1BY2sSNQoOUmVnaXN0ZXJEZXZpY2USFi5ub21vcy5NRGV2aWNlUmVnaXN0ZXIaCy5ub21vcy5NQWNrEjkKEFVucmVnaXN0ZXJEZXZpY2USGC5ub21vcy5NRGV2aWNlVW5yZWdpc3RlchoLLm5vbW9zLk1BY2sSRQoOTGlzdFZhdWx0Tm90ZXMSGC5ub21vcy5NVmF1bHRMaXN0UmVxdWVzdBoZLm5vbW9zLk1WYXVsdExpc3RSZXNwb25zZRI6CgxHZXRWYXVsdE5vdGUSFy5ub21vcy5NVmF1bHRHZXRSZXF1ZXN0GhEubm9tb3MuTVZhdWx0Tm90ZRI4Cg5Xcml0ZVZhdWx0Tm90ZRIZLm5vbW9zLk1WYXVsdFdyaXRlUmVxdWVzdBoLLm5vbW9zLk1BY2sSOgoPRGVsZXRlVmF1bHROb3RlEhoubm9tb3MuTVZhdWx0RGVsZXRlUmVxdWVzdBoLLm5vbW9zLk1BY2sSMAoJTGlzdExvb3BzEgwubm9tb3MuRW1wdHkaFS5ub21vcy5NTG9vcHNSZXNwb25zZRI8Cg5TZXRMb29wRW5hYmxlZBIdLm5vbW9zLk1TZXRMb29wRW5hYmxlZFJlcXVlc3QaCy5ub21vcy5NQWNrEjQKCkRlbGV0ZUxvb3ASGS5ub21vcy5NTG9vcERlbGV0ZVJlcXVlc3QaCy5ub21vcy5NQWNrElgKEVN0dWRpb0NyZWF0ZUFzc2V0EiAubm9tb3MuTVN0dWRpb0NyZWF0ZUFzc2V0UmVxdWVzdBohLm5vbW9zLk1TdHVkaW9DcmVhdGVBc3NldFJlc3BvbnNlEksKEVN0dWRpb0dldEFzc2V0VXJsEhYubm9tb3MuTVN0dWRpb0Fzc2V0UmVmGh4ubm9tb3MuTVN0dWRpb0Fzc2V0VXJsUmVzcG9uc2USPgoKU3R1ZGlvRWRpdBIZLm5vbW9zLk1TdHVkaW9FZGl0UmVxdWVzdBoTLm5vbW9zLk1TdHVkaW9FdmVudDABEkYKDVN0dWRpb0hpc3RvcnkSFi5ub21vcy5NU3R1ZGlvQXNzZXRSZWYaHS5ub21vcy5NU3R1ZGlvSGlzdG9yeVJlc3BvbnNlElUKEFN0dWRpb0xpc3RBc3NldHMSHy5ub21vcy5NU3R1ZGlvTGlzdEFzc2V0c1JlcXVlc3QaIC5ub21vcy5NU3R1ZGlvTGlzdEFzc2V0c1Jlc3BvbnNlEk8KElN0dWRpb1N1Z2dlc3RFZGl0cxIWLm5vbW9zLk1TdHVkaW9Bc3NldFJlZhohLm5vbW9zLk1TdHVkaW9TdWdnZXN0aW9uc1Jlc3BvbnNlEkEKFFN0dWRpb1JlcG9ydElkZW50aXR5Ehwubm9tb3MuTVN0dWRpb0lkZW50aXR5UmVwb3J0Ggsubm9tb3MuTUFjazJICgxPQXV0aERlcG9zaXQSOAoHRGVwb3NpdBIVLm5vbW9zLkRlcG9zaXRSZXF1ZXN0GhYubm9tb3MuRGVwb3NpdFJlc3BvbnNlYgZwcm90bzM", + "Cgtub21vcy5wcm90bxIFbm9tb3MiBwoFRW1wdHkijgEKCExvb3BJbmZvEgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSEAoIc2NoZWR1bGUYAyABKAkSDwoHZW5hYmxlZBgEIAEoCBIOCgZzb3VyY2UYBSABKAkSEwoLZXJyb3JfY291bnQYBiABKAUSEAoIbGFzdF9ydW4YByABKAkSDgoGcHJvbXB0GAggASgJIioKCExvb3BMaXN0Eh4KBWxvb3BzGAEgAygLMg8ubm9tb3MuTG9vcEluZm8iNgoVU2V0TG9vcEVuYWJsZWRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHZW5hYmxlZBgCIAEoCCIhChFMb29wRGVsZXRlUmVxdWVzdBIMCgRuYW1lGAEgASgJIjYKEkxvb3BBY3Rpb25SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiMwoLQ2hhdFJlcXVlc3QSDwoHY29udGVudBgBIAEoCRITCgtzZXNzaW9uX2tleRgCIAEoCSIwCgpBZ2VudEV2ZW50EgwKBHR5cGUYASABKAkSFAoManNvbl9wYXlsb2FkGAIgASgJIjYKDkNvbW1hbmRSZXF1ZXN0Eg8KB2NvbW1hbmQYASABKAkSEwoLc2Vzc2lvbl9rZXkYAiABKAkiMwoPQ29tbWFuZFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJPCg5TdGF0dXNSZXNwb25zZRIPCgdydW5uaW5nGAEgASgIEhkKEWNvbm5lY3RlZF9jbGllbnRzGAIgASgFEhEKCXBsYXRmb3JtcxgDIAMoCSIlCg5TZXNzaW9uUmVxdWVzdBITCgtzZXNzaW9uX2tleRgBIAEoCSJVCg9TZXNzaW9uUmVzcG9uc2USCgoCaWQYASABKAkSEwoLc2Vzc2lvbl9rZXkYAiABKAkSDQoFbW9kZWwYAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoCSI3CgtTZXNzaW9uTGlzdBIoCghzZXNzaW9ucxgBIAMoCzIWLm5vbW9zLlNlc3Npb25SZXNwb25zZSIfCgtEcmFmdEFjdGlvbhIQCghkcmFmdF9pZBgBIAEoCSIxCg1EcmFmdFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSItCglEcmFmdExpc3QSIAoGZHJhZnRzGAEgAygLMhAubm9tb3MuRHJhZnRJdGVtInIKCURyYWZ0SXRlbRIKCgJpZBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCHBsYXRmb3JtGAMgASgJEhIKCmNoYW5uZWxfaWQYBCABKAkSDgoGc3RhdHVzGAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAkiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyKLAQoFTUxvb3ASCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghzY2hlZHVsZRgDIAEoCRIPCgdlbmFibGVkGAQgASgIEg4KBnNvdXJjZRgFIAEoCRITCgtlcnJvcl9jb3VudBgGIAEoBRIQCghsYXN0X3J1bhgHIAEoCRIOCgZwcm9tcHQYCCABKAkiLQoOTUxvb3BzUmVzcG9uc2USGwoFbG9vcHMYASADKAsyDC5ub21vcy5NTG9vcCI3ChZNU2V0TG9vcEVuYWJsZWRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHZW5hYmxlZBgCIAEoCCIiChJNTG9vcERlbGV0ZVJlcXVlc3QSDAoEbmFtZRgBIAEoCSKnAQoFTVRhc2sSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIOCgZwcm9tcHQYAyABKAkSEAoIc2NoZWR1bGUYBCABKAkSFQoNc2NoZWR1bGVfdHlwZRgFIAEoCRIYChBkaXNwbGF5X3NjaGVkdWxlGAYgASgJEg8KB2VuYWJsZWQYByABKAgSDgoGc291cmNlGAggASgJEhAKCGxhc3RfcnVuGAkgASgJIi0KDk1UYXNrc1Jlc3BvbnNlEhsKBXRhc2tzGAEgAygLMgwubm9tb3MuTVRhc2sieAoSTVRhc2tVcGRhdGVSZXF1ZXN0EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSDgoGcHJvbXB0GAMgASgJEhAKCHNjaGVkdWxlGAQgASgJEhUKDXNjaGVkdWxlX3R5cGUYBSABKAkSDwoHZW5hYmxlZBgGIAEoCCIgChJNVGFza0RlbGV0ZVJlcXVlc3QSCgoCaWQYASABKAkiagoKTUJyYWluTm9kZRIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRIMCgRraW5kGAMgASgJEg8KB3N1bW1hcnkYBCABKAkSDgoGZGVncmVlGAUgASgFEhIKCmNvbmZpZGVuY2UYBiABKAEiOAoKTUJyYWluRWRnZRILCgNzcmMYASABKAkSCwoDZHN0GAIgASgJEhAKCHJlbGF0aW9uGAMgASgJIlIKCk1CcmFpbkZhY3QSDAoEdGV4dBgBIAEoCRIOCgZzb3VyY2UYAiABKAkSEgoKY29uZmlkZW5jZRgDIAEoBRISCgpsZWFybmVkX2F0GAQgASgJIqABCg5NQnJhaW5SZXNwb25zZRIgCgVub2RlcxgBIAMoCzIRLm5vbW9zLk1CcmFpbk5vZGUSIAoFZWRnZXMYAiADKAsyES5ub21vcy5NQnJhaW5FZGdlEiAKBWZhY3RzGAMgAygLMhEubm9tb3MuTUJyYWluRmFjdBIUCgxlbnRpdHlfY291bnQYBCABKAUSEgoKZmFjdF9jb3VudBgFIAEoBSJzCgtNSW5ib3hEcmFmdBIKCgJpZBgBIAEoCRIRCglyZWNpcGllbnQYAiABKAkSDwoHcHJldmlldxgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEAoIcGxhdGZvcm0YBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoCSJ6CgpNSW5ib3hDYXRlEgoKAmlkGAEgASgJEhIKCmZyb21fbGFiZWwYAiABKAkSEgoKdHJ1c3RfdGllchgDIAEoCRIPCgdzdWJqZWN0GAQgASgJEhMKC2JvbmRfYW1vdW50GAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAkibwoRTUdldEluYm94UmVzcG9uc2USIgoGZHJhZnRzGAEgAygLMhIubm9tb3MuTUluYm94RHJhZnQSHwoEY2F0ZRgCIAMoCzIRLm5vbW9zLk1JbmJveENhdGUSFQoNYmxvY2tlZF9jb3VudBgDIAEoBSI4CgtNVG9kYXlFdmVudBIMCgR0aW1lGAEgASgJEg0KBXRpdGxlGAIgASgJEgwKBG1ldGEYAyABKAkiQAoQTVRvZGF5Q29tbWl0bWVudBIKCgJpZBgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRILCgNkdWUYAyABKAkiOAoKTVRvZGF5VGFzaxIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhAKCHNjaGVkdWxlGAMgASgJIp4BCg5NVG9kYXlSZXNwb25zZRIYChBicmllZmluZ19lbmFibGVkGAEgASgIEiIKBmV2ZW50cxgCIAMoCzISLm5vbW9zLk1Ub2RheUV2ZW50EiwKC2NvbW1pdG1lbnRzGAMgAygLMhcubm9tb3MuTVRvZGF5Q29tbWl0bWVudBIgCgV0YXNrcxgEIAMoCzIRLm5vbW9zLk1Ub2RheVRhc2siKAoETUFjaxIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiIwoRTVZhdWx0TGlzdFJlcXVlc3QSDgoGcHJlZml4GAEgASgJIkQKEU1WYXVsdE5vdGVTdW1tYXJ5EgwKBHBhdGgYASABKAkSDQoFdGl0bGUYAiABKAkSEgoKdXBkYXRlZF9hdBgDIAEoCSI9ChJNVmF1bHRMaXN0UmVzcG9uc2USJwoFbm90ZXMYASADKAsyGC5ub21vcy5NVmF1bHROb3RlU3VtbWFyeSIgChBNVmF1bHRHZXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiXgoKTVZhdWx0Tm90ZRIMCgRwYXRoGAEgASgJEg0KBXRpdGxlGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSEgoKdXBkYXRlZF9hdBgEIAEoCRIOCgZleGlzdHMYBSABKAgiQgoSTVZhdWx0V3JpdGVSZXF1ZXN0EgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRINCgV0aXRsZRgDIAEoCSIjChNNVmF1bHREZWxldGVSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMTUNoYXRSZXF1ZXN0Eg8KB2NvbnRlbnQYASABKAkSEwoLc2Vzc2lvbl9rZXkYAiABKAkiMAoKTUNoYXRFdmVudBIMCgR0eXBlGAEgASgJEhQKDGpzb25fcGF5bG9hZBgCIAEoCSJMChNNR2V0TWVzc2FnZXNSZXF1ZXN0EhMKC3Nlc3Npb25fa2V5GAEgASgJEg0KBWxpbWl0GAIgASgFEhEKCWJlZm9yZV9pZBgDIAEoCSJJCghNTWVzc2FnZRIKCgJpZBgBIAEoCRIMCgRyb2xlGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoCSI5ChRNR2V0TWVzc2FnZXNSZXNwb25zZRIhCghtZXNzYWdlcxgBIAMoCzIPLm5vbW9zLk1NZXNzYWdlIiAKDE1EcmFmdEFjdGlvbhIQCghkcmFmdF9pZBgBIAEoCSI9ChRNRHJhZnRBY3Rpb25XaXRoRWRpdBIQCghkcmFmdF9pZBgBIAEoCRITCgtlZGl0ZWRfdGV4dBgCIAEoCSI4ChRNRHJhZnRBY3Rpb25SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiLgoNTUluYm94UmVxdWVzdBIOCgZzdGF0dXMYASABKAkSDQoFbGltaXQYAiABKAUimAEKCk1JbmJveEl0ZW0SCgoCaWQYASABKAkSEgoKZnJvbV9sYWJlbBgCIAEoCRISCgp0cnVzdF90aWVyGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDAoEdGltZRgFIAEoCRITCgtib25kX2Ftb3VudBgGIAEoCRIOCgZ1bnJlYWQYByABKAgSEgoKY3JlYXRlZF9hdBgIIAEoCSJJCg5NSW5ib3hSZXNwb25zZRIgCgVpdGVtcxgBIAMoCzIRLm5vbW9zLk1JbmJveEl0ZW0SFQoNYmxvY2tlZF9jb3VudBgCIAEoBSIkChBNRW52ZWxvcGVSZXF1ZXN0EhAKCGluYm94X2lkGAEgASgJIo0BCg1NQ2F0ZUVudmVsb3BlEgsKA2RpZBgBIAEoCRISCgp0cnVzdF90aWVyGAIgASgJEg4KBmludGVudBgDIAEoCRIVCg1jb25zZW50X2dyYW50GAQgASgJEg0KBXN0YW1wGAUgASgJEhMKC2JvbmRfYW1vdW50GAYgASgJEhAKCHJhd19qc29uGAcgASgJIjcKE01JbmJveEFjdGlvblJlcXVlc3QSEAoIaW5ib3hfaWQYASABKAkSDgoGYWN0aW9uGAIgASgJIjgKFE1JbmJveEFjdGlvblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJqCgZNU2tpbGwSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIOCgZzb3VyY2UYAyABKAkSDwoHZW5hYmxlZBgEIAEoCBINCgVjZXJ0cxgFIAMoCRINCgVwcmljZRgGIAEoCSIwCg9NU2tpbGxzUmVzcG9uc2USHQoGc2tpbGxzGAEgAygLMg0ubm9tb3MuTVNraWxsIkEKB01QbHVnaW4SDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRITCgttYXJrZXRwbGFjZRgDIAEoCSIzChBNUGx1Z2luc1Jlc3BvbnNlEh8KB3BsdWdpbnMYASADKAsyDi5ub21vcy5NUGx1Z2luIjQKE01Ta2lsbFRvZ2dsZVJlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdlbmFibGVkGAIgASgIIjgKFE1Ta2lsbFRvZ2dsZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDwoHbWVzc2FnZRgCIAEoCSIiChBNRWFybmluZ3NSZXF1ZXN0Eg4KBnBlcmlvZBgBIAEoCSKKAQoRTUVhcm5pbmdzUmVzcG9uc2USGQoRdGhpc19wZXJpb2RfY2VudHMYASABKAMSEwoLYm9uZHNfY291bnQYAiABKAMSFgoOYXZnX2JvbmRfY2VudHMYAyABKAMSFwoPYWNjZXB0X3JhdGVfcGN0GAQgASgFEhQKDHNlcmllc19jZW50cxgFIAMoAyItCg1NR3JhcGhSZXF1ZXN0Eg0KBWtpbmRzGAEgAygJEg0KBWxpbWl0GAIgASgFIl4KFk1HcmFwaE5laWdoYm9yc1JlcXVlc3QSDwoHbm9kZV9pZBgBIAEoCRINCgVkZXB0aBgCIAEoBRIRCglyZWxfdHlwZXMYAyADKAkSEQoJZGlyZWN0aW9uGAQgASgJIjMKE01HcmFwaFNlYXJjaFJlcXVlc3QSDQoFcXVlcnkYASABKAkSDQoFbGltaXQYAiABKAUilwEKCk1HcmFwaE5vZGUSCgoCaWQYASABKAkSDAoEa2luZBgCIAEoCRIMCgRuYW1lGAMgASgJEg8KB2FsaWFzZXMYBCADKAkSDwoHc3VtbWFyeRgFIAEoCRISCgpjb25maWRlbmNlGAYgASgBEhUKDWV4dGVybmFsX2tpbmQYByABKAkSFAoMZXh0ZXJuYWxfcmVmGAggASgJImgKCk1HcmFwaEVkZ2USCgoCaWQYASABKAkSDgoGc3JjX2lkGAIgASgJEg4KBmRzdF9pZBgDIAEoCRIQCghyZWxfdHlwZRgEIAEoCRIMCgRmYWN0GAUgASgJEg4KBndlaWdodBgGIAEoASJUCg5NR3JhcGhSZXNwb25zZRIgCgVub2RlcxgBIAMoCzIRLm5vbW9zLk1HcmFwaE5vZGUSIAoFZWRnZXMYAiADKAsyES5ub21vcy5NR3JhcGhFZGdlIl4KCk1UcnVzdFRpZXISCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRtb2RlGAQgASgJEhMKC2JvbmRfYW1vdW50GAUgASgJIjkKC01QZXJtaXNzaW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2VuYWJsZWQYAyABKAgiiQEKDE1JbnRlZ3JhdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRIMCgRpY29uGAMgASgJEhEKCWNvbm5lY3RlZBgEIAEoCBIVCg1hY2NvdW50X2VtYWlsGAUgASgJEhQKDHNlbmRfZW5hYmxlZBgGIAEoCBIQCghwcm92aWRlchgHIAEoCSJoCghNUHJvZmlsZRIMCgRuYW1lGAEgASgJEgwKBHBsYW4YAiABKAkSFQoNbWVzc2FnZV9jb3VudBgDIAEoAxIUCgxlYXJuZWRfY2VudHMYBCABKAMSEwoLc2F2ZWRfY2VudHMYBSABKAMizwIKEU1TZXR0aW5nc1Jlc3BvbnNlEiAKB3Byb2ZpbGUYASABKAsyDy5ub21vcy5NUHJvZmlsZRImCgt0cnVzdF90aWVycxgCIAMoCzIRLm5vbW9zLk1UcnVzdFRpZXISJwoLcGVybWlzc2lvbnMYAyADKAsyEi5ub21vcy5NUGVybWlzc2lvbhIpCgxpbnRlZ3JhdGlvbnMYBCADKAsyEy5ub21vcy5NSW50ZWdyYXRpb24SJQoHY29uc2VudBgFIAMoCzIULm5vbW9zLk1Db25zZW50RW50cnkSJwoIaWRlbnRpdHkYBiABKAsyFS5ub21vcy5NQWdlbnRJZGVudGl0eRImCgthcHBfdG9nZ2xlcxgHIAMoCzIRLm5vbW9zLk1BcHBUb2dnbGUSJAoJcHJvYWN0aXZlGAggASgLMhEubm9tb3MuTVByb2FjdGl2ZSIsCgpNUHJvYWN0aXZlEgwKBG1vZGUYASABKAkSEAoIYnJpZWZpbmcYAiABKAkiLwoNTUNvbnNlbnRFbnRyeRIQCghwbGF0Zm9ybRgBIAEoCRIMCgRtb2RlGAIgASgJIj0KDk1BZ2VudElkZW50aXR5EgwKBG5hbWUYASABKAkSDQoFdm9pY2UYAiABKAkSDgoGYXZhdGFyGAMgASgJIioKCk1BcHBUb2dnbGUSCwoDa2V5GAEgASgJEg8KB2VuYWJsZWQYAiABKAgiMAoSTUFwcFNldHRpbmdSZXF1ZXN0EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJEChVNQWdlbnRJZGVudGl0eVJlcXVlc3QSDAoEbmFtZRgBIAEoCRINCgV2b2ljZRgCIAEoCRIOCgZhdmF0YXIYAyABKAkiMQoPTUNvbnNlbnRSZXF1ZXN0EhAKCHBsYXRmb3JtGAEgASgJEgwKBG1vZGUYAiABKAkiQgoRTVRydXN0VGllclJlcXVlc3QSCgoCaWQYASABKAkSDAoEbW9kZRgCIAEoCRITCgtib25kX2Ftb3VudBgDIAEoCSIxChJNUGVybWlzc2lvblJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCCJCChVNSW50ZWdyYXRpb25zUmVzcG9uc2USKQoMaW50ZWdyYXRpb25zGAEgAygLMhMubm9tb3MuTUludGVncmF0aW9uIigKFE1TdGFydENvbm5lY3RSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJIioKFU1TdGFydENvbm5lY3RSZXNwb25zZRIRCglvYXV0aF91cmwYASABKAkiQwoSTURpc2Nvbm5lY3RSZXF1ZXN0EhYKDmludGVncmF0aW9uX2lkGAEgASgJEhUKDWFjY291bnRfZW1haWwYAiABKAkiNAoVTUNvbm5lY3RHb29nbGVSZXF1ZXN0EgwKBGNvZGUYASABKAkSDQoFc3RhdGUYAiABKAkiPwoVTVNldEdvb2dsZVNlbmRSZXF1ZXN0EhUKDWFjY291bnRfZW1haWwYASABKAkSDwoHZW5hYmxlZBgCIAEoCCJRCg9NRGV2aWNlUmVnaXN0ZXISFwoPZXhwb19wdXNoX3Rva2VuGAEgASgJEhAKCHBsYXRmb3JtGAIgASgJEhMKC2FwcF92ZXJzaW9uGAMgASgJIiwKEU1EZXZpY2VVbnJlZ2lzdGVyEhcKD2V4cG9fcHVzaF90b2tlbhgBIAEoCSLsAQoORGVwb3NpdFJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkSDwoHdXNlcl9pZBgCIAEoCRIUCgxhY2Nlc3NfdG9rZW4YAyABKAkSFQoNcmVmcmVzaF90b2tlbhgEIAEoCRISCgpleHBpcmVzX2F0GAUgASgDEg4KBnNjb3BlcxgGIAEoCRI1CghtZXRhZGF0YRgHIAMoCzIjLm5vbW9zLkRlcG9zaXRSZXF1ZXN0Lk1ldGFkYXRhRW50cnkaLwoNTWV0YWRhdGFFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKD0RlcG9zaXRSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSFgoOaW50ZWdyYXRpb25faWQYAyABKAkibQoZTVN0dWRpb0NyZWF0ZUFzc2V0UmVxdWVzdBIMCgRtaW1lGAEgASgJEhQKDGNvbnRlbnRfaGFzaBgCIAEoCRINCgV3aWR0aBgDIAEoBRIOCgZoZWlnaHQYBCABKAUSDQoFYnl0ZXMYBSABKAUiagoaTVN0dWRpb0NyZWF0ZUFzc2V0UmVzcG9uc2USEAoIYXNzZXRfaWQYASABKAkSEgoKdXBsb2FkX3VybBgCIAEoCRISCgpvYmplY3Rfa2V5GAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMiNQoPTVN0dWRpb0Fzc2V0UmVmEhAKCGFzc2V0X2lkGAEgASgJEhAKCG9yaWdpbmFsGAIgASgIIjoKF01TdHVkaW9Bc3NldFVybFJlc3BvbnNlEgsKA3VybBgBIAEoCRISCgpleHBpcmVzX2F0GAIgASgDIp8BChJNU3R1ZGlvRWRpdFJlcXVlc3QSEAoIYXNzZXRfaWQYASABKAkSCgoCb3AYAiABKAkSEwoLcGFyYW1zX2pzb24YAyABKAkSFgoOcGFyZW50X2VkaXRfaWQYBCABKAkSFwoPaWRlbXBvdGVuY3lfa2V5GAUgASgJEhAKCG1hc2tfa2V5GAYgASgJEhMKC2lucHV0X2ltYWdlGAcgASgMIokBCgxNU3R1ZGlvRXZlbnQSDAoEa2luZBgBIAEoCRIPCgdlZGl0X2lkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRITCgtwcmV2aWV3X2tleRgEIAEoCRISCgpvdXRwdXRfa2V5GAUgASgJEhAKCGNvc3RfdXNkGAYgASgBEg8KB21lc3NhZ2UYByABKAkinAEKC01TdHVkaW9FZGl0EgoKAmlkGAEgASgJEgoKAm9wGAIgASgJEg4KBnN0YXR1cxgDIAEoCRITCgtwcmV2aWV3X2tleRgEIAEoCRISCgpvdXRwdXRfa2V5GAUgASgJEhAKCGNvc3RfdXNkGAYgASgBEhYKDnBhcmVudF9lZGl0X2lkGAcgASgJEhIKCmNyZWF0ZWRfYXQYCCABKAkiUQoWTVN0dWRpb0hpc3RvcnlSZXNwb25zZRIhCgVlZGl0cxgBIAMoCzISLm5vbW9zLk1TdHVkaW9FZGl0EhQKDGhlYWRfZWRpdF9pZBgCIAEoCSI3ChVNU3R1ZGlvSWRlbnRpdHlSZXBvcnQSDwoHZWRpdF9pZBgBIAEoCRINCgVzY29yZRgCIAEoASIpChhNU3R1ZGlvTGlzdEFzc2V0c1JlcXVlc3QSDQoFbGltaXQYASABKAUinAEKE01TdHVkaW9Bc3NldFN1bW1hcnkSEAoIYXNzZXRfaWQYASABKAkSEwoLcHJldmlld191cmwYAiABKAkSEgoKdXBkYXRlZF9hdBgDIAEoAxIRCglmaW5hbGl6ZWQYBCABKAgSEgoKZWRpdF9jb3VudBgFIAEoBRIPCgdoZWFkX29wGAYgASgJEhIKCmV4cGlyZXNfYXQYByABKAMiRwoZTVN0dWRpb0xpc3RBc3NldHNSZXNwb25zZRIqCgZhc3NldHMYASADKAsyGi5ub21vcy5NU3R1ZGlvQXNzZXRTdW1tYXJ5IjIKEU1TdHVkaW9TdWdnZXN0aW9uEg0KBWxhYmVsGAEgASgJEg4KBnByb21wdBgCIAEoCSJLChpNU3R1ZGlvU3VnZ2VzdGlvbnNSZXNwb25zZRItCgtzdWdnZXN0aW9ucxgBIAMoCzIYLm5vbW9zLk1TdHVkaW9TdWdnZXN0aW9uMp4FCgpOb21vc0FnZW50Ei8KBENoYXQSEi5ub21vcy5DaGF0UmVxdWVzdBoRLm5vbW9zLkFnZW50RXZlbnQwARI4CgdDb21tYW5kEhUubm9tb3MuQ29tbWFuZFJlcXVlc3QaFi5ub21vcy5Db21tYW5kUmVzcG9uc2USMAoJR2V0U3RhdHVzEgwubm9tb3MuRW1wdHkaFS5ub21vcy5TdGF0dXNSZXNwb25zZRIwCgxMaXN0U2Vzc2lvbnMSDC5ub21vcy5FbXB0eRoSLm5vbW9zLlNlc3Npb25MaXN0EjsKCkdldFNlc3Npb24SFS5ub21vcy5TZXNzaW9uUmVxdWVzdBoWLm5vbW9zLlNlc3Npb25SZXNwb25zZRIsCgpMaXN0RHJhZnRzEgwubm9tb3MuRW1wdHkaEC5ub21vcy5EcmFmdExpc3QSOAoMQXBwcm92ZURyYWZ0EhIubm9tb3MuRHJhZnRBY3Rpb24aFC5ub21vcy5EcmFmdFJlc3BvbnNlEjcKC1JlamVjdERyYWZ0EhIubm9tb3MuRHJhZnRBY3Rpb24aFC5ub21vcy5EcmFmdFJlc3BvbnNlEioKCUxpc3RMb29wcxIMLm5vbW9zLkVtcHR5Gg8ubm9tb3MuTG9vcExpc3QSSQoOU2V0TG9vcEVuYWJsZWQSHC5ub21vcy5TZXRMb29wRW5hYmxlZFJlcXVlc3QaGS5ub21vcy5Mb29wQWN0aW9uUmVzcG9uc2USQQoKRGVsZXRlTG9vcBIYLm5vbW9zLkxvb3BEZWxldGVSZXF1ZXN0Ghkubm9tb3MuTG9vcEFjdGlvblJlc3BvbnNlEikKBFBpbmcSDC5ub21vcy5FbXB0eRoTLm5vbW9zLlBvbmdSZXNwb25zZTLxFwoJTW9iaWxlQXBpEjAKBENoYXQSEy5ub21vcy5NQ2hhdFJlcXVlc3QaES5ub21vcy5NQ2hhdEV2ZW50MAESRgoLR2V0TWVzc2FnZXMSGi5ub21vcy5NR2V0TWVzc2FnZXNSZXF1ZXN0Ghsubm9tb3MuTUdldE1lc3NhZ2VzUmVzcG9uc2USQAoMQXBwcm92ZURyYWZ0EhMubm9tb3MuTURyYWZ0QWN0aW9uGhsubm9tb3MuTURyYWZ0QWN0aW9uUmVzcG9uc2USPwoLUmVqZWN0RHJhZnQSEy5ub21vcy5NRHJhZnRBY3Rpb24aGy5ub21vcy5NRHJhZnRBY3Rpb25SZXNwb25zZRJQChRBcHByb3ZlRHJhZnRXaXRoRWRpdBIbLm5vbW9zLk1EcmFmdEFjdGlvbldpdGhFZGl0Ghsubm9tb3MuTURyYWZ0QWN0aW9uUmVzcG9uc2USOAoJTGlzdEluYm94EhQubm9tb3MuTUluYm94UmVxdWVzdBoVLm5vbW9zLk1JbmJveFJlc3BvbnNlEkAKD0dldENhdGVFbnZlbG9wZRIXLm5vbW9zLk1FbnZlbG9wZVJlcXVlc3QaFC5ub21vcy5NQ2F0ZUVudmVsb3BlEkkKDkFjdE9uSW5ib3hJdGVtEhoubm9tb3MuTUluYm94QWN0aW9uUmVxdWVzdBobLm5vbW9zLk1JbmJveEFjdGlvblJlc3BvbnNlEjIKCkxpc3RTa2lsbHMSDC5ub21vcy5FbXB0eRoWLm5vbW9zLk1Ta2lsbHNSZXNwb25zZRJGCgtUb2dnbGVTa2lsbBIaLm5vbW9zLk1Ta2lsbFRvZ2dsZVJlcXVlc3QaGy5ub21vcy5NU2tpbGxUb2dnbGVSZXNwb25zZRI0CgtMaXN0UGx1Z2lucxIMLm5vbW9zLkVtcHR5Ghcubm9tb3MuTVBsdWdpbnNSZXNwb25zZRJACgtHZXRFYXJuaW5ncxIXLm5vbW9zLk1FYXJuaW5nc1JlcXVlc3QaGC5ub21vcy5NRWFybmluZ3NSZXNwb25zZRI3CghHZXRHcmFwaBIULm5vbW9zLk1HcmFwaFJlcXVlc3QaFS5ub21vcy5NR3JhcGhSZXNwb25zZRJJChFHZXRHcmFwaE5laWdoYm9ycxIdLm5vbW9zLk1HcmFwaE5laWdoYm9yc1JlcXVlc3QaFS5ub21vcy5NR3JhcGhSZXNwb25zZRJACgtTZWFyY2hHcmFwaBIaLm5vbW9zLk1HcmFwaFNlYXJjaFJlcXVlc3QaFS5ub21vcy5NR3JhcGhSZXNwb25zZRI1CgtHZXRTZXR0aW5ncxIMLm5vbW9zLkVtcHR5Ghgubm9tb3MuTVNldHRpbmdzUmVzcG9uc2USNAoNVXBkYXRlQ29uc2VudBIWLm5vbW9zLk1Db25zZW50UmVxdWVzdBoLLm5vbW9zLk1BY2sSOAoPVXBkYXRlVHJ1c3RUaWVyEhgubm9tb3MuTVRydXN0VGllclJlcXVlc3QaCy5ub21vcy5NQWNrEjoKEFVwZGF0ZVBlcm1pc3Npb24SGS5ub21vcy5NUGVybWlzc2lvblJlcXVlc3QaCy5ub21vcy5NQWNrEjoKEFVwZGF0ZUFwcFNldHRpbmcSGS5ub21vcy5NQXBwU2V0dGluZ1JlcXVlc3QaCy5ub21vcy5NQWNrEkAKE1VwZGF0ZUFnZW50SWRlbnRpdHkSHC5ub21vcy5NQWdlbnRJZGVudGl0eVJlcXVlc3QaCy5ub21vcy5NQWNrEj4KEExpc3RJbnRlZ3JhdGlvbnMSDC5ub21vcy5FbXB0eRocLm5vbW9zLk1JbnRlZ3JhdGlvbnNSZXNwb25zZRJUChdTdGFydENvbm5lY3RJbnRlZ3JhdGlvbhIbLm5vbW9zLk1TdGFydENvbm5lY3RSZXF1ZXN0Ghwubm9tb3MuTVN0YXJ0Q29ubmVjdFJlc3BvbnNlEkEKFENvbm5lY3RHb29nbGVBY2NvdW50Ehwubm9tb3MuTUNvbm5lY3RHb29nbGVSZXF1ZXN0Ggsubm9tb3MuTUFjaxI6Cg1TZXRHb29nbGVTZW5kEhwubm9tb3MuTVNldEdvb2dsZVNlbmRSZXF1ZXN0Ggsubm9tb3MuTUFjaxI/ChVEaXNjb25uZWN0SW50ZWdyYXRpb24SGS5ub21vcy5NRGlzY29ubmVjdFJlcXVlc3QaCy5ub21vcy5NQWNrEjUKDlJlZ2lzdGVyRGV2aWNlEhYubm9tb3MuTURldmljZVJlZ2lzdGVyGgsubm9tb3MuTUFjaxI5ChBVbnJlZ2lzdGVyRGV2aWNlEhgubm9tb3MuTURldmljZVVucmVnaXN0ZXIaCy5ub21vcy5NQWNrEkUKDkxpc3RWYXVsdE5vdGVzEhgubm9tb3MuTVZhdWx0TGlzdFJlcXVlc3QaGS5ub21vcy5NVmF1bHRMaXN0UmVzcG9uc2USOgoMR2V0VmF1bHROb3RlEhcubm9tb3MuTVZhdWx0R2V0UmVxdWVzdBoRLm5vbW9zLk1WYXVsdE5vdGUSOAoOV3JpdGVWYXVsdE5vdGUSGS5ub21vcy5NVmF1bHRXcml0ZVJlcXVlc3QaCy5ub21vcy5NQWNrEjoKD0RlbGV0ZVZhdWx0Tm90ZRIaLm5vbW9zLk1WYXVsdERlbGV0ZVJlcXVlc3QaCy5ub21vcy5NQWNrEjAKCUxpc3RMb29wcxIMLm5vbW9zLkVtcHR5GhUubm9tb3MuTUxvb3BzUmVzcG9uc2USPAoOU2V0TG9vcEVuYWJsZWQSHS5ub21vcy5NU2V0TG9vcEVuYWJsZWRSZXF1ZXN0Ggsubm9tb3MuTUFjaxI0CgpEZWxldGVMb29wEhkubm9tb3MuTUxvb3BEZWxldGVSZXF1ZXN0Ggsubm9tb3MuTUFjaxIwCglMaXN0VGFza3MSDC5ub21vcy5FbXB0eRoVLm5vbW9zLk1UYXNrc1Jlc3BvbnNlEjQKClVwZGF0ZVRhc2sSGS5ub21vcy5NVGFza1VwZGF0ZVJlcXVlc3QaCy5ub21vcy5NQWNrEjQKCkRlbGV0ZVRhc2sSGS5ub21vcy5NVGFza0RlbGV0ZVJlcXVlc3QaCy5ub21vcy5NQWNrEi8KCEdldEJyYWluEgwubm9tb3MuRW1wdHkaFS5ub21vcy5NQnJhaW5SZXNwb25zZRIyCghHZXRJbmJveBIMLm5vbW9zLkVtcHR5Ghgubm9tb3MuTUdldEluYm94UmVzcG9uc2USLwoIR2V0VG9kYXkSDC5ub21vcy5FbXB0eRoVLm5vbW9zLk1Ub2RheVJlc3BvbnNlElgKEVN0dWRpb0NyZWF0ZUFzc2V0EiAubm9tb3MuTVN0dWRpb0NyZWF0ZUFzc2V0UmVxdWVzdBohLm5vbW9zLk1TdHVkaW9DcmVhdGVBc3NldFJlc3BvbnNlEksKEVN0dWRpb0dldEFzc2V0VXJsEhYubm9tb3MuTVN0dWRpb0Fzc2V0UmVmGh4ubm9tb3MuTVN0dWRpb0Fzc2V0VXJsUmVzcG9uc2USPgoKU3R1ZGlvRWRpdBIZLm5vbW9zLk1TdHVkaW9FZGl0UmVxdWVzdBoTLm5vbW9zLk1TdHVkaW9FdmVudDABEkYKDVN0dWRpb0hpc3RvcnkSFi5ub21vcy5NU3R1ZGlvQXNzZXRSZWYaHS5ub21vcy5NU3R1ZGlvSGlzdG9yeVJlc3BvbnNlElUKEFN0dWRpb0xpc3RBc3NldHMSHy5ub21vcy5NU3R1ZGlvTGlzdEFzc2V0c1JlcXVlc3QaIC5ub21vcy5NU3R1ZGlvTGlzdEFzc2V0c1Jlc3BvbnNlEk8KElN0dWRpb1N1Z2dlc3RFZGl0cxIWLm5vbW9zLk1TdHVkaW9Bc3NldFJlZhohLm5vbW9zLk1TdHVkaW9TdWdnZXN0aW9uc1Jlc3BvbnNlEkEKFFN0dWRpb1JlcG9ydElkZW50aXR5Ehwubm9tb3MuTVN0dWRpb0lkZW50aXR5UmVwb3J0Ggsubm9tb3MuTUFjazJICgxPQXV0aERlcG9zaXQSOAoHRGVwb3NpdBIVLm5vbW9zLkRlcG9zaXRSZXF1ZXN0GhYubm9tb3MuRGVwb3NpdFJlc3BvbnNlYgZwcm90bzM", ); /** @@ -585,6 +585,538 @@ export const MLoopDeleteRequestSchema: GenMessage /*@__PURE_ 22, ); +/** + * Tasks (the user's scheduled tasks) + * + * @generated from message nomos.MTask + */ +export type MTask = Message<"nomos.MTask"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * the instruction the task runs + * + * @generated from field: string prompt = 3; + */ + prompt: string; + + /** + * raw: "15m" | cron expr | ISO timestamp + * + * @generated from field: string schedule = 4; + */ + schedule: string; + + /** + * every | cron | at + * + * @generated from field: string schedule_type = 5; + */ + scheduleType: string; + + /** + * friendly, display-only + * + * @generated from field: string display_schedule = 6; + */ + displaySchedule: string; + + /** + * @generated from field: bool enabled = 7; + */ + enabled: boolean; + + /** + * @generated from field: string source = 8; + */ + source: string; + + /** + * ISO-8601, empty if never run + * + * @generated from field: string last_run = 9; + */ + lastRun: string; +}; + +/** + * Describes the message nomos.MTask. + * Use `create(MTaskSchema)` to create a new message. + */ +export const MTaskSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 23); + +/** + * @generated from message nomos.MTasksResponse + */ +export type MTasksResponse = Message<"nomos.MTasksResponse"> & { + /** + * @generated from field: repeated nomos.MTask tasks = 1; + */ + tasks: MTask[]; +}; + +/** + * Describes the message nomos.MTasksResponse. + * Use `create(MTasksResponseSchema)` to create a new message. + */ +export const MTasksResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 24, +); + +/** + * Full-state update (the client sends the edited task); empty name/schedule are + * ignored so a toggle-only call never blanks fields. + * + * @generated from message nomos.MTaskUpdateRequest + */ +export type MTaskUpdateRequest = Message<"nomos.MTaskUpdateRequest"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * @generated from field: string prompt = 3; + */ + prompt: string; + + /** + * @generated from field: string schedule = 4; + */ + schedule: string; + + /** + * @generated from field: string schedule_type = 5; + */ + scheduleType: string; + + /** + * @generated from field: bool enabled = 6; + */ + enabled: boolean; +}; + +/** + * Describes the message nomos.MTaskUpdateRequest. + * Use `create(MTaskUpdateRequestSchema)` to create a new message. + */ +export const MTaskUpdateRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 25, +); + +/** + * @generated from message nomos.MTaskDeleteRequest + */ +export type MTaskDeleteRequest = Message<"nomos.MTaskDeleteRequest"> & { + /** + * @generated from field: string id = 1; + */ + id: string; +}; + +/** + * Describes the message nomos.MTaskDeleteRequest. + * Use `create(MTaskDeleteRequestSchema)` to create a new message. + */ +export const MTaskDeleteRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 26, +); + +/** + * Brain (knowledge graph + learned facts) + * + * @generated from message nomos.MBrainNode + */ +export type MBrainNode = Message<"nomos.MBrainNode"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string label = 2; + */ + label: string; + + /** + * person | org | topic | decision | project | value | event | wiki | vault + * + * @generated from field: string kind = 3; + */ + kind: string; + + /** + * @generated from field: string summary = 4; + */ + summary: string; + + /** + * connection count within the returned subgraph + * + * @generated from field: int32 degree = 5; + */ + degree: number; + + /** + * @generated from field: double confidence = 6; + */ + confidence: number; +}; + +/** + * Describes the message nomos.MBrainNode. + * Use `create(MBrainNodeSchema)` to create a new message. + */ +export const MBrainNodeSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 27); + +/** + * @generated from message nomos.MBrainEdge + */ +export type MBrainEdge = Message<"nomos.MBrainEdge"> & { + /** + * @generated from field: string src = 1; + */ + src: string; + + /** + * @generated from field: string dst = 2; + */ + dst: string; + + /** + * @generated from field: string relation = 3; + */ + relation: string; +}; + +/** + * Describes the message nomos.MBrainEdge. + * Use `create(MBrainEdgeSchema)` to create a new message. + */ +export const MBrainEdgeSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 28); + +/** + * @generated from message nomos.MBrainFact + */ +export type MBrainFact = Message<"nomos.MBrainFact"> & { + /** + * @generated from field: string text = 1; + */ + text: string; + + /** + * @generated from field: string source = 2; + */ + source: string; + + /** + * 0..3 + * + * @generated from field: int32 confidence = 3; + */ + confidence: number; + + /** + * ISO-8601 + * + * @generated from field: string learned_at = 4; + */ + learnedAt: string; +}; + +/** + * Describes the message nomos.MBrainFact. + * Use `create(MBrainFactSchema)` to create a new message. + */ +export const MBrainFactSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 29); + +/** + * @generated from message nomos.MBrainResponse + */ +export type MBrainResponse = Message<"nomos.MBrainResponse"> & { + /** + * @generated from field: repeated nomos.MBrainNode nodes = 1; + */ + nodes: MBrainNode[]; + + /** + * @generated from field: repeated nomos.MBrainEdge edges = 2; + */ + edges: MBrainEdge[]; + + /** + * @generated from field: repeated nomos.MBrainFact facts = 3; + */ + facts: MBrainFact[]; + + /** + * @generated from field: int32 entity_count = 4; + */ + entityCount: number; + + /** + * @generated from field: int32 fact_count = 5; + */ + factCount: number; +}; + +/** + * Describes the message nomos.MBrainResponse. + * Use `create(MBrainResponseSchema)` to create a new message. + */ +export const MBrainResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 30, +); + +/** + * Inbox (drafts + CATE agent requests) + * + * @generated from message nomos.MInboxDraft + */ +export type MInboxDraft = Message<"nomos.MInboxDraft"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string recipient = 2; + */ + recipient: string; + + /** + * @generated from field: string preview = 3; + */ + preview: string; + + /** + * pending | approved | sent + * + * @generated from field: string status = 4; + */ + status: string; + + /** + * @generated from field: string platform = 5; + */ + platform: string; + + /** + * @generated from field: string created_at = 6; + */ + createdAt: string; +}; + +/** + * Describes the message nomos.MInboxDraft. + * Use `create(MInboxDraftSchema)` to create a new message. + */ +export const MInboxDraftSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 31); + +/** + * @generated from message nomos.MInboxCate + */ +export type MInboxCate = Message<"nomos.MInboxCate"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string from_label = 2; + */ + fromLabel: string; + + /** + * @generated from field: string trust_tier = 3; + */ + trustTier: string; + + /** + * @generated from field: string subject = 4; + */ + subject: string; + + /** + * @generated from field: string bond_amount = 5; + */ + bondAmount: string; + + /** + * @generated from field: string created_at = 6; + */ + createdAt: string; +}; + +/** + * Describes the message nomos.MInboxCate. + * Use `create(MInboxCateSchema)` to create a new message. + */ +export const MInboxCateSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 32); + +/** + * @generated from message nomos.MGetInboxResponse + */ +export type MGetInboxResponse = Message<"nomos.MGetInboxResponse"> & { + /** + * @generated from field: repeated nomos.MInboxDraft drafts = 1; + */ + drafts: MInboxDraft[]; + + /** + * @generated from field: repeated nomos.MInboxCate cate = 2; + */ + cate: MInboxCate[]; + + /** + * @generated from field: int32 blocked_count = 3; + */ + blockedCount: number; +}; + +/** + * Describes the message nomos.MGetInboxResponse. + * Use `create(MGetInboxResponseSchema)` to create a new message. + */ +export const MGetInboxResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 33, +); + +/** + * Today (the daily brief) + * + * @generated from message nomos.MTodayEvent + */ +export type MTodayEvent = Message<"nomos.MTodayEvent"> & { + /** + * @generated from field: string time = 1; + */ + time: string; + + /** + * @generated from field: string title = 2; + */ + title: string; + + /** + * @generated from field: string meta = 3; + */ + meta: string; +}; + +/** + * Describes the message nomos.MTodayEvent. + * Use `create(MTodayEventSchema)` to create a new message. + */ +export const MTodayEventSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 34); + +/** + * @generated from message nomos.MTodayCommitment + */ +export type MTodayCommitment = Message<"nomos.MTodayCommitment"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string description = 2; + */ + description: string; + + /** + * @generated from field: string due = 3; + */ + due: string; +}; + +/** + * Describes the message nomos.MTodayCommitment. + * Use `create(MTodayCommitmentSchema)` to create a new message. + */ +export const MTodayCommitmentSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 35, +); + +/** + * @generated from message nomos.MTodayTask + */ +export type MTodayTask = Message<"nomos.MTodayTask"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * @generated from field: string schedule = 3; + */ + schedule: string; +}; + +/** + * Describes the message nomos.MTodayTask. + * Use `create(MTodayTaskSchema)` to create a new message. + */ +export const MTodayTaskSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 36); + +/** + * @generated from message nomos.MTodayResponse + */ +export type MTodayResponse = Message<"nomos.MTodayResponse"> & { + /** + * @generated from field: bool briefing_enabled = 1; + */ + briefingEnabled: boolean; + + /** + * @generated from field: repeated nomos.MTodayEvent events = 2; + */ + events: MTodayEvent[]; + + /** + * @generated from field: repeated nomos.MTodayCommitment commitments = 3; + */ + commitments: MTodayCommitment[]; + + /** + * @generated from field: repeated nomos.MTodayTask tasks = 4; + */ + tasks: MTodayTask[]; +}; + +/** + * Describes the message nomos.MTodayResponse. + * Use `create(MTodayResponseSchema)` to create a new message. + */ +export const MTodayResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 37, +); + /** * @generated from message nomos.MAck */ @@ -604,7 +1136,7 @@ export type MAck = Message<"nomos.MAck"> & { * Describes the message nomos.MAck. * Use `create(MAckSchema)` to create a new message. */ -export const MAckSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 23); +export const MAckSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 38); /** * Vault (long-term memory / knowledge base) @@ -624,7 +1156,7 @@ export type MVaultListRequest = Message<"nomos.MVaultListRequest"> & { */ export const MVaultListRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 24, + 39, ); /** @@ -653,7 +1185,7 @@ export type MVaultNoteSummary = Message<"nomos.MVaultNoteSummary"> & { */ export const MVaultNoteSummarySchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 25, + 40, ); /** @@ -672,7 +1204,7 @@ export type MVaultListResponse = Message<"nomos.MVaultListResponse"> & { */ export const MVaultListResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 26, + 41, ); /** @@ -691,7 +1223,7 @@ export type MVaultGetRequest = Message<"nomos.MVaultGetRequest"> & { */ export const MVaultGetRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 27, + 42, ); /** @@ -728,7 +1260,7 @@ export type MVaultNote = Message<"nomos.MVaultNote"> & { * Describes the message nomos.MVaultNote. * Use `create(MVaultNoteSchema)` to create a new message. */ -export const MVaultNoteSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 28); +export const MVaultNoteSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 43); /** * @generated from message nomos.MVaultWriteRequest @@ -756,7 +1288,7 @@ export type MVaultWriteRequest = Message<"nomos.MVaultWriteRequest"> & { */ export const MVaultWriteRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 29, + 44, ); /** @@ -775,7 +1307,7 @@ export type MVaultDeleteRequest = Message<"nomos.MVaultDeleteRequest"> & { */ export const MVaultDeleteRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 30, + 45, ); /** @@ -801,7 +1333,7 @@ export type MChatRequest = Message<"nomos.MChatRequest"> & { */ export const MChatRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 31, + 46, ); /** @@ -823,7 +1355,7 @@ export type MChatEvent = Message<"nomos.MChatEvent"> & { * Describes the message nomos.MChatEvent. * Use `create(MChatEventSchema)` to create a new message. */ -export const MChatEventSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 32); +export const MChatEventSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 47); /** * @generated from message nomos.MGetMessagesRequest @@ -851,7 +1383,7 @@ export type MGetMessagesRequest = Message<"nomos.MGetMessagesRequest"> & { */ export const MGetMessagesRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 33, + 48, ); /** @@ -885,7 +1417,7 @@ export type MMessage = Message<"nomos.MMessage"> & { * Describes the message nomos.MMessage. * Use `create(MMessageSchema)` to create a new message. */ -export const MMessageSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 34); +export const MMessageSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 49); /** * @generated from message nomos.MGetMessagesResponse @@ -902,7 +1434,7 @@ export type MGetMessagesResponse = Message<"nomos.MGetMessagesResponse"> & { * Use `create(MGetMessagesResponseSchema)` to create a new message. */ export const MGetMessagesResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 35); + messageDesc(file_nomos, 50); /** * @generated from message nomos.MDraftAction @@ -920,7 +1452,7 @@ export type MDraftAction = Message<"nomos.MDraftAction"> & { */ export const MDraftActionSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 36, + 51, ); /** @@ -943,7 +1475,7 @@ export type MDraftActionWithEdit = Message<"nomos.MDraftActionWithEdit"> & { * Use `create(MDraftActionWithEditSchema)` to create a new message. */ export const MDraftActionWithEditSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 37); + messageDesc(file_nomos, 52); /** * @generated from message nomos.MDraftActionResponse @@ -965,7 +1497,7 @@ export type MDraftActionResponse = Message<"nomos.MDraftActionResponse"> & { * Use `create(MDraftActionResponseSchema)` to create a new message. */ export const MDraftActionResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 38); + messageDesc(file_nomos, 53); /** * Inbox @@ -992,7 +1524,7 @@ export type MInboxRequest = Message<"nomos.MInboxRequest"> & { */ export const MInboxRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 39, + 54, ); /** @@ -1050,7 +1582,7 @@ export type MInboxItem = Message<"nomos.MInboxItem"> & { * Describes the message nomos.MInboxItem. * Use `create(MInboxItemSchema)` to create a new message. */ -export const MInboxItemSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 40); +export const MInboxItemSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 55); /** * @generated from message nomos.MInboxResponse @@ -1073,7 +1605,7 @@ export type MInboxResponse = Message<"nomos.MInboxResponse"> & { */ export const MInboxResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 41, + 56, ); /** @@ -1092,7 +1624,7 @@ export type MEnvelopeRequest = Message<"nomos.MEnvelopeRequest"> & { */ export const MEnvelopeRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 42, + 57, ); /** @@ -1141,7 +1673,7 @@ export type MCateEnvelope = Message<"nomos.MCateEnvelope"> & { */ export const MCateEnvelopeSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 43, + 58, ); /** @@ -1167,7 +1699,7 @@ export type MInboxActionRequest = Message<"nomos.MInboxActionRequest"> & { */ export const MInboxActionRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 44, + 59, ); /** @@ -1190,7 +1722,7 @@ export type MInboxActionResponse = Message<"nomos.MInboxActionResponse"> & { * Use `create(MInboxActionResponseSchema)` to create a new message. */ export const MInboxActionResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 45); + messageDesc(file_nomos, 60); /** * Skills @@ -1235,7 +1767,7 @@ export type MSkill = Message<"nomos.MSkill"> & { * Describes the message nomos.MSkill. * Use `create(MSkillSchema)` to create a new message. */ -export const MSkillSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 46); +export const MSkillSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 61); /** * @generated from message nomos.MSkillsResponse @@ -1253,7 +1785,52 @@ export type MSkillsResponse = Message<"nomos.MSkillsResponse"> & { */ export const MSkillsResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 47, + 62, +); + +/** + * @generated from message nomos.MPlugin + */ +export type MPlugin = Message<"nomos.MPlugin"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from field: string description = 2; + */ + description: string; + + /** + * @generated from field: string marketplace = 3; + */ + marketplace: string; +}; + +/** + * Describes the message nomos.MPlugin. + * Use `create(MPluginSchema)` to create a new message. + */ +export const MPluginSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 63); + +/** + * @generated from message nomos.MPluginsResponse + */ +export type MPluginsResponse = Message<"nomos.MPluginsResponse"> & { + /** + * @generated from field: repeated nomos.MPlugin plugins = 1; + */ + plugins: MPlugin[]; +}; + +/** + * Describes the message nomos.MPluginsResponse. + * Use `create(MPluginsResponseSchema)` to create a new message. + */ +export const MPluginsResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 64, ); /** @@ -1277,7 +1854,7 @@ export type MSkillToggleRequest = Message<"nomos.MSkillToggleRequest"> & { */ export const MSkillToggleRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 48, + 65, ); /** @@ -1300,7 +1877,7 @@ export type MSkillToggleResponse = Message<"nomos.MSkillToggleResponse"> & { * Use `create(MSkillToggleResponseSchema)` to create a new message. */ export const MSkillToggleResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 49); + messageDesc(file_nomos, 66); /** * Earnings @@ -1322,7 +1899,7 @@ export type MEarningsRequest = Message<"nomos.MEarningsRequest"> & { */ export const MEarningsRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 50, + 67, ); /** @@ -1365,7 +1942,7 @@ export type MEarningsResponse = Message<"nomos.MEarningsResponse"> & { */ export const MEarningsResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 51, + 68, ); /** @@ -1395,7 +1972,7 @@ export type MGraphRequest = Message<"nomos.MGraphRequest"> & { */ export const MGraphRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 52, + 69, ); /** @@ -1434,7 +2011,7 @@ export type MGraphNeighborsRequest = Message<"nomos.MGraphNeighborsRequest"> & { * Use `create(MGraphNeighborsRequestSchema)` to create a new message. */ export const MGraphNeighborsRequestSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 53); + messageDesc(file_nomos, 70); /** * @generated from message nomos.MGraphSearchRequest @@ -1457,7 +2034,7 @@ export type MGraphSearchRequest = Message<"nomos.MGraphSearchRequest"> & { */ export const MGraphSearchRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 54, + 71, ); /** @@ -1511,7 +2088,7 @@ export type MGraphNode = Message<"nomos.MGraphNode"> & { * Describes the message nomos.MGraphNode. * Use `create(MGraphNodeSchema)` to create a new message. */ -export const MGraphNodeSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 55); +export const MGraphNodeSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 72); /** * @generated from message nomos.MGraphEdge @@ -1552,7 +2129,7 @@ export type MGraphEdge = Message<"nomos.MGraphEdge"> & { * Describes the message nomos.MGraphEdge. * Use `create(MGraphEdgeSchema)` to create a new message. */ -export const MGraphEdgeSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 56); +export const MGraphEdgeSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 73); /** * @generated from message nomos.MGraphResponse @@ -1575,7 +2152,7 @@ export type MGraphResponse = Message<"nomos.MGraphResponse"> & { */ export const MGraphResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 57, + 74, ); /** @@ -1616,7 +2193,7 @@ export type MTrustTier = Message<"nomos.MTrustTier"> & { * Describes the message nomos.MTrustTier. * Use `create(MTrustTierSchema)` to create a new message. */ -export const MTrustTierSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 58); +export const MTrustTierSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 75); /** * @generated from message nomos.MPermission @@ -1642,7 +2219,7 @@ export type MPermission = Message<"nomos.MPermission"> & { * Describes the message nomos.MPermission. * Use `create(MPermissionSchema)` to create a new message. */ -export const MPermissionSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 59); +export const MPermissionSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 76); /** * @generated from message nomos.MIntegration @@ -1696,7 +2273,7 @@ export type MIntegration = Message<"nomos.MIntegration"> & { */ export const MIntegrationSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 60, + 77, ); /** @@ -1733,7 +2310,7 @@ export type MProfile = Message<"nomos.MProfile"> & { * Describes the message nomos.MProfile. * Use `create(MProfileSchema)` to create a new message. */ -export const MProfileSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 61); +export const MProfileSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 78); /** * @generated from message nomos.MSettingsResponse @@ -1758,6 +2335,26 @@ export type MSettingsResponse = Message<"nomos.MSettingsResponse"> & { * @generated from field: repeated nomos.MIntegration integrations = 4; */ integrations: MIntegration[]; + + /** + * @generated from field: repeated nomos.MConsentEntry consent = 5; + */ + consent: MConsentEntry[]; + + /** + * @generated from field: nomos.MAgentIdentity identity = 6; + */ + identity?: MAgentIdentity | undefined; + + /** + * @generated from field: repeated nomos.MAppToggle app_toggles = 7; + */ + appToggles: MAppToggle[]; + + /** + * @generated from field: nomos.MProactive proactive = 8; + */ + proactive?: MProactive | undefined; }; /** @@ -1766,9 +2363,171 @@ export type MSettingsResponse = Message<"nomos.MSettingsResponse"> & { */ export const MSettingsResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 62, + 79, +); + +/** + * @generated from message nomos.MProactive + */ +export type MProactive = Message<"nomos.MProactive"> & { + /** + * off | passive | active (app.inboxAutonomy) + * + * @generated from field: string mode = 1; + */ + mode: string; + + /** + * cron for the daily briefing (app.briefingCron) + * + * @generated from field: string briefing = 2; + */ + briefing: string; +}; + +/** + * Describes the message nomos.MProactive. + * Use `create(MProactiveSchema)` to create a new message. + */ +export const MProactiveSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 80); + +/** + * @generated from message nomos.MConsentEntry + */ +export type MConsentEntry = Message<"nomos.MConsentEntry"> & { + /** + * @generated from field: string platform = 1; + */ + platform: string; + + /** + * always_ask | auto_approve | notify_only + * + * @generated from field: string mode = 2; + */ + mode: string; +}; + +/** + * Describes the message nomos.MConsentEntry. + * Use `create(MConsentEntrySchema)` to create a new message. + */ +export const MConsentEntrySchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 81, +); + +/** + * @generated from message nomos.MAgentIdentity + */ +export type MAgentIdentity = Message<"nomos.MAgentIdentity"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * personality / voice & tone (the SOUL) + * + * @generated from field: string voice = 2; + */ + voice: string; + + /** + * chosen avatar (an emoji), empty = monogram fallback + * + * @generated from field: string avatar = 3; + */ + avatar: string; +}; + +/** + * Describes the message nomos.MAgentIdentity. + * Use `create(MAgentIdentitySchema)` to create a new message. + */ +export const MAgentIdentitySchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 82, +); + +/** + * @generated from message nomos.MAppToggle + */ +export type MAppToggle = Message<"nomos.MAppToggle"> & { + /** + * app. config key + * + * @generated from field: string key = 1; + */ + key: string; + + /** + * @generated from field: bool enabled = 2; + */ + enabled: boolean; +}; + +/** + * Describes the message nomos.MAppToggle. + * Use `create(MAppToggleSchema)` to create a new message. + */ +export const MAppToggleSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 83); + +/** + * @generated from message nomos.MAppSettingRequest + */ +export type MAppSettingRequest = Message<"nomos.MAppSettingRequest"> & { + /** + * app. config key (consumer-safe subset only) + * + * @generated from field: string key = 1; + */ + key: string; + + /** + * "true"/"false" for bools, raw string otherwise + * + * @generated from field: string value = 2; + */ + value: string; +}; + +/** + * Describes the message nomos.MAppSettingRequest. + * Use `create(MAppSettingRequestSchema)` to create a new message. + */ +export const MAppSettingRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 84, ); +/** + * @generated from message nomos.MAgentIdentityRequest + */ +export type MAgentIdentityRequest = Message<"nomos.MAgentIdentityRequest"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from field: string voice = 2; + */ + voice: string; + + /** + * @generated from field: string avatar = 3; + */ + avatar: string; +}; + +/** + * Describes the message nomos.MAgentIdentityRequest. + * Use `create(MAgentIdentityRequestSchema)` to create a new message. + */ +export const MAgentIdentityRequestSchema: GenMessage /*@__PURE__*/ = + messageDesc(file_nomos, 85); + /** * @generated from message nomos.MConsentRequest */ @@ -1792,7 +2551,7 @@ export type MConsentRequest = Message<"nomos.MConsentRequest"> & { */ export const MConsentRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 63, + 86, ); /** @@ -1821,7 +2580,7 @@ export type MTrustTierRequest = Message<"nomos.MTrustTierRequest"> & { */ export const MTrustTierRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 64, + 87, ); /** @@ -1845,7 +2604,7 @@ export type MPermissionRequest = Message<"nomos.MPermissionRequest"> & { */ export const MPermissionRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 65, + 88, ); /** @@ -1863,7 +2622,7 @@ export type MIntegrationsResponse = Message<"nomos.MIntegrationsResponse"> & { * Use `create(MIntegrationsResponseSchema)` to create a new message. */ export const MIntegrationsResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 66); + messageDesc(file_nomos, 89); /** * @generated from message nomos.MStartConnectRequest @@ -1882,7 +2641,7 @@ export type MStartConnectRequest = Message<"nomos.MStartConnectRequest"> & { * Use `create(MStartConnectRequestSchema)` to create a new message. */ export const MStartConnectRequestSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 67); + messageDesc(file_nomos, 90); /** * @generated from message nomos.MStartConnectResponse @@ -1903,7 +2662,7 @@ export type MStartConnectResponse = Message<"nomos.MStartConnectResponse"> & { * Use `create(MStartConnectResponseSchema)` to create a new message. */ export const MStartConnectResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 68); + messageDesc(file_nomos, 91); /** * @generated from message nomos.MDisconnectRequest @@ -1928,7 +2687,7 @@ export type MDisconnectRequest = Message<"nomos.MDisconnectRequest"> & { */ export const MDisconnectRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 69, + 92, ); /** @@ -1955,7 +2714,7 @@ export type MConnectGoogleRequest = Message<"nomos.MConnectGoogleRequest"> & { * Use `create(MConnectGoogleRequestSchema)` to create a new message. */ export const MConnectGoogleRequestSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 70); + messageDesc(file_nomos, 93); /** * @generated from message nomos.MSetGoogleSendRequest @@ -1977,7 +2736,7 @@ export type MSetGoogleSendRequest = Message<"nomos.MSetGoogleSendRequest"> & { * Use `create(MSetGoogleSendRequestSchema)` to create a new message. */ export const MSetGoogleSendRequestSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 71); + messageDesc(file_nomos, 94); /** * @generated from message nomos.MDeviceRegister @@ -2007,7 +2766,7 @@ export type MDeviceRegister = Message<"nomos.MDeviceRegister"> & { */ export const MDeviceRegisterSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 72, + 95, ); /** @@ -2026,7 +2785,7 @@ export type MDeviceUnregister = Message<"nomos.MDeviceUnregister"> & { */ export const MDeviceUnregisterSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 73, + 96, ); /** @@ -2086,7 +2845,7 @@ export type DepositRequest = Message<"nomos.DepositRequest"> & { */ export const DepositRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 74, + 97, ); /** @@ -2117,7 +2876,7 @@ export type DepositResponse = Message<"nomos.DepositResponse"> & { */ export const DepositResponseSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 75, + 98, ); /** @@ -2164,7 +2923,7 @@ export type MStudioCreateAssetRequest = Message<"nomos.MStudioCreateAssetRequest * Use `create(MStudioCreateAssetRequestSchema)` to create a new message. */ export const MStudioCreateAssetRequestSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 76); + messageDesc(file_nomos, 99); /** * @generated from message nomos.MStudioCreateAssetResponse @@ -2200,7 +2959,7 @@ export type MStudioCreateAssetResponse = Message<"nomos.MStudioCreateAssetRespon * Use `create(MStudioCreateAssetResponseSchema)` to create a new message. */ export const MStudioCreateAssetResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 77); + messageDesc(file_nomos, 100); /** * @generated from message nomos.MStudioAssetRef @@ -2225,7 +2984,7 @@ export type MStudioAssetRef = Message<"nomos.MStudioAssetRef"> & { */ export const MStudioAssetRefSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 78, + 101, ); /** @@ -2250,7 +3009,7 @@ export type MStudioAssetUrlResponse = Message<"nomos.MStudioAssetUrlResponse"> & * Use `create(MStudioAssetUrlResponseSchema)` to create a new message. */ export const MStudioAssetUrlResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 79); + messageDesc(file_nomos, 102); /** * Apply one op. params_json is the JSON-encoded op params (validated server-side @@ -2307,7 +3066,7 @@ export type MStudioEditRequest = Message<"nomos.MStudioEditRequest"> & { */ export const MStudioEditRequestSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 80, + 103, ); /** @@ -2360,7 +3119,7 @@ export type MStudioEvent = Message<"nomos.MStudioEvent"> & { */ export const MStudioEventSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 81, + 104, ); /** @@ -2412,7 +3171,10 @@ export type MStudioEdit = Message<"nomos.MStudioEdit"> & { * Describes the message nomos.MStudioEdit. * Use `create(MStudioEditSchema)` to create a new message. */ -export const MStudioEditSchema: GenMessage /*@__PURE__*/ = messageDesc(file_nomos, 82); +export const MStudioEditSchema: GenMessage /*@__PURE__*/ = messageDesc( + file_nomos, + 105, +); /** * @generated from message nomos.MStudioHistoryResponse @@ -2434,7 +3196,7 @@ export type MStudioHistoryResponse = Message<"nomos.MStudioHistoryResponse"> & { * Use `create(MStudioHistoryResponseSchema)` to create a new message. */ export const MStudioHistoryResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 83); + messageDesc(file_nomos, 106); /** * @generated from message nomos.MStudioIdentityReport @@ -2458,7 +3220,7 @@ export type MStudioIdentityReport = Message<"nomos.MStudioIdentityReport"> & { * Use `create(MStudioIdentityReportSchema)` to create a new message. */ export const MStudioIdentityReportSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 84); + messageDesc(file_nomos, 107); /** * A recent editing session for the Home launchpad. An asset + the gist of its chain. @@ -2479,7 +3241,7 @@ export type MStudioListAssetsRequest = Message<"nomos.MStudioListAssetsRequest"> * Use `create(MStudioListAssetsRequestSchema)` to create a new message. */ export const MStudioListAssetsRequestSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 85); + messageDesc(file_nomos, 108); /** * @generated from message nomos.MStudioAssetSummary @@ -2537,7 +3299,7 @@ export type MStudioAssetSummary = Message<"nomos.MStudioAssetSummary"> & { */ export const MStudioAssetSummarySchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 86, + 109, ); /** @@ -2555,7 +3317,7 @@ export type MStudioListAssetsResponse = Message<"nomos.MStudioListAssetsResponse * Use `create(MStudioListAssetsResponseSchema)` to create a new message. */ export const MStudioListAssetsResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 87); + messageDesc(file_nomos, 110); /** * A per-photo edit suggestion: a short chip label + the instruction it applies. @@ -2580,7 +3342,7 @@ export type MStudioSuggestion = Message<"nomos.MStudioSuggestion"> & { */ export const MStudioSuggestionSchema: GenMessage /*@__PURE__*/ = messageDesc( file_nomos, - 88, + 111, ); /** @@ -2598,7 +3360,7 @@ export type MStudioSuggestionsResponse = Message<"nomos.MStudioSuggestionsRespon * Use `create(MStudioSuggestionsResponseSchema)` to create a new message. */ export const MStudioSuggestionsResponseSchema: GenMessage /*@__PURE__*/ = - messageDesc(file_nomos, 89); + messageDesc(file_nomos, 112); /** * @generated from service nomos.NomosAgent @@ -2806,6 +3568,14 @@ export const MobileApi: GenService<{ input: typeof MSkillToggleRequestSchema; output: typeof MSkillToggleResponseSchema; }; + /** + * @generated from rpc nomos.MobileApi.ListPlugins + */ + listPlugins: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof MPluginsResponseSchema; + }; /** * Earnings tab * @@ -2876,6 +3646,22 @@ export const MobileApi: GenService<{ input: typeof MPermissionRequestSchema; output: typeof MAckSchema; }; + /** + * @generated from rpc nomos.MobileApi.UpdateAppSetting + */ + updateAppSetting: { + methodKind: "unary"; + input: typeof MAppSettingRequestSchema; + output: typeof MAckSchema; + }; + /** + * @generated from rpc nomos.MobileApi.UpdateAgentIdentity + */ + updateAgentIdentity: { + methodKind: "unary"; + input: typeof MAgentIdentityRequestSchema; + output: typeof MAckSchema; + }; /** * @generated from rpc nomos.MobileApi.ListIntegrations */ @@ -2994,6 +3780,65 @@ export const MobileApi: GenService<{ input: typeof MLoopDeleteRequestSchema; output: typeof MAckSchema; }; + /** + * Tasks tab (the user's scheduled tasks: one-off reminders + recurring jobs, + * editable: reschedule, rename, edit instruction, enable/disable, delete) + * + * @generated from rpc nomos.MobileApi.ListTasks + */ + listTasks: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof MTasksResponseSchema; + }; + /** + * @generated from rpc nomos.MobileApi.UpdateTask + */ + updateTask: { + methodKind: "unary"; + input: typeof MTaskUpdateRequestSchema; + output: typeof MAckSchema; + }; + /** + * @generated from rpc nomos.MobileApi.DeleteTask + */ + deleteTask: { + methodKind: "unary"; + input: typeof MTaskDeleteRequestSchema; + output: typeof MAckSchema; + }; + /** + * Brain tab (the user's knowledge graph + learned facts, for the feed + map) + * + * @generated from rpc nomos.MobileApi.GetBrain + */ + getBrain: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof MBrainResponseSchema; + }; + /** + * Inbox tab (drafts to approve + CATE agent requests). Actions reuse + * ApproveDraft/RejectDraft (drafts) + ActOnInboxItem (CATE). + * + * @generated from rpc nomos.MobileApi.GetInbox + */ + getInbox: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof MGetInboxResponseSchema; + }; + /** + * Today tab (the daily brief: calendar + commitments + tasks, gated on the + * daily briefing being enabled). + * + * @generated from rpc nomos.MobileApi.GetToday + */ + getToday: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof MTodayResponseSchema; + }; /** * Studio (hosted-only feature). Blobs move via presigned PUT/GET, never gRPC. * diff --git a/src/memory/brain.ts b/src/memory/brain.ts new file mode 100644 index 00000000..3bfd1dac --- /dev/null +++ b/src/memory/brain.ts @@ -0,0 +1,107 @@ +/** + * Brain overview -- the read model behind MobileApi.GetBrain. + * + * Composes the consumer Brain page from real per-user memory: the knowledge + * graph (kg_nodes / kg_edges, via getProjection) for the map + entities, and the + * accumulated user_model for the "recently learned" facts feed. Owner-scoped via + * the TenantContext that every query already threads. + */ + +import type { TenantContext } from "../auth/tenant-context.ts"; +import { getProjection } from "./graph.ts"; +import { getUserModel, type UserModelEntry } from "../db/user-model.ts"; +import { getKysely } from "../db/client.ts"; + +export interface BrainNodeView { + id: string; + label: string; + kind: string; // person | org | topic | decision | project | value | event | wiki | vault | ... + summary: string; + degree: number; + confidence: number; +} + +export interface BrainEdgeView { + src: string; + dst: string; + relation: string; +} + +export interface BrainFactView { + text: string; + source: string; + confidence: number; // 0..3 (binned from the 0..1 model confidence) + learnedAt: string; // ISO-8601 +} + +export interface BrainOverview { + nodes: BrainNodeView[]; + edges: BrainEdgeView[]; + facts: BrainFactView[]; + entityCount: number; + factCount: number; +} + +function factText(e: UserModelEntry): string { + const v = e.value; + if (typeof v === "string") return v; + if (v == null) return e.key; + if (typeof v === "object") return `${e.key}: ${JSON.stringify(v)}`; + return `${e.key}: ${String(v)}`; +} + +export async function getBrainOverview( + ctx: TenantContext, + opts: { nodeLimit?: number; factLimit?: number } = {}, +): Promise { + const nodeLimit = opts.nodeLimit ?? 48; + const factLimit = opts.factLimit ?? 12; + + // Map: the most-recent slice of the graph + the edges within it. + const sub = await getProjection(ctx, { limit: nodeLimit }); + + const degree = new Map(); + for (const e of sub.edges) { + degree.set(e.srcId, (degree.get(e.srcId) ?? 0) + 1); + degree.set(e.dstId, (degree.get(e.dstId) ?? 0) + 1); + } + + const nodes: BrainNodeView[] = sub.nodes.map((n) => ({ + id: n.id, + label: n.name, + kind: n.kind, + summary: n.summary ?? "", + degree: degree.get(n.id) ?? 0, + confidence: n.confidence, + })); + + const edges: BrainEdgeView[] = sub.edges.map((e) => ({ + src: e.srcId, + dst: e.dstId, + relation: e.relType.replace(/_/g, " "), + })); + + // Facts: the accumulated user model (already ordered confidence desc, recent). + const model = await getUserModel(ctx.userId); + const facts: BrainFactView[] = model.slice(0, factLimit).map((e) => ({ + text: factText(e), + source: e.category, + confidence: Math.max(0, Math.min(3, Math.round((e.confidence ?? 0.5) * 3))), + learnedAt: e.updatedAt ? new Date(e.updatedAt).toISOString() : "", + })); + + const db = getKysely(); + const counted = await db + .selectFrom("kg_nodes") + .select((eb) => eb.fn.countAll().as("c")) + .where("user_id", "=", ctx.userId) + .executeTakeFirst(); + + return { + nodes, + edges, + facts, + entityCount: Number(counted?.c ?? nodes.length), + factCount: model.length, + }; +} diff --git a/src/plugins/builtin-tools.ts b/src/plugins/builtin-tools.ts new file mode 100644 index 00000000..d496351a --- /dev/null +++ b/src/plugins/builtin-tools.ts @@ -0,0 +1,26 @@ +/** + * The assistant's out-of-the-box capabilities, surfaced read-only in the + * consumer "Built-in tools" page (Advanced). + * + * The Claude marketplace plugins (loadInstalledPlugins) are all developer tools + * (code-review, terraform, mcp-server-dev, ...), so they are not shown to + * consumers. Instead this curated list communicates what the assistant can do + * out of the box, distinct from the user-facing Skills. A real consumer + * marketplace is a later iteration. + */ + +export interface BuiltinTool { + name: string; + description: string; +} + +export const BUILTIN_TOOLS: readonly BuiltinTool[] = [ + { name: "Web search", description: "Searches the web for current information." }, + { name: "Web browser", description: "Opens and reads websites to get things done." }, + { + name: "Long-term memory", + description: "Remembers facts, preferences, and past conversations.", + }, + { name: "File reading & writing", description: "Reads and creates documents and files." }, + { name: "Image generation", description: "Generates images from a description." }, +]; diff --git a/src/sdk/tools.ts b/src/sdk/tools.ts index d9eb5b82..84b8b23a 100644 --- a/src/sdk/tools.ts +++ b/src/sdk/tools.ts @@ -623,6 +623,7 @@ export function createMemoryMcpServer(userId: string = "local"): McpSdkServerCon channelId, enabled: true, errorCount: 0, + source: "agent", // a task the agent scheduled on the user's behalf (not infra) }); // Notify cron engine to refresh (if running in daemon) diff --git a/src/skills/skill-view.test.ts b/src/skills/skill-view.test.ts new file mode 100644 index 00000000..1c11deb5 --- /dev/null +++ b/src/skills/skill-view.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from "vitest"; +import { curateConsumerSkills, resolveSkillName, isConsumerSkill } from "./skill-view.ts"; +import type { Skill } from "./types.ts"; + +function skill(name: string, source: string, description = ""): Skill { + return { name, description, content: "", filePath: `/x/${name}/SKILL.md`, source }; +} + +// Mirrors the real hosted catalog: operator external (Google) + bundled consumer +// + bundled dev/internal/channel skills that must be hidden. +const ALL: Skill[] = [ + skill( + "google-calendar-meeting-prep", + "external", + "Build a practical meeting prep brief from a connected Google Calendar that is long enough to need truncation.", + ), + skill("gmail-inbox-triage", "external", "Triage a Gmail inbox into actionable buckets."), + skill("google-drive", "external", "Find, read, and organize files in Google Drive."), + skill("pdf", "bundled", "Work with PDF files."), + skill("xlsx", "bundled", "Read and write spreadsheets."), + skill("run-evals", "bundled", "Run the Nomos eval suite."), + skill("self-improve", "bundled", "Clone the repo and improve the code."), + skill("skill-creator", "bundled", "Create new skills."), + skill("slack", "bundled", "Interact with Slack workspaces."), +]; + +describe("isConsumerSkill", () => { + it("accepts external + allowlisted bundled, rejects dev/internal/channel", () => { + expect(isConsumerSkill(skill("google-drive", "external"))).toBe(true); + expect(isConsumerSkill(skill("pdf", "bundled"))).toBe(true); + expect(isConsumerSkill(skill("run-evals", "bundled"))).toBe(false); + expect(isConsumerSkill(skill("slack", "bundled"))).toBe(false); + expect(isConsumerSkill(skill("skill-creator", "bundled"))).toBe(false); + }); +}); + +describe("curateConsumerSkills", () => { + it("surfaces only consumer skills, under friendly labels", () => { + const names = curateConsumerSkills(ALL, () => true).map((s) => s.name); + expect(names).toEqual(["Drive", "Inbox triage", "Meeting prep", "PDF tools", "Spreadsheets"]); + }); + + it("hides every dev/internal/channel skill", () => { + const out = curateConsumerSkills(ALL, () => true); + for (const raw of ["run-evals", "self-improve", "skill-creator", "slack"]) { + expect(out.some((s) => resolveSkillName(ALL, s.name) === raw)).toBe(false); + } + }); + + it("badges google skills 'google' and bundled consumer skills 'built-in'", () => { + const out = curateConsumerSkills(ALL, () => true); + expect(out.find((s) => s.name === "Drive")?.source).toBe("google"); + expect(out.find((s) => s.name === "PDF tools")?.source).toBe("built-in"); + }); + + it("folds the persisted enabled state in (default on)", () => { + const off = new Set(["pdf"]); + const out = curateConsumerSkills(ALL, (n) => !off.has(n)); + expect(out.find((s) => s.name === "PDF tools")?.enabled).toBe(false); + expect(out.find((s) => s.name === "Drive")?.enabled).toBe(true); + }); + + it("truncates long descriptions", () => { + const mp = curateConsumerSkills(ALL, () => true).find((s) => s.name === "Meeting prep"); + expect(mp!.description.length).toBeLessThanOrEqual(88); + expect(mp!.description.endsWith("...")).toBe(true); + }); + + it("sanitizes em dashes out of consumer-facing descriptions", () => { + const out = curateConsumerSkills( + [skill("weather", "bundled", "Forecasts — no API key needed.")], + () => true, + ); + expect(out[0].description).not.toContain("—"); + expect(out[0].description).toBe("Forecasts - no API key needed."); + }); +}); + +describe("resolveSkillName", () => { + it("round-trips a friendly label back to the raw skill name for toggling", () => { + expect(resolveSkillName(ALL, "Meeting prep")).toBe("google-calendar-meeting-prep"); + expect(resolveSkillName(ALL, "PDF tools")).toBe("pdf"); + expect(resolveSkillName(ALL, "Drive")).toBe("google-drive"); + }); + + it("falls back to the input when no friendly label matches", () => { + expect(resolveSkillName(ALL, "already-raw")).toBe("already-raw"); + }); +}); diff --git a/src/skills/skill-view.ts b/src/skills/skill-view.ts new file mode 100644 index 00000000..9fa1f75d --- /dev/null +++ b/src/skills/skill-view.ts @@ -0,0 +1,111 @@ +/** + * Consumer Skills view model -- the pure shaping behind MobileApi.ListSkills. + * + * loadSkills() returns the full power-user catalog: bundled dev/internal skills + * (run-evals, self-improve, skill-creator, ...), channel adapters, AND the + * operator-curated external skills (NOMOS_SKILLS_DIR). A consumer should only + * see the genuinely user-facing ones, under friendly labels. Pure + + * dependency-free so it is unit-testable in isolation. + */ + +import type { Skill } from "./types.ts"; + +export interface ConsumerSkill { + /** Friendly label. Also the toggle round-trip key (resolved back server-side). */ + name: string; + description: string; + /** Display badge: google | built-in | add-on. */ + source: string; + enabled: boolean; + certs: string[]; + price: string; +} + +/** Bundled skills appropriate for a consumer. Everything else bundled is + * dev/internal/channel tooling and is hidden. Operator-curated external skills + * (source="external") are always surfaced. */ +export const CONSUMER_BUNDLED_SKILLS = new Set([ + "pdf", + "docx", + "pptx", + "xlsx", + "weather", + "doc-coauthoring", + "internal-comms", +]); + +/** raw skill name -> friendly label. Covers every surfaced skill so both the + * display and the toggle round-trip are deterministic. */ +export const SKILL_LABELS: Record = { + // Operator-curated Google Workspace skills (external). + "gmail-inbox-triage": "Inbox triage", + "google-gmail": "Gmail", + "google-calendar": "Calendar", + "google-calendar-daily-brief": "Daily calendar brief", + "google-calendar-free-up-time": "Free up time", + "google-calendar-group-scheduler": "Group scheduler", + "google-calendar-meeting-prep": "Meeting prep", + "google-drive": "Drive", + // Consumer bundled skills. + pdf: "PDF tools", + docx: "Word documents", + pptx: "Presentations", + xlsx: "Spreadsheets", + weather: "Weather", + "doc-coauthoring": "Document co-authoring", + "internal-comms": "Writing assistant", +}; + +function titleCase(s: string): string { + return s + .split(/[-_]/) + .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)) + .join(" "); +} + +export function friendlySkillName(name: string): string { + return SKILL_LABELS[name] ?? titleCase(name); +} + +function skillBadge(s: Skill): string { + if (/gmail|google|calendar|drive|gws/i.test(s.name)) return "google"; + return s.source === "external" ? "add-on" : "built-in"; +} + +function consumerDescription(s: Skill): string { + // Sanitize author-written copy for the consumer UI (em dashes -> hyphens). + const d = (s.description ?? "").replace(/\s*—\s*/g, " - ").trim(); + return d.length > 88 ? `${d.slice(0, 85).trimEnd()}...` : d; +} + +export function isConsumerSkill(s: Skill): boolean { + return s.source === "external" || CONSUMER_BUNDLED_SKILLS.has(s.name); +} + +/** + * Curate the consumer Skills list: filter to consumer-facing skills, friendly + * labels + badges, with the user's enable/disable state folded in. + * `enabledOf(name)` resolves the persisted toggle (default true). + */ +export function curateConsumerSkills( + skills: Skill[], + enabledOf: (name: string) => boolean, +): ConsumerSkill[] { + return skills + .filter(isConsumerSkill) + .map((s) => ({ + name: friendlySkillName(s.name), + description: consumerDescription(s), + source: skillBadge(s), + enabled: enabledOf(s.name), + certs: [] as string[], + price: "", + })) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +/** Resolve a friendly label back to the raw skill name for the toggle round-trip. + * Falls back to the label itself (already a raw name) when no match. */ +export function resolveSkillName(skills: Skill[], label: string): string { + return skills.find((s) => friendlySkillName(s.name) === label)?.name ?? label; +}