@pellux/goodvibes-agent 1.5.5 → 1.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -1,4 +1,6 @@
1
1
  import type { AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
2
+ import type { AgentWorkspaceEditorSpecEntry } from './agent-workspace-command-editor-engine.ts';
3
+ import { createAgentWorkspaceEditorFromTable } from './agent-workspace-command-editor-engine.ts';
2
4
 
3
5
  export type AgentWorkspaceChannelCommandEditorKind = Extract<
4
6
  AgentWorkspaceEditorKind,
@@ -9,51 +11,41 @@ export function isAgentWorkspaceChannelCommandEditorKind(kind: AgentWorkspaceEdi
9
11
  return kind === 'channel-show' || kind === 'channel-doctor' || kind === 'channel-setup' || kind === 'channel-send';
10
12
  }
11
13
 
12
- export function createAgentWorkspaceChannelCommandEditor(kind: AgentWorkspaceChannelCommandEditorKind): AgentWorkspaceLocalEditor {
13
- if (kind === 'channel-send') {
14
- return {
15
- kind,
16
- mode: 'create',
17
- title: 'Send Channel Message',
18
- selectedFieldIndex: 0,
19
- message: 'Send one message through a configured delivery target. Fill exactly one target field and type yes to confirm.',
20
- fields: [
21
- { id: 'message', label: 'Message', value: '', required: true, multiline: true, hint: 'Plain-text message. Ctrl-J inserts a new line.' },
22
- { id: 'title', label: 'Title', value: '', required: false, multiline: false, hint: 'Optional delivery title. Blank uses the Agent default.' },
23
- { id: 'channel', label: 'Channel target', value: '', required: false, multiline: false, hint: 'Optional surface[:route[:label]], such as slack:ops:Ops or telephony:+15551234567.' },
24
- { id: 'route', label: 'Route target', value: '', required: false, multiline: false, hint: 'Optional route id or route:label.' },
25
- { id: 'webhook', label: 'Webhook URL', value: '', required: false, multiline: false, hint: 'Optional http(s) webhook target.' },
26
- { id: 'link', label: 'Link target', value: '', required: false, multiline: false, hint: 'Optional link delivery target.' },
27
- { id: 'confirm', label: 'Confirm', value: '', required: true, multiline: false, hint: 'Type yes to run /channels send with --yes.' },
28
- ],
29
- };
30
- }
31
- if (kind === 'channel-doctor') {
32
- return {
33
- kind,
34
- mode: 'create',
35
- title: 'Run Channel Doctor',
36
- selectedFieldIndex: 0,
37
- message: 'Inspect one connected channel route without sending messages or changing delivery state.',
38
- fields: [
39
- { id: 'channel', label: 'Channel id', value: '', required: true, multiline: false, hint: 'Channel id, such as slack, telegram, discord, ntfy, signal, whatsapp, or telephony.' },
40
- ],
41
- };
42
- }
43
- if (kind === 'channel-setup') {
44
- return {
45
- kind,
46
- mode: 'create',
47
- title: 'Show Channel Setup',
48
- selectedFieldIndex: 0,
49
- message: 'Show read-only setup guidance for one channel. This does not pair, enable, or send through the channel.',
50
- fields: [
51
- { id: 'channel', label: 'Channel id', value: '', required: true, multiline: false, hint: 'Channel id, such as slack, telegram, discord, ntfy, signal, whatsapp, or telephony.' },
52
- ],
53
- };
54
- }
55
- return {
56
- kind,
14
+ const CHANNEL_COMMAND_EDITOR_SPECS: Readonly<Record<AgentWorkspaceChannelCommandEditorKind, AgentWorkspaceEditorSpecEntry<AgentWorkspaceChannelCommandEditorKind>>> = {
15
+ 'channel-send': {
16
+ mode: 'create',
17
+ title: 'Send Channel Message',
18
+ selectedFieldIndex: 0,
19
+ message: 'Send one message through a configured delivery target. Fill exactly one target field and type yes to confirm.',
20
+ fields: [
21
+ { id: 'message', label: 'Message', value: '', required: true, multiline: true, hint: 'Plain-text message. Ctrl-J inserts a new line.' },
22
+ { id: 'title', label: 'Title', value: '', required: false, multiline: false, hint: 'Optional delivery title. Blank uses the Agent default.' },
23
+ { id: 'channel', label: 'Channel target', value: '', required: false, multiline: false, hint: 'Optional surface[:route[:label]], such as slack:ops:Ops or telephony:+15551234567.' },
24
+ { id: 'route', label: 'Route target', value: '', required: false, multiline: false, hint: 'Optional route id or route:label.' },
25
+ { id: 'webhook', label: 'Webhook URL', value: '', required: false, multiline: false, hint: 'Optional http(s) webhook target.' },
26
+ { id: 'link', label: 'Link target', value: '', required: false, multiline: false, hint: 'Optional link delivery target.' },
27
+ { id: 'confirm', label: 'Confirm', value: '', required: true, multiline: false, hint: 'Type yes to run /channels send with --yes.' },
28
+ ],
29
+ },
30
+ 'channel-doctor': {
31
+ mode: 'create',
32
+ title: 'Run Channel Doctor',
33
+ selectedFieldIndex: 0,
34
+ message: 'Inspect one connected channel route without sending messages or changing delivery state.',
35
+ fields: [
36
+ { id: 'channel', label: 'Channel id', value: '', required: true, multiline: false, hint: 'Channel id, such as slack, telegram, discord, ntfy, signal, whatsapp, or telephony.' },
37
+ ],
38
+ },
39
+ 'channel-setup': {
40
+ mode: 'create',
41
+ title: 'Show Channel Setup',
42
+ selectedFieldIndex: 0,
43
+ message: 'Show read-only setup guidance for one channel. This does not pair, enable, or send through the channel.',
44
+ fields: [
45
+ { id: 'channel', label: 'Channel id', value: '', required: true, multiline: false, hint: 'Channel id, such as slack, telegram, discord, ntfy, signal, whatsapp, or telephony.' },
46
+ ],
47
+ },
48
+ 'channel-show': {
57
49
  mode: 'create',
58
50
  title: 'Show Channel Detail',
59
51
  selectedFieldIndex: 0,
@@ -61,5 +53,9 @@ export function createAgentWorkspaceChannelCommandEditor(kind: AgentWorkspaceCha
61
53
  fields: [
62
54
  { id: 'channel', label: 'Channel id', value: '', required: true, multiline: false, hint: 'Channel id, such as slack, telegram, discord, ntfy, signal, whatsapp, or telephony.' },
63
55
  ],
64
- };
56
+ },
57
+ };
58
+
59
+ export function createAgentWorkspaceChannelCommandEditor(kind: AgentWorkspaceChannelCommandEditorKind): AgentWorkspaceLocalEditor {
60
+ return createAgentWorkspaceEditorFromTable(kind, CHANNEL_COMMAND_EDITOR_SPECS);
65
61
  }
@@ -0,0 +1,133 @@
1
+ // Shared engine for the agent-workspace "command editor" domains (access, channel,
2
+ // knowledge, library, mcp, media, memory, operations, provider, session, skill-bundle,
3
+ // task). Every one of those domains follows the same two-stage shape: a declarative
4
+ // field-spec factory (kind -> { title, mode, message, fields }) and a submission step
5
+ // that reads the filled fields and builds a slash-command string handed to the
6
+ // shell-owned command router. This module factors that repeated structural pattern
7
+ // (the editor-construction factory, the submission dispatch/blocked-editor result
8
+ // shapes, and the handful of small helpers every domain re-implemented locally) into
9
+ // one place. Each domain keeps its own field content and command-building logic
10
+ // (real product value, not boilerplate) as entries in a `Record<Kind, ...>` table.
11
+ //
12
+ // W4-H2 (Wave 4): consolidates the ~26 near-identical editor+submission file pairs
13
+ // identified by the W4-H1 design ruling into this engine + per-domain data tables.
14
+ import type { AgentWorkspaceActionResult, AgentWorkspaceEditorField, AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
15
+ import { quoteSlashCommandArg } from './slash-command-parser.ts';
16
+
17
+ export type AgentWorkspaceFieldReader = (fieldId: string) => string;
18
+
19
+ /** The data shape every domain's per-kind editor literal already followed. */
20
+ export interface AgentWorkspaceEditorSpec {
21
+ readonly mode: 'create' | 'update' | 'delete';
22
+ readonly title: string;
23
+ readonly message: string;
24
+ readonly selectedFieldIndex: number;
25
+ readonly fields: readonly AgentWorkspaceEditorField[];
26
+ }
27
+
28
+ /**
29
+ * A table entry is either a static spec, or (for the handful of kinds whose wording
30
+ * depends on which kind was requested, such as voice-enable/voice-disable sharing one
31
+ * shape) a function of the kind that returns one.
32
+ */
33
+ export type AgentWorkspaceEditorSpecEntry<K extends string> =
34
+ | AgentWorkspaceEditorSpec
35
+ | ((kind: K) => AgentWorkspaceEditorSpec);
36
+
37
+ export function createAgentWorkspaceEditorFromTable<K extends string>(
38
+ kind: K,
39
+ table: Readonly<Record<K, AgentWorkspaceEditorSpecEntry<K>>>,
40
+ ): AgentWorkspaceLocalEditor {
41
+ const entry = table[kind];
42
+ const spec: AgentWorkspaceEditorSpec = typeof entry === 'function' ? (entry as (kind: K) => AgentWorkspaceEditorSpec)(kind) : (entry as AgentWorkspaceEditorSpec);
43
+ return {
44
+ kind: kind as AgentWorkspaceEditorKind,
45
+ mode: spec.mode,
46
+ title: spec.title,
47
+ selectedFieldIndex: spec.selectedFieldIndex,
48
+ message: spec.message,
49
+ fields: spec.fields,
50
+ };
51
+ }
52
+
53
+ export type AgentWorkspaceCommandEditorSubmission =
54
+ | {
55
+ readonly kind: 'editor';
56
+ readonly editor: AgentWorkspaceLocalEditor;
57
+ readonly status: string;
58
+ readonly actionResult?: AgentWorkspaceActionResult;
59
+ }
60
+ | {
61
+ readonly kind: 'dispatch';
62
+ readonly command: string;
63
+ readonly status: string;
64
+ readonly actionResult: AgentWorkspaceActionResult;
65
+ };
66
+
67
+ export type AgentWorkspaceCommandDispatchSafety = NonNullable<AgentWorkspaceActionResult['safety']>;
68
+
69
+ export function isAffirmative(value: string): boolean {
70
+ return /^(y|yes|true)$/i.test(value.trim());
71
+ }
72
+
73
+ export function splitCommaList(value: string): readonly string[] {
74
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
75
+ }
76
+
77
+ /** Pushes `flag, quoteSlashCommandArg(value)` onto `parts` when `value` is non-blank. */
78
+ export function appendOptionalArg(parts: string[], flag: string, value: string): void {
79
+ if (value.trim().length > 0) parts.push(flag, quoteSlashCommandArg(value));
80
+ }
81
+
82
+ /**
83
+ * The confirmed/dispatched result shape every domain built by hand: status is always
84
+ * `${title}.`, and the actionResult always carries kind: 'dispatched' with the same
85
+ * command, title, detail, and safety. Kinds whose actionResult.command legitimately
86
+ * differs from the dispatched command (for example a redacted secret) skip this
87
+ * helper and build the literal directly, same as before consolidation.
88
+ */
89
+ export function dispatchCommandEditorSubmission(
90
+ command: string,
91
+ title: string,
92
+ detail: string,
93
+ safety: AgentWorkspaceCommandDispatchSafety,
94
+ ): AgentWorkspaceCommandEditorSubmission {
95
+ return {
96
+ kind: 'dispatch',
97
+ command,
98
+ status: `${title}.`,
99
+ actionResult: { kind: 'dispatched', title, detail, command, safety },
100
+ };
101
+ }
102
+
103
+ /**
104
+ * The "stay in the editor" result shape used for unconfirmed actions and other
105
+ * validation failures: the editor's message is replaced with `message`, and status
106
+ * defaults to the same text (the convention most domains used). Pass an explicit
107
+ * `status` for the domains that show a shorter status line than the in-editor message.
108
+ */
109
+ export function editorMessageSubmission(
110
+ editor: AgentWorkspaceLocalEditor,
111
+ message: string,
112
+ status: string = message,
113
+ ): AgentWorkspaceCommandEditorSubmission {
114
+ return {
115
+ kind: 'editor',
116
+ editor: { ...editor, message },
117
+ status,
118
+ };
119
+ }
120
+
121
+ export type AgentWorkspaceCommandSubmissionHandler = (
122
+ editor: AgentWorkspaceLocalEditor,
123
+ readField: AgentWorkspaceFieldReader,
124
+ ) => AgentWorkspaceCommandEditorSubmission;
125
+
126
+ export function buildCommandEditorSubmissionFromTable<K extends string>(
127
+ kind: K,
128
+ editor: AgentWorkspaceLocalEditor,
129
+ readField: AgentWorkspaceFieldReader,
130
+ table: Readonly<Record<K, AgentWorkspaceCommandSubmissionHandler>>,
131
+ ): AgentWorkspaceCommandEditorSubmission {
132
+ return table[kind](editor, readField);
133
+ }
@@ -10,6 +10,7 @@ import { buildAgentResearchReportPromptSubmission } from './agent-workspace-rese
10
10
  import { buildAgentResearchRunPromptSubmission } from './agent-workspace-research-run-editor.ts';
11
11
  import { buildAgentResearchSourcePromptSubmission } from './agent-workspace-research-source-editor.ts';
12
12
  import { buildAgentReminderScheduleEditorSubmission } from './agent-workspace-reminder-schedule-editor.ts';
13
+ import { buildAgentWorkspaceCalendarConnectEditorSubmission } from './agent-workspace-calendar-connect-editor.ts';
13
14
  import { buildAgentRoutineScheduleEditorSubmission } from './agent-workspace-routine-schedule-editor.ts';
14
15
  import { buildAgentWorkspaceWebResearchSubmission } from './agent-workspace-web-research-editor.ts';
15
16
  import type { AgentWorkspaceActionResult, AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
@@ -69,6 +70,7 @@ type AgentWorkspaceCommandEditorKind = AgentWorkspaceBasicCommandEditorKind | Ex
69
70
  | 'secret-delete'
70
71
  | 'routine-schedule'
71
72
  | 'reminder-schedule'
73
+ | 'calendar-connect'
72
74
  | 'knowledge-bookmarks'
73
75
  | 'tts-prompt'
74
76
  | 'image-input'
@@ -166,6 +168,7 @@ export function isAgentWorkspaceCommandEditorKind(kind: AgentWorkspaceEditorKind
166
168
  || kind === 'model-compare-analytics'
167
169
  || kind === 'routine-schedule'
168
170
  || kind === 'reminder-schedule'
171
+ || kind === 'calendar-connect'
169
172
  || isAgentWorkspaceBasicCommandEditorKind(kind);
170
173
  }
171
174
 
@@ -263,5 +266,6 @@ export function buildAgentWorkspaceCommandEditorSubmission(
263
266
  return buildAgentWorkspaceBasicCommandEditorSubmission(editor, readField, commandDispatchAvailable);
264
267
  }
265
268
  if (editor.kind === 'reminder-schedule') return buildAgentReminderScheduleEditorSubmission(editor, readField, commandDispatchAvailable);
269
+ if (editor.kind === 'calendar-connect') return buildAgentWorkspaceCalendarConnectEditorSubmission(editor, readField, commandDispatchAvailable);
266
270
  return buildAgentRoutineScheduleEditorSubmission(editor, readField, commandDispatchAvailable);
267
271
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Editor kinds that perform a direct host action on submit (OAuth calls,
3
+ * config/secrets writes) rather than building a slash command for the
4
+ * generic command-editor dispatch pipeline (agent-workspace-command-editor.ts).
5
+ * Centralizing the dispatch here keeps AgentWorkspace.submitLocalEditor()
6
+ * under the architecture line cap as new direct-action editor kinds are
7
+ * added (W4-A5 added email-connect-wizard alongside the existing
8
+ * subscription-login-start/finish/logout kinds).
9
+ */
10
+ import type { CommandContext } from './command-registry.ts';
11
+ import type { AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
12
+ import {
13
+ submitAgentWorkspaceSubscriptionLoginFinishEditor,
14
+ submitAgentWorkspaceSubscriptionLoginStartEditor,
15
+ submitAgentWorkspaceSubscriptionLogoutEditor,
16
+ type AgentWorkspaceSubscriptionEditorHost,
17
+ } from './agent-workspace-subscription-editor.ts';
18
+ import { submitAgentWorkspaceEmailConnectWizardEditor, type AgentWorkspaceEmailConnectEditorHost } from './agent-workspace-email-connect-editor.ts';
19
+ import { submitAgentWorkspaceCalendarSubscribeWizardEditor, type AgentWorkspaceCalendarSubscribeEditorHost } from './agent-workspace-calendar-subscribe-editor.ts';
20
+ import { submitAgentWorkspaceCalendarOAuthEditor, type AgentWorkspaceCalendarOAuthEditorHost } from './agent-workspace-calendar-oauth-editor.ts';
21
+
22
+ type FieldReader = (id: string) => string;
23
+ type DirectEditorHost = AgentWorkspaceSubscriptionEditorHost & AgentWorkspaceEmailConnectEditorHost & AgentWorkspaceCalendarSubscribeEditorHost & AgentWorkspaceCalendarOAuthEditorHost;
24
+
25
+ /** Returns true when this editor kind was handled directly (caller should return without further dispatch). */
26
+ export function trySubmitDirectHostActionEditor(
27
+ host: DirectEditorHost,
28
+ editor: AgentWorkspaceLocalEditor,
29
+ context: CommandContext | null,
30
+ readField: FieldReader,
31
+ requestRender?: () => void,
32
+ ): boolean {
33
+ if (editor.kind === 'subscription-login-start') {
34
+ void submitAgentWorkspaceSubscriptionLoginStartEditor(host, editor, context, readField).finally(() => requestRender?.());
35
+ return true;
36
+ }
37
+ if (editor.kind === 'subscription-login-finish') {
38
+ void submitAgentWorkspaceSubscriptionLoginFinishEditor(host, editor, context, readField).finally(() => requestRender?.());
39
+ return true;
40
+ }
41
+ if (editor.kind === 'subscription-logout') {
42
+ submitAgentWorkspaceSubscriptionLogoutEditor(host, editor, context, readField);
43
+ requestRender?.();
44
+ return true;
45
+ }
46
+ if (editor.kind === 'email-connect-wizard') {
47
+ void submitAgentWorkspaceEmailConnectWizardEditor(host, editor, context, readField).finally(() => requestRender?.());
48
+ return true;
49
+ }
50
+ if (editor.kind === 'calendar-subscribe-wizard') {
51
+ void submitAgentWorkspaceCalendarSubscribeWizardEditor(host, editor, context, readField).finally(() => requestRender?.());
52
+ return true;
53
+ }
54
+ if (editor.kind === 'calendar-oauth-google' || editor.kind === 'calendar-oauth-outlook') {
55
+ void submitAgentWorkspaceCalendarOAuthEditor(host, editor, context, readField).finally(() => requestRender?.());
56
+ return true;
57
+ }
58
+ return false;
59
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Email connect wizard (W4-A5) — promotes the "Inbox workflows" workspace
3
+ * card from a dead guidance card into a real, stepped connect flow.
4
+ *
5
+ * Security: the raw password NEVER travels through the generic slash-command
6
+ * dispatch pipeline (AgentWorkspace.dispatchWorkspaceCommand), because that
7
+ * pipeline echoes its `command` argument back into
8
+ * AgentWorkspace.lastActionResult.command for the 'inline' execution path
9
+ * (see handler.ts dispatchAgentWorkspaceCommand) — a raw command string
10
+ * containing the password would render it in the workspace UI. Instead this
11
+ * editor is a DIRECT host action (the same pattern as the subscription
12
+ * login editors in agent-workspace-subscription-editor.ts): it calls
13
+ * persistEmailConfigField() and EmailService.testConnection() directly
14
+ * against ctx.platform.configManager / ctx.platform.secretsManager and never
15
+ * builds a command string. Credential persistence reuses the exact same
16
+ * persistEmailConfigField() helper the /email set CLI command uses — one
17
+ * storage path, not a fork.
18
+ */
19
+ import type { CommandContext } from './command-registry.ts';
20
+ import { isAffirmative } from './agent-workspace-editors.ts';
21
+ import { EmailService, ensureEmailConfigDefaults, type EmailConnectionTestResult } from '../agent/email/email-service.ts';
22
+ import { requireSecretsManager } from './commands/runtime-services.ts';
23
+ import { persistEmailConfigField } from './commands/email-runtime.ts';
24
+ import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
25
+ import type { AgentWorkspaceActionResult, AgentWorkspaceEmailConnectStatus, AgentWorkspaceLocalEditor, AgentWorkspaceRuntimeSnapshot } from './agent-workspace-types.ts';
26
+
27
+ type FieldReader = (id: string) => string;
28
+
29
+ export interface AgentWorkspaceEmailConnectEditorHost {
30
+ localEditor: AgentWorkspaceLocalEditor | null;
31
+ runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null;
32
+ status: string;
33
+ lastActionResult: AgentWorkspaceActionResult | null;
34
+ }
35
+
36
+ /** Fields, in wizard-step order. imapPort/smtpPort/smtpSecurity carry sane defaults. */
37
+ export function createEmailConnectWizardEditor(status: AgentWorkspaceEmailConnectStatus | null): AgentWorkspaceLocalEditor {
38
+ const message = !status
39
+ ? 'Connect your inbox. Enter your IMAP/SMTP settings; the password is stored through the Agent secret manager, never in plain config.'
40
+ : status.connected
41
+ ? `Currently connected as ${status.username} (${status.imapHost}). Re-enter settings below to reconnect or update credentials.`
42
+ : status.errors.length > 0
43
+ ? 'Not connected — connect your inbox. Enter your IMAP/SMTP settings below.'
44
+ : 'Not connected — connect your inbox. Enter your IMAP/SMTP settings below.';
45
+ return {
46
+ kind: 'email-connect-wizard',
47
+ mode: 'create',
48
+ title: 'Connect Inbox',
49
+ selectedFieldIndex: 0,
50
+ message,
51
+ fields: [
52
+ { id: 'imapHost', label: 'IMAP host', value: status?.imapHost ?? '', required: true, multiline: false, hint: 'For example imap.gmail.com.' },
53
+ { id: 'imapPort', label: 'IMAP port', value: '993', required: true, multiline: false, hint: 'Default 993 (implicit TLS).' },
54
+ { id: 'smtpHost', label: 'SMTP host', value: '', required: true, multiline: false, hint: 'For example smtp.gmail.com.' },
55
+ { id: 'smtpPort', label: 'SMTP port', value: '587', required: true, multiline: false, hint: 'Default 587 (STARTTLS) or 465 (TLS).' },
56
+ { id: 'smtpSecurity', label: 'SMTP security', value: 'auto', required: true, multiline: false, hint: 'tls, starttls, or auto (port-based).' },
57
+ { id: 'username', label: 'Username', value: status?.username ?? '', required: true, multiline: false, hint: 'Mailbox login, usually your email address.' },
58
+ { id: 'password', label: 'Password', value: '', required: true, multiline: false, hint: 'Stored through the Agent secret manager. Masked here and never rendered in results.', redact: true },
59
+ { id: 'fromAddress', label: 'From address', value: status?.username ?? '', required: true, multiline: false, hint: 'Address used in the From: header when sending.' },
60
+ { id: 'confirm', label: 'Confirm', value: '', required: true, multiline: false, hint: 'Type yes to store these settings and test the connection.' },
61
+ ],
62
+ };
63
+ }
64
+
65
+ /** Field ids that persistEmailConfigField expects, in order — 'password' input maps to the 'passwordRef' config field. */
66
+ const FIELD_TO_EMAIL_KEY: Record<string, string> = {
67
+ imapHost: 'imapHost',
68
+ imapPort: 'imapPort',
69
+ smtpHost: 'smtpHost',
70
+ smtpPort: 'smtpPort',
71
+ smtpSecurity: 'smtpSecurity',
72
+ username: 'username',
73
+ fromAddress: 'fromAddress',
74
+ };
75
+
76
+ export type EmailConnectionTester = (
77
+ getConfig: (key: string) => unknown,
78
+ secretsManager: { readonly get: (key: string) => Promise<string | null> },
79
+ ) => { testConnection(): Promise<EmailConnectionTestResult> };
80
+
81
+ const defaultTester: EmailConnectionTester = (getConfig, secretsManager) => new EmailService({ getConfig, secretsManager });
82
+
83
+ export async function submitAgentWorkspaceEmailConnectWizardEditor(
84
+ host: AgentWorkspaceEmailConnectEditorHost,
85
+ editor: AgentWorkspaceLocalEditor,
86
+ context: CommandContext | null,
87
+ readField: FieldReader,
88
+ /** Injectable for tests — avoids real network I/O. Production callers omit this and get the real EmailService. */
89
+ buildTester: EmailConnectionTester = defaultTester,
90
+ ): Promise<void> {
91
+ if (!isAffirmative(readField('confirm'))) {
92
+ host.localEditor = { ...editor, message: 'Inbox connection not confirmed. Type yes, then press Enter.' };
93
+ host.status = 'Inbox connection not confirmed.';
94
+ return;
95
+ }
96
+ if (!context) {
97
+ host.localEditor = { ...editor, message: 'This runtime has no command context; cannot connect the inbox.' };
98
+ host.status = 'Inbox connect unavailable.';
99
+ host.lastActionResult = { kind: 'error', title: 'Inbox connect unavailable', detail: 'No command context is available in this runtime.', safety: 'safe' };
100
+ return;
101
+ }
102
+
103
+ try {
104
+ const secretsManager = requireSecretsManager(context);
105
+ ensureEmailConfigDefaults(context.platform.configManager);
106
+ const cm = context.platform.configManager as unknown as {
107
+ get: (key: string) => unknown;
108
+ setDynamic: (key: string, value: unknown) => void;
109
+ save: () => void;
110
+ };
111
+
112
+ for (const [fieldId, emailKey] of Object.entries(FIELD_TO_EMAIL_KEY)) {
113
+ const result = await persistEmailConfigField(cm, secretsManager, emailKey, readField(fieldId));
114
+ if (!result.ok) {
115
+ host.localEditor = { ...editor, message: result.error ?? `Could not save ${fieldId}.` };
116
+ host.status = result.error ?? 'Inbox connection settings were invalid.';
117
+ host.lastActionResult = { kind: 'error', title: 'Inbox settings invalid', detail: result.error ?? `Could not save ${fieldId}.`, safety: 'safe' };
118
+ return;
119
+ }
120
+ }
121
+ const passwordResult = await persistEmailConfigField(cm, secretsManager, 'passwordRef', readField('password'));
122
+ if (!passwordResult.ok) {
123
+ host.localEditor = { ...editor, message: passwordResult.error ?? 'Could not store the password.' };
124
+ host.status = passwordResult.error ?? 'Inbox password could not be stored.';
125
+ host.lastActionResult = { kind: 'error', title: 'Inbox password invalid', detail: passwordResult.error ?? 'Could not store the password.', safety: 'safe' };
126
+ return;
127
+ }
128
+ cm.save();
129
+
130
+ const service = buildTester((key: string) => cm.get(key), secretsManager);
131
+ const test = await service.testConnection();
132
+ if (!test.ok) {
133
+ host.localEditor = null;
134
+ host.status = `Inbox connection failed (${test.stage ?? 'unknown'}).`;
135
+ host.lastActionResult = {
136
+ kind: 'error',
137
+ title: 'Inbox connection failed',
138
+ detail: `Could not verify the mailbox (${test.stage ?? 'unknown'} stage): ${test.error ?? 'unknown error'}. Settings were saved — fix the value above and reconnect.`,
139
+ safety: 'safe',
140
+ };
141
+ host.runtimeSnapshot = buildAgentWorkspaceRuntimeSnapshot(context);
142
+ return;
143
+ }
144
+
145
+ cm.setDynamic('email.enabled', true);
146
+ cm.save();
147
+ host.localEditor = null;
148
+ host.status = 'Inbox connected.';
149
+ host.lastActionResult = {
150
+ kind: 'refreshed',
151
+ title: 'Inbox connected',
152
+ detail: `IMAP and SMTP were verified for ${readField('username')}. Inbox workflows are ready.`,
153
+ safety: 'safe',
154
+ };
155
+ host.runtimeSnapshot = buildAgentWorkspaceRuntimeSnapshot(context);
156
+ } catch (error) {
157
+ const detail = error instanceof Error ? error.message : String(error);
158
+ host.localEditor = { ...editor, message: detail };
159
+ host.status = detail;
160
+ host.lastActionResult = { kind: 'error', title: 'Inbox connect failed', detail, safety: 'safe' };
161
+ }
162
+ }