@pellux/goodvibes-tui 1.13.1 → 1.14.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 (158) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -6
  3. package/bin/goodvibes +6 -0
  4. package/bin/goodvibes-daemon +6 -0
  5. package/bin/launcher-support.js +34 -1
  6. package/docs/foundation-artifacts/operator-contract.json +8076 -2047
  7. package/package.json +15 -11
  8. package/scripts/postinstall.js +73 -48
  9. package/src/cli/bundle-command.ts +5 -5
  10. package/src/cli/completion.ts +1 -1
  11. package/src/cli/completions/generate.ts +15 -3
  12. package/src/cli/doctor.ts +385 -0
  13. package/src/cli/entrypoint.ts +72 -1
  14. package/src/cli/help.ts +29 -9
  15. package/src/cli/hooks-command.ts +74 -0
  16. package/src/cli/hooks-report.ts +167 -0
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/management.ts +13 -1
  19. package/src/cli/parser.ts +10 -2
  20. package/src/cli/plugin-bundles-command.ts +228 -0
  21. package/src/cli/plugin-command.ts +179 -0
  22. package/src/cli/service-posture.ts +10 -0
  23. package/src/cli/status.ts +275 -2
  24. package/src/cli/tui-startup.ts +119 -1
  25. package/src/cli/types.ts +3 -1
  26. package/src/config/index.ts +16 -0
  27. package/src/config/tui-extension-settings.ts +169 -0
  28. package/src/core/approval-alert.ts +15 -2
  29. package/src/core/compaction-receipt.ts +127 -0
  30. package/src/core/model-routing-chip.ts +82 -0
  31. package/src/core/permission-mode.ts +89 -0
  32. package/src/core/rewind-receipt.ts +83 -0
  33. package/src/core/rewind-turn-anchors.ts +160 -0
  34. package/src/core/scriptable-statusline.ts +141 -0
  35. package/src/core/session-auto-titler.ts +110 -0
  36. package/src/core/stream-event-wiring.ts +20 -0
  37. package/src/core/system-message-router.ts +1 -1
  38. package/src/core/terminal-notifier.ts +102 -0
  39. package/src/core/turn-event-wiring.ts +80 -1
  40. package/src/input/command-reference.ts +96 -0
  41. package/src/input/command-registry.ts +25 -0
  42. package/src/input/commands/channel-pairing.ts +245 -0
  43. package/src/input/commands/channel-runtime.ts +152 -6
  44. package/src/input/commands/checkin-runtime.ts +89 -0
  45. package/src/input/commands/checkpoint-runtime.ts +7 -179
  46. package/src/input/commands/ci-runtime.ts +207 -0
  47. package/src/input/commands/cost-attribution-format.ts +52 -0
  48. package/src/input/commands/cost-runtime.ts +44 -4
  49. package/src/input/commands/editor-runtime.ts +22 -0
  50. package/src/input/commands/flags-runtime.ts +328 -0
  51. package/src/input/commands/health-metrics-format.ts +67 -0
  52. package/src/input/commands/health-runtime.ts +55 -1
  53. package/src/input/commands/memory.ts +23 -0
  54. package/src/input/commands/operator-rpc.ts +95 -0
  55. package/src/input/commands/palette-runtime.ts +22 -0
  56. package/src/input/commands/permissions-provenance.ts +264 -0
  57. package/src/input/commands/permissions-runtime.ts +21 -0
  58. package/src/input/commands/planning-runtime.ts +28 -1
  59. package/src/input/commands/platform-access-runtime.ts +2 -97
  60. package/src/input/commands/principals-runtime.ts +212 -0
  61. package/src/input/commands/product-runtime.ts +43 -3
  62. package/src/input/commands/recall-files-config.ts +54 -0
  63. package/src/input/commands/recall-files-git.ts +57 -0
  64. package/src/input/commands/recall-files.ts +185 -0
  65. package/src/input/commands/recall-query.ts +8 -3
  66. package/src/input/commands/recall-review.ts +2 -2
  67. package/src/input/commands/recall-shared.ts +13 -0
  68. package/src/input/commands/relay-runtime.ts +86 -0
  69. package/src/input/commands/review-runtime.ts +160 -0
  70. package/src/input/commands/rewind-runtime.ts +362 -0
  71. package/src/input/commands/schedule-nl.ts +147 -0
  72. package/src/input/commands/schedule-runtime.ts +29 -10
  73. package/src/input/commands/secret-runtime.ts +55 -0
  74. package/src/input/commands/session-content.ts +19 -8
  75. package/src/input/commands/session-workflow.ts +10 -0
  76. package/src/input/commands/update-runtime.ts +363 -0
  77. package/src/input/commands/workstream-attempt-dependents.ts +103 -0
  78. package/src/input/commands/workstream-attempts.ts +244 -0
  79. package/src/input/commands/workstream-runtime.ts +164 -26
  80. package/src/input/commands/worktree-runtime.ts +56 -2
  81. package/src/input/commands.ts +135 -80
  82. package/src/input/composer-capture.ts +67 -0
  83. package/src/input/composer-editor.ts +134 -0
  84. package/src/input/concealed-input.ts +94 -0
  85. package/src/input/exec-env-scrub-config.ts +84 -0
  86. package/src/input/feature-unit-layout.ts +142 -0
  87. package/src/input/feed-context-factory.ts +2 -0
  88. package/src/input/handler-feed-routes.ts +17 -0
  89. package/src/input/handler-feed.ts +3 -0
  90. package/src/input/handler-modal-routes.ts +5 -2
  91. package/src/input/handler-shortcuts.ts +37 -0
  92. package/src/input/handler.ts +24 -2
  93. package/src/input/keybindings.ts +11 -2
  94. package/src/input/onboarding/onboarding-feature-units.ts +536 -0
  95. package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
  96. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
  97. package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
  98. package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
  99. package/src/input/relay-settings-descriptions.ts +35 -0
  100. package/src/input/sandbox-exec-config.ts +107 -0
  101. package/src/input/settings-modal-adjustment.ts +4 -7
  102. package/src/input/settings-modal-data.ts +178 -8
  103. package/src/input/settings-modal-mutations.ts +41 -13
  104. package/src/input/settings-modal-types.ts +42 -5
  105. package/src/input/settings-modal.ts +67 -15
  106. package/src/input/submission-router.ts +4 -1
  107. package/src/input/worktree-setup-config.ts +93 -0
  108. package/src/main.ts +43 -43
  109. package/src/panels/builtin/development.ts +14 -0
  110. package/src/panels/diff-review-model.ts +189 -0
  111. package/src/panels/diff-review-panel.ts +395 -0
  112. package/src/panels/fleet-panel-format.ts +114 -2
  113. package/src/panels/fleet-panel.ts +65 -78
  114. package/src/panels/fleet-read-model.ts +91 -11
  115. package/src/panels/fleet-stop.ts +19 -4
  116. package/src/panels/modals/memory-modal.ts +11 -2
  117. package/src/permissions/hunk-selection.ts +86 -5
  118. package/src/permissions/prompt.ts +108 -7
  119. package/src/permissions/sandbox-exec-gate.ts +162 -0
  120. package/src/renderer/settings-modal-helpers.ts +21 -1
  121. package/src/renderer/settings-modal.ts +70 -44
  122. package/src/renderer/shell-surface.ts +23 -1
  123. package/src/renderer/system-message.ts +4 -0
  124. package/src/renderer/tool-result-summary.ts +95 -5
  125. package/src/renderer/ui-factory.ts +22 -0
  126. package/src/runtime/bootstrap-command-context.ts +15 -1
  127. package/src/runtime/bootstrap-command-parts.ts +14 -2
  128. package/src/runtime/bootstrap-core.ts +38 -38
  129. package/src/runtime/bootstrap-notifier-sync.ts +41 -0
  130. package/src/runtime/bootstrap-shell.ts +8 -0
  131. package/src/runtime/bootstrap.ts +21 -23
  132. package/src/runtime/context-accounting-source.ts +103 -0
  133. package/src/runtime/conversation-rewind-port.ts +130 -0
  134. package/src/runtime/fleet-needs-input-push.ts +70 -0
  135. package/src/runtime/install-self-check-startup.ts +36 -0
  136. package/src/runtime/install-self-check.ts +149 -0
  137. package/src/runtime/onboarding/fast-path.ts +93 -0
  138. package/src/runtime/process-lifecycle.ts +32 -24
  139. package/src/runtime/relay-reachability-bridge.ts +53 -0
  140. package/src/runtime/release-artifacts.ts +79 -0
  141. package/src/runtime/services.ts +33 -33
  142. package/src/runtime/trust/workspace-registration.ts +184 -0
  143. package/src/runtime/trust/workspace-trust.ts +202 -0
  144. package/src/runtime/ui-services.ts +2 -1
  145. package/src/runtime/update-check.ts +113 -0
  146. package/src/runtime/workspace-checkpointing.ts +32 -0
  147. package/src/runtime/workstream-attempts-validation.ts +77 -0
  148. package/src/runtime/workstream-draft-edits.ts +95 -0
  149. package/src/runtime/workstream-draft-store.ts +101 -0
  150. package/src/runtime/workstream-draft-types.ts +42 -0
  151. package/src/runtime/workstream-services.ts +70 -39
  152. package/src/shell/blocking-input.ts +29 -0
  153. package/src/shell/ui-openers.ts +65 -0
  154. package/src/verification/verification-ledger.ts +56 -2
  155. package/src/version.ts +1 -1
  156. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
  157. package/src/runtime/gateway-verbs.ts +0 -47
  158. package/src/runtime/render-scheduler.ts +0 -90
