@pellux/goodvibes-agent 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -7
  3. package/dist/package/main.js +7424 -12241
  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/performance-snapshot.json +2 -2
  11. package/release/release-notes.md +11 -7
  12. package/release/release-readiness.json +6 -6
  13. package/src/agent/behavior-discovery-summary.ts +4 -18
  14. package/src/agent/calendar-registry.ts +322 -0
  15. package/src/agent/competitive-feature-inventory.ts +268 -130
  16. package/src/agent/document-registry.ts +5 -1
  17. package/src/agent/email/email-service.ts +350 -0
  18. package/src/agent/email/imap-client.ts +596 -0
  19. package/src/agent/email/smtp-client.ts +453 -0
  20. package/src/agent/ics-calendar.ts +662 -0
  21. package/src/agent/ics-timezone.ts +172 -0
  22. package/src/agent/markdown-frontmatter.ts +31 -0
  23. package/src/agent/memory-safety.ts +1 -1
  24. package/src/agent/note-registry.ts +3 -9
  25. package/src/agent/persona-discovery.ts +3 -15
  26. package/src/agent/persona-registry.ts +1 -10
  27. package/src/agent/research-source-registry.ts +5 -1
  28. package/src/agent/routine-discovery.ts +3 -15
  29. package/src/agent/runtime-profile.ts +3 -0
  30. package/src/agent/skill-discovery.ts +3 -15
  31. package/src/agent/skill-draft-proposer.ts +203 -0
  32. package/src/agent/skill-draft-runner.ts +201 -0
  33. package/src/agent/skill-registry.ts +36 -1
  34. package/src/agent/skill-standard.ts +99 -0
  35. package/src/agent/vibe-file.ts +7 -22
  36. package/src/cli/completion.ts +1 -1
  37. package/src/cli/config-overrides.ts +10 -1
  38. package/src/cli/redaction.ts +17 -5
  39. package/src/config/provider-model.ts +2 -1
  40. package/src/config/secret-config.ts +13 -6
  41. package/src/core/activity-feed.ts +97 -0
  42. package/src/core/away-digest.ts +161 -0
  43. package/src/core/conversation-rendering.ts +7 -3
  44. package/src/core/conversation.ts +22 -15
  45. package/src/core/hardware-profile.ts +362 -0
  46. package/src/core/last-seen-store.ts +78 -0
  47. package/src/core/plain-language.ts +52 -0
  48. package/src/core/setup-incomplete-hint.ts +90 -0
  49. package/src/core/system-message-router.ts +38 -87
  50. package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
  51. package/src/input/agent-workspace-basic-command-editors.ts +39 -141
  52. package/src/input/agent-workspace-categories.ts +40 -125
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-host-category.ts +0 -5
  55. package/src/input/agent-workspace-local-editor-submission.ts +3 -1
  56. package/src/input/agent-workspace-navigation.ts +10 -3
  57. package/src/input/agent-workspace-onboarding-actions.ts +132 -0
  58. package/src/input/agent-workspace-onboarding-categories.ts +29 -26
  59. package/src/input/agent-workspace-onboarding-finish.ts +11 -4
  60. package/src/input/agent-workspace-onboarding-state.ts +111 -0
  61. package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
  62. package/src/input/agent-workspace-profile-editors.ts +155 -0
  63. package/src/input/agent-workspace-subscription-editor.ts +7 -0
  64. package/src/input/agent-workspace-types.ts +5 -1
  65. package/src/input/agent-workspace.ts +65 -39
  66. package/src/input/command-registry.ts +18 -5
  67. package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
  68. package/src/input/commands/agent-skills-runtime.ts +60 -3
  69. package/src/input/commands/calendar-runtime.ts +209 -0
  70. package/src/input/commands/channels-runtime.ts +1 -0
  71. package/src/input/commands/command-error.ts +9 -0
  72. package/src/input/commands/compat-runtime.ts +1 -0
  73. package/src/input/commands/config.ts +1 -0
  74. package/src/input/commands/conversation-runtime.ts +1 -1
  75. package/src/input/commands/delegation-runtime.ts +5 -6
  76. package/src/input/commands/email-runtime.ts +387 -0
  77. package/src/input/commands/experience-runtime.ts +17 -4
  78. package/src/input/commands/guidance-runtime.ts +1 -1
  79. package/src/input/commands/health-runtime.ts +6 -1
  80. package/src/input/commands/knowledge-format.ts +73 -0
  81. package/src/input/commands/knowledge.ts +9 -74
  82. package/src/input/commands/local-provider-runtime.ts +2 -1
  83. package/src/input/commands/local-runtime.ts +10 -4
  84. package/src/input/commands/mcp-runtime.ts +7 -0
  85. package/src/input/commands/notify-runtime.ts +17 -1
  86. package/src/input/commands/onboarding-runtime.ts +1 -0
  87. package/src/input/commands/operator-actions-runtime.ts +1 -0
  88. package/src/input/commands/operator-runtime.ts +3 -0
  89. package/src/input/commands/personas-runtime.ts +1 -0
  90. package/src/input/commands/platform-access-runtime.ts +40 -17
  91. package/src/input/commands/product-runtime.ts +6 -1
  92. package/src/input/commands/provider-accounts-runtime.ts +3 -2
  93. package/src/input/commands/qrcode-runtime.ts +1 -0
  94. package/src/input/commands/routines-runtime.ts +1 -0
  95. package/src/input/commands/runtime-services.ts +0 -13
  96. package/src/input/commands/security-runtime.ts +1 -0
  97. package/src/input/commands/session-content.ts +1 -0
  98. package/src/input/commands/shell-core.ts +5 -2
  99. package/src/input/commands/subscription-runtime.ts +33 -9
  100. package/src/input/commands/support-bundle-runtime.ts +8 -1
  101. package/src/input/commands/tasks-runtime.ts +1 -0
  102. package/src/input/commands/tts-runtime.ts +1 -0
  103. package/src/input/commands/vibe-runtime.ts +1 -0
  104. package/src/input/commands.ts +4 -0
  105. package/src/input/feed-context-factory.ts +3 -12
  106. package/src/input/handler-command-route.ts +7 -60
  107. package/src/input/handler-feed-routes.ts +0 -194
  108. package/src/input/handler-feed.ts +2 -54
  109. package/src/input/handler-interactions.ts +0 -2
  110. package/src/input/handler-modal-stack.ts +0 -9
  111. package/src/input/handler-picker-routes.ts +24 -1
  112. package/src/input/handler-shortcuts.ts +11 -40
  113. package/src/input/handler-ui-state.ts +13 -0
  114. package/src/input/handler.ts +8 -35
  115. package/src/input/keybindings.ts +40 -17
  116. package/src/input/model-picker-local-fit.ts +130 -0
  117. package/src/input/submission-router.ts +0 -5
  118. package/src/main.ts +111 -89
  119. package/src/renderer/activity-sidebar.ts +186 -0
  120. package/src/renderer/agent-workspace-context-lines.ts +5 -48
  121. package/src/renderer/agent-workspace-style.ts +1 -1
  122. package/src/renderer/agent-workspace.ts +14 -2
  123. package/src/renderer/compositor.ts +37 -125
  124. package/src/renderer/conversation-overlays.ts +3 -3
  125. package/src/renderer/help-overlay.ts +7 -5
  126. package/src/renderer/model-workspace.ts +101 -86
  127. package/src/{panels → renderer}/polish.ts +3 -3
  128. package/src/renderer/shell-surface.ts +4 -5
  129. package/src/renderer/status-token.ts +31 -11
  130. package/src/renderer/tab-strip.ts +1 -1
  131. package/src/renderer/tool-call.ts +7 -8
  132. package/src/renderer/tool-labels.ts +92 -0
  133. package/src/renderer/ui-factory.ts +48 -96
  134. package/src/runtime/bootstrap-command-context.ts +0 -5
  135. package/src/runtime/bootstrap-command-parts.ts +6 -15
  136. package/src/runtime/bootstrap-core.ts +34 -13
  137. package/src/runtime/bootstrap-hook-bridge.ts +3 -1
  138. package/src/runtime/bootstrap-shell.ts +3 -50
  139. package/src/runtime/bootstrap.ts +3 -4
  140. package/src/runtime/context.ts +1 -1
  141. package/src/runtime/diagnostics/panels/index.ts +0 -1
  142. package/src/runtime/diagnostics/panels/policy.ts +1 -1
  143. package/src/runtime/execution-ledger.ts +16 -1
  144. package/src/runtime/index.ts +6 -1
  145. package/src/runtime/onboarding/index.ts +1 -0
  146. package/src/runtime/onboarding/onboarding-state.ts +200 -0
  147. package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
  148. package/src/runtime/services.ts +16 -4
  149. package/src/runtime/terminal-output-guard.ts +7 -0
  150. package/src/runtime/tool-permission-safety.ts +1 -1
  151. package/src/runtime/ui/model-picker/data-provider.ts +1 -1
  152. package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
  153. package/src/runtime/ui/model-picker/types.ts +2 -2
  154. package/src/runtime/ui/provider-health/data-provider.ts +3 -3
  155. package/src/runtime/ui/provider-health/types.ts +2 -2
  156. package/src/runtime/ui-services.ts +0 -2
  157. package/src/shell/agent-workspace-fullscreen.ts +0 -1
  158. package/src/shell/autonomy-surfacing.ts +272 -0
  159. package/src/shell/session-continuity-hints.ts +0 -6
  160. package/src/shell/startup-wiring.ts +221 -0
  161. package/src/shell/ui-openers.ts +18 -29
  162. package/src/tools/agent-context-policy.ts +1 -1
  163. package/src/tools/agent-harness-background-processes.ts +4 -4
  164. package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
  165. package/src/tools/agent-harness-command-runner.ts +6 -1
  166. package/src/tools/agent-harness-document-ops.ts +26 -53
  167. package/src/tools/agent-harness-execution-history.ts +1 -13
  168. package/src/tools/agent-harness-execution-posture.ts +0 -15
  169. package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
  170. package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
  171. package/src/tools/agent-harness-metadata.ts +16 -0
  172. package/src/tools/agent-harness-mode-catalog.ts +2 -9
  173. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  174. package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
  175. package/src/tools/agent-harness-tool-schema.ts +13 -14
  176. package/src/tools/agent-harness-tool.ts +27 -23
  177. package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
  178. package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
  179. package/src/tools/agent-workspace-tool.ts +2 -33
  180. package/src/utils/terminal-width.ts +9 -3
  181. package/src/version.ts +1 -1
  182. package/src/input/agent-workspace-panel-route.ts +0 -44
  183. package/src/input/panel-integration-actions.ts +0 -26
  184. package/src/panels/approval-panel.ts +0 -149
  185. package/src/panels/automation-control-panel.ts +0 -212
  186. package/src/panels/base-panel.ts +0 -254
  187. package/src/panels/builtin/agent.ts +0 -58
  188. package/src/panels/builtin/knowledge.ts +0 -26
  189. package/src/panels/builtin/operations.ts +0 -121
  190. package/src/panels/builtin/session.ts +0 -138
  191. package/src/panels/builtin/shared.ts +0 -275
  192. package/src/panels/builtin/usage.ts +0 -21
  193. package/src/panels/builtin-panels.ts +0 -23
  194. package/src/panels/confirm-state.ts +0 -61
  195. package/src/panels/context-visualizer-panel.ts +0 -204
  196. package/src/panels/cost-tracker-panel.ts +0 -444
  197. package/src/panels/docs-panel.ts +0 -285
  198. package/src/panels/index.ts +0 -25
  199. package/src/panels/knowledge-panel.ts +0 -417
  200. package/src/panels/memory-panel.ts +0 -226
  201. package/src/panels/panel-list-panel.ts +0 -464
  202. package/src/panels/panel-manager.ts +0 -570
  203. package/src/panels/provider-accounts-panel.ts +0 -233
  204. package/src/panels/provider-health-domains.ts +0 -208
  205. package/src/panels/provider-health-panel.ts +0 -720
  206. package/src/panels/provider-health-tracker.ts +0 -115
  207. package/src/panels/provider-stats-panel.ts +0 -366
  208. package/src/panels/qr-panel.ts +0 -207
  209. package/src/panels/schedule-panel.ts +0 -321
  210. package/src/panels/scrollable-list-panel.ts +0 -491
  211. package/src/panels/search-focus.ts +0 -32
  212. package/src/panels/security-panel.ts +0 -295
  213. package/src/panels/session-browser-panel.ts +0 -395
  214. package/src/panels/session-maintenance.ts +0 -125
  215. package/src/panels/subscription-panel.ts +0 -263
  216. package/src/panels/system-messages-panel.ts +0 -230
  217. package/src/panels/tasks-panel.ts +0 -344
  218. package/src/panels/thinking-panel.ts +0 -304
  219. package/src/panels/token-budget-panel.ts +0 -475
  220. package/src/panels/tool-inspector-panel.ts +0 -436
  221. package/src/panels/types.ts +0 -54
  222. package/src/renderer/panel-composite.ts +0 -158
  223. package/src/renderer/panel-tab-bar.ts +0 -69
  224. package/src/renderer/panel-workspace-bar.ts +0 -42
  225. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  226. package/src/tools/agent-harness-panel-metadata.ts +0 -211
  227. /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
