@pellux/goodvibes-agent 1.4.3 → 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.
- package/CHANGELOG.md +24 -0
- package/README.md +7 -7
- package/dist/package/main.js +7450 -12285
- 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/performance-snapshot.json +2 -2
- package/release/release-notes.md +11 -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 +63 -158
- 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/submission-router.ts +0 -5
- package/src/main.ts +111 -89
- 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,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
|
+
}
|