@pellux/goodvibes-agent 1.4.4 → 1.5.1

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 (230) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +52 -7
  3. package/dist/package/main.js +7491 -12253
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +1 -1
  6. package/docs/getting-started.md +2 -2
  7. package/docs/release-and-publishing.md +1 -1
  8. package/docs/tools-and-commands.md +5 -5
  9. package/package.json +4 -2
  10. package/release/live-verification/live-verification.json +13 -13
  11. package/release/live-verification/live-verification.md +15 -15
  12. package/release/performance-snapshot.json +2 -2
  13. package/release/release-notes.md +5 -7
  14. package/release/release-readiness.json +6 -6
  15. package/src/agent/behavior-discovery-summary.ts +4 -18
  16. package/src/agent/calendar-registry.ts +322 -0
  17. package/src/agent/competitive-feature-inventory.ts +268 -130
  18. package/src/agent/document-registry.ts +5 -1
  19. package/src/agent/email/email-service.ts +350 -0
  20. package/src/agent/email/imap-client.ts +596 -0
  21. package/src/agent/email/smtp-client.ts +453 -0
  22. package/src/agent/ics-calendar.ts +662 -0
  23. package/src/agent/ics-timezone.ts +172 -0
  24. package/src/agent/markdown-frontmatter.ts +31 -0
  25. package/src/agent/memory-safety.ts +1 -1
  26. package/src/agent/note-registry.ts +3 -9
  27. package/src/agent/persona-discovery.ts +3 -15
  28. package/src/agent/persona-registry.ts +1 -10
  29. package/src/agent/research-source-registry.ts +5 -1
  30. package/src/agent/routine-discovery.ts +3 -15
  31. package/src/agent/runtime-profile.ts +3 -0
  32. package/src/agent/skill-discovery.ts +3 -15
  33. package/src/agent/skill-draft-proposer.ts +203 -0
  34. package/src/agent/skill-draft-runner.ts +201 -0
  35. package/src/agent/skill-registry.ts +36 -1
  36. package/src/agent/skill-standard.ts +99 -0
  37. package/src/agent/vibe-file.ts +7 -22
  38. package/src/cli/completion.ts +1 -1
  39. package/src/cli/config-overrides.ts +10 -1
  40. package/src/cli/redaction.ts +17 -5
  41. package/src/config/provider-model.ts +2 -1
  42. package/src/config/secret-config.ts +13 -6
  43. package/src/core/activity-feed.ts +97 -0
  44. package/src/core/away-digest.ts +161 -0
  45. package/src/core/conversation-rendering.ts +7 -3
  46. package/src/core/conversation.ts +22 -15
  47. package/src/core/hardware-profile.ts +362 -0
  48. package/src/core/last-seen-store.ts +78 -0
  49. package/src/core/plain-language.ts +52 -0
  50. package/src/core/setup-incomplete-hint.ts +90 -0
  51. package/src/core/system-message-router.ts +38 -87
  52. package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
  53. package/src/input/agent-workspace-basic-command-editors.ts +39 -141
  54. package/src/input/agent-workspace-categories.ts +40 -125
  55. package/src/input/agent-workspace-command-editor.ts +4 -0
  56. package/src/input/agent-workspace-host-category.ts +0 -5
  57. package/src/input/agent-workspace-local-editor-submission.ts +3 -1
  58. package/src/input/agent-workspace-navigation.ts +10 -3
  59. package/src/input/agent-workspace-onboarding-actions.ts +132 -0
  60. package/src/input/agent-workspace-onboarding-categories.ts +29 -26
  61. package/src/input/agent-workspace-onboarding-finish.ts +11 -4
  62. package/src/input/agent-workspace-onboarding-state.ts +111 -0
  63. package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
  64. package/src/input/agent-workspace-profile-editors.ts +155 -0
  65. package/src/input/agent-workspace-subscription-editor.ts +7 -0
  66. package/src/input/agent-workspace-types.ts +5 -1
  67. package/src/input/agent-workspace.ts +65 -39
  68. package/src/input/command-registry.ts +18 -5
  69. package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
  70. package/src/input/commands/agent-skills-runtime.ts +60 -3
  71. package/src/input/commands/calendar-runtime.ts +209 -0
  72. package/src/input/commands/channels-runtime.ts +1 -0
  73. package/src/input/commands/command-error.ts +9 -0
  74. package/src/input/commands/compat-runtime.ts +1 -0
  75. package/src/input/commands/config.ts +1 -0
  76. package/src/input/commands/conversation-runtime.ts +1 -1
  77. package/src/input/commands/delegation-runtime.ts +5 -6
  78. package/src/input/commands/email-runtime.ts +387 -0
  79. package/src/input/commands/experience-runtime.ts +17 -4
  80. package/src/input/commands/guidance-runtime.ts +1 -1
  81. package/src/input/commands/health-runtime.ts +6 -1
  82. package/src/input/commands/knowledge-format.ts +73 -0
  83. package/src/input/commands/knowledge.ts +9 -74
  84. package/src/input/commands/local-provider-runtime.ts +2 -1
  85. package/src/input/commands/local-runtime.ts +10 -4
  86. package/src/input/commands/mcp-runtime.ts +7 -0
  87. package/src/input/commands/notify-runtime.ts +17 -1
  88. package/src/input/commands/onboarding-runtime.ts +1 -0
  89. package/src/input/commands/operator-actions-runtime.ts +1 -0
  90. package/src/input/commands/operator-runtime.ts +3 -0
  91. package/src/input/commands/personas-runtime.ts +1 -0
  92. package/src/input/commands/platform-access-runtime.ts +40 -17
  93. package/src/input/commands/product-runtime.ts +6 -1
  94. package/src/input/commands/provider-accounts-runtime.ts +3 -2
  95. package/src/input/commands/qrcode-runtime.ts +1 -0
  96. package/src/input/commands/routines-runtime.ts +1 -0
  97. package/src/input/commands/runtime-services.ts +0 -13
  98. package/src/input/commands/security-runtime.ts +1 -0
  99. package/src/input/commands/session-content.ts +1 -0
  100. package/src/input/commands/shell-core.ts +5 -2
  101. package/src/input/commands/subscription-runtime.ts +33 -9
  102. package/src/input/commands/support-bundle-runtime.ts +8 -1
  103. package/src/input/commands/tasks-runtime.ts +1 -0
  104. package/src/input/commands/tts-runtime.ts +1 -0
  105. package/src/input/commands/vibe-runtime.ts +1 -0
  106. package/src/input/commands.ts +4 -0
  107. package/src/input/feed-context-factory.ts +3 -12
  108. package/src/input/handler-command-route.ts +7 -60
  109. package/src/input/handler-feed-routes.ts +0 -194
  110. package/src/input/handler-feed.ts +2 -54
  111. package/src/input/handler-interactions.ts +0 -2
  112. package/src/input/handler-modal-stack.ts +0 -9
  113. package/src/input/handler-picker-routes.ts +24 -1
  114. package/src/input/handler-shortcuts.ts +11 -40
  115. package/src/input/handler-ui-state.ts +13 -0
  116. package/src/input/handler.ts +8 -35
  117. package/src/input/keybindings.ts +40 -17
  118. package/src/input/model-picker-local-fit.ts +130 -0
  119. package/src/input/shell-passthrough.ts +58 -0
  120. package/src/input/submission-router.ts +0 -5
  121. package/src/main.ts +129 -90
  122. package/src/renderer/activity-sidebar.ts +186 -0
  123. package/src/renderer/agent-workspace-context-lines.ts +5 -48
  124. package/src/renderer/agent-workspace-style.ts +1 -1
  125. package/src/renderer/agent-workspace.ts +14 -2
  126. package/src/renderer/compositor.ts +37 -125
  127. package/src/renderer/conversation-overlays.ts +3 -3
  128. package/src/renderer/help-overlay.ts +7 -5
  129. package/src/renderer/model-workspace.ts +101 -86
  130. package/src/{panels → renderer}/polish.ts +3 -3
  131. package/src/renderer/shell-surface.ts +4 -5
  132. package/src/renderer/status-token.ts +31 -11
  133. package/src/renderer/tab-strip.ts +1 -1
  134. package/src/renderer/tool-call.ts +7 -8
  135. package/src/renderer/tool-labels.ts +92 -0
  136. package/src/renderer/ui-factory.ts +48 -96
  137. package/src/runtime/bootstrap-command-context.ts +0 -5
  138. package/src/runtime/bootstrap-command-parts.ts +6 -15
  139. package/src/runtime/bootstrap-core.ts +34 -13
  140. package/src/runtime/bootstrap-hook-bridge.ts +3 -1
  141. package/src/runtime/bootstrap-shell.ts +3 -50
  142. package/src/runtime/bootstrap.ts +3 -4
  143. package/src/runtime/context.ts +1 -1
  144. package/src/runtime/diagnostics/panels/index.ts +0 -1
  145. package/src/runtime/diagnostics/panels/policy.ts +1 -1
  146. package/src/runtime/execution-ledger.ts +16 -1
  147. package/src/runtime/index.ts +6 -1
  148. package/src/runtime/onboarding/index.ts +1 -0
  149. package/src/runtime/onboarding/onboarding-state.ts +200 -0
  150. package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
  151. package/src/runtime/services.ts +16 -4
  152. package/src/runtime/terminal-output-guard.ts +7 -0
  153. package/src/runtime/tool-permission-safety.ts +1 -1
  154. package/src/runtime/ui/model-picker/data-provider.ts +1 -1
  155. package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
  156. package/src/runtime/ui/model-picker/types.ts +2 -2
  157. package/src/runtime/ui/provider-health/data-provider.ts +3 -3
  158. package/src/runtime/ui/provider-health/types.ts +2 -2
  159. package/src/runtime/ui-services.ts +0 -2
  160. package/src/shell/agent-workspace-fullscreen.ts +0 -1
  161. package/src/shell/autonomy-surfacing.ts +272 -0
  162. package/src/shell/session-continuity-hints.ts +0 -6
  163. package/src/shell/startup-wiring.ts +221 -0
  164. package/src/shell/ui-openers.ts +18 -29
  165. package/src/tools/agent-context-policy.ts +1 -1
  166. package/src/tools/agent-harness-background-processes.ts +4 -4
  167. package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
  168. package/src/tools/agent-harness-command-runner.ts +6 -1
  169. package/src/tools/agent-harness-document-ops.ts +26 -53
  170. package/src/tools/agent-harness-execution-history.ts +1 -13
  171. package/src/tools/agent-harness-execution-posture.ts +0 -15
  172. package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
  173. package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
  174. package/src/tools/agent-harness-metadata.ts +16 -0
  175. package/src/tools/agent-harness-mode-catalog.ts +2 -9
  176. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  177. package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
  178. package/src/tools/agent-harness-tool-schema.ts +13 -14
  179. package/src/tools/agent-harness-tool.ts +27 -23
  180. package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
  181. package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
  182. package/src/tools/agent-workspace-tool.ts +2 -33
  183. package/src/utils/terminal-width.ts +9 -3
  184. package/src/version.ts +1 -1
  185. package/src/input/agent-workspace-panel-route.ts +0 -44
  186. package/src/input/panel-integration-actions.ts +0 -26
  187. package/src/panels/approval-panel.ts +0 -149
  188. package/src/panels/automation-control-panel.ts +0 -212
  189. package/src/panels/base-panel.ts +0 -254
  190. package/src/panels/builtin/agent.ts +0 -58
  191. package/src/panels/builtin/knowledge.ts +0 -26
  192. package/src/panels/builtin/operations.ts +0 -121
  193. package/src/panels/builtin/session.ts +0 -138
  194. package/src/panels/builtin/shared.ts +0 -275
  195. package/src/panels/builtin/usage.ts +0 -21
  196. package/src/panels/builtin-panels.ts +0 -23
  197. package/src/panels/confirm-state.ts +0 -61
  198. package/src/panels/context-visualizer-panel.ts +0 -204
  199. package/src/panels/cost-tracker-panel.ts +0 -444
  200. package/src/panels/docs-panel.ts +0 -285
  201. package/src/panels/index.ts +0 -25
  202. package/src/panels/knowledge-panel.ts +0 -417
  203. package/src/panels/memory-panel.ts +0 -226
  204. package/src/panels/panel-list-panel.ts +0 -464
  205. package/src/panels/panel-manager.ts +0 -570
  206. package/src/panels/provider-accounts-panel.ts +0 -233
  207. package/src/panels/provider-health-domains.ts +0 -208
  208. package/src/panels/provider-health-panel.ts +0 -720
  209. package/src/panels/provider-health-tracker.ts +0 -115
  210. package/src/panels/provider-stats-panel.ts +0 -366
  211. package/src/panels/qr-panel.ts +0 -207
  212. package/src/panels/schedule-panel.ts +0 -321
  213. package/src/panels/scrollable-list-panel.ts +0 -491
  214. package/src/panels/search-focus.ts +0 -32
  215. package/src/panels/security-panel.ts +0 -295
  216. package/src/panels/session-browser-panel.ts +0 -395
  217. package/src/panels/session-maintenance.ts +0 -125
  218. package/src/panels/subscription-panel.ts +0 -263
  219. package/src/panels/system-messages-panel.ts +0 -230
  220. package/src/panels/tasks-panel.ts +0 -344
  221. package/src/panels/thinking-panel.ts +0 -304
  222. package/src/panels/token-budget-panel.ts +0 -475
  223. package/src/panels/tool-inspector-panel.ts +0 -436
  224. package/src/panels/types.ts +0 -54
  225. package/src/renderer/panel-composite.ts +0 -158
  226. package/src/renderer/panel-tab-bar.ts +0 -69
  227. package/src/renderer/panel-workspace-bar.ts +0 -42
  228. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  229. package/src/tools/agent-harness-panel-metadata.ts +0 -211
  230. /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
