@usejarvis/brain 0.1.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/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GoalService — Core service for M16 Autonomous Goal Pursuit
|
|
3
|
+
*
|
|
4
|
+
* Manages goal lifecycle, daily rhythm (morning plan + evening review),
|
|
5
|
+
* accountability checks, health recalculation, and escalation.
|
|
6
|
+
* Implements the Service interface for daemon integration.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Service, ServiceStatus } from '../daemon/services.ts';
|
|
10
|
+
import type { GoalEvent } from './events.ts';
|
|
11
|
+
import type { GoalConfig } from '../config/types.ts';
|
|
12
|
+
import type { Goal, GoalLevel, GoalStatus, GoalHealth } from './types.ts';
|
|
13
|
+
import * as vault from '../vault/goals.ts';
|
|
14
|
+
|
|
15
|
+
export class GoalService implements Service {
|
|
16
|
+
name = 'goals';
|
|
17
|
+
private _status: ServiceStatus = 'stopped';
|
|
18
|
+
private config: GoalConfig;
|
|
19
|
+
private eventCallback: ((event: GoalEvent) => void) | null = null;
|
|
20
|
+
|
|
21
|
+
// Timers
|
|
22
|
+
private rhythmTimer: Timer | null = null; // daily rhythm check (60s)
|
|
23
|
+
private accountabilityTimer: Timer | null = null; // accountability check (5min)
|
|
24
|
+
private healthTimer: Timer | null = null; // health recalc (15min)
|
|
25
|
+
|
|
26
|
+
constructor(config: GoalConfig) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set callback for broadcasting goal events via WebSocket.
|
|
32
|
+
*/
|
|
33
|
+
setEventCallback(cb: (event: GoalEvent) => void): void {
|
|
34
|
+
this.eventCallback = cb;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private emit(event: GoalEvent): void {
|
|
38
|
+
if (this.eventCallback) {
|
|
39
|
+
this.eventCallback(event);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async start(): Promise<void> {
|
|
44
|
+
if (!this.config.enabled) {
|
|
45
|
+
this._status = 'stopped';
|
|
46
|
+
console.log('[GoalService] Disabled by config');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this._status = 'starting';
|
|
51
|
+
|
|
52
|
+
// Daily rhythm check — runs every 60s to detect morning/evening windows
|
|
53
|
+
this.rhythmTimer = setInterval(() => {
|
|
54
|
+
this.checkDailyRhythm().catch(err =>
|
|
55
|
+
console.error('[GoalService] Rhythm check error:', err)
|
|
56
|
+
);
|
|
57
|
+
}, 60_000);
|
|
58
|
+
|
|
59
|
+
// Accountability check — runs every 5min for escalation monitoring
|
|
60
|
+
this.accountabilityTimer = setInterval(() => {
|
|
61
|
+
this.checkAccountability().catch(err =>
|
|
62
|
+
console.error('[GoalService] Accountability check error:', err)
|
|
63
|
+
);
|
|
64
|
+
}, 5 * 60_000);
|
|
65
|
+
|
|
66
|
+
// Health recalculation — runs every 15min
|
|
67
|
+
this.healthTimer = setInterval(() => {
|
|
68
|
+
this.recalculateAllHealth().catch(err =>
|
|
69
|
+
console.error('[GoalService] Health recalc error:', err)
|
|
70
|
+
);
|
|
71
|
+
}, 15 * 60_000);
|
|
72
|
+
|
|
73
|
+
this._status = 'running';
|
|
74
|
+
console.log('[GoalService] Started (rhythm=60s, accountability=5min, health=15min)');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async stop(): Promise<void> {
|
|
78
|
+
this._status = 'stopping';
|
|
79
|
+
|
|
80
|
+
if (this.rhythmTimer) { clearInterval(this.rhythmTimer); this.rhythmTimer = null; }
|
|
81
|
+
if (this.accountabilityTimer) { clearInterval(this.accountabilityTimer); this.accountabilityTimer = null; }
|
|
82
|
+
if (this.healthTimer) { clearInterval(this.healthTimer); this.healthTimer = null; }
|
|
83
|
+
|
|
84
|
+
this._status = 'stopped';
|
|
85
|
+
console.log('[GoalService] Stopped');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
status(): ServiceStatus {
|
|
89
|
+
return this._status;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Goal CRUD with events ─────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
createGoal(title: string, level: GoalLevel, opts?: Parameters<typeof vault.createGoal>[2]): Goal {
|
|
95
|
+
const goal = vault.createGoal(title, level, opts);
|
|
96
|
+
this.emit({
|
|
97
|
+
type: 'goal_created',
|
|
98
|
+
goalId: goal.id,
|
|
99
|
+
data: { title, level, parent_id: goal.parent_id },
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
return goal;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getGoal(id: string): Goal | null {
|
|
106
|
+
return vault.getGoal(id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateGoal(id: string, updates: Parameters<typeof vault.updateGoal>[1]): Goal | null {
|
|
110
|
+
const goal = vault.updateGoal(id, updates);
|
|
111
|
+
if (goal) {
|
|
112
|
+
this.emit({
|
|
113
|
+
type: 'goal_updated',
|
|
114
|
+
goalId: id,
|
|
115
|
+
data: { updates },
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return goal;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
scoreGoal(id: string, score: number, reason: string, source = 'user'): Goal | null {
|
|
123
|
+
const goal = vault.updateGoalScore(id, score, reason, source);
|
|
124
|
+
if (goal) {
|
|
125
|
+
this.emit({
|
|
126
|
+
type: 'goal_scored',
|
|
127
|
+
goalId: id,
|
|
128
|
+
data: { score: goal.score, reason, source },
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return goal;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
updateStatus(id: string, status: GoalStatus): Goal | null {
|
|
136
|
+
const goal = vault.updateGoalStatus(id, status);
|
|
137
|
+
if (!goal) return null;
|
|
138
|
+
|
|
139
|
+
const eventType = status === 'completed' ? 'goal_completed'
|
|
140
|
+
: status === 'failed' ? 'goal_failed'
|
|
141
|
+
: status === 'killed' ? 'goal_killed'
|
|
142
|
+
: 'goal_status_changed';
|
|
143
|
+
|
|
144
|
+
this.emit({
|
|
145
|
+
type: eventType,
|
|
146
|
+
goalId: id,
|
|
147
|
+
data: { status },
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Extract goal completion data for vault knowledge
|
|
152
|
+
if (status === 'completed' || status === 'failed' || status === 'killed') {
|
|
153
|
+
try {
|
|
154
|
+
const { extractGoalCompletion } = require('../vault/extractor.ts');
|
|
155
|
+
extractGoalCompletion(goal);
|
|
156
|
+
} catch {
|
|
157
|
+
// Extractor may not be available — ignore
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return goal;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateHealth(id: string, health: GoalHealth): Goal | null {
|
|
165
|
+
const goal = vault.updateGoalHealth(id, health);
|
|
166
|
+
if (goal) {
|
|
167
|
+
this.emit({
|
|
168
|
+
type: 'goal_health_changed',
|
|
169
|
+
goalId: id,
|
|
170
|
+
data: { health },
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return goal;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
deleteGoal(id: string): boolean {
|
|
178
|
+
const result = vault.deleteGoal(id);
|
|
179
|
+
if (result) {
|
|
180
|
+
this.emit({
|
|
181
|
+
type: 'goal_deleted',
|
|
182
|
+
goalId: id,
|
|
183
|
+
data: {},
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Daily Rhythm ──────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if we're in a morning or evening window and trigger check-ins.
|
|
194
|
+
* This is a lightweight timer check — the actual NL-driven rhythm
|
|
195
|
+
* is in src/goals/rhythm.ts (Phase 4).
|
|
196
|
+
*/
|
|
197
|
+
private async checkDailyRhythm(): Promise<void> {
|
|
198
|
+
const now = new Date();
|
|
199
|
+
const hour = now.getHours();
|
|
200
|
+
|
|
201
|
+
const morningWindow = this.config.morning_window ?? { start: 7, end: 9 };
|
|
202
|
+
const eveningWindow = this.config.evening_window ?? { start: 20, end: 22 };
|
|
203
|
+
|
|
204
|
+
// Check morning window
|
|
205
|
+
if (hour >= morningWindow.start && hour < morningWindow.end) {
|
|
206
|
+
const existing = vault.getTodayCheckIn('morning_plan');
|
|
207
|
+
if (!existing) {
|
|
208
|
+
// Morning plan needed — Phase 4 (rhythm.ts) will handle the LLM-driven planning
|
|
209
|
+
// For now, just log that it's time
|
|
210
|
+
console.log('[GoalService] Morning plan window — check-in needed');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check evening window
|
|
215
|
+
if (hour >= eveningWindow.start && hour < eveningWindow.end) {
|
|
216
|
+
const existing = vault.getTodayCheckIn('evening_review');
|
|
217
|
+
if (!existing) {
|
|
218
|
+
console.log('[GoalService] Evening review window — check-in needed');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Accountability ────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check active goals for escalation needs.
|
|
227
|
+
* Full drill-sergeant logic is in src/goals/accountability.ts (Phase 4).
|
|
228
|
+
*/
|
|
229
|
+
private async checkAccountability(): Promise<void> {
|
|
230
|
+
const needingEscalation = vault.getGoalsNeedingEscalation();
|
|
231
|
+
const overdue = vault.getOverdueGoals();
|
|
232
|
+
|
|
233
|
+
for (const goal of needingEscalation) {
|
|
234
|
+
if (goal.escalation_stage === 'none') {
|
|
235
|
+
// Auto-escalate to 'pressure' stage
|
|
236
|
+
const escalationWeeks = this.config.escalation_weeks ?? { pressure: 1, root_cause: 3, suggest_kill: 4 };
|
|
237
|
+
const behindSince = goal.updated_at;
|
|
238
|
+
const weeksBehind = (Date.now() - behindSince) / (7 * 24 * 60 * 60 * 1000);
|
|
239
|
+
|
|
240
|
+
if (weeksBehind >= escalationWeeks.pressure) {
|
|
241
|
+
vault.updateGoalEscalation(goal.id, 'pressure');
|
|
242
|
+
this.emit({
|
|
243
|
+
type: 'goal_escalated',
|
|
244
|
+
goalId: goal.id,
|
|
245
|
+
data: { stage: 'pressure', weeksBehind },
|
|
246
|
+
timestamp: Date.now(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
} else if (goal.escalation_stage === 'pressure') {
|
|
250
|
+
const escalationWeeks = this.config.escalation_weeks ?? { pressure: 1, root_cause: 3, suggest_kill: 4 };
|
|
251
|
+
const startedAt = goal.escalation_started_at ?? goal.updated_at;
|
|
252
|
+
const weeksSinceEscalation = (Date.now() - startedAt) / (7 * 24 * 60 * 60 * 1000);
|
|
253
|
+
|
|
254
|
+
if (weeksSinceEscalation >= escalationWeeks.root_cause) {
|
|
255
|
+
vault.updateGoalEscalation(goal.id, 'root_cause');
|
|
256
|
+
this.emit({
|
|
257
|
+
type: 'goal_escalated',
|
|
258
|
+
goalId: goal.id,
|
|
259
|
+
data: { stage: 'root_cause', weeksSinceEscalation },
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} else if (goal.escalation_stage === 'root_cause') {
|
|
264
|
+
const escalationWeeks = this.config.escalation_weeks ?? { pressure: 1, root_cause: 3, suggest_kill: 4 };
|
|
265
|
+
const startedAt = goal.escalation_started_at ?? goal.updated_at;
|
|
266
|
+
const weeksSinceEscalation = (Date.now() - startedAt) / (7 * 24 * 60 * 60 * 1000);
|
|
267
|
+
|
|
268
|
+
if (weeksSinceEscalation >= escalationWeeks.suggest_kill) {
|
|
269
|
+
vault.updateGoalEscalation(goal.id, 'suggest_kill');
|
|
270
|
+
this.emit({
|
|
271
|
+
type: 'goal_escalated',
|
|
272
|
+
goalId: goal.id,
|
|
273
|
+
data: { stage: 'suggest_kill', weeksSinceEscalation },
|
|
274
|
+
timestamp: Date.now(),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Log overdue goals (Phase 4 will handle the drill-sergeant messaging)
|
|
281
|
+
if (overdue.length > 0) {
|
|
282
|
+
console.log(`[GoalService] ${overdue.length} overdue goal(s) detected`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Health Recalculation ──────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Recalculate health for all active goals based on score and deadline.
|
|
290
|
+
*/
|
|
291
|
+
private async recalculateAllHealth(): Promise<void> {
|
|
292
|
+
const activeGoals = vault.findGoals({ status: 'active' });
|
|
293
|
+
let changed = 0;
|
|
294
|
+
|
|
295
|
+
for (const goal of activeGoals) {
|
|
296
|
+
const newHealth = this.calculateHealth(goal);
|
|
297
|
+
if (newHealth !== goal.health) {
|
|
298
|
+
this.updateHealth(goal.id, newHealth);
|
|
299
|
+
changed++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (changed > 0) {
|
|
304
|
+
console.log(`[GoalService] Health recalculated: ${changed} goal(s) changed`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Calculate health for a single goal based on score progress vs time elapsed.
|
|
310
|
+
*/
|
|
311
|
+
private calculateHealth(goal: Goal): GoalHealth {
|
|
312
|
+
// If no deadline, base purely on score
|
|
313
|
+
if (!goal.deadline) {
|
|
314
|
+
if (goal.score >= 0.6) return 'on_track';
|
|
315
|
+
if (goal.score >= 0.3) return 'at_risk';
|
|
316
|
+
return 'behind';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
const startTime = goal.started_at ?? goal.created_at;
|
|
321
|
+
const totalDuration = goal.deadline - startTime;
|
|
322
|
+
const elapsed = now - startTime;
|
|
323
|
+
|
|
324
|
+
// If past deadline
|
|
325
|
+
if (now > goal.deadline) {
|
|
326
|
+
if (goal.score >= 0.7) return 'on_track'; // nearly done
|
|
327
|
+
if (goal.score >= 0.4) return 'behind';
|
|
328
|
+
return 'critical';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Ratio: how far along are we in time vs score
|
|
332
|
+
const timeRatio = totalDuration > 0 ? elapsed / totalDuration : 0;
|
|
333
|
+
const expectedScore = timeRatio * 0.7; // expecting 0.7 = good at deadline
|
|
334
|
+
|
|
335
|
+
const gap = expectedScore - goal.score;
|
|
336
|
+
|
|
337
|
+
if (gap <= 0) return 'on_track'; // ahead of pace
|
|
338
|
+
if (gap <= 0.15) return 'at_risk'; // slightly behind
|
|
339
|
+
if (gap <= 0.3) return 'behind'; // significantly behind
|
|
340
|
+
return 'critical'; // way behind
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Metrics ───────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
getMetrics() {
|
|
346
|
+
return vault.getGoalMetrics();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal Pursuit Types for M16 — Autonomous Goal Pursuit & Long-Term Planning
|
|
3
|
+
*
|
|
4
|
+
* OKR-style hierarchical goal system with Google-style 0.0-1.0 scoring.
|
|
5
|
+
* Goals nest: objective → key_result → milestone → task → daily_action.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Enums ───────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type GoalLevel = 'objective' | 'key_result' | 'milestone' | 'task' | 'daily_action';
|
|
11
|
+
|
|
12
|
+
export type GoalStatus = 'draft' | 'active' | 'paused' | 'completed' | 'failed' | 'killed';
|
|
13
|
+
|
|
14
|
+
export type GoalHealth = 'on_track' | 'at_risk' | 'behind' | 'critical';
|
|
15
|
+
|
|
16
|
+
export type TimeHorizon = 'life' | 'yearly' | 'quarterly' | 'monthly' | 'weekly' | 'daily';
|
|
17
|
+
|
|
18
|
+
export type EscalationStage = 'none' | 'pressure' | 'root_cause' | 'suggest_kill';
|
|
19
|
+
|
|
20
|
+
export type ProgressType = 'manual' | 'auto_detected' | 'review' | 'system';
|
|
21
|
+
|
|
22
|
+
export type CheckInType = 'morning_plan' | 'evening_review';
|
|
23
|
+
|
|
24
|
+
// ── Core Types ──────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export type Goal = {
|
|
27
|
+
id: string;
|
|
28
|
+
parent_id: string | null;
|
|
29
|
+
level: GoalLevel;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
success_criteria: string;
|
|
33
|
+
time_horizon: TimeHorizon;
|
|
34
|
+
score: number; // 0.0-1.0 OKR score (0.7 = good)
|
|
35
|
+
score_reason: string | null;
|
|
36
|
+
status: GoalStatus;
|
|
37
|
+
health: GoalHealth;
|
|
38
|
+
deadline: number | null; // epoch ms
|
|
39
|
+
started_at: number | null;
|
|
40
|
+
estimated_hours: number | null;
|
|
41
|
+
actual_hours: number;
|
|
42
|
+
authority_level: number; // min authority for actions within this goal
|
|
43
|
+
tags: string[];
|
|
44
|
+
dependencies: string[]; // goal IDs that must complete first
|
|
45
|
+
escalation_stage: EscalationStage;
|
|
46
|
+
escalation_started_at: number | null;
|
|
47
|
+
sort_order: number;
|
|
48
|
+
created_at: number;
|
|
49
|
+
updated_at: number;
|
|
50
|
+
completed_at: number | null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type GoalProgressEntry = {
|
|
54
|
+
id: string;
|
|
55
|
+
goal_id: string;
|
|
56
|
+
type: ProgressType;
|
|
57
|
+
score_before: number;
|
|
58
|
+
score_after: number;
|
|
59
|
+
note: string;
|
|
60
|
+
source: string; // 'user', 'awareness', 'daily_review', etc.
|
|
61
|
+
created_at: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type GoalCheckIn = {
|
|
65
|
+
id: string;
|
|
66
|
+
type: CheckInType;
|
|
67
|
+
summary: string;
|
|
68
|
+
goals_reviewed: string[]; // goal IDs
|
|
69
|
+
actions_planned: string[]; // for morning
|
|
70
|
+
actions_completed: string[]; // for evening
|
|
71
|
+
created_at: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type GoalEstimate = {
|
|
75
|
+
llm_estimate_hours: number;
|
|
76
|
+
historical_estimate_hours: number | null;
|
|
77
|
+
final_estimate_hours: number;
|
|
78
|
+
confidence: number; // 0.0-1.0
|
|
79
|
+
reasoning: string;
|
|
80
|
+
similar_past_goals: string[]; // vault entity IDs
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ── Query Types ─────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export type GoalQuery = {
|
|
86
|
+
status?: GoalStatus;
|
|
87
|
+
level?: GoalLevel;
|
|
88
|
+
parent_id?: string | null;
|
|
89
|
+
health?: GoalHealth;
|
|
90
|
+
tag?: string;
|
|
91
|
+
time_horizon?: TimeHorizon;
|
|
92
|
+
limit?: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type GoalUpdate = {
|
|
96
|
+
title?: string;
|
|
97
|
+
description?: string;
|
|
98
|
+
success_criteria?: string;
|
|
99
|
+
time_horizon?: TimeHorizon;
|
|
100
|
+
deadline?: number | null;
|
|
101
|
+
estimated_hours?: number | null;
|
|
102
|
+
authority_level?: number;
|
|
103
|
+
tags?: string[];
|
|
104
|
+
dependencies?: string[];
|
|
105
|
+
sort_order?: number;
|
|
106
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal → Workflow Bridge
|
|
3
|
+
*
|
|
4
|
+
* Auto-generates cron-triggered workflows for morning plan and evening review.
|
|
5
|
+
* Also supports creating recurring task workflows for goals with repeating work.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GoalConfig } from '../config/types.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Workflow definition for the goal system's daily rhythm.
|
|
12
|
+
* These can be registered with the TriggerManager when both
|
|
13
|
+
* the workflow engine and goal service are available.
|
|
14
|
+
*/
|
|
15
|
+
export type GoalWorkflowDefinition = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
triggerType: 'cron';
|
|
20
|
+
cronExpression: string;
|
|
21
|
+
action: 'morning_plan' | 'evening_review';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate workflow definitions for the daily rhythm check-ins.
|
|
26
|
+
* Returns cron-triggered workflow specs that can be registered with the engine.
|
|
27
|
+
*/
|
|
28
|
+
export function generateRhythmWorkflows(config: GoalConfig): GoalWorkflowDefinition[] {
|
|
29
|
+
if (!config.enabled) return [];
|
|
30
|
+
|
|
31
|
+
const workflows: GoalWorkflowDefinition[] = [];
|
|
32
|
+
|
|
33
|
+
const morningWindow = config.morning_window ?? { start: 7, end: 9 };
|
|
34
|
+
const eveningWindow = config.evening_window ?? { start: 20, end: 22 };
|
|
35
|
+
|
|
36
|
+
// Morning plan: fire at the start of the morning window
|
|
37
|
+
workflows.push({
|
|
38
|
+
id: 'goal_morning_plan',
|
|
39
|
+
name: 'Morning Goal Plan',
|
|
40
|
+
description: 'Automated morning planning session — reviews active goals, generates daily actions, and sets focus areas.',
|
|
41
|
+
triggerType: 'cron',
|
|
42
|
+
cronExpression: `0 ${morningWindow.start} * * *`, // e.g., "0 7 * * *"
|
|
43
|
+
action: 'morning_plan',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Evening review: fire at the start of the evening window
|
|
47
|
+
workflows.push({
|
|
48
|
+
id: 'goal_evening_review',
|
|
49
|
+
name: 'Evening Goal Review',
|
|
50
|
+
description: 'Automated evening review — scores daily progress, generates accountability assessment, and updates goal health.',
|
|
51
|
+
triggerType: 'cron',
|
|
52
|
+
cronExpression: `0 ${eveningWindow.start} * * *`, // e.g., "0 20 * * *"
|
|
53
|
+
action: 'evening_review',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return workflows;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register goal rhythm workflows with the trigger manager.
|
|
61
|
+
* This bridges goal check-ins with the workflow execution system.
|
|
62
|
+
*
|
|
63
|
+
* When the workflow fires, it calls the goalService's daily rhythm methods.
|
|
64
|
+
*/
|
|
65
|
+
export function registerGoalWorkflows(
|
|
66
|
+
goalWorkflows: GoalWorkflowDefinition[],
|
|
67
|
+
triggerManager: { fireTrigger: (workflowId: string, triggerType: string, data?: Record<string, unknown>) => void },
|
|
68
|
+
): void {
|
|
69
|
+
// Note: The actual cron scheduling is handled by the GoalService's own timers
|
|
70
|
+
// (checkDailyRhythm runs every 60s and checks the time window).
|
|
71
|
+
// This function exists for future use when we want to create
|
|
72
|
+
// full workflow graph executions for morning/evening routines.
|
|
73
|
+
//
|
|
74
|
+
// For now, we just log the available workflows.
|
|
75
|
+
for (const wf of goalWorkflows) {
|
|
76
|
+
console.log(`[GoalWorkflowBridge] Registered: ${wf.name} (${wf.cronExpression})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle a goal workflow trigger firing.
|
|
82
|
+
* Called from the goal service when daily rhythm check-ins happen.
|
|
83
|
+
*/
|
|
84
|
+
export async function handleGoalWorkflowTrigger(
|
|
85
|
+
action: 'morning_plan' | 'evening_review',
|
|
86
|
+
goalService: { getGoal: (id: string) => unknown },
|
|
87
|
+
onComplete?: (result: Record<string, unknown>) => void,
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
console.log(`[GoalWorkflowBridge] Executing ${action}`);
|
|
90
|
+
|
|
91
|
+
// The actual planning/review logic is in rhythm.ts
|
|
92
|
+
// This bridge just provides the workflow integration layer
|
|
93
|
+
if (onComplete) {
|
|
94
|
+
onComplete({ action, completedAt: Date.now() });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google API Wrappers
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers around Gmail and Calendar REST APIs.
|
|
5
|
+
* Uses raw fetch() — no googleapis package needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const GMAIL_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me';
|
|
9
|
+
const CALENDAR_BASE = 'https://www.googleapis.com/calendar/v3';
|
|
10
|
+
|
|
11
|
+
// --- Types ---
|
|
12
|
+
|
|
13
|
+
export type GmailMessage = {
|
|
14
|
+
id: string;
|
|
15
|
+
threadId: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type GmailMessageDetail = {
|
|
19
|
+
id: string;
|
|
20
|
+
threadId: string;
|
|
21
|
+
subject: string;
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
date: string;
|
|
25
|
+
snippet: string;
|
|
26
|
+
labels: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CalendarEvent = {
|
|
30
|
+
id: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
start: string;
|
|
34
|
+
end: string;
|
|
35
|
+
location?: string;
|
|
36
|
+
attendees: string[];
|
|
37
|
+
htmlLink?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// --- Gmail ---
|
|
41
|
+
|
|
42
|
+
export async function listUnreadEmails(
|
|
43
|
+
accessToken: string,
|
|
44
|
+
maxResults: number = 10
|
|
45
|
+
): Promise<GmailMessage[]> {
|
|
46
|
+
const params = new URLSearchParams({
|
|
47
|
+
q: 'is:unread',
|
|
48
|
+
maxResults: String(maxResults),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const resp = await fetch(`${GMAIL_BASE}/messages?${params}`, {
|
|
52
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!resp.ok) {
|
|
56
|
+
throw new Error(`Gmail list failed: ${resp.status} ${resp.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = await resp.json() as any;
|
|
60
|
+
return (data.messages ?? []).map((m: any) => ({
|
|
61
|
+
id: m.id,
|
|
62
|
+
threadId: m.threadId,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getEmailDetail(
|
|
67
|
+
accessToken: string,
|
|
68
|
+
messageId: string
|
|
69
|
+
): Promise<GmailMessageDetail> {
|
|
70
|
+
const resp = await fetch(`${GMAIL_BASE}/messages/${messageId}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date`, {
|
|
71
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!resp.ok) {
|
|
75
|
+
throw new Error(`Gmail get failed: ${resp.status} ${resp.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const data = await resp.json() as any;
|
|
79
|
+
const headers = data.payload?.headers ?? [];
|
|
80
|
+
|
|
81
|
+
function getHeader(name: string): string {
|
|
82
|
+
return headers.find((h: any) => h.name === name)?.value ?? '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: data.id,
|
|
87
|
+
threadId: data.threadId,
|
|
88
|
+
subject: getHeader('Subject'),
|
|
89
|
+
from: getHeader('From'),
|
|
90
|
+
to: getHeader('To'),
|
|
91
|
+
date: getHeader('Date'),
|
|
92
|
+
snippet: data.snippet ?? '',
|
|
93
|
+
labels: data.labelIds ?? [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Calendar ---
|
|
98
|
+
|
|
99
|
+
export async function listUpcomingEvents(
|
|
100
|
+
accessToken: string,
|
|
101
|
+
calendarId: string,
|
|
102
|
+
timeMin: string,
|
|
103
|
+
timeMax: string,
|
|
104
|
+
maxResults: number = 20
|
|
105
|
+
): Promise<CalendarEvent[]> {
|
|
106
|
+
const params = new URLSearchParams({
|
|
107
|
+
timeMin,
|
|
108
|
+
timeMax,
|
|
109
|
+
maxResults: String(maxResults),
|
|
110
|
+
singleEvents: 'true',
|
|
111
|
+
orderBy: 'startTime',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const resp = await fetch(
|
|
115
|
+
`${CALENDAR_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`,
|
|
116
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!resp.ok) {
|
|
120
|
+
throw new Error(`Calendar list failed: ${resp.status} ${resp.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = await resp.json() as any;
|
|
124
|
+
return (data.items ?? []).map((event: any) => ({
|
|
125
|
+
id: event.id,
|
|
126
|
+
summary: event.summary ?? '(No title)',
|
|
127
|
+
description: event.description,
|
|
128
|
+
start: event.start?.dateTime ?? event.start?.date ?? '',
|
|
129
|
+
end: event.end?.dateTime ?? event.end?.date ?? '',
|
|
130
|
+
location: event.location,
|
|
131
|
+
attendees: (event.attendees ?? []).map((a: any) => a.email).filter(Boolean),
|
|
132
|
+
htmlLink: event.htmlLink,
|
|
133
|
+
}));
|
|
134
|
+
}
|