@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.
- package/CHANGELOG.md +22 -0
- package/README.md +52 -7
- package/dist/package/main.js +7491 -12253
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/release-and-publishing.md +1 -1
- package/docs/tools-and-commands.md +5 -5
- package/package.json +4 -2
- package/release/live-verification/live-verification.json +13 -13
- package/release/live-verification/live-verification.md +15 -15
- package/release/performance-snapshot.json +2 -2
- package/release/release-notes.md +5 -7
- package/release/release-readiness.json +6 -6
- package/src/agent/behavior-discovery-summary.ts +4 -18
- package/src/agent/calendar-registry.ts +322 -0
- package/src/agent/competitive-feature-inventory.ts +268 -130
- package/src/agent/document-registry.ts +5 -1
- package/src/agent/email/email-service.ts +350 -0
- package/src/agent/email/imap-client.ts +596 -0
- package/src/agent/email/smtp-client.ts +453 -0
- package/src/agent/ics-calendar.ts +662 -0
- package/src/agent/ics-timezone.ts +172 -0
- package/src/agent/markdown-frontmatter.ts +31 -0
- package/src/agent/memory-safety.ts +1 -1
- package/src/agent/note-registry.ts +3 -9
- package/src/agent/persona-discovery.ts +3 -15
- package/src/agent/persona-registry.ts +1 -10
- package/src/agent/research-source-registry.ts +5 -1
- package/src/agent/routine-discovery.ts +3 -15
- package/src/agent/runtime-profile.ts +3 -0
- package/src/agent/skill-discovery.ts +3 -15
- package/src/agent/skill-draft-proposer.ts +203 -0
- package/src/agent/skill-draft-runner.ts +201 -0
- package/src/agent/skill-registry.ts +36 -1
- package/src/agent/skill-standard.ts +99 -0
- package/src/agent/vibe-file.ts +7 -22
- package/src/cli/completion.ts +1 -1
- package/src/cli/config-overrides.ts +10 -1
- package/src/cli/redaction.ts +17 -5
- package/src/config/provider-model.ts +2 -1
- package/src/config/secret-config.ts +13 -6
- package/src/core/activity-feed.ts +97 -0
- package/src/core/away-digest.ts +161 -0
- package/src/core/conversation-rendering.ts +7 -3
- package/src/core/conversation.ts +22 -15
- package/src/core/hardware-profile.ts +362 -0
- package/src/core/last-seen-store.ts +78 -0
- package/src/core/plain-language.ts +52 -0
- package/src/core/setup-incomplete-hint.ts +90 -0
- package/src/core/system-message-router.ts +38 -87
- package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
- package/src/input/agent-workspace-basic-command-editors.ts +39 -141
- package/src/input/agent-workspace-categories.ts +40 -125
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-host-category.ts +0 -5
- package/src/input/agent-workspace-local-editor-submission.ts +3 -1
- package/src/input/agent-workspace-navigation.ts +10 -3
- package/src/input/agent-workspace-onboarding-actions.ts +132 -0
- package/src/input/agent-workspace-onboarding-categories.ts +29 -26
- package/src/input/agent-workspace-onboarding-finish.ts +11 -4
- package/src/input/agent-workspace-onboarding-state.ts +111 -0
- package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
- package/src/input/agent-workspace-profile-editors.ts +155 -0
- package/src/input/agent-workspace-subscription-editor.ts +7 -0
- package/src/input/agent-workspace-types.ts +5 -1
- package/src/input/agent-workspace.ts +65 -39
- package/src/input/command-registry.ts +18 -5
- package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
- package/src/input/commands/agent-skills-runtime.ts +60 -3
- package/src/input/commands/calendar-runtime.ts +209 -0
- package/src/input/commands/channels-runtime.ts +1 -0
- package/src/input/commands/command-error.ts +9 -0
- package/src/input/commands/compat-runtime.ts +1 -0
- package/src/input/commands/config.ts +1 -0
- package/src/input/commands/conversation-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +5 -6
- package/src/input/commands/email-runtime.ts +387 -0
- package/src/input/commands/experience-runtime.ts +17 -4
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +6 -1
- package/src/input/commands/knowledge-format.ts +73 -0
- package/src/input/commands/knowledge.ts +9 -74
- package/src/input/commands/local-provider-runtime.ts +2 -1
- package/src/input/commands/local-runtime.ts +10 -4
- package/src/input/commands/mcp-runtime.ts +7 -0
- package/src/input/commands/notify-runtime.ts +17 -1
- package/src/input/commands/onboarding-runtime.ts +1 -0
- package/src/input/commands/operator-actions-runtime.ts +1 -0
- package/src/input/commands/operator-runtime.ts +3 -0
- package/src/input/commands/personas-runtime.ts +1 -0
- package/src/input/commands/platform-access-runtime.ts +40 -17
- package/src/input/commands/product-runtime.ts +6 -1
- package/src/input/commands/provider-accounts-runtime.ts +3 -2
- package/src/input/commands/qrcode-runtime.ts +1 -0
- package/src/input/commands/routines-runtime.ts +1 -0
- package/src/input/commands/runtime-services.ts +0 -13
- package/src/input/commands/security-runtime.ts +1 -0
- package/src/input/commands/session-content.ts +1 -0
- package/src/input/commands/shell-core.ts +5 -2
- package/src/input/commands/subscription-runtime.ts +33 -9
- package/src/input/commands/support-bundle-runtime.ts +8 -1
- package/src/input/commands/tasks-runtime.ts +1 -0
- package/src/input/commands/tts-runtime.ts +1 -0
- package/src/input/commands/vibe-runtime.ts +1 -0
- package/src/input/commands.ts +4 -0
- package/src/input/feed-context-factory.ts +3 -12
- package/src/input/handler-command-route.ts +7 -60
- package/src/input/handler-feed-routes.ts +0 -194
- package/src/input/handler-feed.ts +2 -54
- package/src/input/handler-interactions.ts +0 -2
- package/src/input/handler-modal-stack.ts +0 -9
- package/src/input/handler-picker-routes.ts +24 -1
- package/src/input/handler-shortcuts.ts +11 -40
- package/src/input/handler-ui-state.ts +13 -0
- package/src/input/handler.ts +8 -35
- package/src/input/keybindings.ts +40 -17
- package/src/input/model-picker-local-fit.ts +130 -0
- package/src/input/shell-passthrough.ts +58 -0
- package/src/input/submission-router.ts +0 -5
- package/src/main.ts +129 -90
- package/src/renderer/activity-sidebar.ts +186 -0
- package/src/renderer/agent-workspace-context-lines.ts +5 -48
- package/src/renderer/agent-workspace-style.ts +1 -1
- package/src/renderer/agent-workspace.ts +14 -2
- package/src/renderer/compositor.ts +37 -125
- package/src/renderer/conversation-overlays.ts +3 -3
- package/src/renderer/help-overlay.ts +7 -5
- package/src/renderer/model-workspace.ts +101 -86
- package/src/{panels → renderer}/polish.ts +3 -3
- package/src/renderer/shell-surface.ts +4 -5
- package/src/renderer/status-token.ts +31 -11
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/tool-call.ts +7 -8
- package/src/renderer/tool-labels.ts +92 -0
- package/src/renderer/ui-factory.ts +48 -96
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +6 -15
- package/src/runtime/bootstrap-core.ts +34 -13
- package/src/runtime/bootstrap-hook-bridge.ts +3 -1
- package/src/runtime/bootstrap-shell.ts +3 -50
- package/src/runtime/bootstrap.ts +3 -4
- package/src/runtime/context.ts +1 -1
- package/src/runtime/diagnostics/panels/index.ts +0 -1
- package/src/runtime/diagnostics/panels/policy.ts +1 -1
- package/src/runtime/execution-ledger.ts +16 -1
- package/src/runtime/index.ts +6 -1
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/onboarding-state.ts +200 -0
- package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
- package/src/runtime/services.ts +16 -4
- package/src/runtime/terminal-output-guard.ts +7 -0
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/runtime/ui/model-picker/data-provider.ts +1 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
- package/src/runtime/ui/model-picker/types.ts +2 -2
- package/src/runtime/ui/provider-health/data-provider.ts +3 -3
- package/src/runtime/ui/provider-health/types.ts +2 -2
- package/src/runtime/ui-services.ts +0 -2
- package/src/shell/agent-workspace-fullscreen.ts +0 -1
- package/src/shell/autonomy-surfacing.ts +272 -0
- package/src/shell/session-continuity-hints.ts +0 -6
- package/src/shell/startup-wiring.ts +221 -0
- package/src/shell/ui-openers.ts +18 -29
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +4 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
- package/src/tools/agent-harness-command-runner.ts +6 -1
- package/src/tools/agent-harness-document-ops.ts +26 -53
- package/src/tools/agent-harness-execution-history.ts +1 -13
- package/src/tools/agent-harness-execution-posture.ts +0 -15
- package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
- package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
- package/src/tools/agent-harness-metadata.ts +16 -0
- package/src/tools/agent-harness-mode-catalog.ts +2 -9
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
- package/src/tools/agent-harness-tool-schema.ts +13 -14
- package/src/tools/agent-harness-tool.ts +27 -23
- package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
- package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
- package/src/tools/agent-workspace-tool.ts +2 -33
- package/src/utils/terminal-width.ts +9 -3
- package/src/version.ts +1 -1
- package/src/input/agent-workspace-panel-route.ts +0 -44
- package/src/input/panel-integration-actions.ts +0 -26
- package/src/panels/approval-panel.ts +0 -149
- package/src/panels/automation-control-panel.ts +0 -212
- package/src/panels/base-panel.ts +0 -254
- package/src/panels/builtin/agent.ts +0 -58
- package/src/panels/builtin/knowledge.ts +0 -26
- package/src/panels/builtin/operations.ts +0 -121
- package/src/panels/builtin/session.ts +0 -138
- package/src/panels/builtin/shared.ts +0 -275
- package/src/panels/builtin/usage.ts +0 -21
- package/src/panels/builtin-panels.ts +0 -23
- package/src/panels/confirm-state.ts +0 -61
- package/src/panels/context-visualizer-panel.ts +0 -204
- package/src/panels/cost-tracker-panel.ts +0 -444
- package/src/panels/docs-panel.ts +0 -285
- package/src/panels/index.ts +0 -25
- package/src/panels/knowledge-panel.ts +0 -417
- package/src/panels/memory-panel.ts +0 -226
- package/src/panels/panel-list-panel.ts +0 -464
- package/src/panels/panel-manager.ts +0 -570
- package/src/panels/provider-accounts-panel.ts +0 -233
- package/src/panels/provider-health-domains.ts +0 -208
- package/src/panels/provider-health-panel.ts +0 -720
- package/src/panels/provider-health-tracker.ts +0 -115
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/qr-panel.ts +0 -207
- package/src/panels/schedule-panel.ts +0 -321
- package/src/panels/scrollable-list-panel.ts +0 -491
- package/src/panels/search-focus.ts +0 -32
- package/src/panels/security-panel.ts +0 -295
- package/src/panels/session-browser-panel.ts +0 -395
- package/src/panels/session-maintenance.ts +0 -125
- package/src/panels/subscription-panel.ts +0 -263
- package/src/panels/system-messages-panel.ts +0 -230
- package/src/panels/tasks-panel.ts +0 -344
- package/src/panels/thinking-panel.ts +0 -304
- package/src/panels/token-budget-panel.ts +0 -475
- package/src/panels/tool-inspector-panel.ts +0 -436
- package/src/panels/types.ts +0 -54
- package/src/renderer/panel-composite.ts +0 -158
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/renderer/panel-workspace-bar.ts +0 -42
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/tools/agent-harness-panel-metadata.ts +0 -211
- /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 = (
|
|
97
|
-
const input = parseDelegationArgs(args,
|
|
96
|
+
const makeHandler = () => async (args: string[], ctx: CommandContext): Promise<void> => {
|
|
97
|
+
const input = parseDelegationArgs(args, false);
|
|
98
98
|
if (!input.task) {
|
|
99
|
-
ctx.print(
|
|
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(
|
|
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
|
+
}
|