@@ -0,0 +1,209 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { AgentCalendarRegistry } from '../../agent/calendar-registry.ts';
3
+ import { parseIcs } from '../../agent/ics-calendar.ts';
4
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
5
+ import { parseAgentLocalLibraryArgs } from './agent-local-library-args.ts';
6
+ import { requireShellPaths } from './runtime-services.ts';
7
+
8
+ const CALENDAR_VALUE_FLAGS = ['title', 'start', 'end', 'location', 'notes', 'dest', 'path', 'days'] as const;
9
+
10
+ function parseCalendarArgs(args: readonly string[]) {
11
+ return parseAgentLocalLibraryArgs(args, { valueFlags: CALENDAR_VALUE_FLAGS });
12
+ }
13
+
14
+ function registryFromContext(ctx: CommandContext): AgentCalendarRegistry {
15
+ return AgentCalendarRegistry.fromShellPaths(requireShellPaths(ctx));
16
+ }
17
+
18
+ function printError(ctx: CommandContext, error: unknown): void {
19
+ ctx.print([
20
+ 'Error',
21
+ ` message ${error instanceof Error ? error.message : String(error)}`,
22
+ ].join('\n'));
23
+ }
24
+
25
+ function formatEventLine(e: { readonly id: string; readonly title: string; readonly start: string; readonly end?: string; readonly allDay: boolean; readonly location?: string }): string {
26
+ const when = e.allDay ? e.start : e.start.slice(0, 16).replace('T', ' ');
27
+ const loc = e.location ? ` at ${e.location}` : '';
28
+ return ` ${e.id} ${when} ${e.title}${loc}`;
29
+ }
30
+
31
+ function renderEventList(events: readonly Parameters<typeof formatEventLine>[0][], path: string): string {
32
+ if (events.length === 0) {
33
+ return [
34
+ 'Local calendar events',
35
+ ` store ${path}`,
36
+ ' No events yet. Add one with /calendar add --title <title> --start <ISO> --yes',
37
+ ].join('\n');
38
+ }
39
+ return [
40
+ `Local calendar events (${events.length})`,
41
+ ` store ${path}`,
42
+ ...events.map(formatEventLine),
43
+ ].join('\n');
44
+ }
45
+
46
+ export function runCalendarRuntimeCommand(args: readonly string[], ctx: CommandContext): void {
47
+ const sub = (args[0] ?? 'list').toLowerCase();
48
+ const registry = registryFromContext(ctx);
49
+
50
+ try {
51
+ // list / upcoming
52
+ if (sub === 'list' || sub === 'all') {
53
+ const snap = registry.snapshot();
54
+ ctx.print(renderEventList([...snap.events], snap.path));
55
+ return;
56
+ }
57
+
58
+ if (sub === 'upcoming') {
59
+ const parsed = parseCalendarArgs(args.slice(1));
60
+ const days = parsed.flags.has('days') ? Math.max(1, parseInt(parsed.flags.get('days') ?? '7', 10) || 7) : 7;
61
+ const snap = registry.snapshot();
62
+ const events = registry.upcoming(days);
63
+ ctx.print(renderEventList([...events], snap.path));
64
+ return;
65
+ }
66
+
67
+ // import <path> [--yes]
68
+ if (sub === 'import') {
69
+ const parsed = parseCalendarArgs(args.slice(1));
70
+ const filePath = parsed.rest[0] ?? parsed.flags.get('path');
71
+ if (!filePath) {
72
+ ctx.print('Usage: /calendar import <path-to-ics> [--yes]');
73
+ return;
74
+ }
75
+ if (!existsSync(filePath)) {
76
+ ctx.print(`File not found: ${filePath}`);
77
+ return;
78
+ }
79
+ const content = readFileSync(filePath, 'utf-8');
80
+ if (!parsed.yes) {
81
+ const preview = parseIcs(content);
82
+ ctx.print([
83
+ 'Calendar import preview',
84
+ ` file ${filePath}`,
85
+ ` events found ${preview.length}`,
86
+ ...preview.slice(0, 5).map((e) => ` - ${e.summary} (${e.dtstart})`),
87
+ preview.length > 5 ? ` ... and ${preview.length - 5} more` : '',
88
+ ' rerun with --yes to import',
89
+ ].filter(Boolean).join('\n'));
90
+ return;
91
+ }
92
+ const { imported, skipped, secretSkipped } = registry.importIcs(content);
93
+ ctx.print([
94
+ `Imported ${imported.length} event${imported.length === 1 ? '' : 's'} from ${filePath}`,
95
+ ...imported.slice(0, 10).map((e) => ` ${e.id} ${e.start} ${e.title}`),
96
+ imported.length > 10 ? ` ... and ${imported.length - 10} more` : '',
97
+ skipped > 0 ? `Skipped ${skipped} event${skipped === 1 ? '' : 's'} that already exist.` : '',
98
+ secretSkipped > 0 ? `Skipped ${secretSkipped} event${secretSkipped === 1 ? '' : 's'} for safety.` : '',
99
+ ].filter(Boolean).join('\n'));
100
+ return;
101
+ }
102
+
103
+ // export [--dest <path>] [--yes]
104
+ if (sub === 'export') {
105
+ const parsed = parseCalendarArgs(args.slice(1));
106
+ const dest = parsed.flags.get('dest') ?? parsed.rest[0];
107
+ if (!parsed.yes) {
108
+ const events = registry.list();
109
+ ctx.print([
110
+ 'Calendar export preview',
111
+ ` events ${events.length}`,
112
+ dest ? ` dest ${dest}` : ' output to stdout (no --dest given)',
113
+ ' rerun with --yes to export',
114
+ ].join('\n'));
115
+ return;
116
+ }
117
+ const ics = registry.exportIcs(undefined, dest);
118
+ if (!dest) {
119
+ ctx.print(ics);
120
+ } else {
121
+ ctx.print(`Exported calendar to ${dest}`);
122
+ }
123
+ return;
124
+ }
125
+
126
+ // add --title <title> --start <ISO> [--end <ISO>] [--location <loc>] [--notes <notes>] --yes
127
+ if (sub === 'add' || sub === 'create') {
128
+ const parsed = parseCalendarArgs(args.slice(1));
129
+ const title = parsed.flags.get('title')?.trim() ?? parsed.rest.join(' ').trim();
130
+ const start = parsed.flags.get('start')?.trim();
131
+ if (!title || !start) {
132
+ ctx.print('Usage: /calendar add --title <title> --start <ISO-date-or-datetime> [--end <ISO>] [--location <loc>] [--notes <notes>] --yes');
133
+ return;
134
+ }
135
+ if (!parsed.yes) {
136
+ ctx.print([
137
+ 'Calendar event preview',
138
+ ` title ${title}`,
139
+ ` start ${start}`,
140
+ parsed.flags.get('end') ? ` end ${parsed.flags.get('end')}` : '',
141
+ parsed.flags.get('location') ? ` location ${parsed.flags.get('location')}` : '',
142
+ parsed.flags.get('notes') ? ` notes ${parsed.flags.get('notes')}` : '',
143
+ ' rerun with --yes to save',
144
+ ].filter(Boolean).join('\n'));
145
+ return;
146
+ }
147
+ const event = registry.create({
148
+ title,
149
+ start,
150
+ end: parsed.flags.get('end')?.trim(),
151
+ location: parsed.flags.get('location')?.trim(),
152
+ notes: parsed.flags.get('notes')?.trim(),
153
+ });
154
+ ctx.print([
155
+ `Added calendar event ${event.id}`,
156
+ ` id ${event.id}`,
157
+ ` title ${event.title}`,
158
+ ` start ${event.start}`,
159
+ event.end ? ` end ${event.end}` : '',
160
+ ].filter(Boolean).join('\n'));
161
+ return;
162
+ }
163
+
164
+ // delete <id> --yes
165
+ if (sub === 'delete' || sub === 'remove') {
166
+ const parsed = parseCalendarArgs(args.slice(1));
167
+ const id = parsed.rest[0];
168
+ if (!id) {
169
+ ctx.print('Usage: /calendar delete <id> --yes');
170
+ return;
171
+ }
172
+ if (!parsed.yes) {
173
+ const event = registry.get(id);
174
+ if (!event) {
175
+ ctx.print(`Unknown calendar event ${id}`);
176
+ return;
177
+ }
178
+ ctx.print([
179
+ `Delete calendar event preview`,
180
+ ` id ${event.id}`,
181
+ ` title ${event.title}`,
182
+ ` start ${event.start}`,
183
+ ' rerun with --yes to delete',
184
+ ].join('\n'));
185
+ return;
186
+ }
187
+ const removed = registry.delete(id);
188
+ ctx.print(`Deleted calendar event ${removed.id} ${removed.title}`);
189
+ return;
190
+ }
191
+
192
+ ctx.print('Usage: /calendar [list|upcoming [--days N]|import <path> [--yes]|export [--dest <path>] [--yes]|add --title <title> --start <ISO> --yes|delete <id> --yes]');
193
+ } catch (error) {
194
+ printError(ctx, error);
195
+ }
196
+ }
197
+
198
+ export function registerCalendarRuntimeCommands(registry: CommandRegistry): void {
199
+ registry.register({
200
+ name: 'calendar',
201
+ aliases: ['cal'],
202
+ description: 'Manage local calendar events',
203
+ hidden: true,
204
+ usage: '[list|upcoming [--days N]|import <path> [--yes]|export [--dest <path>] [--yes]|add --title <title> --start <ISO> [--end <ISO>] [--location <loc>] [--notes <notes>] --yes|delete <id> --yes]',
205
+ handler: async (args: readonly string[], ctx: CommandContext) => {
206
+ runCalendarRuntimeCommand(args, ctx);
207
+ },
208
+ });
209
+ }
@@ -378,6 +378,7 @@ export function registerChannelsRuntimeCommands(registry: CommandRegistry): void
378
378
  name: 'channels',
