@pellux/goodvibes-agent 1.5.5 → 1.5.6

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -197,7 +197,7 @@ None of those modes expose host start, stop, restart, install, expose-listener,
197
197
 
198
198
  ## Visible Autonomy
199
199
 
200
- Use `autonomy action:"intake"` first when the user asks for ongoing work and the safest route is not obvious. It is read-only and returns the likely route, missing fields, confirmation boundary, and trigger workflow posture for time-based wakeups/schedules, incoming webhooks/watchers, Gmail/email connector triggers, and control-plane event streams. Webhook, watcher, Gmail, or event-trigger requests now point to the published `watchers.create` contract when applicable, but watcher creation is an admin connected-host mutation: require trusted source/scope, task or run target, success criteria, `confirm:true`, and `explicitUserRequest`. The intake action also returns watcher receipt success criteria plus a read-only watcher evidence contract for SDK/daemon-owned durable run-history receipts, provider source records, redacted event payload descriptors, and autonomy queue correlation. Confirmed `agent_operator_method` calls summarize `watchers.create/patch/run/start/stop/delete` receipts into certified or follow-up outcomes without exposing operator tokens. Use `schedule action:"create"` for one visible autonomous schedule, `schedule action:"remind"` for reminders, `schedule action:"edit"` for one exact schedule edit, and `schedule action:"run|pause|resume|delete"` for lifecycle controls; it forwards to the same preview, read-only current-state diff, confirmation, and connected-host routes as `agent_autonomy_schedule`, `agent_reminder_schedule`, `agent_schedule_edit`, and allowlisted `agent_operator_action`, then returns post-action next routes for schedule list, autonomy queue inspection, run, edit, pause, resume, and delete where the schedule still exists. Unconfirmed schedule and routine-promotion previews return `confirmationRoutes` so the user or model can confirm the same intent without guessing the next command. Confirmed routine schedule promotion through the workspace or `/schedule promote-routine` returns the same post-action schedule next routes. Use `autonomy action:"queue"` before creating recurring autonomous work, reminders, routine schedules, delegated work, run controls, schedule edits, approval decisions, watcher triggers, or follow-up delivery. The queue is read-only and normalizes work-plan, research-run, connected task, approval, automation, schedule, reminder, routine-promotion, delegated-agent, and delivery cards. Research runs, connected-host tasks, approvals, automation runs, SDK/daemon watcher run/source records, and schedules include live records with status/progress, source ids, next steps, log tails when available, task retry/output/correlation diagnostics, bounded redacted host task and watcher output route/preview descriptors, automation telemetry/delivery/route diagnostics, watcher run-history/source diagnostics, available controls, unavailable controls with reasons, and exact inspect/checkpoint/pause/resume/cancel/approve/deny/retry/run/edit/enable/disable/delete routes where supported. Provider-source watcher records are read-only context with inspect/refresh controls; watcher run effects appear only when the owning SDK or daemon publishes exact confirmed routes. Schedule records expose pause/resume aliases over daemon enable/disable lifecycle routes so users do not have to translate scheduler terminology. Connected-host task cancel/retry uses `agent_operator_method` exact daemon methods with `confirm:true` plus `explicitUserRequest`; slash `/tasks` remains inspection-only. Inspect one card with `autonomy action:"item"`; lower-level `agent_harness mode:"autonomy_intake|autonomy_queue|autonomy_queue_item"` remains available for detailed route inspection. Create, edit, run, pause, resume, cancel, approve, deny, send, schedule, and schedule lifecycle effects stay on the owning confirmed route returned by that card.
200
+ Use `autonomy action:"intake"` first when the user asks for ongoing work and the safest route is not obvious. It is read-only and returns the likely route, missing fields, confirmation boundary, and trigger workflow posture for time-based wakeups/schedules, incoming webhooks/watchers, Gmail/email connector triggers, and control-plane event streams. Webhook, watcher, Gmail, or event-trigger requests now point to the published `watchers.create` contract when applicable, but watcher creation is an admin connected-host mutation: require trusted source/scope, task or run target, success criteria, `confirm:true`, and `explicitUserRequest`. The intake action also returns watcher receipt success criteria plus a read-only watcher evidence contract for SDK/daemon-owned durable run-history receipts, provider source records, redacted event payload descriptors, and autonomy queue correlation. Confirmed `agent_operator_method` calls summarize `watchers.create/update/run/start/stop/delete` receipts into certified or follow-up outcomes without exposing operator tokens. Use `schedule action:"create"` for one visible autonomous schedule, `schedule action:"remind"` for reminders, `schedule action:"edit"` for one exact schedule edit, and `schedule action:"run|pause|resume|delete"` for lifecycle controls; it forwards to the same preview, read-only current-state diff, confirmation, and connected-host routes as `agent_autonomy_schedule`, `agent_reminder_schedule`, `agent_schedule_edit`, and allowlisted `agent_operator_action`, then returns post-action next routes for schedule list, autonomy queue inspection, run, edit, pause, resume, and delete where the schedule still exists. Unconfirmed schedule and routine-promotion previews return `confirmationRoutes` so the user or model can confirm the same intent without guessing the next command. Confirmed routine schedule promotion through the workspace or `/schedule promote-routine` returns the same post-action schedule next routes. Use `autonomy action:"queue"` before creating recurring autonomous work, reminders, routine schedules, delegated work, run controls, schedule edits, approval decisions, watcher triggers, or follow-up delivery. The queue is read-only and normalizes work-plan, research-run, connected task, approval, automation, schedule, reminder, routine-promotion, delegated-agent, and delivery cards. Research runs, connected-host tasks, approvals, automation runs, SDK/daemon watcher run/source records, and schedules include live records with status/progress, source ids, next steps, log tails when available, task retry/output/correlation diagnostics, bounded redacted host task and watcher output route/preview descriptors, automation telemetry/delivery/route diagnostics, watcher run-history/source diagnostics, available controls, unavailable controls with reasons, and exact inspect/checkpoint/pause/resume/cancel/approve/deny/retry/run/edit/enable/disable/delete routes where supported. Provider-source watcher records are read-only context with inspect/refresh controls; watcher run effects appear only when the owning SDK or daemon publishes exact confirmed routes. Schedule records expose pause/resume aliases over daemon enable/disable lifecycle routes so users do not have to translate scheduler terminology. Connected-host task cancel/retry uses `agent_operator_method` exact daemon methods with `confirm:true` plus `explicitUserRequest`; slash `/tasks` remains inspection-only. Inspect one card with `autonomy action:"item"`; lower-level `agent_harness mode:"autonomy_intake|autonomy_queue|autonomy_queue_item"` remains available for detailed route inspection. Create, edit, run, pause, resume, cancel, approve, deny, send, schedule, and schedule lifecycle effects stay on the owning confirmed route returned by that card.
201
201
 
