@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,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Struggle Detector — Behavioral Analysis
|
|
3
|
+
*
|
|
4
|
+
* Detects when the user is actively working but making no progress:
|
|
5
|
+
* trial-and-error editing, repeated failing commands, undo cycles.
|
|
6
|
+
* Complements stuck detection (which only fires when screen is unchanging).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Types ──
|
|
10
|
+
|
|
11
|
+
export type AppCategory =
|
|
12
|
+
| 'code_editor'
|
|
13
|
+
| 'terminal'
|
|
14
|
+
| 'browser'
|
|
15
|
+
| 'creative_app'
|
|
16
|
+
| 'puzzle_game'
|
|
17
|
+
| 'general';
|
|
18
|
+
|
|
19
|
+
export type StruggleSignal = {
|
|
20
|
+
name: string;
|
|
21
|
+
score: number; // 0.0 - 1.0
|
|
22
|
+
detail: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type StruggleAnalysis = {
|
|
26
|
+
isStruggling: boolean;
|
|
27
|
+
compositeScore: number;
|
|
28
|
+
signals: StruggleSignal[];
|
|
29
|
+
appCategory: AppCategory;
|
|
30
|
+
appName: string;
|
|
31
|
+
windowTitle: string;
|
|
32
|
+
durationMs: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type Snapshot = {
|
|
36
|
+
timestamp: number;
|
|
37
|
+
ocrHash: string;
|
|
38
|
+
ocrText: string;
|
|
39
|
+
appName: string;
|
|
40
|
+
outputHash: string; // hash of bottom 500 chars (terminal/compiler output area)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ── Constants ──
|
|
44
|
+
|
|
45
|
+
const MAX_SNAPSHOTS = 30; // ~3.5 min at 7s intervals
|
|
46
|
+
const WINDOW_MS = 3.5 * 60 * 1000; // 3.5 minutes
|
|
47
|
+
const MIN_SNAPSHOTS = 8; // ~56s of data before analysis
|
|
48
|
+
|
|
49
|
+
const SIGNAL_WEIGHTS = {
|
|
50
|
+
trialAndError: 0.30,
|
|
51
|
+
undoRevert: 0.25,
|
|
52
|
+
repeatedOutput: 0.25,
|
|
53
|
+
lowProgress: 0.20,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const STRUGGLE_THRESHOLD = 0.5;
|
|
57
|
+
|
|
58
|
+
// ── App Classification Patterns ──
|
|
59
|
+
|
|
60
|
+
const CODE_EDITORS = /\b(VS\s?Code|Visual Studio Code|IntelliJ|WebStorm|PyCharm|CLion|GoLand|RubyMine|Sublime|Atom|vim|nvim|neovim|Emacs|Cursor|Zed|nano|Code - OSS|code-oss)\b/i;
|
|
61
|
+
const TERMINALS = /\b(Terminal|iTerm|Konsole|Alacritty|Warp|kitty|GNOME Terminal|Windows Terminal|PowerShell|pwsh|cmd\.exe|bash|zsh|tmux|screen|Hyper)\b/i;
|
|
62
|
+
const BROWSERS = /\b(Chrome|Chromium|Firefox|Brave|Edge|Safari|Opera|Arc|Vivaldi)\b/i;
|
|
63
|
+
const CREATIVE_APPS = /\b(Photoshop|Figma|GIMP|Blender|Illustrator|Inkscape|Sketch|Affinity|Canva|Lightroom|Premiere|DaVinci|After Effects|Krita|Paint\.NET)\b/i;
|
|
64
|
+
const PUZZLE_INDICATORS = /\b(score|level|moves|timer|puzzle|sudoku|wordle|crossword|chess|2048|minesweeper|solitaire|tetris)\b/i;
|
|
65
|
+
|
|
66
|
+
// ── Main Class ──
|
|
67
|
+
|
|
68
|
+
export class StruggleDetector {
|
|
69
|
+
private snapshots: Snapshot[] = [];
|
|
70
|
+
private lastStruggleEmitAt = 0;
|
|
71
|
+
private struggleStartedAt: number | null = null;
|
|
72
|
+
private graceMs: number;
|
|
73
|
+
private cooldownMs: number;
|
|
74
|
+
|
|
75
|
+
constructor(opts?: { graceMs?: number; cooldownMs?: number }) {
|
|
76
|
+
this.graceMs = opts?.graceMs ?? 120_000;
|
|
77
|
+
this.cooldownMs = opts?.cooldownMs ?? 180_000;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Evaluate current capture for struggle patterns.
|
|
82
|
+
* Returns StruggleAnalysis when struggle is confirmed (past grace + cooldown), null otherwise.
|
|
83
|
+
*/
|
|
84
|
+
evaluate(ocrText: string, appName: string, windowTitle: string, timestamp: number): StruggleAnalysis | null {
|
|
85
|
+
const ocrHash = simpleHash(ocrText);
|
|
86
|
+
const outputHash = simpleHash(ocrText.slice(-500));
|
|
87
|
+
|
|
88
|
+
this.snapshots.push({ timestamp, ocrHash, ocrText, appName, outputHash });
|
|
89
|
+
|
|
90
|
+
// Evict old entries
|
|
91
|
+
const cutoff = timestamp - WINDOW_MS;
|
|
92
|
+
this.snapshots = this.snapshots.filter(s => s.timestamp > cutoff);
|
|
93
|
+
if (this.snapshots.length > MAX_SNAPSHOTS) {
|
|
94
|
+
this.snapshots = this.snapshots.slice(-MAX_SNAPSHOTS);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Need enough data
|
|
98
|
+
if (this.snapshots.length < MIN_SNAPSHOTS) return null;
|
|
99
|
+
|
|
100
|
+
// All snapshots must be from the same app (struggle resets on app switch)
|
|
101
|
+
if (!this.snapshots.every(s => s.appName === appName)) return null;
|
|
102
|
+
|
|
103
|
+
// Compute signals
|
|
104
|
+
const signals: StruggleSignal[] = [];
|
|
105
|
+
|
|
106
|
+
const trialAndError = this.detectTrialAndError();
|
|
107
|
+
signals.push(trialAndError);
|
|
108
|
+
|
|
109
|
+
const undoRevert = this.detectUndoRevert();
|
|
110
|
+
signals.push(undoRevert);
|
|
111
|
+
|
|
112
|
+
const repeatedOutput = this.detectRepeatedOutput();
|
|
113
|
+
signals.push(repeatedOutput);
|
|
114
|
+
|
|
115
|
+
const lowProgress = this.detectLowProgress();
|
|
116
|
+
signals.push(lowProgress);
|
|
117
|
+
|
|
118
|
+
// Composite score
|
|
119
|
+
const compositeScore =
|
|
120
|
+
trialAndError.score * SIGNAL_WEIGHTS.trialAndError +
|
|
121
|
+
undoRevert.score * SIGNAL_WEIGHTS.undoRevert +
|
|
122
|
+
repeatedOutput.score * SIGNAL_WEIGHTS.repeatedOutput +
|
|
123
|
+
lowProgress.score * SIGNAL_WEIGHTS.lowProgress;
|
|
124
|
+
|
|
125
|
+
if (compositeScore < STRUGGLE_THRESHOLD) {
|
|
126
|
+
this.struggleStartedAt = null;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Grace period: don't fire immediately
|
|
131
|
+
if (this.struggleStartedAt === null) {
|
|
132
|
+
this.struggleStartedAt = timestamp;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (timestamp - this.struggleStartedAt < this.graceMs) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Cooldown: don't fire too often
|
|
140
|
+
if (timestamp - this.lastStruggleEmitAt < this.cooldownMs) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fire!
|
|
145
|
+
this.lastStruggleEmitAt = timestamp;
|
|
146
|
+
this.struggleStartedAt = null;
|
|
147
|
+
|
|
148
|
+
const appCategory = this.classifyApp(appName, windowTitle, ocrText);
|
|
149
|
+
const durationMs = timestamp - this.snapshots[0]!.timestamp;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
isStruggling: true,
|
|
153
|
+
compositeScore,
|
|
154
|
+
signals,
|
|
155
|
+
appCategory,
|
|
156
|
+
appName,
|
|
157
|
+
windowTitle,
|
|
158
|
+
durationMs,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Classify the current app into a category for prompt selection.
|
|
164
|
+
*/
|
|
165
|
+
classifyApp(appName: string, windowTitle: string, ocrText: string): AppCategory {
|
|
166
|
+
const combined = `${appName} ${windowTitle}`;
|
|
167
|
+
|
|
168
|
+
if (CODE_EDITORS.test(combined)) return 'code_editor';
|
|
169
|
+
if (TERMINALS.test(combined)) return 'terminal';
|
|
170
|
+
if (CREATIVE_APPS.test(combined)) return 'creative_app';
|
|
171
|
+
if (BROWSERS.test(combined)) return 'browser';
|
|
172
|
+
|
|
173
|
+
// Puzzle game detection: check window title + OCR heuristics
|
|
174
|
+
if (PUZZLE_INDICATORS.test(combined) || PUZZLE_INDICATORS.test(ocrText.slice(0, 500))) {
|
|
175
|
+
return 'puzzle_game';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return 'general';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Reset state — call on app switch.
|
|
183
|
+
*/
|
|
184
|
+
reset(): void {
|
|
185
|
+
this.snapshots = [];
|
|
186
|
+
this.struggleStartedAt = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Signal Detectors ──
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Signal 1: Trial-and-error editing.
|
|
193
|
+
* High ratio of unique OCR hashes = many small changes that don't converge.
|
|
194
|
+
*/
|
|
195
|
+
private detectTrialAndError(): StruggleSignal {
|
|
196
|
+
const total = this.snapshots.length;
|
|
197
|
+
const uniqueHashes = new Set(this.snapshots.map(s => s.ocrHash)).size;
|
|
198
|
+
const ratio = uniqueHashes / total;
|
|
199
|
+
|
|
200
|
+
// If > 70% of captures are unique AND we have enough unique ones,
|
|
201
|
+
// the user is making lots of small changes without settling
|
|
202
|
+
if (uniqueHashes >= 10 && ratio > 0.7) {
|
|
203
|
+
const score = Math.min(1.0, (ratio - 0.5) * 3);
|
|
204
|
+
return {
|
|
205
|
+
name: 'trial_and_error',
|
|
206
|
+
score,
|
|
207
|
+
detail: `${uniqueHashes}/${total} unique screen states (${Math.round(ratio * 100)}% unique)`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { name: 'trial_and_error', score: 0, detail: 'Normal editing pattern' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Signal 2: Undo/revert patterns.
|
|
216
|
+
* Detects when current OCR hash matches a hash from 2-5 snapshots ago (text reverted).
|
|
217
|
+
*/
|
|
218
|
+
private detectUndoRevert(): StruggleSignal {
|
|
219
|
+
let revertCount = 0;
|
|
220
|
+
|
|
221
|
+
for (let i = 2; i < this.snapshots.length; i++) {
|
|
222
|
+
const current = this.snapshots[i]!.ocrHash;
|
|
223
|
+
// Check if current matches any of the 2-5 snapshots before it
|
|
224
|
+
for (let k = 2; k <= Math.min(5, i); k++) {
|
|
225
|
+
if (current === this.snapshots[i - k]!.ocrHash) {
|
|
226
|
+
revertCount++;
|
|
227
|
+
break; // count each revert once
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (revertCount >= 2) {
|
|
233
|
+
const score = Math.min(1.0, revertCount / 5);
|
|
234
|
+
return {
|
|
235
|
+
name: 'undo_revert',
|
|
236
|
+
score,
|
|
237
|
+
detail: `${revertCount} revert cycles detected (text returning to previous states)`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { name: 'undo_revert', score: 0, detail: 'No revert patterns' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Signal 3: Repeated output.
|
|
246
|
+
* Bottom of screen (terminal/compiler output) stays the same across many captures.
|
|
247
|
+
*/
|
|
248
|
+
private detectRepeatedOutput(): StruggleSignal {
|
|
249
|
+
const outputHashes = this.snapshots.map(s => s.outputHash);
|
|
250
|
+
const counts = new Map<string, number>();
|
|
251
|
+
|
|
252
|
+
for (const h of outputHashes) {
|
|
253
|
+
counts.set(h, (counts.get(h) ?? 0) + 1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Find most repeated output hash
|
|
257
|
+
let maxCount = 0;
|
|
258
|
+
for (const count of counts.values()) {
|
|
259
|
+
maxCount = Math.max(maxCount, count);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const pct = maxCount / this.snapshots.length;
|
|
263
|
+
|
|
264
|
+
if (pct > 0.4 && maxCount >= 6) {
|
|
265
|
+
const score = Math.min(1.0, (pct - 0.3) / 0.4);
|
|
266
|
+
return {
|
|
267
|
+
name: 'repeated_output',
|
|
268
|
+
score,
|
|
269
|
+
detail: `Same output in ${maxCount}/${this.snapshots.length} captures (${Math.round(pct * 100)}%)`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { name: 'repeated_output', score: 0, detail: 'Output is changing normally' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Signal 4: Low meaningful progress.
|
|
278
|
+
* Small average edit distance between consecutive snapshots despite many captures.
|
|
279
|
+
*/
|
|
280
|
+
private detectLowProgress(): StruggleSignal {
|
|
281
|
+
if (this.snapshots.length < 3) {
|
|
282
|
+
return { name: 'low_progress', score: 0, detail: 'Not enough data' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let totalDist = 0;
|
|
286
|
+
let comparisons = 0;
|
|
287
|
+
|
|
288
|
+
for (let i = 1; i < this.snapshots.length; i++) {
|
|
289
|
+
const dist = cheapEditDistance(
|
|
290
|
+
this.snapshots[i - 1]!.ocrText,
|
|
291
|
+
this.snapshots[i]!.ocrText
|
|
292
|
+
);
|
|
293
|
+
totalDist += dist;
|
|
294
|
+
comparisons++;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const avgDist = comparisons > 0 ? totalDist / comparisons : 0;
|
|
298
|
+
|
|
299
|
+
// If average edit distance is small (< 100 chars difference per capture),
|
|
300
|
+
// the user is making minor tweaks without significant progress
|
|
301
|
+
if (avgDist > 0 && avgDist < 100) {
|
|
302
|
+
const score = Math.min(1.0, 1.0 - (avgDist / 200));
|
|
303
|
+
return {
|
|
304
|
+
name: 'low_progress',
|
|
305
|
+
score: Math.max(0, score),
|
|
306
|
+
detail: `Avg ${Math.round(avgDist)} chars changed per capture (minor tweaks)`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { name: 'low_progress', score: 0, detail: `Avg ${Math.round(avgDist)} chars changed (making progress)` };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Helpers ──
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Simple non-cryptographic hash for fast comparison.
|
|
318
|
+
*/
|
|
319
|
+
function simpleHash(str: string): string {
|
|
320
|
+
let hash = 0;
|
|
321
|
+
const sample = str.slice(0, 2000);
|
|
322
|
+
for (let i = 0; i < sample.length; i++) {
|
|
323
|
+
const char = sample.charCodeAt(i);
|
|
324
|
+
hash = ((hash << 5) - hash) + char;
|
|
325
|
+
hash |= 0;
|
|
326
|
+
}
|
|
327
|
+
return hash.toString(36);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Cheap O(n) edit distance — counts character-position mismatches.
|
|
332
|
+
* Not a full Levenshtein, but sufficient for "are these texts similar?"
|
|
333
|
+
*/
|
|
334
|
+
function cheapEditDistance(a: string, b: string): number {
|
|
335
|
+
const maxLen = Math.min(a.length, b.length, 1000);
|
|
336
|
+
let diffs = 0;
|
|
337
|
+
for (let i = 0; i < maxLen; i++) {
|
|
338
|
+
if (a.charCodeAt(i) !== b.charCodeAt(i)) diffs++;
|
|
339
|
+
}
|
|
340
|
+
diffs += Math.abs(a.length - b.length);
|
|
341
|
+
return diffs;
|
|
342
|
+
}
|