@pellux/goodvibes-agent 1.4.4 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +52 -7
  3. package/dist/package/main.js +7491 -12253
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +1 -1
  6. package/docs/getting-started.md +2 -2
  7. package/docs/release-and-publishing.md +1 -1
  8. package/docs/tools-and-commands.md +5 -5
  9. package/package.json +4 -2
  10. package/release/live-verification/live-verification.json +13 -13
  11. package/release/live-verification/live-verification.md +15 -15
  12. package/release/performance-snapshot.json +2 -2
  13. package/release/release-notes.md +5 -7
  14. package/release/release-readiness.json +6 -6
  15. package/src/agent/behavior-discovery-summary.ts +4 -18
  16. package/src/agent/calendar-registry.ts +322 -0
  17. package/src/agent/competitive-feature-inventory.ts +268 -130
  18. package/src/agent/document-registry.ts +5 -1
  19. package/src/agent/email/email-service.ts +350 -0
  20. package/src/agent/email/imap-client.ts +596 -0
  21. package/src/agent/email/smtp-client.ts +453 -0
  22. package/src/agent/ics-calendar.ts +662 -0
  23. package/src/agent/ics-timezone.ts +172 -0
  24. package/src/agent/markdown-frontmatter.ts +31 -0
  25. package/src/agent/memory-safety.ts +1 -1
  26. package/src/agent/note-registry.ts +3 -9
  27. package/src/agent/persona-discovery.ts +3 -15
  28. package/src/agent/persona-registry.ts +1 -10
  29. package/src/agent/research-source-registry.ts +5 -1
  30. package/src/agent/routine-discovery.ts +3 -15
  31. package/src/agent/runtime-profile.ts +3 -0
  32. package/src/agent/skill-discovery.ts +3 -15
  33. package/src/agent/skill-draft-proposer.ts +203 -0
  34. package/src/agent/skill-draft-runner.ts +201 -0
  35. package/src/agent/skill-registry.ts +36 -1
  36. package/src/agent/skill-standard.ts +99 -0
  37. package/src/agent/vibe-file.ts +7 -22
  38. package/src/cli/completion.ts +1 -1
  39. package/src/cli/config-overrides.ts +10 -1
  40. package/src/cli/redaction.ts +17 -5
  41. package/src/config/provider-model.ts +2 -1
  42. package/src/config/secret-config.ts +13 -6
  43. package/src/core/activity-feed.ts +97 -0
  44. package/src/core/away-digest.ts +161 -0
  45. package/src/core/conversation-rendering.ts +7 -3
  46. package/src/core/conversation.ts +22 -15
  47. package/src/core/hardware-profile.ts +362 -0
  48. package/src/core/last-seen-store.ts +78 -0
  49. package/src/core/plain-language.ts +52 -0
  50. package/src/core/setup-incomplete-hint.ts +90 -0
  51. package/src/core/system-message-router.ts +38 -87
  52. package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
  53. package/src/input/agent-workspace-basic-command-editors.ts +39 -141
  54. package/src/input/agent-workspace-categories.ts +40 -125
  55. package/src/input/agent-workspace-command-editor.ts +4 -0
  56. package/src/input/agent-workspace-host-category.ts +0 -5
  57. package/src/input/agent-workspace-local-editor-submission.ts +3 -1
  58. package/src/input/agent-workspace-navigation.ts +10 -3
  59. package/src/input/agent-workspace-onboarding-actions.ts +132 -0
  60. package/src/input/agent-workspace-onboarding-categories.ts +29 -26
  61. package/src/input/agent-workspace-onboarding-finish.ts +11 -4
  62. package/src/input/agent-workspace-onboarding-state.ts +111 -0
  63. package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
  64. package/src/input/agent-workspace-profile-editors.ts +155 -0
  65. package/src/input/agent-workspace-subscription-editor.ts +7 -0
  66. package/src/input/agent-workspace-types.ts +5 -1
  67. package/src/input/agent-workspace.ts +65 -39
  68. package/src/input/command-registry.ts +18 -5
  69. package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
  70. package/src/input/commands/agent-skills-runtime.ts +60 -3
  71. package/src/input/commands/calendar-runtime.ts +209 -0
  72. package/src/input/commands/channels-runtime.ts +1 -0
  73. package/src/input/commands/command-error.ts +9 -0
  74. package/src/input/commands/compat-runtime.ts +1 -0
  75. package/src/input/commands/config.ts +1 -0
  76. package/src/input/commands/conversation-runtime.ts +1 -1
  77. package/src/input/commands/delegation-runtime.ts +5 -6
  78. package/src/input/commands/email-runtime.ts +387 -0
  79. package/src/input/commands/experience-runtime.ts +17 -4
  80. package/src/input/commands/guidance-runtime.ts +1 -1
  81. package/src/input/commands/health-runtime.ts +6 -1
  82. package/src/input/commands/knowledge-format.ts +73 -0
  83. package/src/input/commands/knowledge.ts +9 -74
  84. package/src/input/commands/local-provider-runtime.ts +2 -1
  85. package/src/input/commands/local-runtime.ts +10 -4
  86. package/src/input/commands/mcp-runtime.ts +7 -0
  87. package/src/input/commands/notify-runtime.ts +17 -1
  88. package/src/input/commands/onboarding-runtime.ts +1 -0
  89. package/src/input/commands/operator-actions-runtime.ts +1 -0
  90. package/src/input/commands/operator-runtime.ts +3 -0
  91. package/src/input/commands/personas-runtime.ts +1 -0
  92. package/src/input/commands/platform-access-runtime.ts +40 -17
  93. package/src/input/commands/product-runtime.ts +6 -1
  94. package/src/input/commands/provider-accounts-runtime.ts +3 -2
  95. package/src/input/commands/qrcode-runtime.ts +1 -0
  96. package/src/input/commands/routines-runtime.ts +1 -0
  97. package/src/input/commands/runtime-services.ts +0 -13
  98. package/src/input/commands/security-runtime.ts +1 -0
  99. package/src/input/commands/session-content.ts +1 -0
  100. package/src/input/commands/shell-core.ts +5 -2
  101. package/src/input/commands/subscription-runtime.ts +33 -9
  102. package/src/input/commands/support-bundle-runtime.ts +8 -1
  103. package/src/input/commands/tasks-runtime.ts +1 -0
  104. package/src/input/commands/tts-runtime.ts +1 -0
  105. package/src/input/commands/vibe-runtime.ts +1 -0
  106. package/src/input/commands.ts +4 -0
  107. package/src/input/feed-context-factory.ts +3 -12
  108. package/src/input/handler-command-route.ts +7 -60
  109. package/src/input/handler-feed-routes.ts +0 -194
  110. package/src/input/handler-feed.ts +2 -54
  111. package/src/input/handler-interactions.ts +0 -2
  112. package/src/input/handler-modal-stack.ts +0 -9
  113. package/src/input/handler-picker-routes.ts +24 -1
  114. package/src/input/handler-shortcuts.ts +11 -40
  115. package/src/input/handler-ui-state.ts +13 -0
  116. package/src/input/handler.ts +8 -35
  117. package/src/input/keybindings.ts +40 -17
  118. package/src/input/model-picker-local-fit.ts +130 -0
  119. package/src/input/shell-passthrough.ts +58 -0
  120. package/src/input/submission-router.ts +0 -5
  121. package/src/main.ts +129 -90
  122. package/src/renderer/activity-sidebar.ts +186 -0
  123. package/src/renderer/agent-workspace-context-lines.ts +5 -48
  124. package/src/renderer/agent-workspace-style.ts +1 -1
  125. package/src/renderer/agent-workspace.ts +14 -2
  126. package/src/renderer/compositor.ts +37 -125
  127. package/src/renderer/conversation-overlays.ts +3 -3
  128. package/src/renderer/help-overlay.ts +7 -5
  129. package/src/renderer/model-workspace.ts +101 -86
  130. package/src/{panels → renderer}/polish.ts +3 -3
  131. package/src/renderer/shell-surface.ts +4 -5
  132. package/src/renderer/status-token.ts +31 -11
  133. package/src/renderer/tab-strip.ts +1 -1
  134. package/src/renderer/tool-call.ts +7 -8
  135. package/src/renderer/tool-labels.ts +92 -0
  136. package/src/renderer/ui-factory.ts +48 -96
  137. package/src/runtime/bootstrap-command-context.ts +0 -5
  138. package/src/runtime/bootstrap-command-parts.ts +6 -15
  139. package/src/runtime/bootstrap-core.ts +34 -13
  140. package/src/runtime/bootstrap-hook-bridge.ts +3 -1
  141. package/src/runtime/bootstrap-shell.ts +3 -50
  142. package/src/runtime/bootstrap.ts +3 -4
  143. package/src/runtime/context.ts +1 -1
  144. package/src/runtime/diagnostics/panels/index.ts +0 -1
  145. package/src/runtime/diagnostics/panels/policy.ts +1 -1
  146. package/src/runtime/execution-ledger.ts +16 -1
  147. package/src/runtime/index.ts +6 -1
  148. package/src/runtime/onboarding/index.ts +1 -0
  149. package/src/runtime/onboarding/onboarding-state.ts +200 -0
  150. package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
  151. package/src/runtime/services.ts +16 -4
  152. package/src/runtime/terminal-output-guard.ts +7 -0
  153. package/src/runtime/tool-permission-safety.ts +1 -1
  154. package/src/runtime/ui/model-picker/data-provider.ts +1 -1
  155. package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
  156. package/src/runtime/ui/model-picker/types.ts +2 -2
  157. package/src/runtime/ui/provider-health/data-provider.ts +3 -3
  158. package/src/runtime/ui/provider-health/types.ts +2 -2
  159. package/src/runtime/ui-services.ts +0 -2
  160. package/src/shell/agent-workspace-fullscreen.ts +0 -1
  161. package/src/shell/autonomy-surfacing.ts +272 -0
  162. package/src/shell/session-continuity-hints.ts +0 -6
  163. package/src/shell/startup-wiring.ts +221 -0
  164. package/src/shell/ui-openers.ts +18 -29
  165. package/src/tools/agent-context-policy.ts +1 -1
  166. package/src/tools/agent-harness-background-processes.ts +4 -4
  167. package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
  168. package/src/tools/agent-harness-command-runner.ts +6 -1
  169. package/src/tools/agent-harness-document-ops.ts +26 -53
  170. package/src/tools/agent-harness-execution-history.ts +1 -13
  171. package/src/tools/agent-harness-execution-posture.ts +0 -15
  172. package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
  173. package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
  174. package/src/tools/agent-harness-metadata.ts +16 -0
  175. package/src/tools/agent-harness-mode-catalog.ts +2 -9
  176. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  177. package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
  178. package/src/tools/agent-harness-tool-schema.ts +13 -14
  179. package/src/tools/agent-harness-tool.ts +27 -23
  180. package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
  181. package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
  182. package/src/tools/agent-workspace-tool.ts +2 -33
  183. package/src/utils/terminal-width.ts +9 -3
  184. package/src/version.ts +1 -1
  185. package/src/input/agent-workspace-panel-route.ts +0 -44
  186. package/src/input/panel-integration-actions.ts +0 -26
  187. package/src/panels/approval-panel.ts +0 -149
  188. package/src/panels/automation-control-panel.ts +0 -212
  189. package/src/panels/base-panel.ts +0 -254
  190. package/src/panels/builtin/agent.ts +0 -58
  191. package/src/panels/builtin/knowledge.ts +0 -26
  192. package/src/panels/builtin/operations.ts +0 -121
  193. package/src/panels/builtin/session.ts +0 -138
  194. package/src/panels/builtin/shared.ts +0 -275
  195. package/src/panels/builtin/usage.ts +0 -21
  196. package/src/panels/builtin-panels.ts +0 -23
  197. package/src/panels/confirm-state.ts +0 -61
  198. package/src/panels/context-visualizer-panel.ts +0 -204
  199. package/src/panels/cost-tracker-panel.ts +0 -444
  200. package/src/panels/docs-panel.ts +0 -285
  201. package/src/panels/index.ts +0 -25
  202. package/src/panels/knowledge-panel.ts +0 -417
  203. package/src/panels/memory-panel.ts +0 -226
  204. package/src/panels/panel-list-panel.ts +0 -464
  205. package/src/panels/panel-manager.ts +0 -570
  206. package/src/panels/provider-accounts-panel.ts +0 -233
  207. package/src/panels/provider-health-domains.ts +0 -208
  208. package/src/panels/provider-health-panel.ts +0 -720
  209. package/src/panels/provider-health-tracker.ts +0 -115
  210. package/src/panels/provider-stats-panel.ts +0 -366
  211. package/src/panels/qr-panel.ts +0 -207
  212. package/src/panels/schedule-panel.ts +0 -321
  213. package/src/panels/scrollable-list-panel.ts +0 -491
  214. package/src/panels/search-focus.ts +0 -32
  215. package/src/panels/security-panel.ts +0 -295
  216. package/src/panels/session-browser-panel.ts +0 -395
  217. package/src/panels/session-maintenance.ts +0 -125
  218. package/src/panels/subscription-panel.ts +0 -263
  219. package/src/panels/system-messages-panel.ts +0 -230
  220. package/src/panels/tasks-panel.ts +0 -344
  221. package/src/panels/thinking-panel.ts +0 -304
  222. package/src/panels/token-budget-panel.ts +0 -475
  223. package/src/panels/tool-inspector-panel.ts +0 -436
  224. package/src/panels/types.ts +0 -54
  225. package/src/renderer/panel-composite.ts +0 -158
  226. package/src/renderer/panel-tab-bar.ts +0 -69
  227. package/src/renderer/panel-workspace-bar.ts +0 -42
  228. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  229. package/src/tools/agent-harness-panel-metadata.ts +0 -211
  230. /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
@@ -0,0 +1,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
+ }