@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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationListener — D-Bus Notification Monitor (Linux/WSL2)
|
|
3
|
+
*
|
|
4
|
+
* Monitors system notifications by watching D-Bus for
|
|
5
|
+
* org.freedesktop.Notifications.Notify method calls.
|
|
6
|
+
* Parses notification fields: app_name, summary, body, urgency.
|
|
7
|
+
*
|
|
8
|
+
* Graceful: if dbus-monitor is not found, logs warning and stays no-op.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Observer, ObserverEventHandler } from './index';
|
|
12
|
+
import type { Subprocess } from 'bun';
|
|
13
|
+
|
|
14
|
+
type NotificationData = {
|
|
15
|
+
app: string;
|
|
16
|
+
title: string;
|
|
17
|
+
body: string;
|
|
18
|
+
urgency: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class NotificationListener implements Observer {
|
|
22
|
+
name = 'notifications';
|
|
23
|
+
private running = false;
|
|
24
|
+
private handler: ObserverEventHandler | null = null;
|
|
25
|
+
private process: Subprocess | null = null;
|
|
26
|
+
private available = false;
|
|
27
|
+
|
|
28
|
+
async start(): Promise<void> {
|
|
29
|
+
this.running = true;
|
|
30
|
+
|
|
31
|
+
// Check if dbus-monitor exists
|
|
32
|
+
try {
|
|
33
|
+
const check = Bun.spawnSync(['which', 'dbus-monitor']);
|
|
34
|
+
if (check.exitCode !== 0) {
|
|
35
|
+
console.log('[notifications] dbus-monitor not found — notification monitoring disabled');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.available = true;
|
|
39
|
+
} catch {
|
|
40
|
+
console.log('[notifications] Cannot check for dbus-monitor — notification monitoring disabled');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Spawn dbus-monitor watching for Notify signals
|
|
45
|
+
try {
|
|
46
|
+
this.process = Bun.spawn(
|
|
47
|
+
[
|
|
48
|
+
'dbus-monitor',
|
|
49
|
+
'--session',
|
|
50
|
+
"interface='org.freedesktop.Notifications',member='Notify'",
|
|
51
|
+
],
|
|
52
|
+
{
|
|
53
|
+
stdout: 'pipe',
|
|
54
|
+
stderr: 'ignore',
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
console.log('[notifications] Observer started — monitoring D-Bus notifications');
|
|
59
|
+
|
|
60
|
+
// Parse stdout in background
|
|
61
|
+
this.readOutput();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('[notifications] Failed to start dbus-monitor:', err);
|
|
64
|
+
this.available = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async stop(): Promise<void> {
|
|
69
|
+
this.running = false;
|
|
70
|
+
|
|
71
|
+
if (this.process) {
|
|
72
|
+
try {
|
|
73
|
+
this.process.kill();
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore
|
|
76
|
+
}
|
|
77
|
+
this.process = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('[notifications] Observer stopped');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isRunning(): boolean {
|
|
84
|
+
return this.running;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onEvent(handler: ObserverEventHandler): void {
|
|
88
|
+
this.handler = handler;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read dbus-monitor output and parse notification blocks.
|
|
93
|
+
*
|
|
94
|
+
* D-Bus notification format (method_call):
|
|
95
|
+
* method call ... dest=org.freedesktop.Notifications ... member=Notify
|
|
96
|
+
* string "app_name"
|
|
97
|
+
* uint32 ...
|
|
98
|
+
* string "icon"
|
|
99
|
+
* string "summary"
|
|
100
|
+
* string "body"
|
|
101
|
+
* array [ ... ]
|
|
102
|
+
* ...
|
|
103
|
+
*/
|
|
104
|
+
private async readOutput(): Promise<void> {
|
|
105
|
+
if (!this.process?.stdout) return;
|
|
106
|
+
|
|
107
|
+
const reader = (this.process.stdout as ReadableStream<Uint8Array>).getReader();
|
|
108
|
+
const decoder = new TextDecoder();
|
|
109
|
+
let buffer = '';
|
|
110
|
+
|
|
111
|
+
// State machine for parsing method_call blocks
|
|
112
|
+
let inMethodCall = false;
|
|
113
|
+
let stringIndex = 0;
|
|
114
|
+
let currentNotification: Partial<NotificationData> = {};
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
while (this.running) {
|
|
118
|
+
const { done, value } = await reader.read();
|
|
119
|
+
if (done) break;
|
|
120
|
+
|
|
121
|
+
buffer += decoder.decode(value, { stream: true });
|
|
122
|
+
const lines = buffer.split('\n');
|
|
123
|
+
buffer = lines.pop() ?? '';
|
|
124
|
+
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
|
|
128
|
+
// Detect start of a new method call
|
|
129
|
+
if (trimmed.startsWith('method call') && trimmed.includes('member=Notify')) {
|
|
130
|
+
inMethodCall = true;
|
|
131
|
+
stringIndex = 0;
|
|
132
|
+
currentNotification = {};
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Detect start of next method/signal (end of current block)
|
|
137
|
+
if (inMethodCall && (trimmed.startsWith('method call') || trimmed.startsWith('signal') || trimmed.startsWith('method return'))) {
|
|
138
|
+
// Emit the notification if we got enough data
|
|
139
|
+
this.emitNotification(currentNotification);
|
|
140
|
+
inMethodCall = trimmed.startsWith('method call') && trimmed.includes('member=Notify');
|
|
141
|
+
stringIndex = 0;
|
|
142
|
+
currentNotification = {};
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!inMethodCall) continue;
|
|
147
|
+
|
|
148
|
+
// Parse string values — the Notify method has these string args in order:
|
|
149
|
+
// 0: app_name, 1: (replaces_id is uint32, skip), 2: icon, 3: summary, 4: body
|
|
150
|
+
const stringMatch = trimmed.match(/^string\s+"(.*)"/);
|
|
151
|
+
if (stringMatch) {
|
|
152
|
+
const val = stringMatch[1];
|
|
153
|
+
switch (stringIndex) {
|
|
154
|
+
case 0: currentNotification.app = val; break;
|
|
155
|
+
// index 1 is icon (we skip)
|
|
156
|
+
case 1: break; // icon
|
|
157
|
+
case 2: currentNotification.title = val; break;
|
|
158
|
+
case 3: currentNotification.body = val; break;
|
|
159
|
+
}
|
|
160
|
+
stringIndex++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Parse urgency from hints dict (byte value)
|
|
165
|
+
const urgencyMatch = trimmed.match(/byte\s+(\d+)/);
|
|
166
|
+
if (urgencyMatch) {
|
|
167
|
+
const urgencyLevel = parseInt(urgencyMatch[1]!);
|
|
168
|
+
currentNotification.urgency =
|
|
169
|
+
urgencyLevel === 2 ? 'critical' :
|
|
170
|
+
urgencyLevel === 1 ? 'normal' :
|
|
171
|
+
'low';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (this.running) {
|
|
177
|
+
console.error('[notifications] Read error:', err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private emitNotification(data: Partial<NotificationData>): void {
|
|
183
|
+
if (!data.title && !data.body) return;
|
|
184
|
+
if (!this.handler) return;
|
|
185
|
+
|
|
186
|
+
this.handler({
|
|
187
|
+
type: 'notification',
|
|
188
|
+
data: {
|
|
189
|
+
app: data.app ?? 'unknown',
|
|
190
|
+
title: data.title ?? '',
|
|
191
|
+
body: data.body ?? '',
|
|
192
|
+
urgency: data.urgency ?? 'normal',
|
|
193
|
+
},
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Observer Layer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, expect, describe, beforeAll, afterAll } from 'bun:test';
|
|
6
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import {
|
|
10
|
+
ObserverManager,
|
|
11
|
+
FileWatcher,
|
|
12
|
+
ClipboardMonitor,
|
|
13
|
+
ProcessMonitor,
|
|
14
|
+
NotificationListener,
|
|
15
|
+
CalendarSync,
|
|
16
|
+
EmailSync,
|
|
17
|
+
type ObserverEvent,
|
|
18
|
+
} from './index';
|
|
19
|
+
|
|
20
|
+
// Use an isolated temp dir instead of /tmp to avoid slow recursive watches on CI
|
|
21
|
+
let testDir: string;
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
testDir = mkdtempSync(join(tmpdir(), 'jarvis-test-'));
|
|
24
|
+
});
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
try { rmSync(testDir, { recursive: true }); } catch {}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('ObserverManager', () => {
|
|
30
|
+
test('registers observers', () => {
|
|
31
|
+
const manager = new ObserverManager();
|
|
32
|
+
const watcher = new FileWatcher([testDir]);
|
|
33
|
+
|
|
34
|
+
manager.register(watcher);
|
|
35
|
+
|
|
36
|
+
expect(manager.listObservers()).toEqual(['file-watcher']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('propagates event handler to observers', () => {
|
|
40
|
+
const manager = new ObserverManager();
|
|
41
|
+
const watcher = new FileWatcher([testDir]);
|
|
42
|
+
|
|
43
|
+
manager.register(watcher);
|
|
44
|
+
|
|
45
|
+
let handlerCalled = false;
|
|
46
|
+
manager.setEventHandler(() => {
|
|
47
|
+
handlerCalled = true;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Handler should be set on the observer
|
|
51
|
+
expect(handlerCalled).toBe(false); // Not called yet, just registered
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('starts and stops all observers', async () => {
|
|
55
|
+
const manager = new ObserverManager();
|
|
56
|
+
const watcher = new FileWatcher([testDir]);
|
|
57
|
+
const clipboard = new ClipboardMonitor(5000);
|
|
58
|
+
|
|
59
|
+
manager.register(watcher);
|
|
60
|
+
manager.register(clipboard);
|
|
61
|
+
|
|
62
|
+
await manager.startAll();
|
|
63
|
+
|
|
64
|
+
const status = manager.getStatus();
|
|
65
|
+
expect(status['file-watcher']).toBe(true);
|
|
66
|
+
expect(status['clipboard']).toBe(true);
|
|
67
|
+
|
|
68
|
+
await manager.stopAll();
|
|
69
|
+
|
|
70
|
+
const statusAfter = manager.getStatus();
|
|
71
|
+
expect(statusAfter['file-watcher']).toBe(false);
|
|
72
|
+
expect(statusAfter['clipboard']).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('starts and stops individual observers', async () => {
|
|
76
|
+
const manager = new ObserverManager();
|
|
77
|
+
const watcher = new FileWatcher([testDir]);
|
|
78
|
+
|
|
79
|
+
manager.register(watcher);
|
|
80
|
+
|
|
81
|
+
await manager.startObserver('file-watcher');
|
|
82
|
+
expect(manager.getStatus()['file-watcher']).toBe(true);
|
|
83
|
+
|
|
84
|
+
await manager.stopObserver('file-watcher');
|
|
85
|
+
expect(manager.getStatus()['file-watcher']).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('FileWatcher', () => {
|
|
90
|
+
test('starts and stops', async () => {
|
|
91
|
+
const watcher = new FileWatcher([testDir]);
|
|
92
|
+
|
|
93
|
+
expect(watcher.isRunning()).toBe(false);
|
|
94
|
+
|
|
95
|
+
await watcher.start();
|
|
96
|
+
expect(watcher.isRunning()).toBe(true);
|
|
97
|
+
|
|
98
|
+
await watcher.stop();
|
|
99
|
+
expect(watcher.isRunning()).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('prevents double start', async () => {
|
|
103
|
+
const watcher = new FileWatcher([testDir]);
|
|
104
|
+
|
|
105
|
+
await watcher.start();
|
|
106
|
+
await watcher.start(); // Should not throw
|
|
107
|
+
|
|
108
|
+
expect(watcher.isRunning()).toBe(true);
|
|
109
|
+
|
|
110
|
+
await watcher.stop();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('ClipboardMonitor', () => {
|
|
115
|
+
test('starts and stops', async () => {
|
|
116
|
+
const clipboard = new ClipboardMonitor(5000);
|
|
117
|
+
|
|
118
|
+
expect(clipboard.isRunning()).toBe(false);
|
|
119
|
+
|
|
120
|
+
await clipboard.start();
|
|
121
|
+
expect(clipboard.isRunning()).toBe(true);
|
|
122
|
+
|
|
123
|
+
await clipboard.stop();
|
|
124
|
+
expect(clipboard.isRunning()).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('uses custom poll interval', async () => {
|
|
128
|
+
const clipboard = new ClipboardMonitor(10000);
|
|
129
|
+
|
|
130
|
+
await clipboard.start();
|
|
131
|
+
expect(clipboard.isRunning()).toBe(true);
|
|
132
|
+
|
|
133
|
+
await clipboard.stop();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('ProcessMonitor', () => {
|
|
138
|
+
test('starts and stops', async () => {
|
|
139
|
+
const monitor = new ProcessMonitor(10000);
|
|
140
|
+
|
|
141
|
+
expect(monitor.isRunning()).toBe(false);
|
|
142
|
+
|
|
143
|
+
await monitor.start();
|
|
144
|
+
expect(monitor.isRunning()).toBe(true);
|
|
145
|
+
|
|
146
|
+
await monitor.stop();
|
|
147
|
+
expect(monitor.isRunning()).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('gets process list', async () => {
|
|
151
|
+
const monitor = new ProcessMonitor(10000);
|
|
152
|
+
|
|
153
|
+
const processes = await monitor.getProcessList();
|
|
154
|
+
|
|
155
|
+
expect(Array.isArray(processes)).toBe(true);
|
|
156
|
+
expect(processes.length).toBeGreaterThan(0);
|
|
157
|
+
|
|
158
|
+
// Check structure of first process
|
|
159
|
+
const proc = processes[0];
|
|
160
|
+
expect(proc).toHaveProperty('pid');
|
|
161
|
+
expect(proc).toHaveProperty('name');
|
|
162
|
+
expect(proc).toHaveProperty('cpu');
|
|
163
|
+
expect(proc).toHaveProperty('memory');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('Stub Observers', () => {
|
|
168
|
+
test('NotificationListener starts and stops', async () => {
|
|
169
|
+
const listener = new NotificationListener();
|
|
170
|
+
|
|
171
|
+
expect(listener.isRunning()).toBe(false);
|
|
172
|
+
|
|
173
|
+
await listener.start();
|
|
174
|
+
expect(listener.isRunning()).toBe(true);
|
|
175
|
+
|
|
176
|
+
await listener.stop();
|
|
177
|
+
expect(listener.isRunning()).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('CalendarSync starts and stops', async () => {
|
|
181
|
+
const sync = new CalendarSync();
|
|
182
|
+
|
|
183
|
+
expect(sync.isRunning()).toBe(false);
|
|
184
|
+
|
|
185
|
+
await sync.start();
|
|
186
|
+
expect(sync.isRunning()).toBe(true);
|
|
187
|
+
|
|
188
|
+
await sync.stop();
|
|
189
|
+
expect(sync.isRunning()).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('EmailSync starts and stops', async () => {
|
|
193
|
+
const sync = new EmailSync();
|
|
194
|
+
|
|
195
|
+
expect(sync.isRunning()).toBe(false);
|
|
196
|
+
|
|
197
|
+
await sync.start();
|
|
198
|
+
expect(sync.isRunning()).toBe(true);
|
|
199
|
+
|
|
200
|
+
await sync.stop();
|
|
201
|
+
expect(sync.isRunning()).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessMonitor - Monitors running processes
|
|
3
|
+
*
|
|
4
|
+
* Polls the system process list at regular intervals and detects when processes
|
|
5
|
+
* start or terminate. Emits events for process lifecycle changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Observer, ObserverEvent, ObserverEventHandler } from './index';
|
|
9
|
+
|
|
10
|
+
export type ProcessInfo = {
|
|
11
|
+
pid: number;
|
|
12
|
+
name: string;
|
|
13
|
+
cpu: number;
|
|
14
|
+
memory: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class ProcessMonitor implements Observer {
|
|
18
|
+
name = 'processes';
|
|
19
|
+
private interval: Timer | null = null;
|
|
20
|
+
private knownProcesses: Map<number, string> = new Map();
|
|
21
|
+
private handler: ObserverEventHandler | null = null;
|
|
22
|
+
private running = false;
|
|
23
|
+
private pollMs: number;
|
|
24
|
+
|
|
25
|
+
constructor(pollMs: number = 5000) {
|
|
26
|
+
this.pollMs = pollMs;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async start(): Promise<void> {
|
|
30
|
+
if (this.running) {
|
|
31
|
+
console.log('[processes] Already running');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`[processes] Starting process monitoring (polling every ${this.pollMs}ms)...`);
|
|
36
|
+
|
|
37
|
+
// Initialize with current process list
|
|
38
|
+
try {
|
|
39
|
+
const processes = await this.getProcessList();
|
|
40
|
+
for (const proc of processes) {
|
|
41
|
+
this.knownProcesses.set(proc.pid, proc.name);
|
|
42
|
+
}
|
|
43
|
+
console.log(`[processes] Initialized with ${processes.length} processes`);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('[processes] Failed to get initial process list:', error);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Start polling
|
|
49
|
+
this.interval = setInterval(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const processes = await this.getProcessList();
|
|
52
|
+
const currentPids = new Set<number>();
|
|
53
|
+
|
|
54
|
+
// Check for new processes
|
|
55
|
+
for (const proc of processes) {
|
|
56
|
+
currentPids.add(proc.pid);
|
|
57
|
+
|
|
58
|
+
if (!this.knownProcesses.has(proc.pid)) {
|
|
59
|
+
// New process detected
|
|
60
|
+
this.knownProcesses.set(proc.pid, proc.name);
|
|
61
|
+
|
|
62
|
+
if (this.handler) {
|
|
63
|
+
const event: ObserverEvent = {
|
|
64
|
+
type: 'process_started',
|
|
65
|
+
data: {
|
|
66
|
+
pid: proc.pid,
|
|
67
|
+
name: proc.name,
|
|
68
|
+
cpu: proc.cpu,
|
|
69
|
+
memory: proc.memory,
|
|
70
|
+
},
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.handler(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for terminated processes
|
|
80
|
+
for (const [pid, name] of this.knownProcesses.entries()) {
|
|
81
|
+
if (!currentPids.has(pid)) {
|
|
82
|
+
// Process terminated
|
|
83
|
+
this.knownProcesses.delete(pid);
|
|
84
|
+
|
|
85
|
+
if (this.handler) {
|
|
86
|
+
const event: ObserverEvent = {
|
|
87
|
+
type: 'process_stopped',
|
|
88
|
+
data: {
|
|
89
|
+
pid,
|
|
90
|
+
name,
|
|
91
|
+
},
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.handler(event);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('[processes] Failed to monitor processes:', error);
|
|
101
|
+
}
|
|
102
|
+
}, this.pollMs);
|
|
103
|
+
|
|
104
|
+
this.running = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async stop(): Promise<void> {
|
|
108
|
+
if (!this.running) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log('[processes] Stopping process monitoring...');
|
|
113
|
+
|
|
114
|
+
if (this.interval) {
|
|
115
|
+
clearInterval(this.interval);
|
|
116
|
+
this.interval = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.knownProcesses.clear();
|
|
120
|
+
this.running = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isRunning(): boolean {
|
|
124
|
+
return this.running;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onEvent(handler: ObserverEventHandler): void {
|
|
128
|
+
this.handler = handler;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get list of running processes
|
|
133
|
+
*/
|
|
134
|
+
async getProcessList(): Promise<ProcessInfo[]> {
|
|
135
|
+
const platform = process.platform;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (platform === 'linux' || platform === 'darwin') {
|
|
139
|
+
// Use ps command for Unix-like systems
|
|
140
|
+
const result = await Bun.$`ps aux --no-headers`.quiet();
|
|
141
|
+
const output = result.stdout.toString();
|
|
142
|
+
|
|
143
|
+
return this.parsePS(output);
|
|
144
|
+
} else if (platform === 'win32') {
|
|
145
|
+
// Use PowerShell for Windows
|
|
146
|
+
const result = await Bun.$`powershell.exe Get-Process | Select-Object Id,Name,CPU,WorkingSet | ConvertTo-Csv -NoTypeInformation`.quiet();
|
|
147
|
+
const output = result.stdout.toString();
|
|
148
|
+
|
|
149
|
+
return this.parseWindowsPS(output);
|
|
150
|
+
} else {
|
|
151
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('[processes] Failed to get process list:', error);
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse output from Unix ps command
|
|
161
|
+
*/
|
|
162
|
+
private parsePS(output: string): ProcessInfo[] {
|
|
163
|
+
const processes: ProcessInfo[] = [];
|
|
164
|
+
const lines = output.split('\n').filter(line => line.trim());
|
|
165
|
+
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
// ps aux format: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
|
|
168
|
+
const parts = line.trim().split(/\s+/);
|
|
169
|
+
|
|
170
|
+
if (parts.length < 11) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pid = parseInt(parts[1]!, 10);
|
|
175
|
+
const cpu = parseFloat(parts[2]!);
|
|
176
|
+
const memory = parseFloat(parts[3]!);
|
|
177
|
+
const name = parts.slice(10).join(' '); // COMMAND can have spaces
|
|
178
|
+
|
|
179
|
+
if (!isNaN(pid)) {
|
|
180
|
+
processes.push({
|
|
181
|
+
pid,
|
|
182
|
+
name,
|
|
183
|
+
cpu,
|
|
184
|
+
memory,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return processes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse output from Windows PowerShell Get-Process
|
|
194
|
+
*/
|
|
195
|
+
private parseWindowsPS(output: string): ProcessInfo[] {
|
|
196
|
+
const processes: ProcessInfo[] = [];
|
|
197
|
+
const lines = output.split('\n').filter(line => line.trim());
|
|
198
|
+
|
|
199
|
+
// Skip header line
|
|
200
|
+
for (let i = 1; i < lines.length; i++) {
|
|
201
|
+
const line = lines[i]!;
|
|
202
|
+
const parts = line.split(',').map(p => p.replace(/"/g, '').trim());
|
|
203
|
+
|
|
204
|
+
if (parts.length < 4) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const pid = parseInt(parts[0]!, 10);
|
|
209
|
+
const name: string = parts[1]!;
|
|
210
|
+
const cpu = parseFloat(parts[2]!) || 0;
|
|
211
|
+
const memory = parseFloat(parts[3]!) || 0;
|
|
212
|
+
|
|
213
|
+
if (!isNaN(pid)) {
|
|
214
|
+
processes.push({
|
|
215
|
+
pid,
|
|
216
|
+
name,
|
|
217
|
+
cpu,
|
|
218
|
+
memory,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return processes;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Personality Engine
|
|
2
|
+
|
|
3
|
+
An adaptive personality system for J.A.R.V.I.S. that learns user preferences and adapts communication style over time.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { initDatabase } from '@/vault/schema';
|
|
9
|
+
import {
|
|
10
|
+
getPersonality,
|
|
11
|
+
savePersonality,
|
|
12
|
+
extractSignals,
|
|
13
|
+
applySignals,
|
|
14
|
+
recordInteraction,
|
|
15
|
+
personalityToPrompt,
|
|
16
|
+
} from '@/personality';
|
|
17
|
+
|
|
18
|
+
// Initialize database
|
|
19
|
+
initDatabase('./data/jarvis.db');
|
|
20
|
+
|
|
21
|
+
// Process a user interaction
|
|
22
|
+
let personality = getPersonality();
|
|
23
|
+
const signals = extractSignals("Keep it brief", "Sure!");
|
|
24
|
+
personality = applySignals(personality, signals);
|
|
25
|
+
personality = recordInteraction(personality);
|
|
26
|
+
savePersonality(personality);
|
|
27
|
+
|
|
28
|
+
// Generate LLM prompt
|
|
29
|
+
const prompt = personalityToPrompt(personality);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Modules
|
|
33
|
+
|
|
34
|
+
- **`model.ts`**: Personality state structure and persistence
|
|
35
|
+
- **`learner.ts`**: Signal extraction and learning
|
|
36
|
+
- **`adapter.ts`**: Channel adaptation and prompt generation
|
|
37
|
+
- **`index.ts`**: Public API exports
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
✓ Learns from user feedback (verbosity, formality, humor)
|
|
42
|
+
✓ Detects emoji usage preferences
|
|
43
|
+
✓ Adapts to different channels (WhatsApp, Email, Terminal)
|
|
44
|
+
✓ Builds trust over time
|
|
45
|
+
✓ Generates personality-aware LLM prompts
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
See [docs/PERSONALITY_ENGINE.md](~/jarvis/docs/PERSONALITY_ENGINE.md) for full documentation.
|
|
50
|
+
|
|
51
|
+
## Testing
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bun test src/personality/personality.test.ts
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Demo
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bun run examples/personality-demo.ts
|
|
61
|
+
```
|