@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Graph — Entity Linking
|
|
3
|
+
*
|
|
4
|
+
* Links screen captures to vault entities by matching OCR text and
|
|
5
|
+
* window titles against known entity names. Creates new 'tool' entities
|
|
6
|
+
* for unseen applications.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ScreenContext } from './types.ts';
|
|
10
|
+
import { searchEntitiesByName, createEntity } from '../vault/entities.ts';
|
|
11
|
+
import { getSession, updateSession } from '../vault/awareness.ts';
|
|
12
|
+
|
|
13
|
+
// Cache of known app names to avoid redundant entity creation
|
|
14
|
+
const knownApps = new Set<string>();
|
|
15
|
+
|
|
16
|
+
export class ContextGraph {
|
|
17
|
+
/**
|
|
18
|
+
* Link a capture to vault entities.
|
|
19
|
+
* Searches entity names against OCR text + window title.
|
|
20
|
+
* Returns array of matched entity IDs.
|
|
21
|
+
*/
|
|
22
|
+
linkCaptureToEntities(context: ScreenContext): string[] {
|
|
23
|
+
const linkedIds: string[] = [];
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// 1. Ensure the app itself is a known entity
|
|
27
|
+
if (context.appName && context.appName !== 'Unknown') {
|
|
28
|
+
this.ensureAppEntity(context.appName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Search OCR text and window title for known entity names
|
|
32
|
+
const searchText = `${context.windowTitle} ${context.ocrText.slice(0, 1000)}`;
|
|
33
|
+
const matchedEntities = this.findEntitiesInText(searchText);
|
|
34
|
+
linkedIds.push(...matchedEntities);
|
|
35
|
+
|
|
36
|
+
// 3. Update session entity links
|
|
37
|
+
if (context.sessionId && linkedIds.length > 0) {
|
|
38
|
+
this.updateSessionLinks(context.sessionId, linkedIds);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('[ContextGraph] Entity linking error:', err instanceof Error ? err.message : err);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return linkedIds;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ensure an app has an entity in the vault.
|
|
49
|
+
*/
|
|
50
|
+
private ensureAppEntity(appName: string): void {
|
|
51
|
+
if (knownApps.has(appName)) return;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const existing = searchEntitiesByName(appName);
|
|
55
|
+
if (existing.length === 0) {
|
|
56
|
+
createEntity('tool', appName, { source: 'awareness_auto' }, 'awareness');
|
|
57
|
+
}
|
|
58
|
+
knownApps.add(appName);
|
|
59
|
+
} catch { /* ignore — entity creation is best-effort */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find vault entities mentioned in text.
|
|
64
|
+
* Uses a simple word-boundary search against entity names.
|
|
65
|
+
*/
|
|
66
|
+
private findEntitiesInText(text: string): string[] {
|
|
67
|
+
const ids: string[] = [];
|
|
68
|
+
const textLower = text.toLowerCase();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Search for entities with names >= 3 chars that appear in the text
|
|
72
|
+
// We search for common entity types that would appear in screen context
|
|
73
|
+
const words = this.extractSignificantWords(textLower);
|
|
74
|
+
|
|
75
|
+
for (const word of words) {
|
|
76
|
+
if (word.length < 3) continue;
|
|
77
|
+
|
|
78
|
+
const matches = searchEntitiesByName(word);
|
|
79
|
+
for (const entity of matches) {
|
|
80
|
+
// Exact word match (not substring of longer word)
|
|
81
|
+
const nameLower = entity.name.toLowerCase();
|
|
82
|
+
if (textLower.includes(nameLower) && !ids.includes(entity.id)) {
|
|
83
|
+
ids.push(entity.id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Limit to avoid too many DB queries
|
|
88
|
+
if (ids.length >= 10) break;
|
|
89
|
+
}
|
|
90
|
+
} catch { /* ignore */ }
|
|
91
|
+
|
|
92
|
+
return ids;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract significant words for entity matching (skip common words).
|
|
97
|
+
*/
|
|
98
|
+
private extractSignificantWords(text: string): string[] {
|
|
99
|
+
const stopWords = new Set([
|
|
100
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can',
|
|
101
|
+
'had', 'her', 'was', 'one', 'our', 'out', 'has', 'have', 'from',
|
|
102
|
+
'this', 'that', 'with', 'they', 'been', 'said', 'each', 'which',
|
|
103
|
+
'their', 'will', 'other', 'about', 'more', 'some', 'than', 'them',
|
|
104
|
+
'would', 'make', 'like', 'just', 'over', 'such', 'into', 'also',
|
|
105
|
+
'file', 'new', 'open', 'save', 'close', 'edit', 'view', 'help',
|
|
106
|
+
'true', 'false', 'null', 'undefined', 'error', 'warning',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const words = text.match(/[a-z]{3,}/g) ?? [];
|
|
110
|
+
const unique = [...new Set(words)];
|
|
111
|
+
return unique.filter(w => !stopWords.has(w)).slice(0, 20);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update session's entity_links with newly found links.
|
|
116
|
+
*/
|
|
117
|
+
private updateSessionLinks(sessionId: string, entityIds: string[]): void {
|
|
118
|
+
try {
|
|
119
|
+
const session = getSession(sessionId);
|
|
120
|
+
if (!session) return;
|
|
121
|
+
|
|
122
|
+
const existing: string[] = JSON.parse(session.entity_links || '[]');
|
|
123
|
+
const merged = [...new Set([...existing, ...entityIds])];
|
|
124
|
+
|
|
125
|
+
if (merged.length !== existing.length) {
|
|
126
|
+
updateSession(sessionId, { entity_links: merged });
|
|
127
|
+
}
|
|
128
|
+
} catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Tracker — Screen Context Analysis
|
|
3
|
+
*
|
|
4
|
+
* Maintains current screen context, detects app changes, stuck states,
|
|
5
|
+
* error patterns, and manages activity sessions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AwarenessConfig } from '../config/types.ts';
|
|
9
|
+
import type { ScreenContext, AwarenessEvent } from './types.ts';
|
|
10
|
+
import { createSession, endSession, incrementSessionCaptureCount, updateSession } from '../vault/awareness.ts';
|
|
11
|
+
import { generateId } from '../vault/schema.ts';
|
|
12
|
+
import { StruggleDetector } from './struggle-detector.ts';
|
|
13
|
+
|
|
14
|
+
// Strong error indicators — always trigger (rare in normal output)
|
|
15
|
+
const STRONG_ERROR_PATTERN = /\b(traceback|segfault|SIGSEGV|SIGABRT|panic|undefined is not|cannot read prop|stack overflow|out of memory|unhandled rejection|uncaught exception|TypeError:|ReferenceError:|SyntaxError:|RangeError:|ModuleNotFoundError|ImportError|FileNotFoundError|PermissionError|ConnectionRefusedError|ECONNREFUSED|ENOTFOUND|EPERM|Build failed|Compilation error|npm ERR!)\b/i;
|
|
16
|
+
|
|
17
|
+
// Weaker indicators — only trigger when accompanied by stack trace context
|
|
18
|
+
const WEAK_ERROR_PATTERN = /\b(error|exception|failed|fatal|crash|denied|refused|timeout|ENOENT|EACCES|ECONNREFUSED)\b/i;
|
|
19
|
+
|
|
20
|
+
// Context that confirms a weak error is real (stack traces, error codes, build output, HTTP errors, etc.)
|
|
21
|
+
const ERROR_CONFIRM_PATTERN = /(?:at .+:\d+|line \d+|\.ts:\d+|\.js:\d+|\.py:\d+|\.go:\d+|throw |exit code|status [45]\d{2}|Traceback|^\s+\^|command not found|No such file|permission denied|cannot find module|Module not found|Compilation failed|Build failed|ERR!|npm ERR|error\[\w+\]|✗|✘|FAIL|FAILED|returned non-zero|Process exited)/mi;
|
|
22
|
+
|
|
23
|
+
// JARVIS own log lines — filter these out from error detection
|
|
24
|
+
const JARVIS_LOG_PREFIX = /\[(CaptureEngine|ObserverManager|file-watcher|clipboard|processes|notifications|email|calendar|Daemon|ServiceRegistry|AgentService|WSService|OCREngine|Awareness|DesktopController|Executor|HealthMonitor|ChannelManager|TelegramAdapter|BackgroundAgent|WebSocketServer|EventReactor|Orchestrator|ChannelService|ObserverService)\]/;
|
|
25
|
+
|
|
26
|
+
// URL pattern in text
|
|
27
|
+
const URL_PATTERN = /https?:\/\/[^\s<>"{}|\\^`[\]]+/;
|
|
28
|
+
|
|
29
|
+
// File path patterns
|
|
30
|
+
const FILE_PATH_PATTERN = /(?:\/[\w.-]+){2,}(?:\.\w+)?|[A-Z]:\\[\w\\.-]+/;
|
|
31
|
+
|
|
32
|
+
export class ContextTracker {
|
|
33
|
+
private config: AwarenessConfig;
|
|
34
|
+
private currentContext: ScreenContext | null = null;
|
|
35
|
+
private previousContext: ScreenContext | null = null;
|
|
36
|
+
private currentSessionId: string | null = null;
|
|
37
|
+
private currentSessionApps: Set<string> = new Set();
|
|
38
|
+
private sameWindowSince: number = 0;
|
|
39
|
+
private lastOcrTextHash: string = '';
|
|
40
|
+
private lastActivityTimestamp: number = 0;
|
|
41
|
+
private lastErrorText: string = '';
|
|
42
|
+
private lastErrorTimestamp: number = 0;
|
|
43
|
+
private pendingWindowInfo: { appName: string; windowTitle: string } | null = null;
|
|
44
|
+
private struggleDetector: StruggleDetector;
|
|
45
|
+
|
|
46
|
+
constructor(config: AwarenessConfig) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.struggleDetector = new StruggleDetector({
|
|
49
|
+
graceMs: config.struggle_grace_ms ?? 120_000,
|
|
50
|
+
cooldownMs: config.struggle_cooldown_ms ?? 180_000,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Process a new screen capture. Returns the context and any detected events.
|
|
56
|
+
*/
|
|
57
|
+
processCapture(captureId: string, ocrText: string, rawWindowTitle?: string): {
|
|
58
|
+
context: ScreenContext;
|
|
59
|
+
events: AwarenessEvent[];
|
|
60
|
+
} {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const events: AwarenessEvent[] = [];
|
|
63
|
+
|
|
64
|
+
// Use pending window info from sidecar context_changed if no rawWindowTitle provided
|
|
65
|
+
const effectiveWindowTitle = rawWindowTitle || this.pendingWindowInfo?.windowTitle;
|
|
66
|
+
if (this.pendingWindowInfo && !rawWindowTitle) {
|
|
67
|
+
this.pendingWindowInfo = null; // consumed
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Parse app name and details from window title or OCR
|
|
71
|
+
const { appName, windowTitle, url, filePath } = this.parseWindowInfo(ocrText, effectiveWindowTitle);
|
|
72
|
+
|
|
73
|
+
// Detect context change
|
|
74
|
+
const isAppChange = this.currentContext !== null &&
|
|
75
|
+
(this.currentContext.appName !== appName || this.currentContext.windowTitle !== windowTitle);
|
|
76
|
+
|
|
77
|
+
const isSignificantChange = isAppChange || this.currentContext === null;
|
|
78
|
+
|
|
79
|
+
// Session management
|
|
80
|
+
const idleGap = this.lastActivityTimestamp > 0 ? (now - this.lastActivityTimestamp) : 0;
|
|
81
|
+
const isIdleReturn = idleGap > 5 * 60 * 1000; // 5 min idle
|
|
82
|
+
|
|
83
|
+
if (isAppChange || isIdleReturn || !this.currentSessionId) {
|
|
84
|
+
// End previous session if it exists
|
|
85
|
+
if (this.currentSessionId && isAppChange) {
|
|
86
|
+
this.endCurrentSession();
|
|
87
|
+
events.push({
|
|
88
|
+
type: 'session_ended',
|
|
89
|
+
data: { sessionId: this.currentSessionId, apps: Array.from(this.currentSessionApps) },
|
|
90
|
+
timestamp: now,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Start new session
|
|
95
|
+
const session = createSession({ startedAt: now, apps: [appName] });
|
|
96
|
+
this.currentSessionId = session.id;
|
|
97
|
+
this.currentSessionApps = new Set([appName]);
|
|
98
|
+
events.push({
|
|
99
|
+
type: 'session_started',
|
|
100
|
+
data: { sessionId: session.id, appName },
|
|
101
|
+
timestamp: now,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Track app in current session
|
|
106
|
+
if (this.currentSessionId && appName) {
|
|
107
|
+
this.currentSessionApps.add(appName);
|
|
108
|
+
incrementSessionCaptureCount(this.currentSessionId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Context change event
|
|
112
|
+
if (isAppChange) {
|
|
113
|
+
events.push({
|
|
114
|
+
type: 'context_changed',
|
|
115
|
+
data: {
|
|
116
|
+
fromApp: this.currentContext?.appName ?? 'unknown',
|
|
117
|
+
toApp: appName,
|
|
118
|
+
fromWindow: this.currentContext?.windowTitle ?? '',
|
|
119
|
+
toWindow: windowTitle,
|
|
120
|
+
},
|
|
121
|
+
timestamp: now,
|
|
122
|
+
});
|
|
123
|
+
this.sameWindowSince = now;
|
|
124
|
+
this.lastOcrTextHash = '';
|
|
125
|
+
this.struggleDetector.reset();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Track same-window duration for stuck detection
|
|
129
|
+
if (!isAppChange && this.currentContext) {
|
|
130
|
+
// Same window — check if stuck
|
|
131
|
+
if (this.sameWindowSince === 0) {
|
|
132
|
+
this.sameWindowSince = now;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sameWindowDuration = now - this.sameWindowSince;
|
|
136
|
+
const ocrHash = simpleHash(ocrText);
|
|
137
|
+
const textUnchanged = ocrHash === this.lastOcrTextHash;
|
|
138
|
+
|
|
139
|
+
if (sameWindowDuration > this.config.stuck_threshold_ms && textUnchanged) {
|
|
140
|
+
events.push({
|
|
141
|
+
type: 'stuck_detected',
|
|
142
|
+
data: {
|
|
143
|
+
windowTitle,
|
|
144
|
+
appName,
|
|
145
|
+
durationMs: sameWindowDuration,
|
|
146
|
+
ocrPreview: ocrText.slice(0, 200),
|
|
147
|
+
},
|
|
148
|
+
timestamp: now,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.lastOcrTextHash = ocrHash;
|
|
153
|
+
} else {
|
|
154
|
+
this.sameWindowSince = now;
|
|
155
|
+
this.lastOcrTextHash = simpleHash(ocrText);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Struggle detection (behavioral analysis beyond simple stuck)
|
|
159
|
+
const struggleResult = this.struggleDetector.evaluate(ocrText, appName, windowTitle, now);
|
|
160
|
+
if (struggleResult) {
|
|
161
|
+
events.push({
|
|
162
|
+
type: 'struggle_detected',
|
|
163
|
+
data: {
|
|
164
|
+
appName,
|
|
165
|
+
windowTitle,
|
|
166
|
+
appCategory: struggleResult.appCategory,
|
|
167
|
+
compositeScore: struggleResult.compositeScore,
|
|
168
|
+
signals: struggleResult.signals,
|
|
169
|
+
durationMs: struggleResult.durationMs,
|
|
170
|
+
ocrPreview: ocrText.slice(0, 500),
|
|
171
|
+
},
|
|
172
|
+
timestamp: now,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Error detection in OCR text
|
|
177
|
+
// First, filter out lines that are JARVIS's own log output
|
|
178
|
+
const filteredOcrText = ocrText
|
|
179
|
+
.split('\n')
|
|
180
|
+
.filter(line => !JARVIS_LOG_PREFIX.test(line))
|
|
181
|
+
.join('\n');
|
|
182
|
+
|
|
183
|
+
const strongMatch = filteredOcrText.match(STRONG_ERROR_PATTERN);
|
|
184
|
+
const weakMatch = !strongMatch ? filteredOcrText.match(WEAK_ERROR_PATTERN) : null;
|
|
185
|
+
// Weak errors only fire if there's confirming context (stack trace, etc.)
|
|
186
|
+
const errorMatch = strongMatch ?? (weakMatch && ERROR_CONFIRM_PATTERN.test(filteredOcrText) ? weakMatch : null);
|
|
187
|
+
|
|
188
|
+
if (errorMatch) {
|
|
189
|
+
// Cooldown: don't re-fire same error text within 30 seconds
|
|
190
|
+
const sameError = errorMatch[0].toLowerCase() === this.lastErrorText.toLowerCase();
|
|
191
|
+
const cooldownExpired = (now - this.lastErrorTimestamp) > 30_000;
|
|
192
|
+
|
|
193
|
+
if (!sameError || cooldownExpired) {
|
|
194
|
+
const idx = filteredOcrText.indexOf(errorMatch[0]);
|
|
195
|
+
const errorContext = filteredOcrText.slice(Math.max(0, idx - 100), idx + 200);
|
|
196
|
+
|
|
197
|
+
events.push({
|
|
198
|
+
type: 'error_detected',
|
|
199
|
+
data: {
|
|
200
|
+
errorText: errorMatch[0],
|
|
201
|
+
errorContext: errorContext.trim(),
|
|
202
|
+
appName,
|
|
203
|
+
windowTitle,
|
|
204
|
+
},
|
|
205
|
+
timestamp: now,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this.lastErrorText = errorMatch[0];
|
|
209
|
+
this.lastErrorTimestamp = now;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Build context object
|
|
214
|
+
const context: ScreenContext = {
|
|
215
|
+
captureId,
|
|
216
|
+
timestamp: now,
|
|
217
|
+
appName,
|
|
218
|
+
windowTitle,
|
|
219
|
+
url,
|
|
220
|
+
filePath,
|
|
221
|
+
ocrText,
|
|
222
|
+
sessionId: this.currentSessionId!,
|
|
223
|
+
isSignificantChange,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Update state
|
|
227
|
+
this.previousContext = this.currentContext;
|
|
228
|
+
this.currentContext = context;
|
|
229
|
+
this.lastActivityTimestamp = now;
|
|
230
|
+
|
|
231
|
+
return { context, events };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getCurrentContext(): ScreenContext | null {
|
|
235
|
+
return this.currentContext;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getPreviousContext(): ScreenContext | null {
|
|
239
|
+
return this.previousContext;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getCurrentSession(): { id: string; topic: string | null; startedAt: number } | null {
|
|
243
|
+
if (!this.currentSessionId) return null;
|
|
244
|
+
return {
|
|
245
|
+
id: this.currentSessionId,
|
|
246
|
+
topic: null, // Topic set later by intelligence layer
|
|
247
|
+
startedAt: this.sameWindowSince || Date.now(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get the last known window title (set by updateWindowInfo from sidecar events).
|
|
253
|
+
*/
|
|
254
|
+
getLastWindowTitle(): string | undefined {
|
|
255
|
+
return this.currentContext?.windowTitle ?? undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Update cached window info from a sidecar context_changed event.
|
|
260
|
+
* Called externally when the sidecar pushes a window change.
|
|
261
|
+
*/
|
|
262
|
+
updateWindowInfo(appName: string, windowTitle: string): void {
|
|
263
|
+
if (!this.currentContext) {
|
|
264
|
+
// No capture processed yet — store as pending info for the first processCapture call
|
|
265
|
+
this.pendingWindowInfo = { appName, windowTitle };
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Directly update current context's window info
|
|
269
|
+
this.currentContext = {
|
|
270
|
+
...this.currentContext,
|
|
271
|
+
appName: appName || this.currentContext.appName,
|
|
272
|
+
windowTitle: windowTitle || this.currentContext.windowTitle,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Report idle/stuck state from sidecar idle_detected event.
|
|
278
|
+
*/
|
|
279
|
+
reportIdle(appName: string, durationMs: number): void {
|
|
280
|
+
// The struggle detector already handles stuck detection from processCapture,
|
|
281
|
+
// but this provides an external signal from the sidecar's window observer.
|
|
282
|
+
// We can use it to supplement the stuck detection threshold.
|
|
283
|
+
if (this.currentContext && durationMs > 0) {
|
|
284
|
+
this.sameWindowSince = Date.now() - durationMs;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
endCurrentSession(): void {
|
|
289
|
+
if (this.currentSessionId) {
|
|
290
|
+
try {
|
|
291
|
+
// Update session apps before ending
|
|
292
|
+
updateSession(this.currentSessionId, {
|
|
293
|
+
apps: Array.from(this.currentSessionApps),
|
|
294
|
+
});
|
|
295
|
+
endSession(this.currentSessionId);
|
|
296
|
+
} catch { /* session may not exist in test environments */ }
|
|
297
|
+
this.currentSessionId = null;
|
|
298
|
+
this.currentSessionApps.clear();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Parse window title and OCR text to extract app name, URL, file path.
|
|
304
|
+
*/
|
|
305
|
+
private parseWindowInfo(ocrText: string, rawWindowTitle?: string): {
|
|
306
|
+
appName: string;
|
|
307
|
+
windowTitle: string;
|
|
308
|
+
url: string | null;
|
|
309
|
+
filePath: string | null;
|
|
310
|
+
} {
|
|
311
|
+
const windowTitle = rawWindowTitle || '';
|
|
312
|
+
|
|
313
|
+
// Extract app name from window title (typically "Content - AppName" or "AppName - Content")
|
|
314
|
+
let appName = 'Unknown';
|
|
315
|
+
if (windowTitle) {
|
|
316
|
+
const parts = windowTitle.split(/\s[-–—]\s/);
|
|
317
|
+
if (parts.length >= 2) {
|
|
318
|
+
// Last part is usually the app name
|
|
319
|
+
appName = parts[parts.length - 1]!.trim();
|
|
320
|
+
} else {
|
|
321
|
+
appName = windowTitle.trim();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Extract URL from OCR text or window title
|
|
326
|
+
const urlMatch = (windowTitle + ' ' + ocrText).match(URL_PATTERN);
|
|
327
|
+
const url = urlMatch ? urlMatch[0] : null;
|
|
328
|
+
|
|
329
|
+
// Extract file path from window title
|
|
330
|
+
const fileMatch = windowTitle.match(FILE_PATH_PATTERN);
|
|
331
|
+
const filePath = fileMatch ? fileMatch[0] : null;
|
|
332
|
+
|
|
333
|
+
return { appName, windowTitle, url, filePath };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Simple string hash for quick comparison (not cryptographic).
|
|
339
|
+
*/
|
|
340
|
+
function simpleHash(str: string): string {
|
|
341
|
+
let hash = 0;
|
|
342
|
+
const sample = str.slice(0, 2000); // Only hash first 2000 chars
|
|
343
|
+
for (let i = 0; i < sample.length; i++) {
|
|
344
|
+
const char = sample.charCodeAt(i);
|
|
345
|
+
hash = ((hash << 5) - hash) + char;
|
|
346
|
+
hash |= 0; // Convert to 32bit integer
|
|
347
|
+
}
|
|
348
|
+
return hash.toString(36);
|
|
349
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Awareness Engine — Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { AwarenessService } from './service.ts';
|
|
6
|
+
export { OCREngine } from './ocr-engine.ts';
|
|
7
|
+
export { ContextTracker } from './context-tracker.ts';
|
|
8
|
+
export { AwarenessIntelligence } from './intelligence.ts';
|
|
9
|
+
export { SuggestionEngine } from './suggestion-engine.ts';
|
|
10
|
+
export { ContextGraph } from './context-graph.ts';
|
|
11
|
+
export { BehaviorAnalytics } from './analytics.ts';
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
CaptureFrame,
|
|
15
|
+
OCRResult,
|
|
16
|
+
ScreenContext,
|
|
17
|
+
AwarenessEvent,
|
|
18
|
+
AwarenessEventType,
|
|
19
|
+
SuggestionType,
|
|
20
|
+
Suggestion,
|
|
21
|
+
SessionSummary,
|
|
22
|
+
AppUsageStat,
|
|
23
|
+
DailyReport,
|
|
24
|
+
LiveContext,
|
|
25
|
+
} from './types.ts';
|