@@ -1,13 +1,42 @@
1
+ // ---------------------------------------------------------------------------
2
+ // onboarding-wizard-external-surfaces.ts
3
+ //
4
+ // The channel setup FIELD SET is owned by the SDK: getBuiltinSetupSchema is the
5
+ // single source of truth for which fields each surface has, their config keys,
6
+ // and their input kind. This module no longer re-authors that schema. Instead it
7
+ // builds the onboarding ExternalSurfaceSpec[] by reading the SDK schema and
8
+ // layering a clearly-separated, genuinely-TUI-only PRESENTATION overlay on top:
9
+ // • onboarding field-id namespace (`external-services.<surface>.<field>`),
10
+ // • curated per-field label / hint / placeholder wording,
11
+ // • the per-surface auto-start enablement model (see the auto-start helpers),
12
+ // • numeric UI bounds (min/max) the SDK schema does not carry,
13
+ // • radio option copy (with hints) for select fields.
14
+ // Everything else — a field's existence, its config key, its input kind, and its
15
+ // default value — is derived from the SDK schema + DEFAULT_CONFIG, so the two can
16
+ // never silently drift. A field the overlay names that the SDK no longer defines
17
+ // is a loud build error; a field the SDK adds that the overlay has not curated is
18
+ // appended automatically with SDK-derived presentation.
19
+ //
20
+ // Two fields are genuine TUI-only extras the onboarding exposes but the SDK setup
21
+ // schema intentionally omits (surfaces.ntfy.defaultPriority, surfaces.webhook.
22
+ // timeoutMs). They are declared with `tuiOnly` and are not matched to the SDK.
23
+ // ---------------------------------------------------------------------------
24
+
1
25
  import {
2
26
  GOODVIBES_NTFY_AGENT_TOPIC,
3
27
  GOODVIBES_NTFY_CHAT_TOPIC,
4
28
  GOODVIBES_NTFY_REMOTE_TOPIC,
5
29
  resolveGoodVibesNtfyTopics,
6
30
  } from '@pellux/goodvibes-sdk/platform/integrations';
31
+ import { getBuiltinSetupSchema } from '@pellux/goodvibes-sdk/platform/channels';
32
+ import type {
33
+ ChannelSetupFieldDescriptor,
34
+ ChannelSetupFieldKind,
35
+ ChannelSurface,
36
+ } from '@pellux/goodvibes-sdk/platform/channels';
7
37
  import { DEFAULT_CONFIG, type ConfigKey } from '../../config/index.ts';
8
38
  import type { OnboardingSnapshotState } from '../../runtime/onboarding/index.ts';
9
39
  import { TELEGRAM_MODE_OPTIONS, WHATSAPP_PROVIDER_OPTIONS } from './onboarding-wizard-constants.ts';
10
- import { HOME_ASSISTANT_SURFACE_SPEC, WEBHOOK_SURFACE_SPEC } from './onboarding-wizard-external-surface-extra-specs.ts';
11
40
  import type { ExternalSurfaceSetupFieldSpec, ExternalSurfaceSpec, OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
12
41
 
13
42
  export type { ExternalSurfaceSetupFieldSpec, ExternalSurfaceSpec };
@@ -17,719 +46,385 @@ function normalizeConfigValue(value: unknown): string {
17
46
  return String(value).trim();
18
47
  }
19
48
 
20
- function getDefaultConfigValue(key: ConfigKey): unknown {
49
+ function readConfigPath(config: unknown, key: ConfigKey): unknown {
21
50
  return key.split('.').reduce<unknown>((cursor, part) => (
22
51
  typeof cursor === 'object' && cursor !== null && part in cursor
23
52
  ? (cursor as Record<string, unknown>)[part]
24
53
  : undefined
25
- ), DEFAULT_CONFIG);
54
+ ), config);
26
55
  }
27
56
 
