@pellux/goodvibes-tui 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +2422 -1040
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/conversation-rendering.ts +2 -2
  8. package/src/core/conversation.ts +49 -2
  9. package/src/core/session-recovery.ts +24 -18
  10. package/src/core/system-message-router.ts +42 -26
  11. package/src/core/turn-event-wiring.ts +12 -16
  12. package/src/daemon/{calendar → handlers/calendar}/caldav-client.ts +29 -24
  13. package/src/daemon/handlers/calendar/index.ts +316 -0
  14. package/src/daemon/handlers/context.ts +29 -0
  15. package/src/daemon/handlers/contracts.ts +77 -0
  16. package/src/daemon/{channels → handlers}/drafts/draft-store.ts +114 -50
  17. package/src/daemon/handlers/drafts/index.ts +17 -0
  18. package/src/daemon/handlers/drafts/register.ts +331 -0
  19. package/src/daemon/handlers/email/config.ts +164 -0
  20. package/src/daemon/handlers/email/index.ts +43 -0
  21. package/src/daemon/handlers/email/read-handlers.ts +80 -0
  22. package/src/daemon/handlers/email/runtime.ts +140 -0
  23. package/src/daemon/handlers/email/validation.ts +147 -0
  24. package/src/daemon/handlers/email/write-handlers.ts +133 -0
  25. package/src/daemon/handlers/errors.ts +18 -0
  26. package/src/daemon/{channels → handlers}/inbox/cursor-store.ts +58 -66
  27. package/src/daemon/handlers/inbox/index.ts +211 -0
  28. package/src/daemon/{channels → handlers}/inbox/mapping.ts +8 -6
  29. package/src/daemon/{channels → handlers}/inbox/poller.ts +6 -5
  30. package/src/daemon/{channels → handlers}/inbox/provider-adapter.ts +14 -10
  31. package/src/daemon/{channels → handlers}/inbox/providers/discord.ts +10 -11
  32. package/src/daemon/{channels → handlers}/inbox/providers/email.ts +2 -1
  33. package/src/daemon/{channels → handlers}/inbox/providers/imap-client.ts +1 -1
  34. package/src/daemon/{channels → handlers}/inbox/providers/route-util.ts +4 -3
  35. package/src/daemon/{channels → handlers}/inbox/providers/slack.ts +11 -12
  36. package/src/daemon/handlers/index.ts +107 -0
  37. package/src/daemon/handlers/register.ts +161 -0
  38. package/src/daemon/{remote → handlers/remote}/backends/cloud-terminal.ts +8 -3
  39. package/src/daemon/{remote → handlers/remote}/backends/docker.ts +2 -3
  40. package/src/daemon/handlers/remote/backends/index.ts +40 -0
  41. package/src/daemon/{remote → handlers/remote}/backends/process-runner.ts +16 -40
  42. package/src/daemon/{remote → handlers/remote}/backends/ssh.ts +8 -3
  43. package/src/daemon/{remote → handlers/remote}/backends/types.ts +29 -3
  44. package/src/daemon/{remote → handlers/remote}/dispatcher.ts +27 -6
  45. package/src/daemon/handlers/remote/index.ts +119 -0
  46. package/src/daemon/{remote → handlers/remote}/peer-registry.ts +47 -11
  47. package/src/daemon/handlers/remote/service.ts +191 -0
  48. package/src/daemon/{channels → handlers}/routing/inbox-bridge.ts +33 -20
  49. package/src/daemon/handlers/routing/index.ts +261 -0
  50. package/src/daemon/{channels → handlers}/routing/route-store.ts +57 -16
  51. package/src/daemon/{channels → handlers}/routing/routing-resolver.ts +1 -1
  52. package/src/daemon/{operator → handlers}/sqlite-store.ts +5 -5
  53. package/src/daemon/handlers/triage/index.ts +57 -0
  54. package/src/daemon/handlers/triage/integration.ts +213 -0
  55. package/src/daemon/{triage → handlers/triage}/pipeline.ts +60 -71
  56. package/src/daemon/{triage → handlers/triage}/scorer.ts +21 -21
  57. package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
  58. package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
  59. package/src/daemon/handlers/triage/tagger/index.ts +184 -0
  60. package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
  61. package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
  62. package/src/daemon/handlers/triage/types.ts +50 -0
  63. package/src/export/gist-uploader.ts +3 -1
  64. package/src/input/command-registry.ts +1 -0
  65. package/src/input/commands/cloudflare-runtime.ts +2 -1
  66. package/src/input/commands/guidance-runtime.ts +2 -4
  67. package/src/input/commands/health-runtime.ts +13 -7
  68. package/src/input/commands/knowledge.ts +2 -1
  69. package/src/input/commands/runtime-services.ts +40 -10
  70. package/src/input/handler-command-route.ts +1 -1
  71. package/src/input/handler-interactions.ts +2 -1
  72. package/src/input/handler-modal-routes.ts +2 -1
  73. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  74. package/src/input/handler-onboarding.ts +8 -8
  75. package/src/input/handler-ui-state.ts +1 -1
  76. package/src/input/tts-settings-actions.ts +2 -1
  77. package/src/main.ts +52 -59
  78. package/src/panels/agent-inspector-panel.ts +72 -2
  79. package/src/panels/agent-logs-panel.ts +127 -19
  80. package/src/panels/base-panel.ts +2 -1
  81. package/src/panels/builtin/agent.ts +3 -1
  82. package/src/panels/builtin/development.ts +1 -0
  83. package/src/panels/builtin/session.ts +1 -1
  84. package/src/panels/context-visualizer-panel.ts +24 -14
  85. package/src/panels/cost-tracker-panel.ts +7 -13
  86. package/src/panels/debug-panel.ts +11 -22
  87. package/src/panels/docs-panel.ts +14 -24
  88. package/src/panels/file-explorer-panel.ts +2 -1
  89. package/src/panels/file-preview-panel.ts +2 -1
  90. package/src/panels/git-panel.ts +10 -17
  91. package/src/panels/marketplace-panel.ts +2 -1
  92. package/src/panels/memory-panel.ts +2 -1
  93. package/src/panels/project-planning-panel.ts +5 -4
  94. package/src/panels/provider-health-panel.ts +56 -67
  95. package/src/panels/schedule-panel.ts +4 -7
  96. package/src/panels/session-browser-panel.ts +13 -21
  97. package/src/panels/skills-panel.ts +2 -1
  98. package/src/panels/tasks-panel.ts +2 -7
  99. package/src/panels/thinking-panel.ts +6 -11
  100. package/src/panels/token-budget-panel.ts +31 -15
  101. package/src/panels/tool-inspector-panel.ts +10 -18
  102. package/src/panels/work-plan-panel.ts +2 -1
  103. package/src/panels/wrfc-panel.ts +37 -35
  104. package/src/renderer/agent-detail-modal.ts +2 -2
  105. package/src/renderer/modal-utils.ts +0 -10
  106. package/src/renderer/process-modal.ts +2 -2
  107. package/src/runtime/bootstrap-command-context.ts +3 -0
  108. package/src/runtime/bootstrap-command-parts.ts +3 -1
  109. package/src/runtime/bootstrap-core.ts +31 -31
  110. package/src/runtime/bootstrap-shell.ts +1 -0
  111. package/src/runtime/onboarding/apply.ts +4 -3
  112. package/src/runtime/onboarding/snapshot.ts +4 -3
  113. package/src/runtime/process-lifecycle.ts +195 -0
  114. package/src/runtime/services.ts +52 -36
  115. package/src/runtime/wrfc-persistence.ts +20 -5
  116. package/src/shell/ui-openers.ts +3 -2
  117. package/src/verification/live-verifier.ts +4 -3
  118. package/src/version.ts +1 -1
  119. package/src/core/context-auto-compact.ts +0 -110
  120. package/src/daemon/calendar/index.ts +0 -52
  121. package/src/daemon/calendar/register.ts +0 -527
  122. package/src/daemon/channels/drafts/index.ts +0 -22
  123. package/src/daemon/channels/drafts/register.ts +0 -449
  124. package/src/daemon/channels/inbox/index.ts +0 -58
  125. package/src/daemon/channels/inbox/register.ts +0 -247
  126. package/src/daemon/channels/routing/index.ts +0 -39
  127. package/src/daemon/channels/routing/register.ts +0 -296
  128. package/src/daemon/email/index.ts +0 -68
  129. package/src/daemon/email/register.ts +0 -715
  130. package/src/daemon/operator/index.ts +0 -43
  131. package/src/daemon/operator/register-helper.ts +0 -150
  132. package/src/daemon/operator/surfaces.ts +0 -137
  133. package/src/daemon/operator/types.ts +0 -207
  134. package/src/daemon/remote/backends/index.ts +0 -34
  135. package/src/daemon/remote/index.ts +0 -74
  136. package/src/daemon/remote/register.ts +0 -411
  137. package/src/daemon/triage/index.ts +0 -59
  138. package/src/daemon/triage/integration.ts +0 -179
  139. package/src/daemon/triage/register.ts +0 -231
  140. package/src/daemon/triage/tagger.ts +0 -777
  141. /package/src/daemon/{calendar → handlers/calendar}/ics.ts +0 -0
  142. /package/src/daemon/{operator/credential-store.ts → handlers/credentials.ts} +0 -0
  143. /package/src/daemon/{email → handlers/email}/imap-connector.ts +0 -0
  144. /package/src/daemon/{email → handlers/email}/imap-parsing.ts +0 -0
  145. /package/src/daemon/{email → handlers/email}/smtp-connector.ts +0 -0
  146. /package/src/daemon/{remote → handlers/remote}/backends/local-process.ts +0 -0