@@ -0,0 +1,322 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import type { ShellPathService } from '@/runtime/index.ts';
4
+ import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
5
+ import { assertNoSecretLikeText } from './persona-registry.ts';
6
+ import { parseIcs, renderIcs } from './ics-calendar.ts';
7
+
8
+ // ──────────────────────────────────────────────────────────────────
9
+ // Types
10
+ // ──────────────────────────────────────────────────────────────────
11
+
12
+ export interface AgentCalendarEvent {
13
+ readonly id: string;
14
+ readonly title: string;
15
+ /** ISO-8601 start — YYYY-MM-DD for all-day, YYYY-MM-DDTHH:MM:SS[Z] for timed */
16
+ readonly start: string;
17
+ /** ISO-8601 end, optional */
18
+ readonly end?: string;
19
+ readonly allDay: boolean;
20
+ readonly location?: string;
21
+ readonly notes?: string;
22
+ /**
23
+ * IANA timezone identifier for timed events with a TZID-qualified DTSTART.
24
+ * When present, `start` holds the wall-clock time in this zone (no Z suffix).
25
+ * Undefined for UTC events (start ends with Z) and floating/all-day events.
26
+ */
27
+ readonly tzid?: string;
28
+ /** Source UID from an imported .ics file */
29
+ readonly sourceProvenance?: string;
30
+ readonly createdAt: string;
31
+ readonly updatedAt: string;
32
+ }
33
+
34
+ export interface AgentCalendarEventCreateInput {
35
+ readonly title: string;
36
+ readonly start: string;
37
+ readonly end?: string;
38
+ readonly allDay?: boolean;
39
+ readonly location?: string;
40
+ readonly notes?: string;
41
+ /** IANA timezone identifier, if the start time is wall-clock in a named zone */
42
+ readonly tzid?: string;
43
+ readonly sourceProvenance?: string;
44
+ }
45
+
46
+ export interface AgentCalendarSnapshot {
47
+ readonly path: string;
48
+ readonly events: readonly AgentCalendarEvent[];
49
+ }
50
+
51
+ interface CalendarStoreFile {
52
+ readonly version: 1;
53
+ readonly events: readonly AgentCalendarEvent[];
54
+ }
55
+
56
+ // ──────────────────────────────────────────────────────────────────
57
+ // Helpers
58
+ // ──────────────────────────────────────────────────────────────────
59
+
60
+ const STORE_VERSION = 1;
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
64
+ }
65
+
66
+ function readString(value: unknown, fallback = ''): string {
67
+ return typeof value === 'string' ? value : fallback;
68
+ }
69
+
70
+ function nowIso(): string {
71
+ return new Date().toISOString();
72
+ }
73
+
74
+ function slugify(value: string): string {
75
+ const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
76
+ return slug || 'event';
77
+ }
78
+
79
+ function assertNoCalendarSecretLikeText(fields: readonly string[]): void {
80
+ try {
81
+ assertNoSecretLikeText(fields);
82
+ } catch {
83
+ throw new Error('Calendar events cannot store secret-looking values. Remove the sensitive text.');
84
+ }
85
+ }
86
+
87
+ function parseEvent(value: unknown): AgentCalendarEvent | null {
88
+ if (!isRecord(value)) return null;
89
+ const id = readString(value.id).trim();
90
+ const title = readString(value.title).trim();
91
+ const start = readString(value.start).trim();
92
+ if (!id || !title || !start) return null;
93
+ return {
94
+ id,
95
+ title,
96
+ start,
97
+ end: readString(value.end).trim() || undefined,
98
+ allDay: value.allDay === true,
99
+ location: readString(value.location).trim() || undefined,
100
+ notes: readString(value.notes).trim() || undefined,
101
+ tzid: readString(value.tzid).trim() || undefined,
102
+ sourceProvenance: readString(value.sourceProvenance).trim() || undefined,
103
+ createdAt: readString(value.createdAt, nowIso()),
104
+ updatedAt: readString(value.updatedAt, nowIso()),
105
+ };
106
+ }
107
+
108
+ function parseStore(raw: string): CalendarStoreFile {
109
+ const parsed: unknown = JSON.parse(raw);
110
+ if (!isRecord(parsed)) return { version: STORE_VERSION, events: [] };
111
+ return {
112
+ version: STORE_VERSION,
113
+ events: Array.isArray(parsed.events)
114
+ ? parsed.events.map(parseEvent).filter((e): e is AgentCalendarEvent => e !== null)
115
+ : [],
116
+ };
117
+ }
118
+
119
+ function formatStore(store: CalendarStoreFile): string {
120
+ return `${JSON.stringify(store, null, 2)}\n`;
121
+ }
122
+
123
+ type AgentLocalStorePaths = Pick<ShellPathService, 'resolveUserPath'>;
124
+
125
+ export function calendarStorePath(shellPaths: AgentLocalStorePaths): string {
126
+ return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'calendar', 'events.json');
127
+ }
128
+
129
+ // ──────────────────────────────────────────────────────────────────
130
+ // Registry
131
+ // ──────────────────────────────────────────────────────────────────
132
+
133
+ export class AgentCalendarRegistry {
134
+ public constructor(private readonly storePath: string) {}
135
+
136
+ public static fromShellPaths(shellPaths: AgentLocalStorePaths): AgentCalendarRegistry {
137
+ return new AgentCalendarRegistry(calendarStorePath(shellPaths));
138
+ }
139
+
140
+ public snapshot(): AgentCalendarSnapshot {
141
+ const store = this.readStore();
142
+ const events = [...store.events].sort((a, b) => a.start.localeCompare(b.start));
143
+ return { path: this.storePath, events };
144
+ }
145
+
146
+ public list(): readonly AgentCalendarEvent[] {
147
+ return this.snapshot().events;
148
+ }
149
+
150
+ public get(id: string): AgentCalendarEvent | null {
151
+ const lookup = id.trim().toLowerCase();
152
+ if (!lookup) return null;
153
+ return this.list().find((e) => e.id.toLowerCase() === lookup) ?? null;
154
+ }
155
+
156
+ /**
157
+ * Return events whose start falls within the next `rangeDays` days from now.
158
+ */
159
+ public upcoming(rangeDays = 7): readonly AgentCalendarEvent[] {
160
+ const now = new Date();
161
+ const horizon = new Date(now.getTime() + rangeDays * 86_400_000);
162
+ const nowIsoStr = now.toISOString().slice(0, 10);
163
+ const horizonIsoStr = horizon.toISOString().slice(0, 10);
164
+ return this.list().filter((e) => {
165
+ // Compare lexicographically: ISO date strings sort correctly
166
+ const start = e.allDay ? e.start : e.start.slice(0, 10);
167
+ return start >= nowIsoStr && start <= horizonIsoStr;
168
+ });
169
+ }
170
+
171
+ public create(input: AgentCalendarEventCreateInput): AgentCalendarEvent {
172
+ const store = this.readStore();
173
+ const title = input.title.trim();
174
+ const start = input.start.trim();
175
+ if (!title) throw new Error('Event title is required.');
176
+ if (!start) throw new Error('Event start is required.');
177
+ assertNoCalendarSecretLikeText([title, input.notes ?? '', input.location ?? '']);
178
+ const timestamp = nowIso();
179
+ const event: AgentCalendarEvent = {
180
+ id: this.nextId(title, store.events),
181
+ title,
182
+ start,
183
+ end: input.end?.trim() || undefined,
184
+ allDay: input.allDay ?? !start.includes('T'),
185
+ location: input.location?.trim() || undefined,
186
+ notes: input.notes?.trim() || undefined,
187
+ tzid: input.tzid?.trim() || undefined,
188
+ sourceProvenance: input.sourceProvenance?.trim() || undefined,
189
+ createdAt: timestamp,
190
+ updatedAt: timestamp,
191
+ };
192
+ this.writeStore({ ...store, events: [...store.events, event] });
193
+ return event;
194
+ }
195
+
196
+ public delete(id: string): AgentCalendarEvent {
197
+ const store = this.readStore();
198
+ const existing = this.findInStore(store, id);
199
+ if (!existing) throw new Error(`Unknown calendar event ${id}`);
200
+ this.writeStore({ ...store, events: store.events.filter((e) => e.id !== existing.id) });
201
+ return existing;
202
+ }
203
+
204
+ /**
205
+ * Import events from iCalendar (.ics) text.
206
+ *
207
+ * Each event is individually checked:
208
+ * - Secret-bearing events are skipped and counted in `secretSkipped` (never thrown, never imported).
209
+ * - Duplicate events (same slug / id collision from a prior import) are skipped and counted in `skipped`.
210
+ *
211
+ * Returns { imported, skipped, secretSkipped } so callers can surface feedback to the user.
212
+ */
213
+ public importIcs(content: string): { imported: readonly AgentCalendarEvent[]; skipped: number; secretSkipped: number } {
214
+ const icsEvents = parseIcs(content);
215
+ const imported: AgentCalendarEvent[] = [];
216
+ let skipped = 0;
217
+ let secretSkipped = 0;
218
+ for (const icsEvent of icsEvents) {
219
+ const title = icsEvent.summary.trim();
220
+ const notes = icsEvent.description?.trim();
221
+ // Per-event secret scan: skip (don't abort) if secret-looking values found.
222
+ try {
223
+ assertNoCalendarSecretLikeText([title, notes ?? '', icsEvent.location ?? '', icsEvent.uid ?? '']);
224
+ } catch {
225
+ secretSkipped++;
226
+ continue;
227
+ }
228
+ // Skip events whose UID was already imported (sourceProvenance dedup).
229
+ const uid = icsEvent.uid;
230
+ const alreadyImported = this.list().some((e) => e.sourceProvenance === uid);
231
+ if (alreadyImported) {
232
+ skipped++;
233
+ continue;
234
+ }
235
+ try {
236
+ const created = this.create({
237
+ title,
238
+ start: icsEvent.dtstart,
239
+ end: icsEvent.dtend,
240
+ allDay: icsEvent.allDay,
241
+ location: icsEvent.location?.trim(),
242
+ notes,
243
+ tzid: icsEvent.tzid,
244
+ sourceProvenance: uid,
245
+ });
246
+ imported.push(created);
247
+ } catch {
248
+ // Skip events that fail for any other reason.
249
+ skipped++;
250
+ }
251
+ }
252
+ return { imported, skipped, secretSkipped };
253
+ }
254
+
255
+ /**
256
+ * Export events as iCalendar text.
257
+ * @param eventIds - if provided, export only those IDs; otherwise exports all.
258
+ * @param destPath - if provided, write to disk; throws if file already exists.
259
+ */
260
+ public exportIcs(eventIds?: readonly string[], destPath?: string): string {
261
+ const all = this.list();
262
+ const toExport = eventIds
263
+ ? all.filter((e) => eventIds.includes(e.id))
264
+ : all;
265
+
266
+ const icsContent = renderIcs(
267
+ toExport.map((e) => ({
268
+ uid: e.sourceProvenance ?? e.id,
269
+ summary: e.title,
270
+ description: e.notes,
271
+ location: e.location,
272
+ dtstart: e.start,
273
+ dtend: e.end,
274
+ allDay: e.allDay,
275
+ tzid: e.tzid,
276
+ rrule: undefined,
277
+ })),
278
+ );
279
+
280
+ if (destPath) {
281
+ const abs = resolve(destPath);
282
+ if (existsSync(abs)) throw new Error(`File already exists: ${abs}. Remove it or choose a different path.`);
283
+ mkdirSync(dirname(abs), { recursive: true });
284
+ writeFileSync(abs, icsContent, 'utf-8');
285
+ }
286
+
287
+ return icsContent;
288
+ }
289
+
290
+ private nextId(title: string, events: readonly AgentCalendarEvent[]): string {
291
+ const base = slugify(title);
292
+ const ids = new Set(events.map((e) => e.id));
293
+ if (!ids.has(base)) return base;
294
+ for (let i = 2; i < 1000; i++) {
295
+ const candidate = `${base}-${i}`;
296
+ if (!ids.has(candidate)) return candidate;
297
+ }
298
+ throw new Error(`Could not allocate event id for ${title}.`);
299
+ }
300
+
301
+ private findInStore(store: CalendarStoreFile, id: string): AgentCalendarEvent | null {
302
+ const lookup = id.trim().toLowerCase();
303
+ if (!lookup) return null;
304
+ return store.events.find((e) => e.id.toLowerCase() === lookup) ?? null;
305
+ }
306
+
307
+ private readStore(): CalendarStoreFile {
308
+ if (!existsSync(this.storePath)) return { version: STORE_VERSION, events: [] };
309
+ try {
310
+ return parseStore(readFileSync(this.storePath, 'utf-8'));
311
+ } catch (error) {
312
+ throw new Error(`Could not read calendar store: ${error instanceof Error ? error.message : String(error)}`);
313
+ }
314
+ }
315
+
316
+ private writeStore(store: CalendarStoreFile): void {
317
+ mkdirSync(dirname(this.storePath), { recursive: true });
318
+ const tmpPath = `${this.storePath}.tmp`;
319
+ writeFileSync(tmpPath, formatStore(store), 'utf-8');
320
+ renameSync(tmpPath, this.storePath);
321
+ }
322
+ }