202
202
  ## Agent Knowledge
203
203
 
@@ -290,6 +290,7 @@ Routine promotion is an explicit scheduling route. Local routines stay local unt
290
290
  | `/memory` | Add, search, review, stale, or delete Agent-local memory records. |
291
291
  | `/mode` | Manage Agent interaction mode and per-domain verbosity. |
292
292
  | `/model` | Select or display the current LLM model. |
293
+ | `/network-scan` | Turn local-network scanning for model servers on or off, or check its status. Off until explicitly turned on; never scans silently on first run. |
293
294
  | `/next-error` | Jump to the next error message in the conversation. |
294
295
  | `/notes` | Open Agent-local scratchpad notes in the operator workspace. |
295
296
  | `/notify` | Manage and send configured Agent webhook notifications. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
4
4
  "private": false,
5
5
  "description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
6
6
  "type": "module",
@@ -53,7 +53,10 @@
53
53
  "publish:dry-run": "bun run scripts/publish-package.ts --dry-run",
54
54
  "publish:check": "bun run scripts/publish-check.ts",
55
55
  "package:install-check": "bun run scripts/package-install-check.ts",
56
- "ci:gate": "bun run typecheck && bun run typecheck:test && bun run test && bun run architecture:check && bun run perf:check && bun run build && bun run publish:check && bun run package:install-check && bun run verification:ledger",
56
+ "ci:gate": "bun run typecheck && bun run typecheck:test && bun run test && bun run coverage:gate && bun run architecture:check && bun run perf:check && bun run build && bun run publish:check && bun run package:install-check && bun run verification:ledger",
57
+ "coverage:gate": "bun run scripts/coverage-gate.ts",
58
+ "sdk:gate": "bun run scripts/sdk-release-gates.ts",
59
+ "sdk:dev": "bun run scripts/sdk-dev.ts",
57
60
  "build:prod": "bun run scripts/build.ts",
