@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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommitmentExecutor — Notify-Then-Execute Engine
|
|
3
|
+
*
|
|
4
|
+
* Detects due commitments, announces pending execution to the UI
|
|
5
|
+
* with a cancel window, then forces the agent to execute if not cancelled.
|
|
6
|
+
*
|
|
7
|
+
* Aggressiveness modes:
|
|
8
|
+
* passive: announce only, never auto-execute
|
|
9
|
+
* moderate: 30s cancel window (default)
|
|
10
|
+
* aggressive: 5s cancel window
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getDueCommitments, getUpcoming, updateCommitmentStatus } from '../vault/commitments.ts';
|
|
14
|
+
import type { Commitment } from '../vault/commitments.ts';
|
|
15
|
+
import type { IAgentService } from './agent-service-interface.ts';
|
|
16
|
+
import type { WSMessage } from '../comms/websocket.ts';
|
|
17
|
+
|
|
18
|
+
export type Aggressiveness = 'passive' | 'moderate' | 'aggressive';
|
|
19
|
+
|
|
20
|
+
export type ExecutionState = {
|
|
21
|
+
commitmentId: string;
|
|
22
|
+
what: string;
|
|
23
|
+
announcedAt: number;
|
|
24
|
+
cancelDeadline: number;
|
|
25
|
+
cancelled: boolean;
|
|
26
|
+
executed: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type BroadcastFn = (msg: WSMessage) => void;
|
|
30
|
+
|
|
31
|
+
const CANCEL_WINDOW: Record<Aggressiveness, number> = {
|
|
32
|
+
passive: Infinity,
|
|
33
|
+
moderate: 30_000,
|
|
34
|
+
aggressive: 5_000,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class CommitmentExecutor {
|
|
38
|
+
private agentService: IAgentService | null = null;
|
|
39
|
+
private broadcast: BroadcastFn | null = null;
|
|
40
|
+
private pending: Map<string, ExecutionState> = new Map();
|
|
41
|
+
private executedIds: Set<string> = new Set();
|
|
42
|
+
private checkTimer: Timer | null = null;
|
|
43
|
+
private tickTimer: Timer | null = null;
|
|
44
|
+
private aggressiveness: Aggressiveness;
|
|
45
|
+
private running = false;
|
|
46
|
+
|
|
47
|
+
constructor(aggressiveness: Aggressiveness = 'moderate') {
|
|
48
|
+
this.aggressiveness = aggressiveness;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setAgentService(agent: IAgentService): void {
|
|
52
|
+
this.agentService = agent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setBroadcast(fn: BroadcastFn): void {
|
|
56
|
+
this.broadcast = fn;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
start(): void {
|
|
60
|
+
if (this.running) return;
|
|
61
|
+
this.running = true;
|
|
62
|
+
|
|
63
|
+
// Check for due commitments every 60 seconds
|
|
64
|
+
this.checkTimer = setInterval(() => {
|
|
65
|
+
this.checkAndAnnounce();
|
|
66
|
+
}, 60_000);
|
|
67
|
+
|
|
68
|
+
// Tick pending executions every 5 seconds
|
|
69
|
+
this.tickTimer = setInterval(() => {
|
|
70
|
+
this.tickExecutions().catch((err) =>
|
|
71
|
+
console.error('[Executor] Tick error:', err)
|
|
72
|
+
);
|
|
73
|
+
}, 5_000);
|
|
74
|
+
|
|
75
|
+
// Run an immediate check
|
|
76
|
+
this.checkAndAnnounce();
|
|
77
|
+
|
|
78
|
+
console.log(`[Executor] Started (mode: ${this.aggressiveness})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
stop(): void {
|
|
82
|
+
this.running = false;
|
|
83
|
+
if (this.checkTimer) {
|
|
84
|
+
clearInterval(this.checkTimer);
|
|
85
|
+
this.checkTimer = null;
|
|
86
|
+
}
|
|
87
|
+
if (this.tickTimer) {
|
|
88
|
+
clearInterval(this.tickTimer);
|
|
89
|
+
this.tickTimer = null;
|
|
90
|
+
}
|
|
91
|
+
console.log('[Executor] Stopped');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check for commitments that are due or due within 2 minutes.
|
|
96
|
+
* Announce each one as a pending execution.
|
|
97
|
+
*/
|
|
98
|
+
checkAndAnnounce(): void {
|
|
99
|
+
try {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const dueNow = getDueCommitments(); // when_due <= now
|
|
102
|
+
const upcoming = getUpcoming(20); // all upcoming with when_due
|
|
103
|
+
|
|
104
|
+
// Filter upcoming to those due within 2 minutes
|
|
105
|
+
const dueSoon = upcoming.filter(
|
|
106
|
+
(c) => c.when_due && c.when_due > now && c.when_due <= now + 2 * 60_000
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const candidates = [...dueNow, ...dueSoon];
|
|
110
|
+
|
|
111
|
+
for (const commitment of candidates) {
|
|
112
|
+
// Skip if already announced, executed, or terminal status
|
|
113
|
+
if (this.pending.has(commitment.id)) continue;
|
|
114
|
+
if (this.executedIds.has(commitment.id)) continue;
|
|
115
|
+
if (commitment.status === 'completed' || commitment.status === 'failed') continue;
|
|
116
|
+
|
|
117
|
+
this.announceExecution(commitment);
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error('[Executor] Check error:', err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cancel a pending execution. Returns true if successfully cancelled.
|
|
126
|
+
*/
|
|
127
|
+
cancelExecution(commitmentId: string): boolean {
|
|
128
|
+
const state = this.pending.get(commitmentId);
|
|
129
|
+
if (!state || state.executed || state.cancelled) return false;
|
|
130
|
+
|
|
131
|
+
state.cancelled = true;
|
|
132
|
+
console.log(`[Executor] Cancelled execution: ${state.what}`);
|
|
133
|
+
|
|
134
|
+
// Broadcast cancellation confirmation
|
|
135
|
+
this.broadcast?.({
|
|
136
|
+
type: 'notification',
|
|
137
|
+
payload: {
|
|
138
|
+
source: 'commitment_executor',
|
|
139
|
+
action: 'execution_cancelled',
|
|
140
|
+
commitmentId,
|
|
141
|
+
what: state.what,
|
|
142
|
+
},
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Clean up
|
|
147
|
+
this.pending.delete(commitmentId);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get all pending executions (for UI display).
|
|
153
|
+
*/
|
|
154
|
+
getPending(): ExecutionState[] {
|
|
155
|
+
return Array.from(this.pending.values()).filter((s) => !s.cancelled && !s.executed);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Private ---
|
|
159
|
+
|
|
160
|
+
private announceExecution(commitment: Commitment): void {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const cancelWindow = CANCEL_WINDOW[this.aggressiveness];
|
|
163
|
+
|
|
164
|
+
const state: ExecutionState = {
|
|
165
|
+
commitmentId: commitment.id,
|
|
166
|
+
what: commitment.what,
|
|
167
|
+
announcedAt: now,
|
|
168
|
+
cancelDeadline: cancelWindow === Infinity ? Infinity : now + cancelWindow,
|
|
169
|
+
cancelled: false,
|
|
170
|
+
executed: false,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
this.pending.set(commitment.id, state);
|
|
174
|
+
|
|
175
|
+
if (this.aggressiveness === 'passive') {
|
|
176
|
+
console.log(`[Executor] Announced (passive, no auto-execute): ${commitment.what}`);
|
|
177
|
+
} else {
|
|
178
|
+
const windowSec = Math.round(cancelWindow / 1000);
|
|
179
|
+
console.log(`[Executor] Announced: "${commitment.what}" — executing in ${windowSec}s unless cancelled`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Broadcast announcement to all WebSocket clients
|
|
183
|
+
this.broadcast?.({
|
|
184
|
+
type: 'notification',
|
|
185
|
+
payload: {
|
|
186
|
+
source: 'commitment_executor',
|
|
187
|
+
action: 'pending_execution',
|
|
188
|
+
commitmentId: commitment.id,
|
|
189
|
+
what: commitment.what,
|
|
190
|
+
executeAt: state.cancelDeadline === Infinity ? null : state.cancelDeadline,
|
|
191
|
+
cancelWindowMs: cancelWindow === Infinity ? null : cancelWindow,
|
|
192
|
+
},
|
|
193
|
+
timestamp: now,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Also broadcast as a chat message so the user sees it
|
|
197
|
+
this.broadcast?.({
|
|
198
|
+
type: 'chat',
|
|
199
|
+
payload: {
|
|
200
|
+
text: this.aggressiveness === 'passive'
|
|
201
|
+
? `Task due: "${commitment.what}". Waiting for your instruction to proceed.`
|
|
202
|
+
: `Executing "${commitment.what}" in ${Math.round(cancelWindow / 1000)}s. Send cancel to abort.`,
|
|
203
|
+
source: 'proactive',
|
|
204
|
+
},
|
|
205
|
+
priority: 'urgent',
|
|
206
|
+
timestamp: now,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async tickExecutions(): Promise<void> {
|
|
211
|
+
if (!this.agentService) return;
|
|
212
|
+
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
|
|
215
|
+
for (const [id, state] of this.pending) {
|
|
216
|
+
if (state.cancelled || state.executed) {
|
|
217
|
+
this.pending.delete(id);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if cancel window has expired
|
|
222
|
+
if (now < state.cancelDeadline) continue;
|
|
223
|
+
|
|
224
|
+
// Execute!
|
|
225
|
+
state.executed = true;
|
|
226
|
+
this.pending.delete(id);
|
|
227
|
+
this.executedIds.add(id);
|
|
228
|
+
|
|
229
|
+
// Cap executedIds memory
|
|
230
|
+
if (this.executedIds.size > 500) {
|
|
231
|
+
const arr = Array.from(this.executedIds);
|
|
232
|
+
this.executedIds = new Set(arr.slice(arr.length - 250));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await this.executeCommitment(state);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error(`[Executor] Failed to execute "${state.what}":`, err);
|
|
239
|
+
try {
|
|
240
|
+
const reason = err instanceof Error ? err.message : 'Execution failed';
|
|
241
|
+
updateCommitmentStatus(state.commitmentId, 'failed', reason);
|
|
242
|
+
} catch { /* ignore */ }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async executeCommitment(state: ExecutionState): Promise<void> {
|
|
248
|
+
console.log(`[Executor] Executing: "${state.what}"`);
|
|
249
|
+
|
|
250
|
+
// Mark as active
|
|
251
|
+
try {
|
|
252
|
+
updateCommitmentStatus(state.commitmentId, 'active');
|
|
253
|
+
} catch { /* ignore */ }
|
|
254
|
+
|
|
255
|
+
// Build a mandatory execution prompt
|
|
256
|
+
const prompt = [
|
|
257
|
+
'[COMMITMENT EXECUTION — MANDATORY]',
|
|
258
|
+
'',
|
|
259
|
+
`You previously committed to: "${state.what}"`,
|
|
260
|
+
'This commitment is now due. Execute it NOW using your tools.',
|
|
261
|
+
'',
|
|
262
|
+
'Instructions:',
|
|
263
|
+
'1. Use your available tools (browser, terminal, file operations) to complete this task.',
|
|
264
|
+
'2. Be thorough — actually perform the work, don\'t just describe it.',
|
|
265
|
+
'3. After completion, summarize what you did.',
|
|
266
|
+
'4. If the task is impossible or unclear, explain why and suggest alternatives.',
|
|
267
|
+
'',
|
|
268
|
+
'BEGIN EXECUTION.',
|
|
269
|
+
].join('\n');
|
|
270
|
+
|
|
271
|
+
const response = await this.agentService!.handleMessage(prompt, 'system');
|
|
272
|
+
|
|
273
|
+
// Broadcast the execution result
|
|
274
|
+
this.broadcast?.({
|
|
275
|
+
type: 'chat',
|
|
276
|
+
payload: {
|
|
277
|
+
text: response ?? 'Task executed (no response).',
|
|
278
|
+
source: 'proactive',
|
|
279
|
+
},
|
|
280
|
+
priority: 'normal',
|
|
281
|
+
timestamp: Date.now(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Mark commitment as completed
|
|
285
|
+
const resultSummary = response
|
|
286
|
+
? response.length > 500 ? response.slice(0, 497) + '...' : response
|
|
287
|
+
: 'Executed successfully';
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
updateCommitmentStatus(state.commitmentId, 'completed', resultSummary);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error('[Executor] Failed to update commitment status:', err);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log(`[Executor] Completed: "${state.what}"`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Classifier — Priority Router
|
|
3
|
+
*
|
|
4
|
+
* Classifies ObserverEvents into priority levels using rule-based logic.
|
|
5
|
+
* No LLM calls — must be instant. Critical/high events trigger immediate
|
|
6
|
+
* reactions; normal/low events get batched for the next heartbeat.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ObserverEvent } from '../observers/index.ts';
|
|
10
|
+
import { getDueCommitments, getUpcoming } from '../vault/commitments.ts';
|
|
11
|
+
|
|
12
|
+
export type EventPriority = 'critical' | 'high' | 'normal' | 'low';
|
|
13
|
+
|
|
14
|
+
export type ClassifiedEvent = {
|
|
15
|
+
event: ObserverEvent;
|
|
16
|
+
priority: EventPriority;
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Patterns that suggest high-intent clipboard content
|
|
21
|
+
const URL_PATTERN = /https?:\/\/\S+/i;
|
|
22
|
+
const EMAIL_PATTERN = /[\w.-]+@[\w.-]+\.\w{2,}/;
|
|
23
|
+
const PHONE_PATTERN = /(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/;
|
|
24
|
+
|
|
25
|
+
// Directories where file changes are low-priority noise
|
|
26
|
+
const LOW_PRIORITY_DIRS = ['/tmp', '/var/tmp', '/dev/shm', 'node_modules', '.git', '__pycache__', '.cache'];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Classify an observer event into a priority level.
|
|
30
|
+
* Rules are evaluated top-down; first match wins.
|
|
31
|
+
*/
|
|
32
|
+
export function classifyEvent(event: ObserverEvent): ClassifiedEvent {
|
|
33
|
+
const { type, data } = event;
|
|
34
|
+
|
|
35
|
+
// --- Commitment-related events (injected by heartbeat checks) ---
|
|
36
|
+
if (type === 'commitment_overdue') {
|
|
37
|
+
return { event, priority: 'critical', reason: `Commitment overdue: ${data.what}` };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (type === 'commitment_due_soon') {
|
|
41
|
+
return { event, priority: 'high', reason: `Commitment due within 15 min: ${data.what}` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Clipboard events ---
|
|
45
|
+
if (type === 'clipboard') {
|
|
46
|
+
const text = String(data.content ?? '');
|
|
47
|
+
|
|
48
|
+
if (text.length < 3) {
|
|
49
|
+
return { event, priority: 'low', reason: 'Clipboard: trivial content' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (URL_PATTERN.test(text)) {
|
|
53
|
+
return { event, priority: 'high', reason: 'Clipboard contains URL — possible intent to browse/research' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (EMAIL_PATTERN.test(text)) {
|
|
57
|
+
return { event, priority: 'high', reason: 'Clipboard contains email address — possible intent to contact' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (PHONE_PATTERN.test(text)) {
|
|
61
|
+
return { event, priority: 'high', reason: 'Clipboard contains phone number' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (text.length > 200) {
|
|
65
|
+
return { event, priority: 'normal', reason: 'Clipboard: substantial text copied' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { event, priority: 'low', reason: 'Clipboard: short text' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- File change events ---
|
|
72
|
+
if (type === 'file_change') {
|
|
73
|
+
const filePath = String(data.path ?? '');
|
|
74
|
+
const changeType = String(data.changeType ?? data.type ?? '');
|
|
75
|
+
|
|
76
|
+
// Check if file is in a low-priority directory
|
|
77
|
+
if (LOW_PRIORITY_DIRS.some(dir => filePath.includes(dir))) {
|
|
78
|
+
return { event, priority: 'low', reason: `File change in noisy directory: ${filePath}` };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Large file deletions are noteworthy
|
|
82
|
+
if (changeType === 'delete' || changeType === 'rename') {
|
|
83
|
+
return { event, priority: 'high', reason: `File ${changeType}: ${filePath}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { event, priority: 'normal', reason: `File modified: ${filePath}` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- Process events ---
|
|
90
|
+
if (type === 'process_started') {
|
|
91
|
+
const name = String(data.name ?? data.command ?? '');
|
|
92
|
+
|
|
93
|
+
// Interesting process launches
|
|
94
|
+
if (/chrome|firefox|code|slack|discord|telegram|zoom/i.test(name)) {
|
|
95
|
+
return { event, priority: 'normal', reason: `Notable app launched: ${name}` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { event, priority: 'low', reason: `Process started: ${name}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (type === 'process_stopped') {
|
|
102
|
+
return { event, priority: 'low', reason: `Process stopped: ${data.name ?? data.pid}` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Notification events ---
|
|
106
|
+
if (type === 'notification') {
|
|
107
|
+
const urgency = String(data.urgency ?? '');
|
|
108
|
+
|
|
109
|
+
if (urgency === 'critical') {
|
|
110
|
+
return { event, priority: 'critical', reason: `System notification (critical): ${data.summary}` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { event, priority: 'normal', reason: `System notification: ${data.summary}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Calendar / Email events ---
|
|
117
|
+
if (type === 'calendar') {
|
|
118
|
+
return { event, priority: 'high', reason: `Calendar event: ${data.summary ?? data.title}` };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (type === 'email') {
|
|
122
|
+
const subject = String(data.subject ?? '');
|
|
123
|
+
const labels = Array.isArray(data.labels) ? data.labels as string[] : [];
|
|
124
|
+
|
|
125
|
+
// IMPORTANT/STARRED labels → high priority
|
|
126
|
+
if (labels.includes('IMPORTANT') || labels.includes('STARRED')) {
|
|
127
|
+
return { event, priority: 'high', reason: `Important email: ${subject}` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Urgent keywords in subject
|
|
131
|
+
const urgentKeywords = /\b(urgent|asap|critical|emergency|action required|immediate|deadline)\b/i;
|
|
132
|
+
if (urgentKeywords.test(subject)) {
|
|
133
|
+
return { event, priority: 'high', reason: `Urgent email: ${subject}` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { event, priority: 'normal', reason: `New email: ${subject || 'no subject'}` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Awareness events (M13) ---
|
|
140
|
+
if (type === 'error_detected') {
|
|
141
|
+
return { event, priority: 'high', reason: `Screen error detected: ${data.errorText} in ${data.appName}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (type === 'struggle_detected') {
|
|
145
|
+
const score = data.compositeScore as number;
|
|
146
|
+
const priority = score >= 0.7 ? 'high' : 'normal';
|
|
147
|
+
return { event, priority, reason: `User struggling in ${data.appName} (score: ${score?.toFixed(2)}, ${data.appCategory})` };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (type === 'stuck_detected') {
|
|
151
|
+
return { event, priority: 'normal', reason: `User appears stuck in ${data.appName} (${Math.round((data.durationMs as number) / 1000)}s)` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (type === 'context_changed') {
|
|
155
|
+
return { event, priority: 'low', reason: `Switched from ${data.fromApp} to ${data.toApp}` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (type === 'session_started' || type === 'session_ended') {
|
|
159
|
+
return { event, priority: 'low', reason: `Activity session ${type === 'session_started' ? 'started' : 'ended'}` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (type === 'suggestion_ready') {
|
|
163
|
+
return { event, priority: 'normal', reason: `Awareness suggestion: ${data.title}` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (type === 'screen_capture') {
|
|
167
|
+
return { event, priority: 'low', reason: `Screen captured (${Math.round((data.pixelChangePct as number) * 100)}% change)` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- Sidecar events ---
|
|
171
|
+
if (type === 'sidecar_register') {
|
|
172
|
+
return { event, priority: 'normal', reason: `Sidecar registered: ${data.name ?? data.sidecar_id}` };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (type === 'sidecar_disconnect') {
|
|
176
|
+
return { event, priority: 'normal', reason: `Sidecar disconnected: ${data.name ?? data.sidecar_id}` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (type === 'sidecar_rpc_error') {
|
|
180
|
+
return { event, priority: 'high', reason: `Sidecar RPC error: ${data.error ?? data.method} on ${data.name ?? data.sidecar_id}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (type === 'sidecar_rpc_complete') {
|
|
184
|
+
return { event, priority: 'low', reason: `Sidecar RPC complete: ${data.method} on ${data.name ?? data.sidecar_id}` };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (type.startsWith('sidecar_')) {
|
|
188
|
+
return { event, priority: 'normal', reason: `Sidecar event: ${type}` };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Default ---
|
|
192
|
+
return { event, priority: 'low', reason: `Unclassified event: ${type}` };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check commitments and generate synthetic events for due/overdue items.
|
|
197
|
+
* Called periodically (e.g., every heartbeat) to inject commitment-awareness.
|
|
198
|
+
*/
|
|
199
|
+
export function checkCommitments(): ClassifiedEvent[] {
|
|
200
|
+
const events: ClassifiedEvent[] = [];
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Check overdue commitments
|
|
204
|
+
const overdue = getDueCommitments();
|
|
205
|
+
for (const c of overdue) {
|
|
206
|
+
events.push({
|
|
207
|
+
event: {
|
|
208
|
+
type: 'commitment_overdue',
|
|
209
|
+
data: { id: c.id, what: c.what, when_due: c.when_due, priority: c.priority },
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
},
|
|
212
|
+
priority: 'critical',
|
|
213
|
+
reason: `Commitment overdue: ${c.what}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check commitments due within 15 minutes
|
|
218
|
+
const upcoming = getUpcoming(10);
|
|
219
|
+
const fifteenMinFromNow = Date.now() + 15 * 60 * 1000;
|
|
220
|
+
|
|
221
|
+
for (const c of upcoming) {
|
|
222
|
+
if (c.when_due && c.when_due <= fifteenMinFromNow && c.when_due > Date.now()) {
|
|
223
|
+
events.push({
|
|
224
|
+
event: {
|
|
225
|
+
type: 'commitment_due_soon',
|
|
226
|
+
data: { id: c.id, what: c.what, when_due: c.when_due, priority: c.priority },
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
},
|
|
229
|
+
priority: 'high',
|
|
230
|
+
reason: `Commitment due within 15 min: ${c.what}`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.error('[EventClassifier] Error checking commitments:', err);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return events;
|
|
239
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Coalescer — Batch Buffer
|
|
3
|
+
*
|
|
4
|
+
* Collects normal/low priority events in memory and flushes them
|
|
5
|
+
* as a formatted summary string at heartbeat time. Groups events
|
|
6
|
+
* by type for clean LLM consumption.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ClassifiedEvent, EventPriority } from './event-classifier.ts';
|
|
10
|
+
|
|
11
|
+
const MAX_BUFFER_SIZE = 100;
|
|
12
|
+
|
|
13
|
+
export class EventCoalescer {
|
|
14
|
+
private buffer: ClassifiedEvent[] = [];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add a classified event to the buffer.
|
|
18
|
+
* Oldest events are dropped if buffer exceeds max size.
|
|
19
|
+
*/
|
|
20
|
+
addEvent(event: ClassifiedEvent): void {
|
|
21
|
+
this.buffer.push(event);
|
|
22
|
+
|
|
23
|
+
if (this.buffer.length > MAX_BUFFER_SIZE) {
|
|
24
|
+
// Drop oldest events
|
|
25
|
+
const overflow = this.buffer.length - MAX_BUFFER_SIZE;
|
|
26
|
+
this.buffer.splice(0, overflow);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the current buffer size.
|
|
32
|
+
*/
|
|
33
|
+
get size(): number {
|
|
34
|
+
return this.buffer.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Flush the buffer and return a formatted summary string.
|
|
39
|
+
* Returns empty string if no events buffered.
|
|
40
|
+
*/
|
|
41
|
+
flush(): string {
|
|
42
|
+
if (this.buffer.length === 0) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const events = [...this.buffer];
|
|
47
|
+
this.buffer = [];
|
|
48
|
+
|
|
49
|
+
// Group by event type
|
|
50
|
+
const groups = new Map<string, ClassifiedEvent[]>();
|
|
51
|
+
for (const evt of events) {
|
|
52
|
+
const type = evt.event.type;
|
|
53
|
+
if (!groups.has(type)) {
|
|
54
|
+
groups.set(type, []);
|
|
55
|
+
}
|
|
56
|
+
groups.get(type)!.push(evt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Format each group
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
lines.push(`## Recent Activity (${events.length} events since last check)`);
|
|
62
|
+
lines.push('');
|
|
63
|
+
|
|
64
|
+
for (const [type, groupEvents] of groups) {
|
|
65
|
+
const label = formatEventType(type);
|
|
66
|
+
lines.push(`**${label}** (${groupEvents.length}):`);
|
|
67
|
+
|
|
68
|
+
// Show up to 5 details per group, summarize the rest
|
|
69
|
+
const shown = groupEvents.slice(0, 5);
|
|
70
|
+
for (const evt of shown) {
|
|
71
|
+
lines.push(` - ${evt.reason}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (groupEvents.length > 5) {
|
|
75
|
+
lines.push(` - ... and ${groupEvents.length - 5} more`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Peek at the buffer without clearing it.
|
|
86
|
+
*/
|
|
87
|
+
peek(): ClassifiedEvent[] {
|
|
88
|
+
return [...this.buffer];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clear the buffer without returning anything.
|
|
93
|
+
*/
|
|
94
|
+
clear(): void {
|
|
95
|
+
this.buffer = [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Human-readable label for event types.
|
|
101
|
+
*/
|
|
102
|
+
function formatEventType(type: string): string {
|
|
103
|
+
switch (type) {
|
|
104
|
+
case 'file_change': return 'File Changes';
|
|
105
|
+
case 'clipboard': return 'Clipboard Activity';
|
|
106
|
+
case 'process_started': return 'Apps Launched';
|
|
107
|
+
case 'process_stopped': return 'Apps Closed';
|
|
108
|
+
case 'notification': return 'System Notifications';
|
|
109
|
+
case 'calendar': return 'Calendar Events';
|
|
110
|
+
case 'email': return 'Emails';
|
|
111
|
+
case 'browser': return 'Browser Activity';
|
|
112
|
+
case 'commitment_overdue': return 'Overdue Commitments';
|
|
113
|
+
case 'commitment_due_soon': return 'Upcoming Commitments';
|
|
114
|
+
case 'screen_capture': return 'Screen Captures';
|
|
115
|
+
case 'context_changed': return 'Context Switches';
|
|
116
|
+
case 'error_detected': return 'Screen Errors';
|
|
117
|
+
case 'stuck_detected': return 'Stuck Detection';
|
|
118
|
+
case 'session_started': return 'Sessions Started';
|
|
119
|
+
case 'session_ended': return 'Sessions Ended';
|
|
120
|
+
case 'suggestion_ready': return 'Awareness Suggestions';
|
|
121
|
+
default: return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
122
|
+
}
|
|
123
|
+
}
|