@pellux/goodvibes-tui 0.24.1 → 0.25.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 (56) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/daemon/calendar/caldav-client.ts +657 -0
  5. package/src/daemon/calendar/ics.ts +556 -0
  6. package/src/daemon/calendar/index.ts +52 -0
  7. package/src/daemon/calendar/register.ts +527 -0
  8. package/src/daemon/channels/drafts/draft-store.ts +363 -0
  9. package/src/daemon/channels/drafts/index.ts +22 -0
  10. package/src/daemon/channels/drafts/register.ts +449 -0
  11. package/src/daemon/channels/inbox/cursor-store.ts +298 -0
  12. package/src/daemon/channels/inbox/index.ts +58 -0
  13. package/src/daemon/channels/inbox/mapping.ts +190 -0
  14. package/src/daemon/channels/inbox/poller.ts +155 -0
  15. package/src/daemon/channels/inbox/provider-adapter.ts +152 -0
  16. package/src/daemon/channels/inbox/providers/discord.ts +253 -0
  17. package/src/daemon/channels/inbox/providers/email.ts +151 -0
  18. package/src/daemon/channels/inbox/providers/imap-client.ts +300 -0
  19. package/src/daemon/channels/inbox/providers/route-util.ts +23 -0
  20. package/src/daemon/channels/inbox/providers/slack.ts +264 -0
  21. package/src/daemon/channels/inbox/register.ts +247 -0
  22. package/src/daemon/channels/routing/inbox-bridge.ts +58 -0
  23. package/src/daemon/channels/routing/index.ts +39 -0
  24. package/src/daemon/channels/routing/register.ts +296 -0
  25. package/src/daemon/channels/routing/route-store.ts +278 -0
  26. package/src/daemon/channels/routing/routing-resolver.ts +75 -0
  27. package/src/daemon/email/imap-connector.ts +441 -0
  28. package/src/daemon/email/imap-parsing.ts +499 -0
  29. package/src/daemon/email/index.ts +68 -0
  30. package/src/daemon/email/register.ts +715 -0
  31. package/src/daemon/email/smtp-connector.ts +557 -0
  32. package/src/daemon/operator/credential-store.ts +129 -0
  33. package/src/daemon/operator/index.ts +43 -0
  34. package/src/daemon/operator/register-helper.ts +150 -0
  35. package/src/daemon/operator/sqlite-store.ts +124 -0
  36. package/src/daemon/operator/surfaces.ts +137 -0
  37. package/src/daemon/operator/types.ts +207 -0
  38. package/src/daemon/remote/backends/cloud-terminal.ts +137 -0
  39. package/src/daemon/remote/backends/docker.ts +80 -0
  40. package/src/daemon/remote/backends/index.ts +34 -0
  41. package/src/daemon/remote/backends/local-process.ts +113 -0
  42. package/src/daemon/remote/backends/process-runner.ts +151 -0
  43. package/src/daemon/remote/backends/ssh.ts +120 -0
  44. package/src/daemon/remote/backends/types.ts +71 -0
  45. package/src/daemon/remote/dispatcher.ts +160 -0
  46. package/src/daemon/remote/index.ts +74 -0
  47. package/src/daemon/remote/peer-registry.ts +321 -0
  48. package/src/daemon/remote/register.ts +411 -0
  49. package/src/daemon/triage/index.ts +59 -0
  50. package/src/daemon/triage/integration.ts +179 -0
  51. package/src/daemon/triage/pipeline.ts +285 -0
  52. package/src/daemon/triage/register.ts +231 -0
  53. package/src/daemon/triage/scorer.ts +287 -0
  54. package/src/daemon/triage/tagger.ts +777 -0
  55. package/src/runtime/services.ts +35 -0
  56. package/src/version.ts +1 -1