28
- export function getExternalSurfaceAutoStartFieldId(surface: ExternalSurfaceSpec): string {
29
- return `${surface.enabledFieldId}.auto-start`;
57
+ function getDefaultConfigValue(key: ConfigKey): unknown {
58
+ return readConfigPath(DEFAULT_CONFIG, key);
30
59
  }
31
60
 
32
- export function getExternalSurfaceAutoStartDefaultValue(
33
- surface: ExternalSurfaceSpec,
34
- snapshot: OnboardingSnapshotState | null,
35
- ): 'yes' | 'no' {
36
- return surface.defaultEnabled(snapshot) ? 'yes' : 'no';
61
+ /** camelCase / kebab-in → kebab-case (for onboarding field ids). */
62
+ function kebab(value: string): string {
63
+ return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
37
64
  }
38
65
 
39
- export function isExternalSurfaceSelectedByDefault(
40
- surface: ExternalSurfaceSpec,
41
- snapshot: OnboardingSnapshotState | null,
42
- ): boolean {
43
- if (surface.defaultEnabled(snapshot)) return true;
44
- if (!snapshot) return false;
66
+ /**
67
+ * The default value shown for a field. Derived, not hand-authored: the current
68
+ * snapshot value if present, otherwise the config default (DEFAULT_CONFIG) — which
69
+ * is exactly what the previous per-field literals returned. Numbers stringify.
70
+ */
71
+ function readFieldDefault(configKey: ConfigKey, snapshot: OnboardingSnapshotState | null): string {
72
+ const fromSnapshot = snapshot ? readConfigPath(snapshot.config, configKey) : undefined;
73
+ const chosen = fromSnapshot ?? getDefaultConfigValue(configKey);
74
+ return chosen === null || chosen === undefined ? '' : String(chosen);
75
+ }
45
76
 
46
- return surface.fields.some((field) => {
47
- const current = normalizeConfigValue(field.defaultValue(snapshot));
48
- const defaultValue = normalizeConfigValue(getDefaultConfigValue(field.configKey));
49
- return current.length > 0 && current !== defaultValue;
50
- });
77
+ /** Map an SDK setup-field kind to an onboarding input kind. */
78
+ function mapSdkKind(kind: ChannelSetupFieldKind): 'text' | 'masked' | 'radio' {
79
+ if (kind === 'secret') return 'masked';
80
+ if (kind === 'select') return 'radio';
81
+ return 'text'; // string | url | number | boolean → text (boolean=enabled is handled separately)
51
82
  }
52
83
 
53
- export function getNtfyProtocolTopicLines(snapshot: OnboardingSnapshotState | null): readonly string[] {
54
- const topics = resolveGoodVibesNtfyTopics({
55
- chatTopic: snapshot?.config.surfaces.ntfy.chatTopic,
56
- agentTopic: snapshot?.config.surfaces.ntfy.agentTopic,
57
- remoteTopic: snapshot?.config.surfaces.ntfy.remoteTopic,
58
- });
59
- return [
60
- `Chat topic: ${topics.chatTopic}`,
61
- `Agent topic: ${topics.agentTopic}`,
62
- `Daemon-only remote topic: ${topics.remoteTopic}`,
63
- ];
84
+ // ---- the TUI-only presentation overlay -----------------------------------
85
+
86
+ interface SurfaceFieldOverlay {
87
+ /** Join key to the SDK schema (and the config path). */
88
+ readonly configKey: ConfigKey;
89
+ readonly label: string;
90
+ readonly hint: string;
91
+ readonly placeholder: string;
92
+ /** Radio option copy (with hints) for a select field. */
93
+ readonly options?: readonly OnboardingWizardRadioOption[];
94
+ /** Numeric UI bounds the SDK schema does not carry. */
95
+ readonly defaultNumber?: number;
96
+ readonly min?: number;
97
+ readonly max?: number;
98
+ /** Set only for a field the SDK setup schema does not define (TUI-only extra). */
99
+ readonly tuiOnly?: { readonly kind: 'text' | 'masked'; readonly valueType?: 'number' };
100
+ }
101
+
102
+ interface SurfaceOverlay {
103
+ /** ExternalSurfaceSpec.id and the `surfaces.<id>.*` config segment (camelCase for google chat). */
104
+ readonly onboardingId: string;
105
+ /** The SDK ChannelSurface whose setup schema owns this surface's field set. */
106
+ readonly sdkSurface: ChannelSurface;
107
+ readonly label: string;
108
+ readonly hint: string;
109
+ /** Fields in onboarding display order; each references an SDK field by configKey (or a tuiOnly extra). */
110
+ readonly fields: readonly SurfaceFieldOverlay[];
64
111
  }
65
112
 
66
- export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = [
113
+ const SURFACE_OVERLAYS: readonly SurfaceOverlay[] = [
67
114
  {
68
- id: 'slack',
69
- enabledFieldId: 'external-services.slack',
70
- enabledConfigKey: 'surfaces.slack.enabled',
115
+ onboardingId: 'slack',
116
+ sdkSurface: 'slack',
71
117
  label: 'Slack surface',
72
118
  hint: 'Enable Slack messages, slash-command/event handling, and thread-aware replies.',
73
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.slack.enabled ?? false,
74
119
  fields: [
75
- {
76
- id: 'external-services.slack.bot-token',
77
- configKey: 'surfaces.slack.botToken',
78
- kind: 'masked',
79
- label: 'Slack bot token',
80
- hint: 'Token used by the Slack bot integration.',
81
- placeholder: 'xoxb-...',
82
- defaultValue: (snapshot) => snapshot?.config.surfaces.slack.botToken ?? '',
83
- },
84
- {
85
- id: 'external-services.slack.app-token',
86
- configKey: 'surfaces.slack.appToken',
87
- kind: 'masked',
88
- label: 'Slack app token',
89
- hint: 'Optional app-level token for Socket Mode deployments.',
90
- placeholder: 'xapp-...',
91
- defaultValue: (snapshot) => snapshot?.config.surfaces.slack.appToken ?? '',
92
- },
93
- {
94
- id: 'external-services.slack.signing-secret',
95
- configKey: 'surfaces.slack.signingSecret',
96
- kind: 'masked',
97
- label: 'Slack signing secret',
98
- hint: 'Signing secret used to verify inbound Slack events.',
99
- placeholder: 'signing secret',
100
- defaultValue: (snapshot) => snapshot?.config.surfaces.slack.signingSecret ?? '',
101
- },
102
- {
103
- id: 'external-services.slack.workspace-id',
104
- configKey: 'surfaces.slack.workspaceId',
105
- kind: 'text',
106
- label: 'Slack workspace ID',
107
- hint: 'Workspace/team ID used to scope Slack routing.',
108
- placeholder: 'T...',
109
- defaultValue: (snapshot) => snapshot?.config.surfaces.slack.workspaceId ?? '',
110
- },
111
- {
112
- id: 'external-services.slack.default-channel',
113
- configKey: 'surfaces.slack.defaultChannel',
114
- kind: 'text',
115
- label: 'Slack default channel',
116
- hint: 'Fallback Slack channel for messages that do not specify a target.',
117
- placeholder: '#goodvibes',
118
- defaultValue: (snapshot) => snapshot?.config.surfaces.slack.defaultChannel ?? '',
119
- },
120
+ { configKey: 'surfaces.slack.botToken', label: 'Slack bot token', hint: 'Token used by the Slack bot integration.', placeholder: 'xoxb-...' },
121
+ { configKey: 'surfaces.slack.appToken', label: 'Slack app token', hint: 'Optional app-level token for Socket Mode deployments.', placeholder: 'xapp-...' },
122
+ { configKey: 'surfaces.slack.signingSecret', label: 'Slack signing secret', hint: 'Signing secret used to verify inbound Slack events.', placeholder: 'signing secret' },
123
+ { configKey: 'surfaces.slack.workspaceId', label: 'Slack workspace ID', hint: 'Workspace/team ID used to scope Slack routing.', placeholder: 'T...' },
124
+ { configKey: 'surfaces.slack.defaultChannel', label: 'Slack default channel', hint: 'Fallback Slack channel for messages that do not specify a target.', placeholder: '#goodvibes' },
120
125
  ],
121
126
  },
122
127
  {
123
- id: 'discord',
124
- enabledFieldId: 'external-services.discord',
125
- enabledConfigKey: 'surfaces.discord.enabled',
128
+ onboardingId: 'discord',
129
+ sdkSurface: 'discord',
126
130
  label: 'Discord surface',
127
131
  hint: 'Enable Discord command/event handling and channel replies.',
128
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.discord.enabled ?? false,
129
132
  fields: [
130
- {
131
- id: 'external-services.discord.bot-token',
132
- configKey: 'surfaces.discord.botToken',
133
- kind: 'masked',
134
- label: 'Discord bot token',
135
- hint: 'Token used by the Discord bot integration.',
136
- placeholder: 'bot token',
137
- defaultValue: (snapshot) => snapshot?.config.surfaces.discord.botToken ?? '',
138
- },
139
- {
140
- id: 'external-services.discord.public-key',
141
- configKey: 'surfaces.discord.publicKey',
142
- kind: 'masked',
143
- label: 'Discord public key',
144
- hint: 'Application public key for verifying interaction signatures.',
145
- placeholder: 'public key',
146
- defaultValue: (snapshot) => snapshot?.config.surfaces.discord.publicKey ?? '',
147
- },
148
- {
149
- id: 'external-services.discord.application-id',
150
- configKey: 'surfaces.discord.applicationId',
151
- kind: 'text',
152
- label: 'Discord application ID',
153
- hint: 'Application/client ID for the Discord integration.',
154
- placeholder: 'application id',
155
- defaultValue: (snapshot) => snapshot?.config.surfaces.discord.applicationId ?? '',
156
- },
157
- {
158
- id: 'external-services.discord.guild-id',
159
- configKey: 'surfaces.discord.guildId',
160
- kind: 'text',
161
- label: 'Discord guild ID',
162
- hint: 'Optional default server/guild ID.',
163
- placeholder: 'guild id',
164
- defaultValue: (snapshot) => snapshot?.config.surfaces.discord.guildId ?? '',
165
- },
166
- {
167
- id: 'external-services.discord.default-channel-id',
168
- configKey: 'surfaces.discord.defaultChannelId',
169
- kind: 'text',
170
- label: 'Discord default channel ID',
171
- hint: 'Fallback Discord channel for outbound messages.',
172
- placeholder: 'channel id',
173
- defaultValue: (snapshot) => snapshot?.config.surfaces.discord.defaultChannelId ?? '',
174
- },
133
+ { configKey: 'surfaces.discord.botToken', label: 'Discord bot token', hint: 'Token used by the Discord bot integration.', placeholder: 'bot token' },
134
+ { configKey: 'surfaces.discord.publicKey', label: 'Discord public key', hint: 'Application public key for verifying interaction signatures.', placeholder: 'public key' },
135
+ { configKey: 'surfaces.discord.applicationId', label: 'Discord application ID', hint: 'Application/client ID for the Discord integration.', placeholder: 'application id' },
136
+ { configKey: 'surfaces.discord.guildId', label: 'Discord guild ID', hint: 'Optional default server/guild ID.', placeholder: 'guild id' },
137
+ { configKey: 'surfaces.discord.defaultChannelId', label: 'Discord default channel ID', hint: 'Fallback Discord channel for outbound messages.', placeholder: 'channel id' },
175
138
  ],
176
139
  },
177
140
  {
178
- id: 'telegram',
179
- enabledFieldId: 'external-services.telegram',
180
- enabledConfigKey: 'surfaces.telegram.enabled',
141
+ onboardingId: 'telegram',
142
+ sdkSurface: 'telegram',
181
143
  label: 'Telegram surface',
182
144
  hint: 'Enable Telegram bot messaging and event handling.',
183
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.telegram.enabled ?? false,
184
145
  fields: [
185
- {
186
- id: 'external-services.telegram.bot-token',
187
- configKey: 'surfaces.telegram.botToken',
188
- kind: 'masked',
189
- label: 'Telegram bot token',
190
- hint: 'Token issued by BotFather.',
191
- placeholder: 'bot token',
192
- defaultValue: (snapshot) => snapshot?.config.surfaces.telegram.botToken ?? '',
193
- },
194
- {
195
- id: 'external-services.telegram.webhook-secret',
196
- configKey: 'surfaces.telegram.webhookSecret',
197
- kind: 'masked',
198
- label: 'Telegram webhook secret',
199
- hint: 'Secret token used to verify Telegram webhook requests.',
200
- placeholder: 'webhook secret',
201
- defaultValue: (snapshot) => snapshot?.config.surfaces.telegram.webhookSecret ?? '',
202
- },
203
- {
204
- id: 'external-services.telegram.default-chat-id',
205
- configKey: 'surfaces.telegram.defaultChatId',
206
- kind: 'text',
207
- label: 'Telegram default chat ID',
208
- hint: 'Fallback chat ID for outbound Telegram messages.',
209
- placeholder: 'chat id',
210
- defaultValue: (snapshot) => snapshot?.config.surfaces.telegram.defaultChatId ?? '',
211
- },
212
- {
213
- id: 'external-services.telegram.bot-username',
214
- configKey: 'surfaces.telegram.botUsername',
215
- kind: 'text',
216
- label: 'Telegram bot username',
217
- hint: 'Bot username used in visible routing labels.',
218
- placeholder: '@goodvibes_bot',
219
- defaultValue: (snapshot) => snapshot?.config.surfaces.telegram.botUsername ?? '',
220
- },
221
- {
222
- id: 'external-services.telegram.mode',
223
- configKey: 'surfaces.telegram.mode',
224
- kind: 'radio',
225
- label: 'Telegram delivery mode',
226
- hint: 'Choose webhook or polling transport.',
227
- placeholder: 'webhook',
228
- options: TELEGRAM_MODE_OPTIONS,
229
- defaultValue: (snapshot) => snapshot?.config.surfaces.telegram.mode ?? 'webhook',
230
- },
146
+ { configKey: 'surfaces.telegram.botToken', label: 'Telegram bot token', hint: 'Token issued by BotFather.', placeholder: 'bot token' },
147
+ { configKey: 'surfaces.telegram.webhookSecret', label: 'Telegram webhook secret', hint: 'Secret token used to verify Telegram webhook requests.', placeholder: 'webhook secret' },
148
+ { configKey: 'surfaces.telegram.defaultChatId', label: 'Telegram default chat ID', hint: 'Fallback chat ID for outbound Telegram messages.', placeholder: 'chat id' },
149
+ { configKey: 'surfaces.telegram.botUsername', label: 'Telegram bot username', hint: 'Bot username used in visible routing labels.', placeholder: '@goodvibes_bot' },
150
+ { configKey: 'surfaces.telegram.mode', label: 'Telegram delivery mode', hint: 'Choose webhook or polling transport.', placeholder: 'webhook', options: TELEGRAM_MODE_OPTIONS },
231
151
  ],
232
152
  },
233
153
  {
234
- id: 'ntfy',
235
- enabledFieldId: 'external-services.ntfy',
236
- enabledConfigKey: 'surfaces.ntfy.enabled',
154
+ onboardingId: 'ntfy',
155
+ sdkSurface: 'ntfy',
237
156
  label: 'ntfy surface',
238
157
  hint: 'Configure ntfy chat, agent, remote-session, and notification delivery topics.',
239
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.ntfy.enabled ?? false,
240
158
  fields: [
241
- {
242
- id: 'external-services.ntfy.base-url',
243
- configKey: 'surfaces.ntfy.baseUrl',
244
- kind: 'text',
245
- label: 'ntfy base URL',
246
- hint: 'Base URL of the ntfy server.',
247
- placeholder: 'https://ntfy.sh',
248
- defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.baseUrl ?? 'https://ntfy.sh',
249
- },
250
- {
251
- id: 'external-services.ntfy.chat-topic',
252
- configKey: 'surfaces.ntfy.chatTopic',
253
- kind: 'text',
254
- label: 'ntfy chat topic',
255
- hint: 'Messages sent here attach to the active terminal TUI session and reply back to ntfy.',
256
- placeholder: GOODVIBES_NTFY_CHAT_TOPIC,
257
- defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.chatTopic ?? GOODVIBES_NTFY_CHAT_TOPIC,
258
- },
259
- {
260
- id: 'external-services.ntfy.agent-topic',
261
- configKey: 'surfaces.ntfy.agentTopic',
262
- kind: 'text',
263
- label: 'ntfy agent topic',
264
- hint: 'Messages sent here start agent work attached to the active TUI session.',
265
- placeholder: GOODVIBES_NTFY_AGENT_TOPIC,
266
- defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.agentTopic ?? GOODVIBES_NTFY_AGENT_TOPIC,
267
- },
268
- {
269
- id: 'external-services.ntfy.remote-topic',
270
- configKey: 'surfaces.ntfy.remoteTopic',
271
- kind: 'text',
272
- label: 'ntfy daemon-only remote topic',
273
- hint: 'Messages sent here start an ntfy remote session in the daemon and do not appear in the TUI.',
274
- placeholder: GOODVIBES_NTFY_REMOTE_TOPIC,
275
- defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.remoteTopic ?? GOODVIBES_NTFY_REMOTE_TOPIC,
276
- },
277
- {
278
- id: 'external-services.ntfy.topic',
279
- configKey: 'surfaces.ntfy.topic',
280
- kind: 'text',
281
- label: 'ntfy default delivery topic',
282
- hint: 'Optional outbound notification topic. It does not control chat, agent, or daemon-only remote routing.',
283
- placeholder: 'goodvibes',
284
- defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.topic ?? '',
285
- },
286
- {
287
- id: 'external-services.ntfy.token',
288
- configKey: 'surfaces.ntfy.token',
289
- kind: 'masked',
290
- label: 'ntfy token',
291
- hint: 'Optional token for authenticated ntfy servers.',
292
- placeholder: 'empty for anonymous ntfy',
293
- defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.token ?? '',
294
- },
295
- {
296
- id: 'external-services.ntfy.default-priority',
297
- configKey: 'surfaces.ntfy.defaultPriority',
298
- kind: 'text',
299
- valueType: 'number',
300
- label: 'ntfy default priority',
301
- hint: 'Default ntfy priority from 1 to 5.',
302
- placeholder: '3',
303
- defaultNumber: 3,
304
- min: 1,
305
- max: 5,
306
- defaultValue: (snapshot) => String(snapshot?.config.surfaces.ntfy.defaultPriority ?? 3),
307
- },
159
+ { configKey: 'surfaces.ntfy.baseUrl', label: 'ntfy base URL', hint: 'Base URL of the ntfy server.', placeholder: 'https://ntfy.sh' },
160
+ { configKey: 'surfaces.ntfy.chatTopic', label: 'ntfy chat topic', hint: 'Messages sent here attach to the active terminal TUI session and reply back to ntfy.', placeholder: GOODVIBES_NTFY_CHAT_TOPIC },
161
+ { configKey: 'surfaces.ntfy.agentTopic', label: 'ntfy agent topic', hint: 'Messages sent here start agent work attached to the active TUI session.', placeholder: GOODVIBES_NTFY_AGENT_TOPIC },
162
+ { configKey: 'surfaces.ntfy.remoteTopic', label: 'ntfy daemon-only remote topic', hint: 'Messages sent here start an ntfy remote session in the daemon and do not appear in the TUI.', placeholder: GOODVIBES_NTFY_REMOTE_TOPIC },
163
+ { configKey: 'surfaces.ntfy.topic', label: 'ntfy default delivery topic', hint: 'Optional outbound notification topic. It does not control chat, agent, or daemon-only remote routing.', placeholder: 'goodvibes' },
164
+ { configKey: 'surfaces.ntfy.token', label: 'ntfy token', hint: 'Optional token for authenticated ntfy servers.', placeholder: 'empty for anonymous ntfy' },
165
+ { configKey: 'surfaces.ntfy.defaultPriority', label: 'ntfy default priority', hint: 'Default ntfy priority from 1 to 5.', placeholder: '3', defaultNumber: 3, min: 1, max: 5, tuiOnly: { kind: 'text', valueType: 'number' } },
308
166
  ],
309
167
  },
310
- WEBHOOK_SURFACE_SPEC,
311
- HOME_ASSISTANT_SURFACE_SPEC,
312
168
  {
313
- id: 'googleChat',
314
- enabledFieldId: 'external-services.google-chat',
315
- enabledConfigKey: 'surfaces.googleChat.enabled',
169
+ onboardingId: 'webhook',
170
+ sdkSurface: 'webhook',
171
+ label: 'Outbound webhook surface',
172
+ hint: 'Enable outbound webhook delivery targets.',
173
+ fields: [
174
+ { configKey: 'surfaces.webhook.defaultTarget', label: 'Default webhook target', hint: 'Fallback URL used for outbound webhook deliveries.', placeholder: 'https://example.com/goodvibes' },
175
+ { configKey: 'surfaces.webhook.secret', label: 'Webhook signing secret', hint: 'Secret used to sign outbound webhook payloads.', placeholder: 'secret' },
176
+ { configKey: 'surfaces.webhook.timeoutMs', label: 'Webhook timeout ms', hint: 'Request timeout for outbound webhook deliveries.', placeholder: '10000', defaultNumber: 10000, min: 1000, max: 60000, tuiOnly: { kind: 'text', valueType: 'number' } },
177
+ ],
178
+ },
179
+ {
180
+ onboardingId: 'homeassistant',
181
+ sdkSurface: 'homeassistant',
182
+ label: 'Home Assistant surface',
183
+ hint: 'Enable the Home Assistant companion surface, daemon callbacks, and event delivery.',
184
+ fields: [
185
+ { configKey: 'surfaces.homeassistant.instanceUrl', label: 'Home Assistant URL', hint: 'Base URL of the Home Assistant instance.', placeholder: 'http://homeassistant.local:8123' },
186
+ { configKey: 'surfaces.homeassistant.accessToken', label: 'Home Assistant access token', hint: 'Long-lived Home Assistant access token or goodvibes:// secret reference.', placeholder: 'long-lived access token' },
187
+ { configKey: 'surfaces.homeassistant.webhookSecret', label: 'Home Assistant webhook secret', hint: 'Shared secret used to verify inbound Home Assistant callbacks.', placeholder: 'webhook secret' },
188
+ { configKey: 'surfaces.homeassistant.defaultConversationId', label: 'Default conversation ID', hint: 'Default Home Assistant conversation id used for route binding.', placeholder: 'goodvibes' },
189
+ { configKey: 'surfaces.homeassistant.remoteSessionTtlMs', label: 'Remote session idle TTL ms', hint: 'How long an idle Home Assistant remote conversation session remains open before the daemon closes it.', placeholder: '1200000', defaultNumber: 1_200_000, min: 60_000, max: 86_400_000 },
190
+ { configKey: 'surfaces.homeassistant.deviceId', label: 'Home Assistant device ID', hint: 'Stable device identifier exposed by the GoodVibes daemon.', placeholder: 'goodvibes-daemon' },
191
+ { configKey: 'surfaces.homeassistant.deviceName', label: 'Home Assistant device name', hint: 'Display name for the GoodVibes daemon device in Home Assistant.', placeholder: 'GoodVibes Daemon' },
192
+ { configKey: 'surfaces.homeassistant.eventType', label: 'Home Assistant event type', hint: 'Event type used for daemon-to-Home Assistant deliveries.', placeholder: 'goodvibes_message' },
193
+ ],
194
+ },
195
+ {
196
+ onboardingId: 'googleChat',
197
+ sdkSurface: 'google-chat',
316
198
  label: 'Google Chat surface',
317
199
  hint: 'Enable Google Chat webhook and app routing.',
318
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.googleChat.enabled ?? false,
319
200
  fields: [
320
- {
321
- id: 'external-services.google-chat.webhook-url',
322
- configKey: 'surfaces.googleChat.webhookUrl',
323
- kind: 'masked',
324
- label: 'Google Chat webhook URL',
325
- hint: 'Incoming webhook URL for Google Chat space delivery.',
326
- placeholder: 'webhook URL',
327
- defaultValue: (snapshot) => snapshot?.config.surfaces.googleChat.webhookUrl ?? '',
328
- },
329
- {
330
- id: 'external-services.google-chat.verification-token',
331
- configKey: 'surfaces.googleChat.verificationToken',
332
- kind: 'masked',
333
- label: 'Google Chat verification token',
334
- hint: 'Token used to verify inbound Google Chat events.',
335
- placeholder: 'verification token',
336
- defaultValue: (snapshot) => snapshot?.config.surfaces.googleChat.verificationToken ?? '',
337
- },
338
- {
339
- id: 'external-services.google-chat.app-id',
340
- configKey: 'surfaces.googleChat.appId',
341
- kind: 'text',
342
- label: 'Google Chat app ID',
343
- hint: 'Google Chat app identifier.',
344
- placeholder: 'app id',
345
- defaultValue: (snapshot) => snapshot?.config.surfaces.googleChat.appId ?? '',
346
- },
347
- {
348
- id: 'external-services.google-chat.space-id',
349
- configKey: 'surfaces.googleChat.spaceId',
350
- kind: 'text',
351
- label: 'Google Chat space ID',
352
- hint: 'Default Google Chat space for outbound messages.',
353
- placeholder: 'space id',
354
- defaultValue: (snapshot) => snapshot?.config.surfaces.googleChat.spaceId ?? '',
355
- },
201
+ { configKey: 'surfaces.googleChat.webhookUrl', label: 'Google Chat webhook URL', hint: 'Incoming webhook URL for Google Chat space delivery.', placeholder: 'webhook URL' },
202
+ { configKey: 'surfaces.googleChat.verificationToken', label: 'Google Chat verification token', hint: 'Token used to verify inbound Google Chat events.', placeholder: 'verification token' },
203
+ { configKey: 'surfaces.googleChat.appId', label: 'Google Chat app ID', hint: 'Google Chat app identifier.', placeholder: 'app id' },
204
+ { configKey: 'surfaces.googleChat.spaceId', label: 'Google Chat space ID', hint: 'Default Google Chat space for outbound messages.', placeholder: 'space id' },
356
205
  ],
357
206
  },
358
207
  {
359
- id: 'signal',
360
- enabledFieldId: 'external-services.signal',
361
- enabledConfigKey: 'surfaces.signal.enabled',
208
+ onboardingId: 'signal',
209
+ sdkSurface: 'signal',
362
210
  label: 'Signal surface',
363
211
  hint: 'Enable Signal bridge messaging.',
364
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.signal.enabled ?? false,
365
212
  fields: [
366
- {
367
- id: 'external-services.signal.bridge-url',
368
- configKey: 'surfaces.signal.bridgeUrl',
369
- kind: 'text',
370
- label: 'Signal bridge URL',
371
- hint: 'Base URL for the Signal bridge service.',
372
- placeholder: 'https://signal-bridge.local',
373
- defaultValue: (snapshot) => snapshot?.config.surfaces.signal.bridgeUrl ?? '',
374
- },
375
- {
376
- id: 'external-services.signal.account',
377
- configKey: 'surfaces.signal.account',
378
- kind: 'text',
379
- label: 'Signal account',
380
- hint: 'Signal account identifier used by the bridge.',
381
- placeholder: '+15551234567',
382
- defaultValue: (snapshot) => snapshot?.config.surfaces.signal.account ?? '',
383
- },
384
- {
385
- id: 'external-services.signal.token',
386
- configKey: 'surfaces.signal.token',
387
- kind: 'masked',
388
- label: 'Signal bridge token',
389
- hint: 'Authentication token for the Signal bridge.',
390
- placeholder: 'token',
391
- defaultValue: (snapshot) => snapshot?.config.surfaces.signal.token ?? '',
392
- },
393
- {
394
- id: 'external-services.signal.default-recipient',
395
- configKey: 'surfaces.signal.defaultRecipient',
396
- kind: 'text',
397
- label: 'Signal default recipient',
398
- hint: 'Fallback Signal recipient for outbound messages.',
399
- placeholder: '+15551234567',
400
- defaultValue: (snapshot) => snapshot?.config.surfaces.signal.defaultRecipient ?? '',
401
- },
213
+ { configKey: 'surfaces.signal.bridgeUrl', label: 'Signal bridge URL', hint: 'Base URL for the Signal bridge service.', placeholder: 'https://signal-bridge.local' },
214
+ { configKey: 'surfaces.signal.account', label: 'Signal account', hint: 'Signal account identifier used by the bridge.', placeholder: '+15551234567' },
215
+ { configKey: 'surfaces.signal.token', label: 'Signal bridge token', hint: 'Authentication token for the Signal bridge.', placeholder: 'token' },
216
+ { configKey: 'surfaces.signal.defaultRecipient', label: 'Signal default recipient', hint: 'Fallback Signal recipient for outbound messages.', placeholder: '+15551234567' },
402
217
  ],
403
218
  },
404
219
  {
405
- id: 'whatsapp',
406
- enabledFieldId: 'external-services.whatsapp',
407
- enabledConfigKey: 'surfaces.whatsapp.enabled',
220
+ onboardingId: 'whatsapp',
221
+ sdkSurface: 'whatsapp',
408
222
  label: 'WhatsApp surface',
409
223
  hint: 'Enable WhatsApp Cloud API or bridge messaging.',
410
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.whatsapp.enabled ?? false,
411
224
  fields: [
412
- {
413
- id: 'external-services.whatsapp.provider',
414
- configKey: 'surfaces.whatsapp.provider',
415
- kind: 'radio',
416
- label: 'WhatsApp provider',
417
- hint: 'Choose Meta Cloud API or a bridge provider.',
418
- placeholder: 'meta-cloud',
419
- options: WHATSAPP_PROVIDER_OPTIONS,
420
- defaultValue: (snapshot) => snapshot?.config.surfaces.whatsapp.provider ?? 'meta-cloud',
421
- },
422
- {
423
- id: 'external-services.whatsapp.access-token',
424
- configKey: 'surfaces.whatsapp.accessToken',
425
- kind: 'masked',
426
- label: 'WhatsApp access token',
427
- hint: 'Access token for the WhatsApp provider.',
428
- placeholder: 'access token',
429
- defaultValue: (snapshot) => snapshot?.config.surfaces.whatsapp.accessToken ?? '',
430
- },
431
- {
432
- id: 'external-services.whatsapp.verify-token',
433
- configKey: 'surfaces.whatsapp.verifyToken',
434
- kind: 'masked',
435
- label: 'WhatsApp verify token',
436
- hint: 'Verification token for webhook setup.',
437
- placeholder: 'verify token',
438
- defaultValue: (snapshot) => snapshot?.config.surfaces.whatsapp.verifyToken ?? '',
439
- },
440
- {
441
- id: 'external-services.whatsapp.signing-secret',
442
- configKey: 'surfaces.whatsapp.signingSecret',
443
- kind: 'masked',
444
- label: 'WhatsApp signing secret',
445
- hint: 'Secret used to verify signed WhatsApp events.',
446
- placeholder: 'signing secret',
447
- defaultValue: (snapshot) => snapshot?.config.surfaces.whatsapp.signingSecret ?? '',
448
- },
449
- {
450
- id: 'external-services.whatsapp.phone-number-id',
451
- configKey: 'surfaces.whatsapp.phoneNumberId',
452
- kind: 'text',
453
- label: 'WhatsApp phone number ID',
454
- hint: 'Phone number ID used by the WhatsApp Cloud API.',
455
- placeholder: 'phone number id',
456
- defaultValue: (snapshot) => snapshot?.config.surfaces.whatsapp.phoneNumberId ?? '',
457
- },
458
- {
459
- id: 'external-services.whatsapp.business-account-id',
460
- configKey: 'surfaces.whatsapp.businessAccountId',
461
- kind: 'text',
462
- label: 'WhatsApp business account ID',
463
- hint: 'Business account ID for Cloud API routing.',
464
- placeholder: 'business account id',
465
- defaultValue: (snapshot) => snapshot?.config.surfaces.whatsapp.businessAccountId ?? '',
466
- },
467
- {
468
- id: 'external-services.whatsapp.default-recipient',
469
- configKey: 'surfaces.whatsapp.defaultRecipient',
470
- kind: 'text',
471
- label: 'WhatsApp default recipient',
472
- hint: 'Fallback recipient for outbound WhatsApp messages.',
473
- placeholder: '+15551234567',
474
- defaultValue: (snapshot) => snapshot?.config.surfaces.whatsapp.defaultRecipient ?? '',
475
- },
225
+ { configKey: 'surfaces.whatsapp.provider', label: 'WhatsApp provider', hint: 'Choose Meta Cloud API or a bridge provider.', placeholder: 'meta-cloud', options: WHATSAPP_PROVIDER_OPTIONS },
226
+ { configKey: 'surfaces.whatsapp.accessToken', label: 'WhatsApp access token', hint: 'Access token for the WhatsApp provider.', placeholder: 'access token' },
227
+ { configKey: 'surfaces.whatsapp.verifyToken', label: 'WhatsApp verify token', hint: 'Verification token for webhook setup.', placeholder: 'verify token' },
228
+ { configKey: 'surfaces.whatsapp.signingSecret', label: 'WhatsApp signing secret', hint: 'Secret used to verify signed WhatsApp events.', placeholder: 'signing secret' },
229
+ { configKey: 'surfaces.whatsapp.phoneNumberId', label: 'WhatsApp phone number ID', hint: 'Phone number ID used by the WhatsApp Cloud API.', placeholder: 'phone number id' },
230
+ { configKey: 'surfaces.whatsapp.businessAccountId', label: 'WhatsApp business account ID', hint: 'Business account ID for Cloud API routing.', placeholder: 'business account id' },
231
+ { configKey: 'surfaces.whatsapp.defaultRecipient', label: 'WhatsApp default recipient', hint: 'Fallback recipient for outbound WhatsApp messages.', placeholder: '+15551234567' },
476
232
  ],
477
233
  },
478
234
  {
479
- id: 'imessage',
480
- enabledFieldId: 'external-services.imessage',
481
- enabledConfigKey: 'surfaces.imessage.enabled',
235
+ onboardingId: 'imessage',
236
+ sdkSurface: 'imessage',
482
237
  label: 'iMessage surface',
483
238
  hint: 'Enable iMessage bridge messaging.',
484
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.imessage.enabled ?? false,
485
239
  fields: [
486
- {
487
- id: 'external-services.imessage.bridge-url',
488
- configKey: 'surfaces.imessage.bridgeUrl',
489
- kind: 'text',
490
- label: 'iMessage bridge URL',
491
- hint: 'Base URL for the iMessage bridge.',
492
- placeholder: 'https://imessage-bridge.local',
493
- defaultValue: (snapshot) => snapshot?.config.surfaces.imessage.bridgeUrl ?? '',
494
- },
495
- {
496
- id: 'external-services.imessage.account',
497
- configKey: 'surfaces.imessage.account',
498
- kind: 'text',
499
- label: 'iMessage account',
500
- hint: 'Bridge account identifier.',
501
- placeholder: 'account',
502
- defaultValue: (snapshot) => snapshot?.config.surfaces.imessage.account ?? '',
503
- },
504
- {
505
- id: 'external-services.imessage.token',
506
- configKey: 'surfaces.imessage.token',
507
- kind: 'masked',
508
- label: 'iMessage bridge token',
509
- hint: 'Authentication token for the iMessage bridge.',
510
- placeholder: 'token',
511
- defaultValue: (snapshot) => snapshot?.config.surfaces.imessage.token ?? '',
512
- },
513
- {
514
- id: 'external-services.imessage.default-chat-id',
515
- configKey: 'surfaces.imessage.defaultChatId',
516
- kind: 'text',
517
- label: 'iMessage default chat ID',
518
- hint: 'Fallback chat ID for outbound iMessage delivery.',
519
- placeholder: 'chat id',
520
- defaultValue: (snapshot) => snapshot?.config.surfaces.imessage.defaultChatId ?? '',
521
- },
240
+ { configKey: 'surfaces.imessage.bridgeUrl', label: 'iMessage bridge URL', hint: 'Base URL for the iMessage bridge.', placeholder: 'https://imessage-bridge.local' },
241
+ { configKey: 'surfaces.imessage.account', label: 'iMessage account', hint: 'Bridge account identifier.', placeholder: 'account' },
242
+ { configKey: 'surfaces.imessage.token', label: 'iMessage bridge token', hint: 'Authentication token for the iMessage bridge.', placeholder: 'token' },
243
+ { configKey: 'surfaces.imessage.defaultChatId', label: 'iMessage default chat ID', hint: 'Fallback chat ID for outbound iMessage delivery.', placeholder: 'chat id' },
522
244
  ],
523
245
  },
524
246
  {
525
- id: 'msteams',
526
- enabledFieldId: 'external-services.msteams',
527
- enabledConfigKey: 'surfaces.msteams.enabled',
247
+ onboardingId: 'msteams',
248
+ sdkSurface: 'msteams',
528
249
  label: 'Microsoft Teams surface',
529
250
  hint: 'Enable Microsoft Teams bot conversations and channel replies.',
530
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.msteams.enabled ?? false,
531
251
  fields: [
532
- {
533
- id: 'external-services.msteams.app-id',
534
- configKey: 'surfaces.msteams.appId',
535
- kind: 'text',
536
- label: 'Teams app ID',
537
- hint: 'Application ID for the Teams bot registration.',
538
- placeholder: 'app id',
539
- defaultValue: (snapshot) => snapshot?.config.surfaces.msteams.appId ?? '',
540
- },
541
- {
542
- id: 'external-services.msteams.app-password',
543
- configKey: 'surfaces.msteams.appPassword',
544
- kind: 'masked',
545
- label: 'Teams app password',
546
- hint: 'Client secret for the Teams bot registration.',
547
- placeholder: 'app password',
548
- defaultValue: (snapshot) => snapshot?.config.surfaces.msteams.appPassword ?? '',
549
- },
550
- {
551
- id: 'external-services.msteams.tenant-id',
552
- configKey: 'surfaces.msteams.tenantId',
553
- kind: 'text',
554
- label: 'Teams tenant ID',
555
- hint: 'Tenant ID that owns the Teams bot registration.',
556
- placeholder: 'tenant id',
557
- defaultValue: (snapshot) => snapshot?.config.surfaces.msteams.tenantId ?? '',
558
- },
559
- {
560
- id: 'external-services.msteams.service-url',
561
- configKey: 'surfaces.msteams.serviceUrl',
562
- kind: 'text',
563
- label: 'Teams service URL',
564
- hint: 'Optional Bot Framework service URL for replies.',
565
- placeholder: 'https://smba.trafficmanager.net/amer/',
566
- defaultValue: (snapshot) => snapshot?.config.surfaces.msteams.serviceUrl ?? '',
567
- },
568
- {
569
- id: 'external-services.msteams.bot-id',
570
- configKey: 'surfaces.msteams.botId',
571
- kind: 'text',
572
- label: 'Teams bot ID',
573
- hint: 'Optional bot ID when it differs from the app ID.',
574
- placeholder: 'bot id',
575
- defaultValue: (snapshot) => snapshot?.config.surfaces.msteams.botId ?? '',
576
- },
577
- {
578
- id: 'external-services.msteams.default-conversation-id',
579
- configKey: 'surfaces.msteams.defaultConversationId',
580
- kind: 'text',
581
- label: 'Teams default conversation ID',
582
- hint: 'Fallback Teams conversation for outbound messages.',
583
- placeholder: 'conversation id',
584
- defaultValue: (snapshot) => snapshot?.config.surfaces.msteams.defaultConversationId ?? '',
585
- },
586
- {
587
- id: 'external-services.msteams.default-channel-id',
588
- configKey: 'surfaces.msteams.defaultChannelId',
589
- kind: 'text',
590
- label: 'Teams default channel ID',
591
- hint: 'Fallback Teams channel for outbound messages.',
592
- placeholder: 'channel id',
593
- defaultValue: (snapshot) => snapshot?.config.surfaces.msteams.defaultChannelId ?? '',
594
- },
252
+ { configKey: 'surfaces.msteams.appId', label: 'Teams app ID', hint: 'Application ID for the Teams bot registration.', placeholder: 'app id' },
253
+ { configKey: 'surfaces.msteams.appPassword', label: 'Teams app password', hint: 'Client secret for the Teams bot registration.', placeholder: 'app password' },
254
+ { configKey: 'surfaces.msteams.tenantId', label: 'Teams tenant ID', hint: 'Tenant ID that owns the Teams bot registration.', placeholder: 'tenant id' },
255
+ { configKey: 'surfaces.msteams.serviceUrl', label: 'Teams service URL', hint: 'Optional Bot Framework service URL for replies.', placeholder: 'https://smba.trafficmanager.net/amer/' },
256
+ { configKey: 'surfaces.msteams.botId', label: 'Teams bot ID', hint: 'Optional bot ID when it differs from the app ID.', placeholder: 'bot id' },
257
+ { configKey: 'surfaces.msteams.defaultConversationId', label: 'Teams default conversation ID', hint: 'Fallback Teams conversation for outbound messages.', placeholder: 'conversation id' },
258
+ { configKey: 'surfaces.msteams.defaultChannelId', label: 'Teams default channel ID', hint: 'Fallback Teams channel for outbound messages.', placeholder: 'channel id' },
595
259
  ],
596
260
  },
597
261
  {
598
- id: 'bluebubbles',
599
- enabledFieldId: 'external-services.bluebubbles',
600
- enabledConfigKey: 'surfaces.bluebubbles.enabled',
262
+ onboardingId: 'bluebubbles',
263
+ sdkSurface: 'bluebubbles',
601
264
  label: 'BlueBubbles surface',
602
265
  hint: 'Enable BlueBubbles bridge messaging for iMessage-compatible deployments.',
603
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.bluebubbles.enabled ?? false,
604
266
  fields: [
605
- {
606
- id: 'external-services.bluebubbles.server-url',
607
- configKey: 'surfaces.bluebubbles.serverUrl',
608
- kind: 'text',
609
- label: 'BlueBubbles server URL',
610
- hint: 'Base URL of the BlueBubbles server.',
611
- placeholder: 'https://bluebubbles.local',
612
- defaultValue: (snapshot) => snapshot?.config.surfaces.bluebubbles.serverUrl ?? '',
613
- },
614
- {
615
- id: 'external-services.bluebubbles.password',
616
- configKey: 'surfaces.bluebubbles.password',
617
- kind: 'masked',
618
- label: 'BlueBubbles password',
619
- hint: 'Password used to authenticate with BlueBubbles.',
620
- placeholder: 'password',
621
- defaultValue: (snapshot) => snapshot?.config.surfaces.bluebubbles.password ?? '',
622
- },
623
- {
624
- id: 'external-services.bluebubbles.account',
625
- configKey: 'surfaces.bluebubbles.account',
626
- kind: 'text',
627
- label: 'BlueBubbles account',
628
- hint: 'Optional account identifier used by the bridge.',
629
- placeholder: 'account',
630
- defaultValue: (snapshot) => snapshot?.config.surfaces.bluebubbles.account ?? '',
631
- },
632
- {
633
- id: 'external-services.bluebubbles.default-chat-guid',
634
- configKey: 'surfaces.bluebubbles.defaultChatGuid',
635
- kind: 'text',
636
- label: 'BlueBubbles default chat GUID',
637
- hint: 'Fallback chat GUID for outbound BlueBubbles delivery.',
638
- placeholder: 'chat guid',
639
- defaultValue: (snapshot) => snapshot?.config.surfaces.bluebubbles.defaultChatGuid ?? '',
640
- },
267
+ { configKey: 'surfaces.bluebubbles.serverUrl', label: 'BlueBubbles server URL', hint: 'Base URL of the BlueBubbles server.', placeholder: 'https://bluebubbles.local' },
268
+ { configKey: 'surfaces.bluebubbles.password', label: 'BlueBubbles password', hint: 'Password used to authenticate with BlueBubbles.', placeholder: 'password' },
269
+ { configKey: 'surfaces.bluebubbles.account', label: 'BlueBubbles account', hint: 'Optional account identifier used by the bridge.', placeholder: 'account' },
270
+ { configKey: 'surfaces.bluebubbles.defaultChatGuid', label: 'BlueBubbles default chat GUID', hint: 'Fallback chat GUID for outbound BlueBubbles delivery.', placeholder: 'chat guid' },
641
271
  ],
642
272
  },
643
273
  {
644
- id: 'mattermost',
645
- enabledFieldId: 'external-services.mattermost',
646
- enabledConfigKey: 'surfaces.mattermost.enabled',
274
+ onboardingId: 'mattermost',
275
+ sdkSurface: 'mattermost',
647
276
  label: 'Mattermost surface',
648
277
  hint: 'Enable Mattermost bot messaging and channel replies.',
649
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.mattermost.enabled ?? false,
650
278
  fields: [
651
- {
652
- id: 'external-services.mattermost.base-url',
653
- configKey: 'surfaces.mattermost.baseUrl',
654
- kind: 'text',
655
- label: 'Mattermost base URL',
656
- hint: 'Base URL of the Mattermost server.',
657
- placeholder: 'https://mattermost.example.com',
658
- defaultValue: (snapshot) => snapshot?.config.surfaces.mattermost.baseUrl ?? '',
659
- },
660
- {
661
- id: 'external-services.mattermost.bot-token',
662
- configKey: 'surfaces.mattermost.botToken',
663
- kind: 'masked',
664
- label: 'Mattermost bot token',
665
- hint: 'Bot token used for Mattermost API calls.',
666
- placeholder: 'bot token',
667
- defaultValue: (snapshot) => snapshot?.config.surfaces.mattermost.botToken ?? '',
668
- },
669
- {
670
- id: 'external-services.mattermost.team-id',
671
- configKey: 'surfaces.mattermost.teamId',
672
- kind: 'text',
673
- label: 'Mattermost team ID',
674
- hint: 'Optional default team for routing.',
675
- placeholder: 'team id',
676
- defaultValue: (snapshot) => snapshot?.config.surfaces.mattermost.teamId ?? '',
677
- },
678
- {
679
- id: 'external-services.mattermost.default-channel-id',
680
- configKey: 'surfaces.mattermost.defaultChannelId',
681
- kind: 'text',
682
- label: 'Mattermost default channel ID',
683
- hint: 'Fallback Mattermost channel for outbound messages.',
684
- placeholder: 'channel id',
685
- defaultValue: (snapshot) => snapshot?.config.surfaces.mattermost.defaultChannelId ?? '',
686
- },
279
+ { configKey: 'surfaces.mattermost.baseUrl', label: 'Mattermost base URL', hint: 'Base URL of the Mattermost server.', placeholder: 'https://mattermost.example.com' },
280
+ { configKey: 'surfaces.mattermost.botToken', label: 'Mattermost bot token', hint: 'Bot token used for Mattermost API calls.', placeholder: 'bot token' },
281
+ { configKey: 'surfaces.mattermost.teamId', label: 'Mattermost team ID', hint: 'Optional default team for routing.', placeholder: 'team id' },
282
+ { configKey: 'surfaces.mattermost.defaultChannelId', label: 'Mattermost default channel ID', hint: 'Fallback Mattermost channel for outbound messages.', placeholder: 'channel id' },
687
283
  ],
688
284
  },
689
285
  {
690
- id: 'matrix',
691
- enabledFieldId: 'external-services.matrix',
692
- enabledConfigKey: 'surfaces.matrix.enabled',
286
+ onboardingId: 'matrix',
287
+ sdkSurface: 'matrix',
693
288
  label: 'Matrix surface',
694
289
  hint: 'Enable Matrix bot messaging and room replies.',
695
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.matrix.enabled ?? false,
696
290
  fields: [
697
- {
698
- id: 'external-services.matrix.homeserver-url',
699
- configKey: 'surfaces.matrix.homeserverUrl',
700
- kind: 'text',
701
- label: 'Matrix homeserver URL',
702
- hint: 'Base URL of the Matrix homeserver.',
703
- placeholder: 'https://matrix.example.com',
704
- defaultValue: (snapshot) => snapshot?.config.surfaces.matrix.homeserverUrl ?? '',
705
- },
706
- {
707
- id: 'external-services.matrix.access-token',
708
- configKey: 'surfaces.matrix.accessToken',
709
- kind: 'masked',
710
- label: 'Matrix access token',
711
- hint: 'Access token for the Matrix bot user.',
712
- placeholder: 'access token',
713
- defaultValue: (snapshot) => snapshot?.config.surfaces.matrix.accessToken ?? '',
714
- },
715
- {
716
- id: 'external-services.matrix.user-id',
717
- configKey: 'surfaces.matrix.userId',
718
- kind: 'text',
719
- label: 'Matrix user ID',
720
- hint: 'Matrix user ID for the bot account.',
721
- placeholder: '@goodvibes:example.com',
722
- defaultValue: (snapshot) => snapshot?.config.surfaces.matrix.userId ?? '',
723
- },
724
- {
725
- id: 'external-services.matrix.default-room-id',
726
- configKey: 'surfaces.matrix.defaultRoomId',
727
- kind: 'text',
728
- label: 'Matrix default room ID',
729
- hint: 'Fallback Matrix room for outbound messages.',
730
- placeholder: '!room:example.com',
731
- defaultValue: (snapshot) => snapshot?.config.surfaces.matrix.defaultRoomId ?? '',
732
- },
291
+ { configKey: 'surfaces.matrix.homeserverUrl', label: 'Matrix homeserver URL', hint: 'Base URL of the Matrix homeserver.', placeholder: 'https://matrix.example.com' },
292
+ { configKey: 'surfaces.matrix.accessToken', label: 'Matrix access token', hint: 'Access token for the Matrix bot user.', placeholder: 'access token' },
293
+ { configKey: 'surfaces.matrix.userId', label: 'Matrix user ID', hint: 'Matrix user ID for the bot account.', placeholder: '@goodvibes:example.com' },
294
+ { configKey: 'surfaces.matrix.defaultRoomId', label: 'Matrix default room ID', hint: 'Fallback Matrix room for outbound messages.', placeholder: '!room:example.com' },
733
295
  ],
734
296
  },
735
297
  ];
298
+
299
+ // ---- the builder: SDK schema (source of truth) + overlay (presentation) ---
300
+
301
+ function buildFieldFromOverlay(
302
+ sdkSurface: ChannelSurface,
303
+ sdkFields: ReadonlyMap<string, ChannelSetupFieldDescriptor>,
304
+ overlay: SurfaceFieldOverlay,
305
+ ): ExternalSurfaceSetupFieldSpec {
306
+ const lastSegment = overlay.configKey.split('.').pop() ?? overlay.configKey;
307
+ const id = `external-services.${kebab(sdkSurface)}.${kebab(lastSegment)}`;
308
+
309
+ let kind: 'text' | 'masked' | 'radio';
310
+ let valueType: 'number' | undefined;
311
+ if (overlay.tuiOnly) {
312
+ kind = overlay.tuiOnly.kind;
313
+ valueType = overlay.tuiOnly.valueType;
314
+ } else {
315
+ const sdkField = sdkFields.get(overlay.configKey);
316
+ if (!sdkField) {
317
+ throw new Error(`onboarding overlay references ${overlay.configKey}, which the SDK setup schema for ${sdkSurface} no longer defines`);
318
+ }
319
+ kind = mapSdkKind(sdkField.kind);
320
+ valueType = sdkField.kind === 'number' ? 'number' : undefined;
321
+ }
322
+
323
+ return {
324
+ id,
325
+ configKey: overlay.configKey,
326
+ kind,
327
+ label: overlay.label,
328
+ hint: overlay.hint,
329
+ placeholder: overlay.placeholder,
330
+ ...(valueType ? { valueType } : {}),
331
+ ...(overlay.options ? { options: overlay.options } : {}),
332
+ ...(overlay.defaultNumber !== undefined ? { defaultNumber: overlay.defaultNumber } : {}),
333
+ ...(overlay.min !== undefined ? { min: overlay.min } : {}),
334
+ ...(overlay.max !== undefined ? { max: overlay.max } : {}),
335
+ defaultValue: (snapshot) => readFieldDefault(overlay.configKey, snapshot),
336
+ };
337
+ }
338
+
339
+ /** Append any SDK setup field the overlay has not curated, with SDK-derived presentation. */
340
+ function buildUncoveredSdkField(
341
+ sdkSurface: ChannelSurface,
342
+ sdkField: ChannelSetupFieldDescriptor,
343
+ ): ExternalSurfaceSetupFieldSpec {
344
+ const lastSegment = (sdkField.configKey ?? sdkField.id).split('.').pop() ?? sdkField.id;
345
+ return {
346
+ id: `external-services.${kebab(sdkSurface)}.${kebab(lastSegment)}`,
347
+ configKey: (sdkField.configKey ?? '') as ConfigKey,
348
+ kind: mapSdkKind(sdkField.kind),
349
+ label: sdkField.label,
350
+ hint: sdkField.detail ?? sdkField.label,
351
+ placeholder: sdkField.placeholder ?? '',
352
+ ...(sdkField.kind === 'number' ? { valueType: 'number' as const } : {}),
353
+ ...(sdkField.options ? { options: sdkField.options.map((o) => ({ id: o.value, label: o.label, hint: o.label })) } : {}),
354
+ defaultValue: (snapshot) => readFieldDefault((sdkField.configKey ?? '') as ConfigKey, snapshot),
355
+ };
356
+ }
357
+
358
+ function buildSurfaceSpec(overlay: SurfaceOverlay): ExternalSurfaceSpec {
359
+ const schema = getBuiltinSetupSchema(overlay.sdkSurface);
360
+ const sdkFields = new Map<string, ChannelSetupFieldDescriptor>();
361
+ for (const f of schema.fields) {
362
+ if (f.configKey) sdkFields.set(f.configKey, f);
363
+ }
364
+
365
+ const curated = overlay.fields.map((f) => buildFieldFromOverlay(overlay.sdkSurface, sdkFields, f));
366
+
367
+ // Auto-include any SDK field (other than the enabled toggle) the overlay did
368
+ // not curate, so a new SDK channel field surfaces in onboarding automatically.
369
+ const coveredKeys = new Set<string>(overlay.fields.filter((f) => !f.tuiOnly).map((f) => f.configKey));
370
+ const enabledKey = `surfaces.${overlay.onboardingId}.enabled`;
371
+ const appended: ExternalSurfaceSetupFieldSpec[] = [];
372
+ for (const sdkField of schema.fields) {
373
+ if (!sdkField.configKey) continue;
374
+ if (sdkField.kind === 'boolean' || sdkField.configKey === enabledKey) continue;
375
+ if (coveredKeys.has(sdkField.configKey)) continue;
376
+ appended.push(buildUncoveredSdkField(overlay.sdkSurface, sdkField));
377
+ }
378
+
379
+ return {
380
+ id: overlay.onboardingId,
381
+ enabledFieldId: `external-services.${kebab(overlay.sdkSurface)}`,
382
+ enabledConfigKey: `${enabledKey}` as ConfigKey,
383
+ label: overlay.label,
384
+ hint: overlay.hint,
385
+ defaultEnabled: (snapshot) => Boolean(snapshot ? readConfigPath(snapshot.config, enabledKey as ConfigKey) : false),
386
+ fields: [...curated, ...appended],
387
+ };
388
+ }
389
+
390
+ export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = SURFACE_OVERLAYS.map(buildSurfaceSpec);
391
+
392
+ // ---- helpers (unchanged behavior) ----------------------------------------
393
+
394
+ export function getExternalSurfaceAutoStartFieldId(surface: ExternalSurfaceSpec): string {
395
+ return `${surface.enabledFieldId}.auto-start`;
396
+ }
397
+
398
+ export function getExternalSurfaceAutoStartDefaultValue(
399
+ surface: ExternalSurfaceSpec,
400
+ snapshot: OnboardingSnapshotState | null,
401
+ ): 'yes' | 'no' {
402
+ return surface.defaultEnabled(snapshot) ? 'yes' : 'no';
403
+ }
404
+
405
+ export function isExternalSurfaceSelectedByDefault(
406
+ surface: ExternalSurfaceSpec,
407
+ snapshot: OnboardingSnapshotState | null,
408
+ ): boolean {
409
+ if (surface.defaultEnabled(snapshot)) return true;
410
+ if (!snapshot) return false;
411
+
412
+ return surface.fields.some((field) => {
413
+ const current = normalizeConfigValue(field.defaultValue(snapshot));
414
+ const defaultValue = normalizeConfigValue(getDefaultConfigValue(field.configKey));
415
+ return current.length > 0 && current !== defaultValue;
416
+ });
417
+ }
418
+
419
+ export function getNtfyProtocolTopicLines(snapshot: OnboardingSnapshotState | null): readonly string[] {
420
+ const topics = resolveGoodVibesNtfyTopics({
421
+ chatTopic: snapshot?.config.surfaces.ntfy.chatTopic,
422
+ agentTopic: snapshot?.config.surfaces.ntfy.agentTopic,
423
+ remoteTopic: snapshot?.config.surfaces.ntfy.remoteTopic,
424
+ });
425
+ return [
426
+ `Chat topic: ${topics.chatTopic}`,
427
+ `Agent topic: ${topics.agentTopic}`,
428
+ `Daemon-only remote topic: ${topics.remoteTopic}`,
429
+ ];
430
+ }