58
61
  "build:all": "bun run scripts/build.ts --all",
59
62
  "perf:check": "bun run scripts/perf-check.ts",
@@ -89,7 +92,7 @@
89
92
  },
90
93
  "dependencies": {},
91
94
  "devDependencies": {
92
- "@pellux/goodvibes-sdk": "0.35.0",
95
+ "@pellux/goodvibes-sdk": "1.0.0",
93
96
  "sql.js": "^1.14.1",
94
97
  "sqlite-vec": "^0.1.9",
95
98
  "zustand": "^5.0.12",
@@ -90,7 +90,7 @@ export function formatAutonomyScheduleFailure(failure: AutonomyScheduleFailure):
90
90
  ? ' next make the connected GoodVibes host available outside Agent, then retry.'
91
91
  : null,
92
92
  failure.kind === 'connected_host_incompatible' || failure.kind === 'connected_host_route_unavailable'
93
- ? ' next update the connected GoodVibes host so public schedules.create is available.'
93
+ ? ' next update the connected GoodVibes host so public automation.schedules.create is available.'
94
94
  : null,
95
95
  ].filter((line): line is string => Boolean(line)).join('\n');
96
96
  }
@@ -15,8 +15,8 @@ import {
15
15
  type RoutineScheduleSpec,
16
16
  } from './routine-schedule-promotion.ts';
17
17
 
18
- type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
19
- type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
18
+ type ScheduleCreateInput = OperatorMethodInput<'automation.schedules.create'>;
19
+ type ScheduleCreateOutput = OperatorMethodOutput<'automation.schedules.create'>;
20
20
  type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
21
21
  type ScheduleDeliveryTargetInput = ScheduleDeliveryInput['targets'] extends readonly (infer T)[] ? T : never;
22
22
 