@@ -0,0 +1,527 @@
1
+ // Operator-method registration for the CalDAV calendar surface.
2
+ //
3
+ // Published method IDs (exactly):
4
+ // calendar.events.list (read-only-network, no confirm)
5
+ // calendar.events.get (read-only-network, no confirm)
6
+ // calendar.events.create (confirmed-effect, confirm:true + explicitUserRequest)
7
+ // calendar.ics.import (confirmed-effect, confirm:true)
8
+ // calendar.ics.export (read-only, no confirm)
9
+ //
10
+ // Exactly these five IDs are published — matching the SDK handoff contract
11
+ // ("the daemon must implement exactly these IDs"). Capability advertisement is
12
+ // satisfied by each descriptor's catalog metadata rather than by an extra
13
+ // calendar.* operator method.
14
+ //
15
+ // SECURITY: CalDAV credentials and authenticated URLs NEVER appear in any
16
+ // response. `calendarId` is a logical id. Attendees are surfaced as display
17
+ // names only (no raw addresses). Organizer is surfaced as a SHA-256 digest.
18
+
19
+ import {
20
+ declareOperatorMethods,
21
+ OperatorError,
22
+ sha256First,
23
+ type CalendarEventSummary,
24
+ type OperatorContext,
25
+ type OperatorHandler,
26
+ type OperatorMethodDescriptor,
27
+ type Unregister,
28
+ } from '../operator/index.ts';
29
+ import {
30
+ createCalDavClient,
31
+ resolveCalDavConfig,
32
+ type CalDavClient,
33
+ type CalDavEvent,
34
+ } from './caldav-client.ts';
35
+
36
+ /**
37
+ * The exact set of method IDs this surface publishes. Exposed so integration
38
+ * and tests have a single source of truth for the calendar capability set.
39
+ */
40
+ export const CALENDAR_METHOD_IDS = [
41
+ 'calendar.events.list',
42
+ 'calendar.events.get',
43
+ 'calendar.events.create',
44
+ 'calendar.ics.import',
45
+ 'calendar.ics.export',
46
+ ] as const;
47
+
48
+ const CATEGORY = 'calendar';
49
+ const TRANSPORT: OperatorMethodDescriptor['transport'] = ['ws', 'internal'];
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Client factory injection (real client by default; tests inject a stub).
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export type CalDavClientFactory = (ctx: OperatorContext) => Promise<CalDavClient>;
56
+
57
+ const realClientFactory: CalDavClientFactory = async (ctx) => {
58
+ const config = await resolveCalDavConfig(ctx);
59
+ return createCalDavClient({ config });
60
+ };
61
+
62
+ export interface RegisterCalendarOptions {
63
+ /** Override the CalDAV client factory (for tests). */
64
+ clientFactory?: CalDavClientFactory;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Input validation helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function asRecord(body: unknown): Record<string, unknown> {
72
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
73
+ throw new OperatorError('Request body must be an object.', 'CALENDAR_BAD_INPUT', 400);
74
+ }
75
+ return body as Record<string, unknown>;
76
+ }
77
+
78
+ function optionalString(record: Record<string, unknown>, key: string): string | undefined {
79
+ const value = record[key];
80
+ if (value === undefined || value === null) return undefined;
81
+ if (typeof value !== 'string') {
82
+ throw new OperatorError(`Field '${key}' must be a string.`, 'CALENDAR_BAD_INPUT', 400);
83
+ }
84
+ const trimmed = value.trim();
85
+ return trimmed.length > 0 ? trimmed : undefined;
86
+ }
87
+
88
+ function requiredString(record: Record<string, unknown>, key: string): string {
89
+ const value = optionalString(record, key);
90
+ if (value === undefined) {
91
+ throw new OperatorError(`Field '${key}' is required.`, 'CALENDAR_BAD_INPUT', 400);
92
+ }
93
+ return value;
94
+ }
95
+
96
+ function optionalNumber(record: Record<string, unknown>, key: string): number | undefined {
97
+ const value = record[key];
98
+ if (value === undefined || value === null) return undefined;
99
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
100
+ throw new OperatorError(`Field '${key}' must be a number.`, 'CALENDAR_BAD_INPUT', 400);
101
+ }
102
+ return value;
103
+ }
104
+
105
+ function optionalStringArray(record: Record<string, unknown>, key: string): string[] | undefined {
106
+ const value = record[key];
107
+ if (value === undefined || value === null) return undefined;
108
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
109
+ throw new OperatorError(`Field '${key}' must be an array of strings.`, 'CALENDAR_BAD_INPUT', 400);
110
+ }
111
+ return (value as string[]).map((item) => item.trim()).filter((item) => item.length > 0);
112
+ }
113
+
114
+ function validateIsoDate(value: string, field: string): string {
115
+ if (Number.isNaN(new Date(value).getTime())) {
116
+ throw new OperatorError(`Field '${field}' must be a valid ISO-8601 date.`, 'CALENDAR_BAD_INPUT', 400);
117
+ }
118
+ return value;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Response mapping (credential-free, PII-stripped)
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Wire shape returned by `calendar.events.list`. Extends the shared operator
127
+ * `CalendarEventSummary` with the two fields the daemon handoff I/O contract
128
+ * requires on the summary (`description` and display-name-only `attendees`),
129
+ * which the base operator type leaves to its open `metadata` slot. Keeping them
130
+ * as first-class fields makes the response an exact match for the handoff
131
+ * `CalendarEventSummary` block rather than a partial subset.
132
+ */
133
+ export interface CalendarEventSummaryResult extends CalendarEventSummary {
134
+ /** Free-text DESCRIPTION, when present. */
135
+ description?: string;
136
+ /** Display names only — never raw addresses. Omitted when there are none. */
137
+ attendees?: string[];
138
+ }
139
+
140
+ function toSummary(event: CalDavEvent): CalendarEventSummaryResult {
141
+ const attendees = event.attendees.map((a) => a.displayName).filter((name) => name.length > 0);
142
+ return {
143
+ id: event.href || event.uid,
144
+ calendarId: event.calendarId,
145
+ title: event.summary,
146
+ start: event.start,
147
+ end: event.end,
148
+ allDay: event.allDay,
149
+ description: event.description,
150
+ location: event.location,
151
+ attendees: attendees.length > 0 ? attendees : undefined,
152
+ organizerDigest: event.organizerRaw ? sha256First(event.organizerRaw, 16) : undefined,
153
+ status: event.status,
154
+ };
155
+ }
156
+
157
+ export interface CalendarEventDetail {
158
+ id: string;
159
+ calendarId: string;
160
+ uid: string;
161
+ title: string;
162
+ start: string;
163
+ end: string;
164
+ allDay: boolean;
165
+ description?: string;
166
+ location?: string;
167
+ status?: string;
168
+ recurrence?: string;
169
+ /** Display names only — never raw addresses. */
170
+ attendees: string[];
171
+ organizerDigest?: string;
172
+ }
173
+
174
+ function toDetail(event: CalDavEvent): CalendarEventDetail {
175
+ return {
176
+ id: event.href || event.uid,
177
+ calendarId: event.calendarId,
178
+ uid: event.uid,
179
+ title: event.summary,
180
+ start: event.start,
181
+ end: event.end,
182
+ allDay: event.allDay,
183
+ description: event.description,
184
+ location: event.location,
185
+ status: event.status,
186
+ recurrence: event.recurrence,
187
+ attendees: event.attendees.map((a) => a.displayName),
188
+ organizerDigest: event.organizerRaw ? sha256First(event.organizerRaw, 16) : undefined,
189
+ };
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // JSON Schemas
194
+ // ---------------------------------------------------------------------------
195
+
196
+ const calendarEventSummarySchema: Record<string, unknown> = {
197
+ type: 'object',
198
+ required: ['id', 'title', 'start', 'end'],
199
+ properties: {
200
+ id: { type: 'string' },
201
+ calendarId: { type: 'string' },
202
+ title: { type: 'string' },
203
+ start: { type: 'string', format: 'date-time' },
204
+ end: { type: 'string', format: 'date-time' },
205
+ allDay: { type: 'boolean' },
206
+ description: { type: 'string' },
207
+ location: { type: 'string' },
208
+ attendees: { type: 'array', items: { type: 'string' } },
209
+ organizerDigest: { type: 'string' },
210
+ status: { type: 'string' },
211
+ },
212
+ };
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Handlers
216
+ // ---------------------------------------------------------------------------
217
+
218
+ interface ListBody {
219
+ calendarId?: string;
220
+ from?: string;
221
+ to?: string;
222
+ limit?: number;
223
+ }
224
+
225
+ interface GetBody {
226
+ eventId: string;
227
+ calendarId?: string;
228
+ }
229
+
230
+ interface CreateBody {
231
+ title: string;
232
+ start: string;
233
+ end: string;
234
+ description?: string;
235
+ attendees?: string[];
236
+ location?: string;
237
+ calendarId?: string;
238
+ confirm: true;
239
+ }
240
+
241
+ interface ImportBody {
242
+ icsContent: string;
243
+ calendarId?: string;
244
+ confirm: true;
245
+ }
246
+
247
+ interface ExportBody {
248
+ calendarId?: string;
249
+ from?: string;
250
+ to?: string;
251
+ }
252
+
253
+ /**
254
+ * Register all five calendar operator methods against the catalog. Returns a
255
+ * single Unregister that tears all of them down.
256
+ */
257
+ export function registerCalendarMethods(
258
+ ctx: OperatorContext,
259
+ options: RegisterCalendarOptions = {},
260
+ ): Unregister {
261
+ const clientFactory = options.clientFactory ?? realClientFactory;
262
+ const getClient = (): Promise<CalDavClient> => clientFactory(ctx);
263
+
264
+ const listHandler: OperatorHandler<unknown, { events: CalendarEventSummaryResult[] }> = async ({ body }) => {
265
+ const record = body === undefined || body === null ? {} : asRecord(body);
266
+ const input: ListBody = {
267
+ calendarId: optionalString(record, 'calendarId'),
268
+ from: optionalString(record, 'from'),
269
+ to: optionalString(record, 'to'),
270
+ limit: optionalNumber(record, 'limit'),
271
+ };
272
+ if (input.from) validateIsoDate(input.from, 'from');
273
+ if (input.to) validateIsoDate(input.to, 'to');
274
+ const limit = input.limit !== undefined ? Math.max(1, Math.min(200, Math.floor(input.limit))) : 20;
275
+ const client = await getClient();
276
+ const events = await client.listEvents({ ...input, limit });
277
+ return { events: events.map(toSummary) };
278
+ };
279
+
280
+ const getHandler: OperatorHandler<unknown, { event: CalendarEventDetail }> = async ({ body }) => {
281
+ const record = asRecord(body);
282
+ const input: GetBody = {
283
+ eventId: requiredString(record, 'eventId'),
284
+ calendarId: optionalString(record, 'calendarId'),
285
+ };
286
+ const client = await getClient();
287
+ const event = await client.getEvent(input.eventId, input.calendarId);
288
+ if (!event) {
289
+ throw new OperatorError(`Event not found: ${input.eventId}`, 'CALENDAR_NOT_FOUND', 404);
290
+ }
291
+ return { event: toDetail(event) };
292
+ };
293
+
294
+ const createHandler: OperatorHandler<unknown, { eventId: string; uid: string; createdAt: string }> = async ({ body }) => {
295
+ const record = asRecord(body);
296
+ const input: CreateBody = {
297
+ title: requiredString(record, 'title'),
298
+ start: validateIsoDate(requiredString(record, 'start'), 'start'),
299
+ end: validateIsoDate(requiredString(record, 'end'), 'end'),
300
+ description: optionalString(record, 'description'),
301
+ attendees: optionalStringArray(record, 'attendees'),
302
+ location: optionalString(record, 'location'),
303
+ calendarId: optionalString(record, 'calendarId'),
304
+ confirm: true,
305
+ };
306
+ if (new Date(input.end).getTime() < new Date(input.start).getTime()) {
307
+ throw new OperatorError("Field 'end' must not be before 'start'.", 'CALENDAR_BAD_INPUT', 400);
308
+ }
309
+ const client = await getClient();
310
+ const created = await client.createEvent({
311
+ title: input.title,
312
+ start: input.start,
313
+ end: input.end,
314
+ description: input.description,
315
+ attendees: input.attendees,
316
+ location: input.location,
317
+ calendarId: input.calendarId,
318
+ });
319
+ return { eventId: created.eventId, uid: created.uid, createdAt: created.createdAt };
320
+ };
321
+
322
+ const importHandler: OperatorHandler<unknown, { imported: number; eventIds: string[]; errors: string[] }> = async ({ body }) => {
323
+ const record = asRecord(body);
324
+ const input: ImportBody = {
325
+ icsContent: requiredString(record, 'icsContent'),
326
+ calendarId: optionalString(record, 'calendarId'),
327
+ confirm: true,
328
+ };
329
+ const client = await getClient();
330
+ return client.importIcs(input.icsContent, input.calendarId);
331
+ };
332
+
333
+ const exportHandler: OperatorHandler<unknown, { icsContent: string; eventCount: number }> = async ({ body }) => {
334
+ const record = body === undefined || body === null ? {} : asRecord(body);
335
+ const input: ExportBody = {
336
+ calendarId: optionalString(record, 'calendarId'),
337
+ from: optionalString(record, 'from'),
338
+ to: optionalString(record, 'to'),
339
+ };
340
+ if (input.from) validateIsoDate(input.from, 'from');
341
+ if (input.to) validateIsoDate(input.to, 'to');
342
+ const client = await getClient();
343
+ return client.exportIcs(input);
344
+ };
345
+
346
+ return declareOperatorMethods(ctx, [
347
+ {
348
+ descriptor: {
349
+ id: 'calendar.events.list',
350
+ title: 'List calendar events',
351
+ description: 'List events from a CalDAV calendar within an optional time range.',
352
+ category: CATEGORY,
353
+ source: 'daemon',
354
+ access: 'operator',
355
+ transport: TRANSPORT,
356
+ scopes: ['calendar:read'],
357
+ effect: 'read-only-network',
358
+ confirm: false,
359
+ inputSchema: {
360
+ type: 'object',
361
+ properties: {
362
+ calendarId: { type: 'string' },
363
+ from: { type: 'string', format: 'date-time' },
364
+ to: { type: 'string', format: 'date-time' },
365
+ limit: { type: 'number', minimum: 1, maximum: 200, default: 20 },
366
+ },
367
+ },
368
+ outputSchema: {
369
+ type: 'object',
370
+ required: ['events'],
371
+ properties: { events: { type: 'array', items: calendarEventSummarySchema } },
372
+ },
373
+ },
374
+ handler: listHandler as OperatorHandler<unknown, unknown>,
375
+ },
376
+ {
377
+ descriptor: {
378
+ id: 'calendar.events.get',
379
+ title: 'Get calendar event',
380
+ description: 'Fetch a single calendar event with attendees, recurrence, and iCal UID.',
381
+ category: CATEGORY,
382
+ source: 'daemon',
383
+ access: 'operator',
384
+ transport: TRANSPORT,
385
+ scopes: ['calendar:read'],
386
+ effect: 'read-only-network',
387
+ confirm: false,
388
+ inputSchema: {
389
+ type: 'object',
390
+ required: ['eventId'],
391
+ properties: {
392
+ eventId: { type: 'string' },
393
+ calendarId: { type: 'string' },
394
+ },
395
+ },
396
+ outputSchema: {
397
+ type: 'object',
398
+ required: ['event'],
399
+ properties: {
400
+ event: {
401
+ type: 'object',
402
+ required: ['id', 'uid', 'title', 'start', 'end', 'attendees'],
403
+ properties: {
404
+ id: { type: 'string' },
405
+ calendarId: { type: 'string' },
406
+ uid: { type: 'string' },
407
+ title: { type: 'string' },
408
+ start: { type: 'string', format: 'date-time' },
409
+ end: { type: 'string', format: 'date-time' },
410
+ allDay: { type: 'boolean' },
411
+ description: { type: 'string' },
412
+ location: { type: 'string' },
413
+ status: { type: 'string' },
414
+ recurrence: { type: 'string' },
415
+ attendees: { type: 'array', items: { type: 'string' } },
416
+ organizerDigest: { type: 'string' },
417
+ },
418
+ },
419
+ },
420
+ },
421
+ },
422
+ handler: getHandler as OperatorHandler<unknown, unknown>,
423
+ },
424
+ {
425
+ descriptor: {
426
+ id: 'calendar.events.create',
427
+ title: 'Create calendar event',
428
+ description: 'Create an event on the user’s CalDAV calendar. Requires explicit confirmation.',
429
+ category: CATEGORY,
430
+ source: 'daemon',
431
+ access: 'operator',
432
+ transport: TRANSPORT,
433
+ scopes: ['calendar:write'],
434
+ effect: 'confirmed-effect',
435
+ confirm: true,
436
+ inputSchema: {
437
+ type: 'object',
438
+ required: ['title', 'start', 'end', 'confirm'],
439
+ properties: {
440
+ title: { type: 'string' },
441
+ start: { type: 'string', format: 'date-time' },
442
+ end: { type: 'string', format: 'date-time' },
443
+ description: { type: 'string' },
444
+ attendees: { type: 'array', items: { type: 'string' } },
445
+ location: { type: 'string' },
446
+ calendarId: { type: 'string' },
447
+ confirm: { const: true },
448
+ },
449
+ },
450
+ outputSchema: {
451
+ type: 'object',
452
+ required: ['eventId', 'uid', 'createdAt'],
453
+ properties: {
454
+ eventId: { type: 'string' },
455
+ uid: { type: 'string' },
456
+ createdAt: { type: 'string', format: 'date-time' },
457
+ },
458
+ },
459
+ },
460
+ handler: createHandler as OperatorHandler<unknown, unknown>,
461
+ },
462
+ {
463
+ descriptor: {
464
+ id: 'calendar.ics.import',
465
+ title: 'Import .ics to calendar',
466
+ description: 'Import one or more events from a raw RFC 5545 .ics object. Requires confirmation.',
467
+ category: CATEGORY,
468
+ source: 'daemon',
469
+ access: 'operator',
470
+ transport: TRANSPORT,
471
+ scopes: ['calendar:write'],
472
+ effect: 'confirmed-effect',
473
+ confirm: true,
474
+ inputSchema: {
475
+ type: 'object',
476
+ required: ['icsContent', 'confirm'],
477
+ properties: {
478
+ icsContent: { type: 'string' },
479
+ calendarId: { type: 'string' },
480
+ confirm: { const: true },
481
+ },
482
+ },
483
+ outputSchema: {
484
+ type: 'object',
485
+ required: ['imported', 'eventIds', 'errors'],
486
+ properties: {
487
+ imported: { type: 'number' },
488
+ eventIds: { type: 'array', items: { type: 'string' } },
489
+ errors: { type: 'array', items: { type: 'string' } },
490
+ },
491
+ },
492
+ },
493
+ handler: importHandler as OperatorHandler<unknown, unknown>,
494
+ },
495
+ {
496
+ descriptor: {
497
+ id: 'calendar.ics.export',
498
+ title: 'Export calendar as .ics',
499
+ description: 'Export a CalDAV calendar (optionally within a time range) as an RFC 5545 .ics document.',
500
+ category: CATEGORY,
501
+ source: 'daemon',
502
+ access: 'operator',
503
+ transport: TRANSPORT,
504
+ scopes: ['calendar:read'],
505
+ effect: 'read-only',
506
+ confirm: false,
507
+ inputSchema: {
508
+ type: 'object',
509
+ properties: {
510
+ calendarId: { type: 'string' },
511
+ from: { type: 'string', format: 'date-time' },
512
+ to: { type: 'string', format: 'date-time' },
513
+ },
514
+ },
515
+ outputSchema: {
516
+ type: 'object',
517
+ required: ['icsContent', 'eventCount'],
518
+ properties: {
519
+ icsContent: { type: 'string' },
520
+ eventCount: { type: 'number' },
521
+ },
522
+ },
523
+ },
524
+ handler: exportHandler as OperatorHandler<unknown, unknown>,
525
+ },
526
+ ]);
527
+ }