379
379
  aliases: ['channel'],
380
380
  description: 'Inspect Agent channel readiness or send an explicitly confirmed delivery message',
381
+ hidden: true,
381
382
  usage: '[list|readiness|ready|attention|triage [limit]|guide [id]|show <id>|deliveries [limit]|send --channel <id> --message <text> --yes|accounts|policies|status|doctor <id>|setup <id>]',
382
383
  argsHint: 'list|readiness|ready|attention|triage|guide|show|deliveries|send|accounts|policies|status|doctor|setup',
383
384
  async handler(args, ctx) {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * summarizeCommandError - format a thrown value into a plain-language message
3
+ * suitable for printing directly to the conversation. Matches the style used
4
+ * by routines, personas, and vibe (short prefix + colon + reason).
5
+ */
6
+ export function summarizeCommandError(error: unknown): string {
7
+ const reason = error instanceof Error ? error.message : String(error);
8
+ return `Command failed: ${reason}`;
9
+ }
@@ -9,6 +9,7 @@ export function registerCompatRuntimeCommands(registry: CommandRegistry): void {
9
9
  name: 'compat',
10
10
  aliases: ['compatibility'],
11
11
  description: 'Inspect connected-host compatibility and Agent Knowledge route readiness',
12
+ hidden: true,
12
13
  usage: '[--json]',
13
14
  argsHint: '[--json]',
14
15
  async handler(args, ctx) {
@@ -5,6 +5,7 @@ export function registerConfigCommand(registry: CommandRegistry): void {
5
5
  name: 'config',
6
6
  aliases: ['cfg'],
7
7
  description: 'Open the fullscreen configuration workspace',
8
+ hidden: true,
8
9
  usage: '[category|key]',
9
10
  argsHint: '[category|key]',
10
11
  handler(args, ctx) {
@@ -79,7 +79,6 @@ function buildComposerReview(ctx: CommandContext): string[] {
79
79
  const composer = deriveComposerState({
80
80
  text: '',
81
81
  commandMode: false,
82
- panelFocused: false,
83
82
  pendingApproval: session.pendingApproval,
84
83
  hasAttachments: false,
85
84
  turnState: session.turnState,
@@ -137,6 +136,7 @@ export function registerConversationRuntimeCommands(registry: CommandRegistry):
137
136
  name: 'conversation',
138
137
  aliases: ['transcript', 'composer'],
139
138
  description: 'Review conversation structure, transcript hotspots, and composer posture',
139
+ hidden: true,
140
140
  usage: '[review|events [kind]|groups [kind]|hotspots|composer|find <query> [kind]|next [kind]|prev [kind]|restore]',
141
141
  handler(args, ctx) {
142
142
  const sub = (args[0] ?? 'review').toLowerCase();
@@ -93,12 +93,10 @@ function buildDelegationBody(input: DelegationCommandInput): string {
93
93
  }
94
94
 
95
95
  export function registerDelegationRuntimeCommands(registry: CommandRegistry): void {
96
- const makeHandler = (defaultReview: boolean) => async (args: string[], ctx: CommandContext): Promise<void> => {
97
- const input = parseDelegationArgs(args, defaultReview);
96
+ const makeHandler = () => async (args: string[], ctx: CommandContext): Promise<void> => {
97
+ const input = parseDelegationArgs(args, false);
98
98
  if (!input.task) {
99
- ctx.print(defaultReview
100
- ? 'Usage: /delegate --review [--reason <why>] [--success <evidence>] [--workspace <hint>] [--priority <priority>] <build/fix/review task>'
101
- : 'Usage: /delegate [--review] [--reason <why>] [--success <evidence>] [--workspace <hint>] [--priority <priority>] <build/fix/review task>');
99
+ ctx.print('Usage: /delegate [--review] [--reason <why>] [--success <evidence>] [--workspace <hint>] [--priority <priority>] <build/fix/review task>');
102
100
  return;
103
101
  }
104
102
  const operator = ctx.clients?.operator;
@@ -183,8 +181,9 @@ export function registerDelegationRuntimeCommands(registry: CommandRegistry): vo
183
181
  name: 'delegate',
184
182
  aliases: ['build'],
185
183
  description: 'Explicitly delegate build/fix/review work to GoodVibes TUI through shared-session routes',
184
+ hidden: true,
186
185
  usage: '[--review] [--reason <why>] [--success <evidence>] [--workspace <hint>] [--priority <priority>] <task>',
187
186
  argsHint: '[--review] [--reason <why>] [--success <evidence>] [--workspace <hint>] [--priority <priority>] <task>',
188
- handler: makeHandler(false),
187
+ handler: makeHandler(),
189
188
  });
190
189
  }
@@ -0,0 +1,387 @@
1
+ /**
2
+ * /email command — direct email access via IMAP/SMTP.
3
+ *
4
+ * Subcommands:
5
+ * status Show config readiness (redacted, no secrets)
6
+ * config Show current email config (keys redacted)
7
+ * set <key> <value> [--yes] Set an email config key; --yes required for passwordRef
8
+ * check [n] List up to n unread inbox summaries (default 10);
9
+ * includes a 2-line body preview of the newest message
10
+ * send <to> <subject words> -- <body words> --yes Send a plain-text email; preview without --yes
11
+ *
12
+ * Settable keys:
13
+ * email.enabled, email.imapHost, email.imapPort, email.smtpHost, email.smtpPort,
14
+ * email.smtpSecurity, email.username, email.passwordRef (requires --yes), email.fromAddress
15
+ *
16
+ * Safety policy:
17
+ * - All inbox reads use EXAMINE (read-only); unread status is never modified.
18
+ * - Sending requires explicit --yes; previews are shown without it.
19
+ * - No credential values are ever printed; passwordRef is displayed as [configured].
20
+ * - email.passwordRef is always stored as a goodvibes:// secret reference; the raw value
21
+ * is passed to SecretsManager and never written to settings.json.
22
+ */
23
+
24
+ import type { CommandRegistry } from '../command-registry.ts';
25
+ import type { CommandContext } from '../command-registry.ts';
26
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
27
+ import {
28
+ EmailService,
29
+ readEmailConfig,
30
+ validateEmailConfig,
31
+ ensureEmailConfigDefaults,
32
+ } from '../../agent/email/email-service.ts';
33
+ import { requireSecretsManager } from './runtime-services.ts';
34
+ import type { ConfigKey } from '../../config/index.ts';
35
+ import { persistSecretBackedConfigValue } from '../../config/secret-config.ts';
36
+
37
+ /**
38
+ * Whitelist of settable email config fields (without the 'email.' prefix).
39
+ * Reject any key not in this set to prevent accidental writes to unknown paths.
40
+ */
41
+ const KNOWN_EMAIL_FIELDS = new Set([
42
+ 'enabled',
43
+ 'imapHost',
44
+ 'imapPort',
45
+ 'smtpHost',
46
+ 'smtpPort',
47
+ 'smtpSecurity',
48
+ 'username',
49
+ 'passwordRef',
50
+ 'fromAddress',
51
+ ]);
52
+
53
+ function readLimitArg(arg: string | undefined, fallback: number): number {
54
+ if (!arg) return fallback;
55
+ const n = parseInt(arg, 10);
56
+ return isFinite(n) && n > 0 ? Math.min(n, 100) : fallback;
57
+ }
58
+
59
+ function getConfigManager(ctx: CommandContext): { get: (key: string) => unknown } {
60
+ ensureEmailConfigDefaults(ctx.platform.configManager);
61
+ return ctx.platform.configManager as unknown as { get: (key: string) => unknown };
62
+ }
63
+
64
+ function buildEmailService(ctx: CommandContext): EmailService {
65
+ const secretsManager = requireSecretsManager(ctx);
66
+ const cm = getConfigManager(ctx);
67
+ return new EmailService({
68
+ getConfig: (key: string) => cm.get(key),
69
+ secretsManager,
70
+ });
71
+ }
72
+
73
+ function formatStatus(ctx: CommandContext): void {
74
+ const cm = getConfigManager(ctx);
75
+ const config = readEmailConfig((key) => cm.get(key));
76
+ const errors = validateEmailConfig(config);
77
+ const lines = [
78
+ 'Email Status',
79
+ ` enabled: ${config.enabled ? 'yes' : 'no'}`,
80
+ ` imap: ${config.imapHost || '[not set]'}:${config.imapPort}`,
81
+ ` smtp: ${config.smtpHost || '[not set]'}:${config.smtpPort}`,
82
+ ` username: ${config.username || '[not set]'}`,
83
+ ` password: ${config.passwordRef ? '[configured]' : '[missing]'}`,
84
+ ` from: ${config.fromAddress || '[not set]'}`,
85
+ ` ready: ${errors.length === 0 && config.enabled ? 'yes' : 'no'}`,
86
+ ];
87
+ if (errors.length > 0) {
88
+ lines.push(` issues (${errors.length}):`);
89
+ for (const err of errors) lines.push(` - ${err}`);
90
+ }
91
+ lines.push(' policy: credentials are never printed; passwordRef displays as [configured]');
92
+ lines.push(' Usage: /email set email.<key> <value> [--yes] — see /email config');
93
+ ctx.print(lines.join('\n'));
94
+ }
95
+
96
+ async function handleCheck(
97
+ args: readonly string[],
98
+ ctx: CommandContext,
99
+ ): Promise<void> {
100
+ const limit = readLimitArg(args[0], 10);
101
+ const service = buildEmailService(ctx);
102
+ const summaries = await service.checkInbox(limit);
103
+
104
+ if (summaries.length === 0) {
105
+ ctx.print('Inbox: no unread messages.');
106
+ return;
107
+ }
108
+
109
+ const lines = [
110
+ `Inbox: ${summaries.length} unread message${summaries.length !== 1 ? 's' : ''}`,
111
+ ' policy: read-only; messages are not marked read',
112
+ ];
113
+ for (let i = 0; i < summaries.length; i += 1) {
114
+ const msg = summaries[i];
115
+ if (!msg) continue;
116
+ lines.push(` ${i + 1}. from=${msg.from || '(unknown)'} subject=${msg.subject || '(none)'} date=${msg.date || '(none)'}`);
117
+ }
118
+
119
+ // Wire in body preview for the newest message (MIN-2)
120
+ const newest = summaries[0];
121
+ if (newest?.bodyPreview) {
122
+ const preview = newest.bodyPreview
123
+ .split('\n')
124
+ .map((l) => l.trim())
125
+ .filter((l) => l.length > 0)
126
+ .slice(0, 2)
127
+ .join(' … ');
128
+ if (preview) lines.push(` preview: ${preview}`);
129
+ }
130
+
131
+ ctx.print(lines.join('\n'));
132
+ }
133
+
134
+ /**
135
+ * Parse send args with optional body separator.
136
+ * Format: <to> <subject words...> [-- <body words...>]
137
+ * Returns { to, subject, body } or null if to/subject are missing.
138
+ */
139
+ function parseSendArgs(args: readonly string[]): { to: string; subject: string; body: string } | null {
140
+ const to = args[0];
141
+ if (!to) return null;
142
+
143
+ const rest = args.slice(1);
144
+ const sepIdx = rest.indexOf('--');
145
+
146
+ let subjectParts: string[];
147
+ let bodyParts: string[];
148
+
149
+ if (sepIdx === -1) {
150
+ subjectParts = rest;
151
+ bodyParts = [];
152
+ } else {
153
+ subjectParts = rest.slice(0, sepIdx);
154
+ bodyParts = rest.slice(sepIdx + 1);
155
+ }
156
+
157
+ const subject = subjectParts.join(' ').trim();
158
+ if (!subject) return null;
159
+
160
+ const body = bodyParts.join(' ').trim();
161
+ return { to, subject, body };
162
+ }
163
+
164
+ function formatConfig(ctx: CommandContext): void {
165
+ const cm = getConfigManager(ctx);
166
+ const config = readEmailConfig((key) => cm.get(key));
167
+ const lines = [
168
+ 'Email Config',
169
+ ` email.enabled: ${String(config.enabled)}`,
170
+ ` email.imapHost: ${config.imapHost || '[not set]'}`,
171
+ ` email.imapPort: ${config.imapPort}`,
172
+ ` email.smtpHost: ${config.smtpHost || '[not set]'}`,
173
+ ` email.smtpPort: ${config.smtpPort}`,
174
+ ` email.smtpSecurity: ${config.smtpSecurity} (tls|starttls|auto)`,
175
+ ` email.username: ${config.username || '[not set]'}`,
176
+ ` email.passwordRef: ${config.passwordRef ? '[configured]' : '[not set]'}`,
177
+ ` email.fromAddress: ${config.fromAddress || '[not set]'}`,
178
+ '',
179
+ ' Settable keys: email.enabled, email.imapHost, email.imapPort, email.smtpHost,',
180
+ ' email.smtpPort, email.smtpSecurity, email.username,',
181
+ ' email.passwordRef (--yes required), email.fromAddress',
182
+ ' Usage: /email set email.<key> <value> [--yes]',
183
+ ];
184
+ ctx.print(lines.join('\n'));
185
+ }
186
+
187
+ async function handleSet(
188
+ args: readonly string[],
189
+ ctx: CommandContext,
190
+ ): Promise<void> {
191
+ const parsed = stripYesFlag(args);
192
+ const rawArgs = [...parsed.rest];
193
+
194
+ const rawKey = rawArgs[0];
195
+ const rawValue = rawArgs.slice(1).join(' ');
196
+
197
+ if (!rawKey) {
198
+ ctx.print(
199
+ 'Usage: /email set email.<key> <value> [--yes]\n' +
200
+ 'Keys: email.enabled, email.imapHost, email.imapPort, email.smtpHost,\n' +
201
+ ' email.smtpPort, email.username, email.passwordRef (--yes required),\n' +
202
+ ' email.fromAddress',
203
+ );
204
+ return;
205
+ }
206
+
207
+ // Accept both "email.imapHost" and "imapHost" forms
208
+ const fieldName = rawKey.startsWith('email.') ? rawKey.slice('email.'.length) : rawKey;
209
+ const configKey = `email.${fieldName}`;
210
+
211
+ if (!KNOWN_EMAIL_FIELDS.has(fieldName)) {
212
+ ctx.print(
213
+ `Unknown email config key: ${rawKey}\n` +
214
+ 'Valid keys: email.enabled, email.imapHost, email.imapPort, email.smtpHost,\n' +
215
+ ' email.smtpPort, email.username, email.passwordRef (--yes required),\n' +
216
+ ' email.fromAddress',
217
+ );
218
+ return;
219
+ }
220
+
221
+ if (rawValue === '') {
222
+ ctx.print(`Usage: /email set ${configKey} <value>\nValue is required.`);
223
+ return;
224
+ }
225
+
226
+ // Ensure the email section exists in the config manager before any writes
227
+ ensureEmailConfigDefaults(ctx.platform.configManager);
228
+ const cm = ctx.platform.configManager as unknown as {
229
+ get: (key: string) => unknown;
230
+ setDynamic: (key: string, value: unknown) => void;
231
+ save: () => void;
232
+ };
233
+
234
+ if (fieldName === 'passwordRef') {
235
+ // Secret path: --yes is required; raw value is routed through SecretsManager
236
+ if (!parsed.yes) {
237
+ const lines = [
238
+ 'Email Password Set Preview',
239
+ ` key: ${configKey}`,
240
+ ' value: [redacted — raw password will be stored as a secure secret]',
241
+ ' policy: the raw password is never written to settings.json; only a',
242
+ ' goodvibes:// reference is stored there',
243
+ ];
244
+ ctx.print(lines.join('\n'));
245
+ requireYesFlag(ctx, 'set email.passwordRef secret', '/email set email.passwordRef <password> --yes');
246
+ return;
247
+ }
248
+
249
+ const secretsManager = requireSecretsManager(ctx);
250
+ const configKeyTyped = configKey as unknown as ConfigKey;
251
+ const storedRef = await persistSecretBackedConfigValue(
252
+ cm as unknown as Parameters<typeof persistSecretBackedConfigValue>[0],
253
+ secretsManager,
254
+ configKeyTyped,
255
+ rawValue,
256
+ { scope: 'user' },
257
+ );
258
+ cm.save();
259
+ ctx.print(
260
+ `Email config updated\n` +
261
+ ` key: ${configKey}\n` +
262
+ ` value: [stored as secret; reference: ${storedRef.slice(0, 40)}...]\n` +
263
+ ' policy: raw password stored in secrets manager; settings.json contains only the goodvibes:// ref',
264
+ );
265
+ return;
266
+ }
267
+
268
+ // Non-secret path: coerce value and persist
269
+ let coerced: unknown = rawValue;
270
+ if (fieldName === 'imapPort' || fieldName === 'smtpPort') {
271
+ const n = parseInt(rawValue, 10);
272
+ if (!isFinite(n) || n < 1 || n > 65535) {
273
+ ctx.print(`Invalid port number: ${rawValue}. Must be an integer between 1 and 65535.`);
274
+ return;
275
+ }
276
+ coerced = n;
277
+ } else if (fieldName === 'smtpSecurity') {
278
+ // MIN-2: validate at set time so invalid values never reach the socket factory
279
+ if (rawValue !== 'tls' && rawValue !== 'starttls' && rawValue !== 'auto') {
280
+ ctx.print(
281
+ `Invalid smtpSecurity value: "${rawValue}".\n` +
282
+ 'Valid values: tls (direct TLS, e.g. port 465), starttls (STARTTLS upgrade, e.g. port 587), auto (port-based default).',
283
+ );
284
+ return;
285
+ }
286
+ coerced = rawValue;
287
+ } else if (fieldName === 'enabled') {
288
+ const lower = rawValue.toLowerCase();
289
+ if (lower === 'true' || lower === '1' || lower === 'yes') {
290
+ coerced = true;
291
+ } else if (lower === 'false' || lower === '0' || lower === 'no') {
292
+ coerced = false;
293
+ } else {
294
+ ctx.print(`Invalid boolean value: ${rawValue}. Use true or false.`);
295
+ return;
296
+ }
297
+ }
298
+
299
+ cm.setDynamic(configKey as unknown as ConfigKey, coerced);
300
+ cm.save();
301
+ ctx.print(`Email config updated\n key: ${configKey}\n value: ${String(coerced)}`);
302
+ }
303
+
304
+ async function handleSend(
305
+ args: readonly string[],
306
+ ctx: CommandContext,
307
+ ): Promise<void> {
308
+ const parsed = stripYesFlag(args);
309
+ const remaining = [...parsed.rest];
310
+
311
+ const sendArgs = parseSendArgs(remaining);
312
+ if (!sendArgs) {
313
+ ctx.print('Usage: /email send <to> <subject words...> [-- <body words...>] --yes\nRecipient and subject are required.');
314
+ return;
315
+ }
316
+
317
+ const { to, subject, body } = sendArgs;
318
+
319
+ if (!parsed.yes) {
320
+ const lines = [
321
+ 'Email Send Preview',
322
+ ` to: ${to}`,
323
+ ` subject: ${subject}`,
324
+ ` body: ${body || '(empty)'}`,
325
+ ' policy: this is a preview; rerun with --yes to send',
326
+ ];
327
+ ctx.print(lines.join('\n'));
328
+ requireYesFlag(ctx, `send email to ${to}`, `/email send ${to} ${subject}${body ? ` -- ${body}` : ''} --yes`);
329
+ return;
330
+ }
331
+
332
+ const service = buildEmailService(ctx);
333
+ await service.sendMail({
334
+ to,
335
+ subject,
336
+ body,
337
+ confirm: true,
338
+ });
339
+ ctx.print(`Email sent to ${to} with subject "${subject}"`);
340
+ }
341
+
342
+ export function registerEmailRuntimeCommands(registry: CommandRegistry): void {
343
+ registry.register({
344
+ name: 'email',
345
+ aliases: [],
346
+ description: 'Direct IMAP/SMTP email: configure, check inbox, or send a confirmed message',
347
+ hidden: true,
348
+ usage: 'status | config | set <key> <value> [--yes] | check [n] | send <to> <subject...> [-- <body...>] --yes',
349
+ argsHint: 'status|config|set|check|send',
350
+ async handler(args, ctx) {
351
+ const sub = (args[0] ?? 'status').trim().toLowerCase();
352
+
353
+ try {
354
+ if (sub === 'status') {
355
+ formatStatus(ctx);
356
+ return;
357
+ }
358
+
359
+ if (sub === 'config') {
360
+ formatConfig(ctx);
361
+ return;
362
+ }
363
+
364
+ if (sub === 'set') {
365
+ await handleSet(args.slice(1), ctx);
366
+ return;
367
+ }
368
+
369
+ if (sub === 'check') {
370
+ await handleCheck(args.slice(1), ctx);
371
+ return;
372
+ }
373
+
374
+ if (sub === 'send') {
375
+ await handleSend(args.slice(1), ctx);
376
+ return;
377
+ }
378
+
379
+ ctx.print('Usage: /email status | config | set <key> <value> [--yes] | check [n] | send <to> <subject...> [-- <body...>] --yes');
380
+ } catch (err) {
381
+ // MIN-3: print config/validation errors as plain language, never a raw exception
382
+ const msg = err instanceof Error ? err.message : String(err);
383
+ ctx.print(`Email error: ${msg}`);
384
+ }
385
+ },
386
+ });
387
+ }