@@ -466,7 +466,7 @@ async function classifyAutonomyScheduleError(
466
466
  return {
467
467
  ok: false,
468
468
  kind: 'connected_host_incompatible',
469
- error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; schedules.create is unavailable.',
469
+ error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; automation.schedules.create is unavailable.',
470
470
  route: AUTONOMY_SCHEDULE_ROUTE,
471
471
  baseUrl: connection.baseUrl,
472
472
  };
@@ -0,0 +1,324 @@
1
+ /**
2
+ * calendar-oauth-service.ts — the agent's bridge to the SDK calendar OAuth connector.
3
+ *
4
+ * It wires the SDK CalendarConnector with the REAL runtime adapters the SDK module
5
+ * deliberately leaves injected (so the SDK stays pure and testable): the runtime
6
+ * fetch, and a loopback redirect listener built on the SDK's createOAuthLocalListener.
7
+ * Tokens persist through the Agent secret manager. Client id/secret OVERRIDES are
8
+ * read from config (id) + the secret store (secret); the default is the bundled
9
+ * project client id shipped in the SDK profile.
10
+ *
11
+ * Every connect returns an honest STAGED result ({ ok, stage, error }) — config,
12
+ * authorize, or token — the same honesty shape the email connect wizard uses. The
13
+ * connector is injectable so tests drive the whole flow against fakes with no real
14
+ * network, port, or browser.
15
+ */
16
+
17
+ import {
18
+ CalendarConnector,
19
+ fetchAdapter,
20
+ providerProfile,
21
+ resolveClientConfig,
22
+ type CalendarProviderId,
23
+ type ConnectedAccount,
24
+ type ConnectionState,
25
+ type DeviceCodeFlowStart,
26
+ type EventWindow,
27
+ type LoopbackListenerFactory,
28
+ type MergedCalendarEvent,
29
+ type OAuthClientOverrides,
30
+ type ResolvedClientConfig,
31
+ } from '@pellux/goodvibes-sdk/platform/calendar';
32
+ import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
33
+ import type { ConfigKey } from '../../config/index.ts';
34
+
35
+ /** The config keys the connector reads. clientId is not a secret (RFC 8252); the
36
+ * secret ref is secret-backed and listed in SECRET_CONFIG_KEYS. */
37
+ export const CALENDAR_OAUTH_CLIENT_ID_KEYS: Record<CalendarProviderId, string> = {
38
+ google: 'calendar.google.clientId',
39
+ microsoft: 'calendar.microsoft.clientId',
40
+ };
41
+ export const CALENDAR_OAUTH_CLIENT_SECRET_KEYS: Record<CalendarProviderId, string> = {
42
+ google: 'calendar.google.clientSecretRef',
43
+ microsoft: 'calendar.microsoft.clientSecretRef',
44
+ };
45
+
46
+ /** The narrow config-manager slice this service reads. */
47
+ export interface CalendarConfigReader {
48
+ get(key: string): unknown;
49
+ }
50
+
51
+ /** The narrow secret slice used for token storage + reading a stored client secret. */
52
+ export interface CalendarSecretSlice {
53
+ get(key: string): Promise<string | null>;
54
+ set(key: string, value: string): Promise<void>;
55
+ delete(key: string): Promise<void>;
56
+ }
57
+
58
+ /** Default (empty) calendar config section, seeded so get()/setDynamic() resolve the
59
+ * nested path. Mirrors ensureEmailConfigDefaults — the sanctioned app-layer pattern
60
+ * for a category absent from the SDK config schema. */
61
+ const CALENDAR_CONFIG_DEFAULTS = {
62
+ google: { clientId: '', clientSecretRef: '' },
63
+ microsoft: { clientId: '', clientSecretRef: '' },
64
+ };
65
+
66
+ /** Seed the calendar config section on the real ConfigManager if absent. */
67
+ export function ensureCalendarConfigDefaults(configManager: object): void {
68
+ const cm = configManager as unknown as { config?: Record<string, unknown> };
69
+ if (cm.config && !('calendar' in cm.config)) {
70
+ cm.config['calendar'] = structuredClone(CALENDAR_CONFIG_DEFAULTS);
71
+ }
72
+ }
73
+
74
+ export type ConnectStage = 'config' | 'authorize' | 'token';
75
+
76
+ export interface ConnectResult {
77
+ readonly ok: boolean;
78
+ readonly stage?: ConnectStage;
79
+ readonly error?: string;
80
+ readonly account?: ConnectedAccount;
81
+ readonly transport?: 'loopback' | 'device-code';
82
+ }
83
+
84
+ export interface AccountStatus {
85
+ readonly account: ConnectedAccount;
86
+ readonly state: ConnectionState;
87
+ }
88
+
89
+ /** The real loopback factory — binds 127.0.0.1 and waits for the browser callback. */
90
+ const realLoopbackFactory: LoopbackListenerFactory = async ({ expectedState, host, port, timeoutMs }) => {
91
+ const listener = await createOAuthLocalListener({
92
+ expectedState,
93
+ ...(host ? { host } : {}),
94
+ ...(typeof port === 'number' ? { port } : {}),
95
+ ...(typeof timeoutMs === 'number' ? { timeoutMs } : {}),
96
+ });
97
+ return {
98
+ redirectUri: listener.redirectUri,
99
+ async waitForCode() {
100
+ const { code } = await listener.waitForCode();
101
+ return { code, state: expectedState };
102
+ },
103
+ close() {
104
+ listener.close();
105
+ },
106
+ };
107
+ };
108
+
109
+ export interface CalendarOAuthServiceOptions {
110
+ readonly config: CalendarConfigReader;
111
+ readonly secrets: CalendarSecretSlice;
112
+ /** Injected for tests; production builds a real connector over runtime fetch + loopback. */
113
+ readonly connector?: CalendarConnector;
114
+ /** Injected fetch for tests; ignored when a connector is injected. */
115
+ readonly fetchImpl?: Parameters<typeof fetchAdapter>[0];
116
+ /** Injected loopback factory for tests; ignored when a connector is injected. */
117
+ readonly listenerFactory?: LoopbackListenerFactory;
118
+ }
119
+
120
+ export class CalendarOAuthService {
121
+ private readonly config: CalendarConfigReader;
122
+ private readonly secrets: CalendarSecretSlice;
123
+ private readonly connector: CalendarConnector;
124
+
125
+ constructor(options: CalendarOAuthServiceOptions) {
126
+ this.config = options.config;
127
+ this.secrets = options.secrets;
128
+ this.connector = options.connector ?? new CalendarConnector({
129
+ secrets: options.secrets,
130
+ fetchImpl: options.fetchImpl ? fetchAdapter(options.fetchImpl) : fetchAdapter(),
131
+ listenerFactory: options.listenerFactory ?? realLoopbackFactory,
132
+ });
133
+ }
134
+
135
+ /** Resolve the client config (bundled default or config/secret-store overrides). */
136
+ async resolveConfig(provider: CalendarProviderId): Promise<ResolvedClientConfig> {
137
+ const overrides = await this.resolveOverrides(provider);
138
+ return resolveClientConfig(providerProfile(provider), overrides);
139
+ }
140
+
141
+ /** Read client-id (config) + client-secret (secret store) overrides, if any. */
142
+ async resolveOverrides(provider: CalendarProviderId): Promise<OAuthClientOverrides> {
143
+ const clientId = this.readString(CALENDAR_OAUTH_CLIENT_ID_KEYS[provider]);
144
+ const clientSecret = await this.resolveSecretBacked(CALENDAR_OAUTH_CLIENT_SECRET_KEYS[provider]);
145
+ const overrides: OAuthClientOverrides = {
146
+ ...(clientId ? { clientId } : {}),
147
+ ...(clientSecret ? { clientSecret } : {}),
148
+ };
149
+ return overrides;
150
+ }
151
+
152
+ /** Connect via the loopback authorization-code flow. `openUrl` is best-effort. */
153
+ async connectLoopback(
154
+ provider: CalendarProviderId,
155
+ hooks: { openUrl?: (url: string) => void } = {},
156
+ ): Promise<ConnectResult> {
157
+ const config = await this.resolveConfig(provider);
158
+ const guard = this.guardConfigured(config);
159
+ if (guard) return guard;
160
+ let begun;
161
+ try {
162
+ begun = await this.connector.beginConnectAuthCode(config);
163
+ } catch (error) {
164
+ return { ok: false, stage: 'authorize', error: `Could not start the sign-in flow: ${describe(error)}` };
165
+ }
166
+ hooks.openUrl?.(begun.start.authorizationUrl);
167
+ let code: string;
168
+ try {
169
+ code = (await begun.waiter.waitForCode()).code;
170
+ } catch (error) {
171
+ begun.waiter.close();
172
+ return { ok: false, stage: 'authorize', error: `The browser sign-in did not complete: ${describe(error)}` };
173
+ }
174
+ try {
175
+ const account = await this.connector.completeConnectAuthCode(config, {
176
+ code,
177
+ verifier: begun.start.verifier,
178
+ redirectUri: begun.start.redirectUri,
179
+ });
180
+ return { ok: true, account, transport: 'loopback' };
181
+ } catch (error) {
182
+ return { ok: false, stage: 'token', error: `The provider rejected the sign-in: ${describe(error)}` };
183
+ } finally {
184
+ begun.waiter.close();
185
+ }
186
+ }
187
+
188
+ /** Connect via the device-code flow (headless fallback). */
189
+ async connectDeviceCode(
190
+ provider: CalendarProviderId,
191
+ hooks: { onDeviceCode?: (start: DeviceCodeFlowStart) => void } = {},
192
+ ): Promise<ConnectResult> {
193
+ const config = await this.resolveConfig(provider);
194
+ const guard = this.guardConfigured(config);
195
+ if (guard) return guard;
196
+ let start: DeviceCodeFlowStart;
197
+ try {
198
+ start = await this.connector.beginConnectDeviceCode(config);
199
+ } catch (error) {
200
+ return { ok: false, stage: 'authorize', error: `Could not start device sign-in: ${describe(error)}` };
201
+ }
202
+ hooks.onDeviceCode?.(start);
203
+ try {
204
+ const account = await this.connector.completeConnectDeviceCode(config, start);
205
+ return { ok: true, account, transport: 'device-code' };
206
+ } catch (error) {
207
+ return { ok: false, stage: 'token', error: `Device sign-in was not approved: ${describe(error)}` };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Auto-select: try the loopback flow; if the listener cannot bind (headless), fall
213
+ * back to device-code. `--device` callers use connectDeviceCode directly.
214
+ */
215
+ async connectAuto(
216
+ provider: CalendarProviderId,
217
+ hooks: { openUrl?: (url: string) => void; onDeviceCode?: (start: DeviceCodeFlowStart) => void } = {},
218
+ ): Promise<ConnectResult> {
219
+ const config = await this.resolveConfig(provider);
220
+ const guard = this.guardConfigured(config);
221
+ if (guard) return guard;
222
+ try {
223
+ // Probe: beginning the loopback flow binds the port; a bind failure throws here.
224
+ const begun = await this.connector.beginConnectAuthCode(config);
225
+ hooks.openUrl?.(begun.start.authorizationUrl);
226
+ let code: string;
227
+ try {
228
+ code = (await begun.waiter.waitForCode()).code;
229
+ } catch (error) {
230
+ begun.waiter.close();
231
+ return { ok: false, stage: 'authorize', error: `The browser sign-in did not complete: ${describe(error)}` };
232
+ }
233
+ try {
234
+ const account = await this.connector.completeConnectAuthCode(config, {
235
+ code,
236
+ verifier: begun.start.verifier,
237
+ redirectUri: begun.start.redirectUri,
238
+ });
239
+ return { ok: true, account, transport: 'loopback' };
240
+ } finally {
241
+ begun.waiter.close();
242
+ }
243
+ } catch {
244
+ // No loopback available — fall back to the headless device-code flow.
245
+ return this.connectDeviceCode(provider, hooks);
246
+ }
247
+ }
248
+
249
+ /** Disconnect a provider: revoke where possible + clear stored tokens. */
250
+ async disconnect(provider: CalendarProviderId): Promise<{ readonly revokedRemotely: boolean }> {
251
+ const overrides = await this.resolveOverrides(provider);
252
+ return this.connector.disconnect(provider, overrides);
253
+ }
254
+
255
+ /** List connected accounts with their honest connection state. */
256
+ async listAccounts(): Promise<AccountStatus[]> {
257
+ const accounts = await this.connector.listAccounts();
258
+ const out: AccountStatus[] = [];
259
+ for (const account of accounts) {
260
+ out.push({ account, state: await this.connector.connectionState(account.provider) });
261
+ }
262
+ return out;
263
+ }
264
+
265
+ /** Events for one already-resolved provider config over a window (source-labeled). */
266
+ async listEventsForProvider(config: ResolvedClientConfig, window: EventWindow): Promise<MergedCalendarEvent[]> {
267
+ return this.connector.listEvents(config, window);
268
+ }
269
+
270
+ /** Merged, source-labeled events across every connected provider in a window. */
271
+ async listEvents(window: EventWindow): Promise<MergedCalendarEvent[]> {
272
+ const accounts = await this.connector.listAccounts();
273
+ const out: MergedCalendarEvent[] = [];
274
+ for (const account of accounts) {
275
+ const config = await this.resolveConfig(account.provider);
276
+ const events = await this.connector.listEvents(config, window);
277
+ out.push(...events);
278
+ }
279
+ return out;
280
+ }
281
+
282
+ // --- internals ------------------------------------------------------------
283
+
284
+ private guardConfigured(config: ResolvedClientConfig): ConnectResult | null {
285
+ if (!config.isPlaceholder) return null;
286
+ const label = config.provider === 'google' ? 'Google Calendar' : 'Microsoft Outlook';
287
+ const cardLabel = config.provider === 'google' ? 'Connect Google Calendar (advanced)' : 'Connect Microsoft Outlook (advanced)';
288
+ return {
289
+ ok: false,
290
+ stage: 'config',
291
+ error:
292
+ `This build ships no ${label} client id — connecting needs your own. ` +
293
+ `Run /calendar connect for the setup steps, or open "${cardLabel}" under /agent personal-ops and paste your client id in.`,
294
+ };
295
+ }
296
+
297
+ private readString(key: string): string | undefined {
298
+ // get() throws when the config section does not exist yet (nothing has seeded
299
+ // 'calendar' via the advanced card). Treat that as "unset" so the bundled default
300
+ // is used rather than surfacing a schema error.
301
+ let value: unknown;
302
+ try {
303
+ value = this.config.get(key);
304
+ } catch {
305
+ return undefined;
306
+ }
307
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
308
+ }
309
+
310
+ /** Read a secret-backed config value: resolve a goodvibes:// ref to its secret. */
311
+ private async resolveSecretBacked(configKey: string): Promise<string | undefined> {
312
+ const ref = this.readString(configKey);
313
+ if (!ref) return undefined;
314
+ const prefix = 'goodvibes://secrets/goodvibes/';
315
+ if (!ref.startsWith(prefix)) return ref; // a literal value (unusual, but honored)
316
+ const secretKey = decodeURIComponent(ref.slice(prefix.length));
317
+ const value = await this.secrets.get(secretKey);
318
+ return value ?? undefined;
319
+ }
320
+ }
321
+
322
+ function describe(error: unknown): string {
323
+ return error instanceof Error ? error.message : String(error);
324
+ }