@crossworks/client-types 0.232.133 → 0.232.142

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,882 @@
1
+ /**
2
+ * @mantle/client-types · rows
3
+ *
4
+ * Row/DTO shapes lifted out of the server packages at the jackdaw split,
5
+ * the hand-written mirrors of @mantle/db jsonb/enum shapes, and the redacted
6
+ * account DTOs.
7
+ *
8
+ * Split out of the 2548-line index.ts on 2026-09-02 (audit, tier 3) with the
9
+ * contents unchanged. index.ts re-exports every one of these, so the package's
10
+ * public surface is byte-identical — only the file a symbol lives in moved.
11
+ */
12
+
13
+ // ── Row/DTO shapes moved from the server packages (jackdaw split P0) ─────────
14
+ // Sources: @mantle/content, @mantle/email, @mantle/microsoft, @mantle/runtime/agent
15
+ // re-export these names, so server code keeps its original import paths.
16
+
17
+ export type TaskRow = {
18
+ id: string;
19
+ title: string;
20
+ body: string;
21
+ status: TaskStatus;
22
+ priority: TaskPriority;
23
+ dueAt: string | null;
24
+ tags: string[];
25
+ /** Checklist inside the task ("task breakup"). Stored in `data.todos`. */
26
+ todos: TaskTodo[];
27
+ /** Fractional ordering key for the board (within-column order). Stored in
28
+ * `data.rank`; null on tasks never dragged — they sort after ranked ones. */
29
+ rank: string | null;
30
+ /** Comments on this task (node_comments rows). List/detail badge material. */
31
+ commentCount: number;
32
+ summary: string | null;
33
+ /** When the task was filed away, or null while it is live. Archived tasks are
34
+ * excluded from every list, count and board unless explicitly requested —
35
+ * it is what keeps a Done column from growing without bound. Orthogonal to
36
+ * `status`: an archived task keeps the status it had. */
37
+ archivedAt: string | null;
38
+ createdAt: string;
39
+ updatedAt: string;
40
+ };
41
+
42
+ /** One checklist item inside a task. Server assigns `id` on write. */
43
+ export type TaskTodo = {
44
+ id: string;
45
+ text: string;
46
+ done: boolean;
47
+ };
48
+
49
+ /** Mirrors @mantle/db `NodeCommentAuthorKind`. */
50
+ export type NodeCommentAuthorKind = 'owner' | 'member' | 'agent';
51
+
52
+ /**
53
+ * A comment on a node (tasks first; the table is node-generic). `authorName`
54
+ * is a display snapshot at post time; `mine` is computed server-side per
55
+ * viewer (an owner login sees its own comments as mine, a team member sees
56
+ * theirs), so clients never reconcile the two auth worlds themselves.
57
+ */
58
+ export type NodeComment = {
59
+ id: string;
60
+ nodeId: string;
61
+ authorKind: NodeCommentAuthorKind;
62
+ authorName: string;
63
+ mine: boolean;
64
+ body: string;
65
+ createdAt: string;
66
+ editedAt: string | null;
67
+ };
68
+
69
+ export type JournalRow = {
70
+ id: string;
71
+ title: string;
72
+ body: string;
73
+ /** Who wrote the entry. Stamped server-side; agent tool calls can't spoof it. */
74
+ author: 'user' | 'agent';
75
+ /** Authoring agent's slug when author='agent'; null for user-authored rows. */
76
+ agentSlug: string | null;
77
+ /** Kind key (see KINDS in journal-options). Legacy pre-v2 rows map their old
78
+ * `category` to a kind at read time; free text is tolerated. */
79
+ kind: string | null;
80
+ /** Gap lifecycle — only entries with kind='gap' carry one ('open'|'resolved'). */
81
+ status: string | null;
82
+ entryDate: string | null;
83
+ tags: string[];
84
+ summary: string | null;
85
+ createdAt: string;
86
+ updatedAt: string;
87
+ };
88
+
89
+ export type EventRow = {
90
+ id: string;
91
+ title: string;
92
+ body: string;
93
+ startsAt: string;
94
+ endsAt: string | null;
95
+ location: string | null;
96
+ remindMinutesBefore: number;
97
+ remindAt: string;
98
+ reminderSentAt: string | null;
99
+ /** IANA timezone (e.g. "Africa/Johannesburg") captured from the
100
+ * client at create time. Used for display only — `starts_at` is
101
+ * always a UTC instant so the reminder fires at the right moment
102
+ * regardless of where the agent process or DB run. Defaults to
103
+ * 'UTC' if the client didn't supply one. */
104
+ timezone: string;
105
+ /** Recurrence frequency; 'none' for a one-shot event. */
106
+ recur: RecurFreq;
107
+ /** Optional end-of-series cutoff (ISO). null = repeats until deleted. */
108
+ recurUntil: string | null;
109
+ tags: string[];
110
+ summary: string | null;
111
+ createdAt: string;
112
+ updatedAt: string;
113
+ };
114
+
115
+ /** Notion-style content width: centered/narrow vs full available space. */
116
+ export type PageWidth = 'narrow' | 'wide';
117
+
118
+ export type PageRow = {
119
+ id: string;
120
+ /** Parent page id, or null for a top-level page. Drives the /pages tree
121
+ * and the `childPage` card (Phase 4a sub-pages). */
122
+ parentId: string | null;
123
+ title: string;
124
+ icon: string | null;
125
+ tags: string[];
126
+ summary: string | null;
127
+ visibility: PageVisibility;
128
+ width: PageWidth;
129
+ createdAt: string;
130
+ updatedAt: string;
131
+ };
132
+
133
+ export type AppRow = {
134
+ id: string;
135
+ title: string;
136
+ icon: string | null;
137
+ tags: string[];
138
+ summary: string | null;
139
+ description: string | null;
140
+ /** Number of declared api_tool slugs. */
141
+ toolCount: number;
142
+ /** Whether the published source has a green build (renders today). */
143
+ hasBuild: boolean;
144
+ /** Whether an uncommitted draft exists. */
145
+ hasDraft: boolean;
146
+ /**
147
+ * The app's exposure: mode of its active share ('public' | 'team'), or null
148
+ * when it has never been shared / the share is revoked (owner-only).
149
+ */
150
+ shareMode: ShareMode | null;
151
+ /** Whether this app is the designated Team Hub (prefs.teamHubAppId). */
152
+ isHub: boolean;
153
+ createdAt: string;
154
+ updatedAt: string;
155
+ };
156
+
157
+ export type AppDetail = AppRow & {
158
+ source: AppSource;
159
+ draft: AppSource | null;
160
+ manifest: AppManifest;
161
+ draftBuild: BuildRef | null;
162
+ publishedBuild: BuildRef | null;
163
+ };
164
+
165
+ export type ProfilePreferences = {
166
+ /** IANA timezone, e.g. 'Africa/Johannesburg'. UTC when not set. */
167
+ timezone: string;
168
+ /** The last zone the auto-from-location hook DERIVED (not necessarily the one
169
+ * in `timezone`, if the user manually overrode since). Used purely for
170
+ * hysteresis: the hook only acts when the freshly-derived zone differs from
171
+ * this, so it won't fight a manual change or re-switch every turn at the same
172
+ * place. See auto-timezone.ts. */
173
+ lastAutoTimezone?: string;
174
+ /** BCP-47 locale, e.g. 'en-GB'. Drives date/number/currency
175
+ * formatting. Falls back to en-GB to match the legacy pinned
176
+ * format-datetime behaviour, so existing UI doesn't shift for
177
+ * users who haven't visited /settings/profile yet. */
178
+ locale: string;
179
+ /** Avatar style id — the BRAIN's avatar visual language, applied to every
180
+ * generated avatar (the owner's and every agent's). Brain-level alongside
181
+ * colorTheme and the display fonts, because it is a branding choice, not a
182
+ * personal one: one style with a different seed per entity reads as one
183
+ * product, six unrelated styles at once read as noise. Individuality lives
184
+ * in `avatarSeed`, which stays personal. See @mantle/web-ui/avatar for the
185
+ * registry; unknown ids resolve to the default rather than stranding. */
186
+ avatarStyle?: string;
187
+ /** How much of the theme generated avatars take on: 'native' (the style's own
188
+ * palette), 'mixed' (themed background, original artwork — the default) or
189
+ * 'theme' (theme colours throughout). Brain-level for the same reason as
190
+ * avatarStyle: it describes how this brain's avatars look, not one login's
191
+ * taste. Read via projectAvatarTint, never raw. */
192
+ avatarTint?: string;
193
+ /** Which generated background each area of the shell shows, as
194
+ * `area=style` pairs (`menu=waves,header=off`). Brain-level for the same
195
+ * reason as avatarStyle and colorTheme: it is the look of the product.
196
+ * `off` is a real, storable choice, see @mantle/web-ui/backgrounds. Areas
197
+ * on their default are omitted, so a default change still reaches brains
198
+ * that never chose. Read via projectBackgrounds, never raw. */
199
+ backgrounds?: string;
200
+ /** The generated whole-surface Neat gradient (login screen, content area),
201
+ * as a compact JSON spec `{v,seed,tone,speed}` — colours are DERIVED from
202
+ * the live theme tokens client-side, never stored, so the background
203
+ * follows every colour theme and mode. Brain-level for the same reason as
204
+ * backgrounds: it is the look of the product. Unset ⇒ the plain themed
205
+ * fill. Read via projectNeatBackground, never raw. */
206
+ neatBackground?: string;
207
+ /** The brain's default light/dark mode for surfaces where the visitor has
208
+ * not chosen one themselves — today the public /s share reader, which stamps
209
+ * it server-side and lets the visitor's own toggle override it locally.
210
+ * 'light' | 'dark' | 'system'; unset ⇒ 'light' (the share surface's
211
+ * historical rendering, so an unset brain looks exactly as before). Brain-
212
+ * level like colorTheme: it is the look of the product's public face. Read
213
+ * via projectDefaultMode, never raw. */
214
+ defaultMode?: string;
215
+ /** Whether the public /s share reader paints the saved Neat gradient at all.
216
+ * Default ON (only an explicit `false` disables, the streamThoughts
217
+ * contract): the switch exists for owners who want share links to stay on
218
+ * the plain themed surface — the printable rendering — while the app keeps
219
+ * its background. Brain-level like neatBackground itself. */
220
+ shareNeat?: boolean;
221
+ /** Seed for THIS user's avatar; the UI defaults it to the user id when unset
222
+ * so an avatar still renders. Personal — two admins share the brain's style
223
+ * but never the same avatar. */
224
+ avatarSeed?: string;
225
+ /** Avatar-builder component choices for THIS login's avatar, layered over
226
+ * the seed: component name → pinned variant, or null to hide an optional
227
+ * component. Per-login (the profile routes address the ACTOR's row). Stale
228
+ * entries (saved under another brain style) are ignored at render time.
229
+ * READ: absent/empty = seed only. WRITE (profile PUT): applied only when
230
+ * SENT; `{}` clears. */
231
+ avatarParts?: Record<string, string | null>;
232
+ /** Content-addressed storage key of THIS login's uploaded profile PHOTO —
233
+ * when set, clients show the photo instead of the generated avatar
234
+ * (photo → generated seed → initials). Per-login (the photo routes address
235
+ * the ACTOR's row); set only from Settings → Profile, never for agents.
236
+ * Served privately by GET /api/profile/photo (cookie or asset token). */
237
+ avatarPhotoKey?: string;
238
+ /** Content-Type of the photo bytes (png/jpeg/webp — never SVG). */
239
+ avatarPhotoType?: string;
240
+ /** Slug of the responder agent whose Telegram bot delivers event reminders.
241
+ * Unset → the reminder worker falls back to the most-recently-active allowed
242
+ * DM (whichever bot you last messaged). Set it to pin reminders to one
243
+ * persona, e.g. 'telegram-default' (Saskia), so they don't come from
244
+ * whichever bot happened to be most recent. */
245
+ reminderAgentSlug?: string;
246
+ /** Where event reminders are delivered: 'telegram' (a bot DM) or 'mobile' (a
247
+ * push to the companion app). Auto-tracked — it follows the last channel the
248
+ * user actually messaged on (see noteInboundChannel), and can be set manually
249
+ * from the profile; a manual choice holds until the next message on the other
250
+ * channel supersedes it. Unset ⇒ the reminder worker defaults to 'telegram'
251
+ * (backward-compatible). See docs/reminder-delivery-routing.md. */
252
+ reminderChannel?: ReminderChannel;
253
+ /** What the user likes to be called (captured during onboarding). Cosmetic —
254
+ * the assistant's real knowledge of the user comes from the Journal identity
255
+ * block; this is for greetings/UI. */
256
+ displayName?: string;
257
+ /** Custom site name rendered as the header wordmark in place of "mantle" —
258
+ * a per-box label (e.g. 'Refinery') so anyone with several brains can see at
259
+ * a glance which one they're on. Cosmetic only; unset ⇒ the Mantle wordmark.
260
+ * Read via projectSiteName, never raw. */
261
+ siteName?: string;
262
+ /** This brain's peer name — shown in the header CENTRE (replacing the old page
263
+ * title) as this node's federation-facing identity label. Cosmetic; unset ⇒
264
+ * the header centre is empty. Read via projectPeerName, never raw. */
265
+ peerName?: string;
266
+ /** The owner's writing conventions, in their own words — appended to EVERY
267
+ * agent's composed system prompt as a `## House style` block (see
268
+ * composeSystemPromptWithSkills). Brain-level, because it describes how this
269
+ * brain writes, not how one login works.
270
+ *
271
+ * Free text rather than a checkbox on purpose: the first rule anyone wants
272
+ * is "no em dashes", the second is "don't say 'delve'", and a boolean per
273
+ * rule is a migration per taste. Unset ⇒ no block is emitted at all, so the
274
+ * cached prompt prefix is byte-identical to before the feature existed.
275
+ * Read via projectHouseStyle, never raw. */
276
+ houseStyle?: string;
277
+ /** The UI colour-theme id (the header theme toggler / random shuffle). The
278
+ * DB copy is the source of truth so the choice follows the owner across
279
+ * browsers and brands member-facing surfaces (/s, /team) — localStorage
280
+ * stays only as the before-paint fast path. Unset ⇒ the default theme.
281
+ * Read via projectColorTheme, never raw. */
282
+ colorTheme?: string;
283
+ /** Selectable header WORDMARK font key (Settings → Appearance → Fonts). The
284
+ * font LIST lives in the web app (server/web/lib/display-fonts.ts); the server
285
+ * stores any well-formed slug and the client falls back to the default for
286
+ * keys it doesn't know, so trimming the library never strands the preference.
287
+ * Unset ⇒ the default wordmark face (Bricolage Grotesque). Read via
288
+ * projectFontKey, never raw. */
289
+ fontLogo?: string;
290
+ /** Selectable header page-TITLE font key — same contract as `fontLogo`.
291
+ * Unset ⇒ the default UI sans. Read via projectFontKey, never raw. */
292
+ fontTitle?: string;
293
+ /** The INTERFACE font key — what the whole UI is set in, not just a header
294
+ * ornament. Same contract as `fontLogo`; unset ⇒ Inter (the always-loaded
295
+ * next/font face). Read via projectFontKey, never raw. */
296
+ fontUi?: string;
297
+ /** The PAGES/NOTES font key — what long-form prose is set in, in the editor,
298
+ * on shared pages, and in the PDF export. Same contract as `fontLogo`; unset
299
+ * ⇒ 'inherit' (follow the interface font). This is the one slot where the
300
+ * choice leaves the browser: a page exported to PDF is typeset in it. */
301
+ fontProse?: string;
302
+ /** UI scale: 'xsmall' | 'small' | 'medium' | 'large'. Drives the ROOT
303
+ * font-size, so the rem-based shell scales with it rather than only the
304
+ * letters. Unset ⇒ 'medium'. Read via projectFontSize, never raw. */
305
+ fontSize?: string;
306
+ /** Wordmark scale — same vocabulary as `fontSize`, but a LOCAL multiplier on
307
+ * one element rather than the root font-size (a wordmark that rescaled the
308
+ * whole shell would be a bug). Unset ⇒ 'medium'. */
309
+ fontLogoSize?: string;
310
+ /** Peer-name scale. Same contract as `fontLogoSize`. */
311
+ fontTitleSize?: string;
312
+ /** Pages/Notes prose scale. Same contract as `fontLogoSize`. */
313
+ fontProseSize?: string;
314
+ /** Brand logo: the content-addressed storage key of the uploaded image
315
+ * (attachments/aa/bb/<sha256> — @mantle/storage contentKey). Set/cleared
316
+ * ONLY via PUT/DELETE /api/profile/logo, which validates the bytes; when
317
+ * set, both headers render the image in place of the siteName wordmark.
318
+ * The sha in the key doubles as the cache-busting version. Read via
319
+ * projectLogoKey, never raw. */
320
+ logoKey?: string;
321
+ /** The logo's mime type, from the validated upload (svg/png/jpeg/webp
322
+ * allowlist — projectLogoType). The public serve route replays it. */
323
+ logoType?: string;
324
+ /** Optional DARK-MODE logo variant — same storage/validation contract as
325
+ * logoKey, uploaded via PUT /api/profile/logo?variant=dark. Renderers show
326
+ * it when the UI is in dark mode and fall back to the base logo (then the
327
+ * wordmark) when unset — so a light-on-transparent mark stays readable on
328
+ * both themes without forcing every brain to upload two files. */
329
+ logoDarkKey?: string;
330
+ /** The dark variant's mime type (same allowlist as logoType). */
331
+ logoDarkType?: string;
332
+ /** Free-text "what this brain is for" — captured at onboarding, editable in
333
+ * Settings → Profile. Injected as the "# Purpose of this brain" section of the
334
+ * always-on identity block (identity-context.ts), so every agent knows the
335
+ * brain's mission. */
336
+ purpose?: string;
337
+ /** The brain's speciality archetype key (see onboarding-questions.ts
338
+ * PURPOSE_ARCHETYPES — 'personal' | 'analytics' | 'research' | 'robotics' |
339
+ * 'team' | 'custom'). Descriptive for now; the seam a later phase can branch
340
+ * default provisioning on. */
341
+ purposeArchetype?: string;
342
+ /** ISO instant onboarding was completed. Unset ⇒ the onboarding wizard runs
343
+ * on next login; the (app) shell redirects there. Set ⇒ shell renders normally. */
344
+ onboardedAt?: string;
345
+ /** Resume marker for the onboarding wizard — the key of the furthest step the
346
+ * user has reached. Lets a refreshed/re-entered wizard pick up where it left off. */
347
+ onboardingStep?: string;
348
+ /** Model choices captured by the onboarding "Models" step — the operator
349
+ * overlay `provisionDefaults()` applies on top of the manifest seed (the
350
+ * assistant's chat model + the indexing workers' fast model). When
351
+ * `route: 'azure'`, those rows are pinned to an Azure OpenAI endpoint via
352
+ * the `custom` provider (key stored under service `custom`). */
353
+ onboardingModels?: OnboardingModelChoices;
354
+ /** When true, tools an AGENT authors (via Toolsmith / api_tool_create) start
355
+ * confirm-gated: every call parks for operator approval until the operator
356
+ * clears "requires confirm" for that tool in Settings → Tools. Defaults
357
+ * OFF — a simple single-owner brain trusts itself; turn it ON if you grant
358
+ * tool-authoring to an agent that reads untrusted content (email/web), so an
359
+ * injected agent can't stand up a silent exfiltration endpoint. Independent
360
+ * of the always-on guards (self-grant block, no-lower-via-update, SSRF). */
361
+ toolsmithRequireApproval?: boolean;
362
+ /** APP_VERSION the boot-time manifest reconcile last synced this brain to.
363
+ * The reconcile (server/web instrumentation → reconcileManifestOnBoot) runs once
364
+ * per version on a deployed/updated instance, so a self-hoster who only pulls a
365
+ * new image still gets new tools/skills/group-membership without running seed
366
+ * scripts. Equal to APP_VERSION ⇒ already reconciled, skip. */
367
+ lastReconciledVersion?: string;
368
+ /** When true, outbound/egress tools (email_send, web_fetch, web_search)
369
+ * fired during an UNATTENDED heartbeat run park for operator approval
370
+ * instead of executing inline. Only tools that reach OUT are gated — the
371
+ * heartbeat's own surface reply (the final Telegram message) is not a tool
372
+ * and still goes through. Defaults OFF: most heartbeats are trusted
373
+ * routines. Turn it ON for an agent that reads untrusted content on a
374
+ * timer, so an injected instruction can't silently email or fetch on your
375
+ * behalf while you're away. Pairs with the interactive Telegram approval
376
+ * card so a parked egress call can be cleared from a phone. */
377
+ heartbeatEgressGate?: boolean;
378
+ /** Show the live "thinking" trail + stream the reply token-by-token in the
379
+ * /assistant chat (and the companion). **Defaults ON** (undefined → on); set
380
+ * false to fall back to a static thinking bubble + the reply appearing whole
381
+ * on completion. This is the per-brain runtime control for live turn
382
+ * streaming; the `MANTLE_TURN_STREAMING` env var is a deploy-level override
383
+ * (env off wins). Read by the web turn route (202 vs blocking + the SSE gate)
384
+ * via `isStreamThoughtsEnabled`. */
385
+ streamThoughts?: boolean;
386
+ /** How the LIVE thinking trail renders during a turn: 'list' stacks completed
387
+ * actions above the active line (default); 'replace' shows only the current
388
+ * action, each one replacing the last (compact, single line). The frozen
389
+ * record view (after the turn) is unaffected. */
390
+ thoughtTrailMode?: ThoughtTrailMode;
391
+ /** Persist the thought trail onto the finished message so it survives a page
392
+ * refresh — reconstructed from the turn's tool actions and stored on the
393
+ * durable row, so it reloads on web AND the companion. **Defaults ON**; set
394
+ * false to keep it ephemeral (in-memory only; clears on reload). See
395
+ * `isPersistThoughtsEnabled`. */
396
+ persistThoughts?: boolean;
397
+ /** Per-user thinking budget in tokens. Real model reasoning is requested only
398
+ * when the live-thinking switch is ON (`streamThoughts`) AND this is > 0;
399
+ * 0 / unset = no thinking. Maps to the provider's knob in the adapters
400
+ * (Anthropic adaptive, OpenRouter `reasoning.max_tokens`, Gemini
401
+ * `thinkingConfig`, Copilot `reasoning_effort`). This is the per-user
402
+ * replacement for the old per-box `MANTLE_THINKING_BUDGET` env gate. Resolve
403
+ * via `resolveThinkingBudget` — never read raw, so the switch gate always
404
+ * applies. **Defaults unset (off).** */
405
+ thinkingBudget?: number;
406
+ /** Whether this box exposes its remote MCP connector (the OAuth-gated
407
+ * `/api/mcp` endpoint addable as a claude.ai custom connector). **Defaults
408
+ * OFF** — it's an explicit opt-in because it puts the tool surface on the
409
+ * public internet (behind OAuth). When off, `/api/mcp` + the OAuth
410
+ * authorize/register endpoints 404, so no new client can connect and existing
411
+ * tokens stop working. Flip it in Settings → MCP. */
412
+ remoteMcpEnabled?: boolean;
413
+ /** Whether the external Team Chat responder may read the owner's PRIVATE
414
+ * corpus — email + journal — on a team member's behalf. **Defaults OFF**:
415
+ * team members always get brain-wide knowledge reads (search, files, notes,
416
+ * pages, tables, tasks, contacts, app data), but the owner's personal email
417
+ * history and journal stay off-limits unless this is explicitly turned on.
418
+ * Enforced at the team turn's tool resolution (`isTeamPrivateReadsEnabled`
419
+ * strips `email_*`/`journal_*` when off), independent of the `team-read`
420
+ * group grant, so the switch can't be bypassed by a manifest change. Flip it
421
+ * from the Team admin surface. */
422
+ teamPrivateReads?: boolean;
423
+ /** Node id of the mini-app designated as this brain's TEAM HUB. When set (and
424
+ * the app has a green published build + an active team-mode share), the /team
425
+ * shell renders that app full-bleed in place of the built-in hub body; the
426
+ * built-in hub remains the fallback for every other state. Resolve via
427
+ * `resolveTeamHubApp` (team-hub.ts), never raw — designation is only honoured
428
+ * when the whole chain (pref → app → build → share) is intact. Read via
429
+ * projectTeamHubAppId, never raw. */
430
+ teamHubAppId?: string;
431
+ /** Tags the owner curates as Dashboard sections on the /team overview: each
432
+ * tag renders a section of up to 5 team-visible shared pages carrying it
433
+ * (newest-updated first, title + summary + /s link). Order here = section
434
+ * order. The share stays the single source of truth for WHAT is visible —
435
+ * this pref only chooses which tag groupings get pinned. Unset/empty ⇒ no
436
+ * curated sections. Read via projectTeamHubTags, never raw. */
437
+ teamHubTags?: string[];
438
+ };
439
+
440
+ export type BackupConfig = {
441
+ enabled: boolean;
442
+ frequency: BackupFrequency;
443
+ /** Hour of day (0-23) in the USER's timezone (profiles.preferences.timezone). */
444
+ hour: number;
445
+ /** Newest N dumps retained in the directory. */
446
+ keep: number;
447
+ /** Absolute destination directory. Empty/unset → resolveBackupDir default. */
448
+ location?: string;
449
+ };
450
+
451
+ export type BackupFile = { name: string; bytes: number; mtime: string };
452
+
453
+ export type BackupStatus = {
454
+ lastRunAt: string;
455
+ ok: boolean;
456
+ /** Set when ok=false. */
457
+ error?: string;
458
+ file?: string;
459
+ bytes?: number;
460
+ durationMs?: number;
461
+ /** 'schedule' | 'manual' — what triggered the run. */
462
+ trigger: string;
463
+ /** When the last SUCCESSFUL run finished — preserved across failed runs,
464
+ * so the /debug/integrity staleness check can tell "failing for a week"
465
+ * from "failed once after last night's good dump". */
466
+ lastSuccessAt?: string;
467
+ /** Sqlite-native table workbooks snapshotted beside the dump (durability
468
+ * gate 2). failed>0 is surfaced in the settings card — a backup that
469
+ * silently skips a workbook is the gap this closes. */
470
+ tableDbs?: { snapshotted: number; missing: number; failed: number };
471
+ /** Per-app mini-app SQLite databases snapshotted beside the dump. Same
472
+ * durability gate as tableDbs: these live on their own volume, so pg_dump
473
+ * alone misses them and a scheduled backup would silently omit all app
474
+ * data (e.g. a Team Hub app's DB) without this pass. */
475
+ appDbs?: { snapshotted: number; missing: number; failed: number };
476
+ };
477
+
478
+ export type CuratedTeamSection = {
479
+ /** The curated tag — the section heading (display-cased by the UI). */
480
+ tag: string;
481
+ /** Up to {@link TEAM_CURATED_SECTION_LIMIT} team-visible page shares carrying
482
+ * the tag, newest node update first. */
483
+ items: TeamVisibleShare[];
484
+ };
485
+
486
+ export type TeamMemberActivity = {
487
+ contactId: string;
488
+ /** Contact node title; '(deleted contact)' can't occur here — membership
489
+ * rows cascade with the contact. */
490
+ contactName: string;
491
+ memberSince: string;
492
+ tokenLastUsedAt: string | null;
493
+ lastMessageAt: string | null;
494
+ lastMessageText: string | null;
495
+ lastMessageDirection: 'inbound' | 'outbound' | null;
496
+ messageCount: number;
497
+ /** Member inbound messages since the owner last read this thread in
498
+ * /team-admin (all inbound when never read). Drives the unread badge. */
499
+ unread: number;
500
+ };
501
+
502
+ export type TeamRequest = {
503
+ taskId: string;
504
+ title: string;
505
+ body: string;
506
+ status: 'open' | 'done';
507
+ priority: string;
508
+ createdAt: string;
509
+ /** Provenance from data.teamRequest — null contactId means a malformed row
510
+ * (shouldn't happen; team_request_create always stamps it). */
511
+ contactId: string | null;
512
+ contactName: string | null;
513
+ /** When the owner last posted a resolution to the member for this request. */
514
+ notifiedAt: string | null;
515
+ };
516
+
517
+ export type ForumTopicListItem = {
518
+ id: string;
519
+ title: string;
520
+ kind: ForumTopicKind;
521
+ visibility: ForumTopicVisibility;
522
+ pinned: boolean;
523
+ status: ForumTopicStatus;
524
+ authorName: string;
525
+ createdByContactId: string | null;
526
+ postCount: number;
527
+ lastPostAt: string;
528
+ createdAt: string;
529
+ lastPostAuthor: string | null;
530
+ lastPostPreview: string | null;
531
+ /** Posts by OTHERS since this viewer last read the topic (all of them when
532
+ * never read). Drives the unread dot. */
533
+ unread: number;
534
+ };
535
+
536
+ export type ForumMemberActivity = {
537
+ contactId: string;
538
+ postCount: number;
539
+ topicsStarted: number;
540
+ lastPostAt: string | null;
541
+ lastPostBody: string | null;
542
+ lastPostTopicTitle: string | null;
543
+ /** This member's posts newer than the OWNER's read cursor on the containing
544
+ * topic. Deliberately only cleared by opening the TOPIC — reading someone's
545
+ * activity feed is not reading the thread the whole room saw. */
546
+ unread: number;
547
+ };
548
+
549
+ export type ForumMemberPost = {
550
+ id: string;
551
+ body: string;
552
+ createdAt: string;
553
+ /** Set when this post filed a review/feature/bug request. */
554
+ kind: ForumPostRequestKind | null;
555
+ attachments: ConversationAttachment[];
556
+ topicId: string;
557
+ topicTitle: string;
558
+ topicVisibility: ForumTopicVisibility;
559
+ topicStatus: ForumTopicStatus;
560
+ /** The agent's answer to THIS post, or null when the turn was waved off
561
+ * ("no answer needed") or is still owed. */
562
+ reply: {
563
+ id: string;
564
+ body: string;
565
+ authorName: string;
566
+ traceId: string | null;
567
+ status: 'pending' | 'complete' | 'failed';
568
+ error: string | null;
569
+ createdAt: string;
570
+ } | null;
571
+ };
572
+
573
+ export type ForumAuthoredTopic = {
574
+ id: string;
575
+ title: string;
576
+ kind: ForumTopicKind;
577
+ visibility: ForumTopicVisibility;
578
+ status: ForumTopicStatus;
579
+ pinned: boolean;
580
+ postCount: number;
581
+ lastPostAt: string | null;
582
+ createdAt: string;
583
+ };
584
+
585
+ export type PendingForumUpload = {
586
+ id: string;
587
+ topicId: string | null;
588
+ postId: string | null;
589
+ topicTitle: string | null;
590
+ contactId: string | null;
591
+ contactName: string | null;
592
+ filename: string;
593
+ mime: string;
594
+ sizeBytes: number;
595
+ createdAt: string;
596
+ };
597
+
598
+ export type AccountFoldersResult =
599
+ | {
600
+ ok: true;
601
+ address: string;
602
+ /** Every folder the server reports right now (the pick list). */
603
+ allFolders: string[];
604
+ /** The current explicit allow-list, or null = "scan all non-excluded". */
605
+ included: string[] | null;
606
+ /** Folders the operator opted OUT of (rendered disabled). */
607
+ excluded: string[];
608
+ /** Folders the sync has actually touched (per the cursor). */
609
+ scanned: string[];
610
+ }
611
+ | { ok: false; error: string };
612
+
613
+ export interface FolderFacet {
614
+ folder: string;
615
+ count: number;
616
+ unread: number;
617
+ }
618
+
619
+ export interface MessageListItem {
620
+ id: string;
621
+ fromAddr: string;
622
+ fromName: string | null;
623
+ subject: string | null;
624
+ snippet: string | null;
625
+ internalDate: Date;
626
+ isRead: boolean;
627
+ }
628
+
629
+ export interface MsConfigStatus {
630
+ configured: boolean;
631
+ /** Where the active config comes from — drives the UI ("set here" vs "from
632
+ * environment, read-only"). */
633
+ source: 'db' | 'env' | null;
634
+ clientId: string | null;
635
+ tenant: string;
636
+ redirectUri: string | null;
637
+ /** Masked secret for display; never the plaintext. */
638
+ secretMasked: string | null;
639
+ }
640
+
641
+ /** One retrieved (or near-miss) item: capped text + its ranking distance. */
642
+ export type SnapshotItem = {
643
+ text: string;
644
+ /** Ranking distance (cosine, salience/recency-adjusted where the section
645
+ * ranks that way). Null for always-injected items (preferences) that
646
+ * bypass the vector race. */
647
+ dist: number | null;
648
+ kind?: string | null;
649
+ entity?: string | null;
650
+ nodeId?: string | null;
651
+ title?: string | null;
652
+ heading?: string | null;
653
+ };
654
+
655
+ export type ContextSnapshot = {
656
+ query: {
657
+ /** The inbound text as given to retrieval (snipped). */
658
+ inbound: string;
659
+ /** The anaphora-enriched text actually embedded, when it differs. */
660
+ enriched: string | null;
661
+ /** False when embedding was skipped or failed — retrieval ran blind. */
662
+ embedded: boolean;
663
+ };
664
+ facts: { sent: SnapshotItem[]; dropped: SnapshotItem[]; guard: number };
665
+ contentHits: { sent: SnapshotItem[]; dropped: SnapshotItem[]; cutoff: number };
666
+ chunkHits: { sent: SnapshotItem[]; dropped: SnapshotItem[]; cutoff: number };
667
+ relations: string[];
668
+ digests: { count: number; topics: string[] };
669
+ history: {
670
+ count: number;
671
+ /** How many outbound turns carried a [tool record: …] read-back suffix. */
672
+ toolRecords: number;
673
+ /** How many turns carried a [media record: …] read-back suffix. */
674
+ mediaRecords: number;
675
+ };
676
+ personaNotes: { count: number };
677
+ corpusMap: { count: number; truncated: boolean };
678
+ };
679
+
680
+ export type BackupFrequency = 'daily' | 'weekly';
681
+
682
+ export type PageVisibility = 'private' | 'public';
683
+
684
+ /**
685
+ * Recurrence frequencies an event can repeat on. `none` is the default —
686
+ * a one-shot event. The reminder worker rolls a recurring event's single
687
+ * row forward to its next occurrence after each ping (no instance
688
+ * materialisation), so one node always represents the next upcoming hit.
689
+ */
690
+ export type RecurFreq = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly';
691
+
692
+ /** Transports that can deliver a reminder out-of-band. A browser ('web') can't
693
+ * receive a push, so it never becomes a reminder target. */
694
+ export type ReminderChannel = 'telegram' | 'mobile';
695
+
696
+ /** Live thinking-trail display modes. */
697
+ export type ThoughtTrailMode = 'list' | 'replace';
698
+
699
+ /** The onboarding "Models" step's stored choices. Kept as one object so the
700
+ * projection can't half-apply; every field optional so partial saves survive. */
701
+ export interface OnboardingModelChoices {
702
+ /** OpenRouter slug for the assistant/persona agent (e.g. `anthropic/claude-sonnet-4.6`). */
703
+ assistantModel?: string;
704
+ /** OpenRouter slug for the indexing workers (e.g. `google/gemini-3.1-flash-lite`). */
705
+ workerModel?: string;
706
+ /** Where the models run: OpenRouter (default) or an Azure OpenAI endpoint. */
707
+ route?: 'openrouter' | 'azure';
708
+ /** Azure OpenAI base URL (the OpenAI-compatible v1 endpoint), when route=azure. */
709
+ azureBaseUrl?: string;
710
+ }
711
+
712
+ /**
713
+ * Who a share admits. Lives in `shares.settings.mode` (absent = 'public', so
714
+ * every pre-existing share keeps its behavior).
715
+ *
716
+ * public — anyone with the link (the original model).
717
+ * team — the visitor must additionally present a live team credential
718
+ * (see @mantle/content/team-tokens). Enforced for every kind on
719
+ * the /s/ surface (page render, asset bytes, app brokers).
720
+ * Team-mode PAGE shares double as the /team hub's briefing
721
+ * sections (see ./team-hub).
722
+ */
723
+ export type ShareMode = 'public' | 'team';
724
+
725
+ export type TeamVisibleShare = {
726
+ /** Share token — the workspace opens /s/<token>. */
727
+ token: string;
728
+ nodeId: string;
729
+ title: string;
730
+ icon: string | null;
731
+ summary: string | null;
732
+ updatedAt: string;
733
+ /** 'team' or 'public' — a member may open both, the badge tells them apart. */
734
+ mode: 'team' | 'public';
735
+ /** Parent node id — lets the pages section rebuild the sub-page tree over
736
+ * the SHARED subset (an unshared parent leaves its children as roots). */
737
+ parentId: string | null;
738
+ tags: string[];
739
+ /**
740
+ * EVENTS ONLY — the event's own start, from `nodes.data.starts_at`.
741
+ *
742
+ * Every other field here describes the SHARE; this one describes the thing
743
+ * shared, and it is carried because for an event the two are not
744
+ * interchangeable. `updatedAt` says when the row was last written, which is
745
+ * the right meta line for a note or a table and useless for an event: a
746
+ * member scanning what is coming up needs WHEN IT HAPPENS, and an event
747
+ * edited this morning sorts above one starting tomorrow.
748
+ *
749
+ * Optional so a client pinned to an older server still parses the payload,
750
+ * and null for every non-event type.
751
+ */
752
+ startsAt?: string | null;
753
+ };
754
+
755
+ // ── Mirrors of @mantle/db jsonb/enum shapes (jackdaw split P0) ────────────────
756
+ // Kept standalone so this package stays zero-dep (same convention as ToolHandler
757
+ // above). Drift is caught where the server builds these DTOs from db rows —
758
+ // an incompatible change there is a compile error at the row-builder.
759
+
760
+ /** Task lifecycle vocabulary — mirrors content's TASK_STATUSES/TASK_PRIORITIES
761
+ * consts, which are `satisfies`-checked against these unions. */
762
+ export type TaskStatus = 'open' | 'in_progress' | 'blocked' | 'done';
763
+ export type TaskPriority = 'low' | 'normal' | 'high';
764
+
765
+ /** Mirrors @mantle/db `ForumTopicKind`. */
766
+ export type ForumTopicKind = 'question' | 'review' | 'feature' | 'bug' | 'discussion';
767
+ /** Mirrors @mantle/db `ForumTopicVisibility`. */
768
+ export type ForumTopicVisibility = 'team' | 'private';
769
+ /** Mirrors @mantle/db `ForumTopicStatus`. */
770
+ export type ForumTopicStatus = 'open' | 'answered' | 'closed';
771
+ /** Mirrors @mantle/db `ForumPostRequestKind` — the topic kinds that file an
772
+ * owner review task. */
773
+ export type ForumPostRequestKind = 'review' | 'feature' | 'bug';
774
+
775
+ /** Mirrors @mantle/db `ConversationAttachment` (jsonb on conversation rows). */
776
+ export type ConversationAttachment = {
777
+ kind: 'image' | 'audio' | 'voice' | 'document' | 'video';
778
+ mime?: string;
779
+ caption?: string;
780
+ nodeId?: string;
781
+ fileId?: string;
782
+ url?: string;
783
+ };
784
+
785
+ /** Mirrors @mantle/db `AppSource` — a mini app's virtual file tree. */
786
+ export type AppSource = {
787
+ /** Path of the entry module within `files`; must `export default App`. */
788
+ entry: string;
789
+ /** path → TSX/TS source. Bounded (~30 files / ~256 KB) to stay a mini app. */
790
+ files: Record<string, string>;
791
+ };
792
+
793
+ /** Mirrors @mantle/db `AppManifest` — the runtime contract for a running app. */
794
+ export type AppManifest = {
795
+ toolSlugs?: string[];
796
+ sqlite?: { schemaSql: string; schemaVersion: number };
797
+ description?: string;
798
+ };
799
+
800
+ /** Mirrors @mantle/db `BuildRef` — pointer to a bundled artifact in storage. */
801
+ export type BuildRef = {
802
+ storageKey: string;
803
+ sha256: string;
804
+ builtAt: string;
805
+ esbuildVersion: string;
806
+ bytes: number;
807
+ ok: boolean;
808
+ warnings?: string[];
809
+ css?: { storageKey: string; sha256: string; bytes: number };
810
+ };
811
+
812
+ // ── Redacted account DTOs (hand-mirrored; jackdaw split P0) ───────────────────
813
+ // These mirror db-derived server types (`Omit<EmailAccount,…>` etc.) that can't
814
+ // be re-exported without dragging the postgres type graph in. Timestamps are
815
+ // ISO strings here — the wire truth — where the server-side originals carry
816
+ // `Date`. Key-set drift checks live next to the server definitions
817
+ // (email/accounts.ts, microsoft/accounts.ts, email's sync-runs consumer).
818
+
819
+ /** Mirrors @mantle/email `PublicEmailAccount` (an `email_accounts` row minus
820
+ * the sealed IMAP secret). */
821
+ export interface PublicEmailAccount {
822
+ id: string;
823
+ userId: string;
824
+ provider: 'gmail' | 'microsoft' | 'imap';
825
+ address: string;
826
+ displayName: string | null;
827
+ imapHost: string | null;
828
+ imapPort: number | null;
829
+ imapSecure: boolean;
830
+ smtpHost: string | null;
831
+ smtpPort: number | null;
832
+ smtpSecure: boolean;
833
+ /** @deprecated historical reads only (migration 0002). */
834
+ imapFolders: string[];
835
+ imapExcludedFolders: string[];
836
+ imapIncludedFolders: string[] | null;
837
+ firstScanDays: number;
838
+ ingestPolicy: 'approve_list' | 'block_list';
839
+ branchPath: string;
840
+ msAccountId: string | null;
841
+ syncState: Record<string, unknown>;
842
+ lastSyncAt: string | null;
843
+ lastSyncError: string | null;
844
+ enabled: boolean;
845
+ createdAt: string;
846
+ updatedAt: string;
847
+ }
848
+
849
+ /** Mirrors @mantle/db `SyncRun` (a `sync_runs` row) as it crosses the wire. */
850
+ export interface SyncRun {
851
+ id: string;
852
+ accountId: string;
853
+ startedAt: string;
854
+ finishedAt: string | null;
855
+ durationMs: number | null;
856
+ status: 'running' | 'ok' | 'error';
857
+ scanned: number;
858
+ ingested: number;
859
+ error: string | null;
860
+ }
861
+
862
+ /** Mirrors @mantle/microsoft `PublicMsAccount` (an `ms_accounts` row with the
863
+ * sealed OAuth tokens replaced by presence flags). */
864
+ export interface PublicMsAccount {
865
+ id: string;
866
+ userId: string;
867
+ upn: string;
868
+ displayName: string | null;
869
+ tenantId: string | null;
870
+ tokenExpiresAt: string | null;
871
+ scopes: string[];
872
+ branchPath: string;
873
+ surfaces: Record<string, boolean>;
874
+ syncState: Record<string, unknown>;
875
+ lastSyncAt: string | null;
876
+ lastSyncError: string | null;
877
+ enabled: boolean;
878
+ createdAt: string;
879
+ updatedAt: string;
880
+ hasAccessToken: boolean;
881
+ hasRefreshToken: boolean;
882
+ }