@@ -1,247 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Surface register: `channels.inbox.list`.
3
- //
4
- // register(ctx) =>
5
- // 1. registers built-in provider adapter factories (slack/discord/email)
6
- // 2. constructs adapters with a daemon credential store
7
- // 3. opens the inbox cursor store and seeds an initial poll
8
- // 4. starts the per-provider polling loops
9
- // 5. publishes the read-only `channels.inbox.list` operator method
10
- // 6. returns an Unregister that stops the poller, closes the store, and
11
- // removes the method.
12
- //
13
- // `channels.inbox.list` is READ-ONLY (no confirm). It reads the persisted feed
14
- // (filtered by providers/limit/since) and reports per-provider state from the
15
- // live poller status. nextSince advances monotonically (max receivedAt in the
16
- // returned window, never below the requested `since`).
17
- // ---------------------------------------------------------------------------
18
-
19
- import {
20
- createDaemonCredentialStore,
21
- declareOperatorMethod,
22
- } from '../../operator/index.ts';
23
- import type {
24
- OperatorContext,
25
- OperatorInvocation,
26
- SurfaceRegister,
27
- Unregister,
28
- } from '../../operator/index.ts';
29
- import {
30
- buildAdapters,
31
- registerAdapterFactory,
32
- type AdapterContext,
33
- type InboundChannelItem,
34
- type RouteResolver,
35
- } from './provider-adapter.ts';
36
- import { InboxCursorStore } from './cursor-store.ts';
37
- import { InboundPoller, type ProviderStatus } from './poller.ts';
38
- import { createSlackAdapter, SLACK_PROVIDER_ID } from './providers/slack.ts';
39
- import { createDiscordAdapter, DISCORD_PROVIDER_ID } from './providers/discord.ts';
40
- import { createEmailAdapter, EMAIL_PROVIDER_ID } from './providers/email.ts';
41
-
42
- export const INBOX_LIST_METHOD_ID = 'channels.inbox.list';
43
- export const INBOX_LIST_SCOPES = ['channels:inbox:read'];
44
- const DEFAULT_LIMIT = 50;
45
- const MAX_LIMIT = 500;
46
-
47
- export interface InboxListInput {
48
- providers?: string[];
49
- limit?: number;
50
- since?: number;
51
- }
52
-
53
- export interface InboxProviderReport {
54
- id: string;
55
- state: ProviderStatus['state'];
56
- itemCount: number;
57
- error?: string;
58
- }
59
-
60
- export interface InboxListOutput {
61
- items: InboundChannelItem[];
62
- nextSince: number;
63
- providers: InboxProviderReport[];
64
- }
65
-
66
- export interface RegisterInboxOptions {
67
- /** Route resolver injected by the routing surface (optional, best-effort). */
68
- resolveRouteId?: RouteResolver;
69
- /** Override the cursor-store filename (tests). */
70
- storeFileName?: string;
71
- /** Skip the initial seed poll (tests that drive polling manually). */
72
- skipInitialPoll?: boolean;
73
- /** Replace the default built-in adapter set (tests). */
74
- registerBuiltins?: boolean;
75
- }
76
-
77
- function registerBuiltinAdapters(): void {
78
- registerAdapterFactory(SLACK_PROVIDER_ID, (ctx) => createSlackAdapter(ctx));
79
- registerAdapterFactory(DISCORD_PROVIDER_ID, (ctx) => createDiscordAdapter(ctx));
80
- registerAdapterFactory(EMAIL_PROVIDER_ID, (ctx) => createEmailAdapter(ctx));
81
- }
82
-
83
- const INBOX_LIST_OUTPUT_SCHEMA: Record<string, unknown> = {
84
- type: 'object',
85
- required: ['items', 'nextSince', 'providers'],
86
- properties: {
87
- items: {
88
- type: 'array',
89
- items: {
90
- type: 'object',
91
- required: [
92
- 'id', 'provider', 'kind', 'fromDigest',
93
- 'subjectPreview', 'bodyPreview', 'receivedAt', 'unread',
94
- ],
95
- properties: {
96
- id: { type: 'string' },
97
- provider: { type: 'string' },
98
- kind: { type: 'string', enum: ['dm', 'thread', 'mention', 'reaction'] },
99
- fromDigest: { type: 'string' },
100
- subjectPreview: { type: 'string', maxLength: 200 },
101
- bodyPreview: { type: 'string', maxLength: 500 },
102
- routeId: { type: 'string' },
103
- receivedAt: { type: 'number' },
104
- unread: { type: 'boolean' },
105
- triageScore: { type: 'number' },
106
- triageTags: { type: 'array', items: { type: 'string' } },
107
- },
108
- },
109
- },
110
- nextSince: { type: 'number' },
111
- providers: {
112
- type: 'array',
113
- items: {
114
- type: 'object',
115
- required: ['id', 'state', 'itemCount'],
116
- properties: {
117
- id: { type: 'string' },
118
- state: { type: 'string', enum: ['ready', 'unavailable', 'empty'] },
119
- itemCount: { type: 'number' },
120
- error: { type: 'string' },
121
- },
122
- },
123
- },
124
- },
125
- };
126
-
127
- const INBOX_LIST_INPUT_SCHEMA: Record<string, unknown> = {
128
- type: 'object',
129
- properties: {
130
- providers: { type: 'array', items: { type: 'string' } },
131
- limit: { type: 'number', minimum: 1, maximum: MAX_LIMIT },
132
- since: { type: 'number', minimum: 0 },
133
- },
134
- };
135
-
136
- function normalizeInput(body: unknown): { providers?: string[]; limit: number; since?: number } {
137
- const input = (body ?? {}) as InboxListInput;
138
- const providers = Array.isArray(input.providers)
139
- ? input.providers.filter((p): p is string => typeof p === 'string' && p.length > 0)
140
- : undefined;
141
- let limit = typeof input.limit === 'number' && Number.isFinite(input.limit)
142
- ? Math.floor(input.limit)
143
- : DEFAULT_LIMIT;
144
- limit = Math.min(Math.max(1, limit), MAX_LIMIT);
145
- const since = typeof input.since === 'number' && Number.isFinite(input.since) && input.since >= 0
146
- ? Math.floor(input.since)
147
- : undefined;
148
- return {
149
- ...(providers && providers.length > 0 ? { providers } : {}),
150
- limit,
151
- ...(since !== undefined ? { since } : {}),
152
- };
153
- }
154
-
155
- /**
156
- * Register the inbox surface. Returns an Unregister that tears down the poller,
157
- * closes the store, and removes the operator method.
158
- */
159
- export function registerInboxMethods(
160
- ctx: OperatorContext,
161
- options: RegisterInboxOptions = {},
162
- ): Unregister {
163
- if (options.registerBuiltins !== false) {
164
- registerBuiltinAdapters();
165
- }
166
-
167
- const credentials = createDaemonCredentialStore(ctx.secrets);
168
- const adapterContext: AdapterContext = {
169
- credentials,
170
- logger: ctx.logger,
171
- ...(options.resolveRouteId ? { resolveRouteId: options.resolveRouteId } : {}),
172
- };
173
- const adapters = buildAdapters(adapterContext);
174
- const store = new InboxCursorStore(ctx.workingDirectory, options.storeFileName);
175
- const poller = new InboundPoller({ adapters, store, logger: ctx.logger });
176
-
177
- // Async bootstrap: init store, seed one poll, start loops. Failures are
178
- // logged but never thrown out of register() — the method still serves the
179
- // (possibly empty) persisted feed and reports provider states.
180
- const ready: Promise<void> = (async () => {
181
- await store.init();
182
- if (!options.skipInitialPoll) {
183
- await poller.pollOnce();
184
- }
185
- poller.start();
186
- })().catch((error: unknown) => {
187
- ctx.logger.error('inbox surface bootstrap failed', {
188
- error: error instanceof Error ? error.message : String(error),
189
- });
190
- });
191
-
192
- const handler = async (
193
- invocation: OperatorInvocation<InboxListInput>,
194
- ): Promise<InboxListOutput> => {
195
- await ready;
196
- const { providers, limit, since } = normalizeInput(invocation.body);
197
- const items = store.listItems({ ...(providers ? { providers } : {}), ...(since !== undefined ? { since } : {}), limit });
198
- const maxReceived = store.maxReceivedAt(providers);
199
- const nextSince = Math.max(since ?? 0, maxReceived);
200
- const statuses = poller.snapshotStatuses(providers);
201
- const reports: InboxProviderReport[] = statuses.map((status) => ({
202
- id: status.id,
203
- state: status.state,
204
- itemCount: status.itemCount,
205
- ...(status.error ? { error: status.error } : {}),
206
- }));
207
- return { items, nextSince, providers: reports };
208
- };
209
-
210
- const unregisterMethod = declareOperatorMethod<InboxListInput, InboxListOutput>(
211
- ctx,
212
- {
213
- id: INBOX_LIST_METHOD_ID,
214
- title: 'List inbound channel items',
215
- description:
216
- 'Aggregated, deduplicated inbound feed (DMs/threads/mentions) across '
217
- + 'configured providers. Read-only; advances a monotonic nextSince cursor.',
218
- category: 'channels',
219
- source: 'daemon',
220
- access: 'operator',
221
- transport: ['ws', 'internal'],
222
- scopes: INBOX_LIST_SCOPES,
223
- effect: 'read-only',
224
- confirm: false,
225
- inputSchema: INBOX_LIST_INPUT_SCHEMA,
226
- outputSchema: INBOX_LIST_OUTPUT_SCHEMA,
227
- },
228
- handler,
229
- );
230
-
231
- return () => {
232
- try {
233
- unregisterMethod();
234
- } finally {
235
- poller.stop();
236
- void store.close();
237
- }
238
- };
239
- }
240
-
241
- /**
242
- * SurfaceRegister contract entry point. Integration wires the inbox surface by
243
- * calling this from its surface bootstrap (the same mechanism every other
244
- * daemon surface uses, e.g. routing/triage), retaining the returned Unregister
245
- * for teardown. This is the production caller of `registerInboxMethods`.
246
- */
247
- export const register: SurfaceRegister = (ctx) => registerInboxMethods(ctx);
@@ -1,39 +0,0 @@
1
- // Channel-to-Profile Routing Control Plane — surface barrel.
2
- //
3
- // Integration wires this surface by calling `register(ctx)` (or
4
- // `registerRoutingMethods(ctx)` when it needs the returned store/resolver).
5
- // The inbox surface reuses `resolveProfile` / `createRoutingResolver` for
6
- // identical online/offline routing resolution.
7
-
8
- export { register, registerRoutingMethods } from './register.ts';
9
- export type {
10
- RoutingRegistration,
11
- RoutingListInput,
12
- RoutingListOutput,
13
- RoutingAssignInput,
14
- RoutingAssignOutput,
15
- RoutingDeleteInput,
16
- RoutingDeleteOutput,
17
- } from './register.ts';
18
-
19
- export {
20
- RouteStore,
21
- parseChannelId,
22
- buildChannelId,
23
- } from './route-store.ts';
24
- export type {
25
- RoutingChannelRoute,
26
- ParsedChannelId,
27
- RouteUpsertInput,
28
- RouteUpsertResult,
29
- RouteListFilter,
30
- } from './route-store.ts';
31
-
32
- export {
33
- resolveProfile,
34
- createRoutingResolver,
35
- WILDCARD_SURFACE,
36
- } from './routing-resolver.ts';
37
- export type { RoutingResolver } from './routing-resolver.ts';
38
-
39
- export { createInboxRouteResolver } from './inbox-bridge.ts';
@@ -1,296 +0,0 @@
1
- import {
2
- declareOperatorMethods,
3
- OperatorError,
4
- type OperatorContext,
5
- type OperatorHandler,
6
- type OperatorMethodDescriptor,
7
- type SurfaceRegister,
8
- type Unregister,
9
- } from '../../operator/index.ts';
10
- import { RouteStore, type RoutingChannelRoute } from './route-store.ts';
11
- import { createRoutingResolver, type RoutingResolver } from './routing-resolver.ts';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Wire contracts (request/response bodies) for the channels.routing.* methods.
15
- //
16
- // These interfaces and the JSON Schemas below mirror the handoff's literal
17
- // Input/Output blocks exactly. Per the handoff's "Confirmation / Effect
18
- // Semantics" note, `channels.routing.assign` and `channels.routing.delete` are
19
- // control-plane mutations that additionally require confirmation. That
20
- // confirmation is NOT part of the documented Input body shape: it is a
21
- // framework confirm-guard envelope flag (`confirm: true` on the raw request
22
- // body, plus `explicitUserRequest` in the call metadata) enforced by
23
- // `assertConfirmed` inside `declareOperatorMethods` when `descriptor.confirm`
24
- // is set. We therefore keep `confirm` out of the advertised contract here and
25
- // rely on the framework guard, so the wire Input stays a faithful match for the
26
- // handoff rather than a strict superset.
27
- // ---------------------------------------------------------------------------
28
-
29
- export interface RoutingListInput {
30
- profileId?: string;
31
- surfaceKind?: string;
32
- }
33
- export interface RoutingListOutput {
34
- routes: RoutingChannelRoute[];
35
- }
36
-
37
- export interface RoutingAssignInput {
38
- channelId: string;
39
- profileId: string;
40
- label?: string;
41
- }
42
- export interface RoutingAssignOutput {
43
- assignmentId: string;
44
- channelId: string;
45
- profileId: string;
46
- created: boolean;
47
- }
48
-
49
- export interface RoutingDeleteInput {
50
- assignmentId: string;
51
- }
52
- export interface RoutingDeleteOutput {
53
- deleted: boolean;
54
- }
55
-
56
- function asObject(body: unknown): Record<string, unknown> {
57
- return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {};
58
- }
59
-
60
- function optionalString(value: unknown): string | undefined {
61
- return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // JSON Schemas (advertised on the catalog descriptors).
66
- // ---------------------------------------------------------------------------
67
-
68
- const CHANNEL_ROUTE_SCHEMA: Record<string, unknown> = {
69
- type: 'object',
70
- required: ['assignmentId', 'channelId', 'surfaceKind', 'profileId', 'createdAt', 'updatedAt'],
71
- properties: {
72
- assignmentId: { type: 'string' },
73
- channelId: { type: 'string' },
74
- surfaceKind: { type: 'string' },
75
- routeId: { type: 'string' },
76
- profileId: { type: 'string' },
77
- label: { type: 'string' },
78
- createdAt: { type: 'string' },
79
- updatedAt: { type: 'string' },
80
- },
81
- };
82
-
83
- const LIST_INPUT_SCHEMA: Record<string, unknown> = {
84
- type: 'object',
85
- properties: {
86
- profileId: { type: 'string' },
87
- surfaceKind: { type: 'string' },
88
- },
89
- };
90
-
91
- const LIST_OUTPUT_SCHEMA: Record<string, unknown> = {
92
- type: 'object',
93
- required: ['routes'],
94
- properties: {
95
- routes: { type: 'array', items: CHANNEL_ROUTE_SCHEMA },
96
- },
97
- };
98
-
99
- const ASSIGN_INPUT_SCHEMA: Record<string, unknown> = {
100
- type: 'object',
101
- required: ['channelId', 'profileId'],
102
- properties: {
103
- channelId: { type: 'string' },
104
- profileId: { type: 'string' },
105
- label: { type: 'string' },
106
- },
107
- };
108
-
109
- const ASSIGN_OUTPUT_SCHEMA: Record<string, unknown> = {
110
- type: 'object',
111
- required: ['assignmentId', 'channelId', 'profileId', 'created'],
112
- properties: {
113
- assignmentId: { type: 'string' },
114
- channelId: { type: 'string' },
115
- profileId: { type: 'string' },
116
- created: { type: 'boolean' },
117
- },
118
- };
119
-
120
- const DELETE_INPUT_SCHEMA: Record<string, unknown> = {
121
- type: 'object',
122
- required: ['assignmentId'],
123
- properties: {
124
- assignmentId: { type: 'string' },
125
- },
126
- };
127
-
128
- const DELETE_OUTPUT_SCHEMA: Record<string, unknown> = {
129
- type: 'object',
130
- required: ['deleted'],
131
- properties: {
132
- deleted: { type: 'boolean' },
133
- },
134
- };
135
-
136
- // ---------------------------------------------------------------------------
137
- // Registration
138
- // ---------------------------------------------------------------------------
139
-
140
- /**
141
- * Handle returned by {@link registerRoutingMethods}: tears down the catalog
142
- * methods and closes the underlying SQLite store. Also exposes the resolver so
143
- * integration / the inbox surface can reuse routing resolution without going
144
- * through the catalog.
145
- */
146
- export interface RoutingRegistration extends Unregister {
147
- (): void;
148
- readonly store: RouteStore;
149
- readonly resolver: RoutingResolver;
150
- }
151
-
152
- /**
153
- * Register the channel-to-profile routing control-plane methods against the
154
- * operator catalog:
155
- * - channels.routing.list (read-only)
156
- * - channels.routing.assign (confirmed local-state mutation)
157
- * - channels.routing.delete (confirmed local-state mutation)
158
- *
159
- * The store is lazily initialized on first method invocation (the SurfaceRegister
160
- * contract is synchronous), so registration never blocks on disk I/O.
161
- */
162
- export function registerRoutingMethods(ctx: OperatorContext): RoutingRegistration {
163
- const store = new RouteStore({ workingDirectory: ctx.workingDirectory });
164
- const resolver = createRoutingResolver(store);
165
-
166
- let initPromise: Promise<void> | null = null;
167
- const ensureInit = async (): Promise<void> => {
168
- if (!initPromise) {
169
- initPromise = store.init().catch((error) => {
170
- // Allow a later invocation to retry initialization.
171
- initPromise = null;
172
- throw error;
173
- });
174
- }
175
- await initPromise;
176
- };
177
-
178
- const listHandler: OperatorHandler<RoutingListInput, RoutingListOutput> = async (input) => {
179
- await ensureInit();
180
- const body = asObject(input.body);
181
- const routes = store.list({
182
- profileId: optionalString(body.profileId),
183
- surfaceKind: optionalString(body.surfaceKind),
184
- });
185
- return { routes };
186
- };
187
-
188
- const assignHandler: OperatorHandler<RoutingAssignInput, RoutingAssignOutput> = async (input) => {
189
- await ensureInit();
190
- const body = asObject(input.body);
191
- const channelId = optionalString(body.channelId);
192
- const profileId = optionalString(body.profileId);
193
- if (channelId === undefined) {
194
- throw new OperatorError('channelId is required', 'ROUTING_INVALID_CHANNEL_ID', 400);
195
- }
196
- if (profileId === undefined) {
197
- throw new OperatorError('profileId is required', 'ROUTING_INVALID_PROFILE_ID', 400);
198
- }
199
- const { route, created } = await store.upsert({
200
- channelId,
201
- profileId,
202
- label: optionalString(body.label),
203
- });
204
- ctx.logger.info('channels.routing.assign', {
205
- assignmentId: route.assignmentId,
206
- channelId: route.channelId,
207
- profileId: route.profileId,
208
- created,
209
- principalId: input.context.principalId,
210
- });
211
- return {
212
- assignmentId: route.assignmentId,
213
- channelId: route.channelId,
214
- profileId: route.profileId,
215
- created,
216
- };
217
- };
218
-
219
- const deleteHandler: OperatorHandler<RoutingDeleteInput, RoutingDeleteOutput> = async (input) => {
220
- await ensureInit();
221
- const body = asObject(input.body);
222
- const assignmentId = optionalString(body.assignmentId);
223
- if (assignmentId === undefined) {
224
- throw new OperatorError('assignmentId is required', 'ROUTING_INVALID_ASSIGNMENT_ID', 400);
225
- }
226
- const deleted = await store.delete(assignmentId);
227
- ctx.logger.info('channels.routing.delete', {
228
- assignmentId,
229
- deleted,
230
- principalId: input.context.principalId,
231
- });
232
- return { deleted };
233
- };
234
-
235
- const listDescriptor: OperatorMethodDescriptor = {
236
- id: 'channels.routing.list',
237
- title: 'List channel routes',
238
- description: 'List channel-to-profile routing assignments, optionally filtered by profile or surface.',
239
- category: 'channels',
240
- source: 'daemon',
241
- access: 'authenticated',
242
- transport: ['ws', 'internal'],
243
- scopes: ['channels:routing:read'],
244
- effect: 'read-only',
245
- inputSchema: LIST_INPUT_SCHEMA,
246
- outputSchema: LIST_OUTPUT_SCHEMA,
247
- };
248
-
249
- const assignDescriptor: OperatorMethodDescriptor = {
250
- id: 'channels.routing.assign',
251
- title: 'Assign channel to profile',
252
- description: 'Create or update the profile that handles inbound messages on a channel.',
253
- category: 'channels',
254
- source: 'daemon',
255
- access: 'authenticated',
256
- transport: ['ws', 'internal'],
257
- scopes: ['channels:routing:write'],
258
- effect: 'local-state-mutation',
259
- confirm: true,
260
- inputSchema: ASSIGN_INPUT_SCHEMA,
261
- outputSchema: ASSIGN_OUTPUT_SCHEMA,
262
- };
263
-
264
- const deleteDescriptor: OperatorMethodDescriptor = {
265
- id: 'channels.routing.delete',
266
- title: 'Delete channel route',
267
- description: 'Remove a channel-to-profile routing assignment by assignmentId.',
268
- category: 'channels',
269
- source: 'daemon',
270
- access: 'authenticated',
271
- transport: ['ws', 'internal'],
272
- scopes: ['channels:routing:write'],
273
- effect: 'local-state-mutation',
274
- confirm: true,
275
- inputSchema: DELETE_INPUT_SCHEMA,
276
- outputSchema: DELETE_OUTPUT_SCHEMA,
277
- };
278
-
279
- const unregister = declareOperatorMethods(ctx, [
280
- { descriptor: listDescriptor, handler: listHandler as OperatorHandler<unknown, unknown> },
281
- { descriptor: assignDescriptor, handler: assignHandler as OperatorHandler<unknown, unknown> },
282
- { descriptor: deleteDescriptor, handler: deleteHandler as OperatorHandler<unknown, unknown> },
283
- ]);
284
-
285
- // Build the callable teardown handle, then attach the store/resolver as
286
- // read-only properties so callers (integration, the inbox surface) can reuse
287
- // routing resolution without going through the catalog.
288
- const teardown = (): void => {
289
- unregister();
290
- store.close();
291
- };
292
- return Object.assign(teardown, { store, resolver }) as RoutingRegistration;
293
- }
294
-
295
- /** SurfaceRegister contract entry point (integration calls this). */
296
- export const register: SurfaceRegister = (ctx) => registerRoutingMethods(ctx);
@@ -1,68 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Email operator-method surface barrel.
3
- //
4
- // Integration wires this surface by calling registerEmailMethods(ctx) once and
5
- // retaining the returned Unregister. Connectors and pure helpers are re-exported
6
- // for daemon-internal reuse and unit testing.
7
- // ---------------------------------------------------------------------------
8
-
9
- export { registerEmailMethods, resolveEmailSettings } from './register.ts';
10
- export type {
11
- ResolvedEmailSettings,
12
- EmailMethodsOptions,
13
- ImapClient,
14
- SmtpClient,
15
- } from './register.ts';
16
-
17
- export {
18
- ImapConnector,
19
- ImapError,
20
- toImapSearchDate,
21
- quoteImapString,
22
- parseSearchUids,
23
- parseAppendUid,
24
- parseEnvelope,
25
- parseFetchSummaries,
26
- parseFullMessage,
27
- parseAddressList,
28
- parseMimeMessage,
29
- splitHeadersBody,
30
- tokenizeParen,
31
- extractParenValue,
32
- extractLiteralFor,
33
- unescapeImapString,
34
- decodeTransferEncoding,
35
- decodeQuotedPrintable,
36
- decodeMimeWords,
37
- stripHtml,
38
- collapseWhitespace,
39
- } from './imap-connector.ts';
40
- export type {
41
- ImapConnectionSettings,
42
- ImapEnvelopeSummary,
43
- ImapFullMessage,
44
- ImapAttachmentSummary,
45
- ImapListOptions,
46
- ImapAppendResult,
47
- ParsedMime,
48
- } from './imap-connector.ts';
49
-
50
- export {
51
- SmtpConnector,
52
- SmtpError,
53
- extractCompleteReply,
54
- extractAddress,
55
- parseRecipients,
56
- dotStuff,
57
- generateMessageId,
58
- buildRfc5322Message,
59
- encodeHeaderValue,
60
- formatRfc2822Date,
61
- encodeQuotedPrintable,
62
- } from './smtp-connector.ts';
63
- export type {
64
- SmtpConnectionSettings,
65
- SmtpMessage,
66
- SmtpSendResult,
67
- Rfc5322Parts,
68
- } from './smtp-connector.ts';