@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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for working with roles
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RoleDefinition } from './types.ts';
|
|
6
|
+
import type { ActionCategory } from './authority.ts';
|
|
7
|
+
import { canPerform, listAllowedActions } from './authority.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Find roles that can perform a specific action
|
|
11
|
+
*/
|
|
12
|
+
export function findRolesWithPermission(
|
|
13
|
+
roles: Map<string, RoleDefinition>,
|
|
14
|
+
action: ActionCategory
|
|
15
|
+
): RoleDefinition[] {
|
|
16
|
+
const result: RoleDefinition[] = [];
|
|
17
|
+
|
|
18
|
+
for (const role of roles.values()) {
|
|
19
|
+
if (canPerform(role, action)) {
|
|
20
|
+
result.push(role);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result.sort((a, b) => a.authority_level - b.authority_level);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find the least privileged role that can perform an action
|
|
29
|
+
*/
|
|
30
|
+
export function findMinimalRoleForAction(
|
|
31
|
+
roles: Map<string, RoleDefinition>,
|
|
32
|
+
action: ActionCategory
|
|
33
|
+
): RoleDefinition | null {
|
|
34
|
+
const capable = findRolesWithPermission(roles, action);
|
|
35
|
+
return capable.length > 0 ? capable[0]! : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compare two roles and show permission differences
|
|
40
|
+
*/
|
|
41
|
+
export function compareRoles(
|
|
42
|
+
role1: RoleDefinition,
|
|
43
|
+
role2: RoleDefinition
|
|
44
|
+
): {
|
|
45
|
+
onlyInRole1: ActionCategory[];
|
|
46
|
+
onlyInRole2: ActionCategory[];
|
|
47
|
+
inBoth: ActionCategory[];
|
|
48
|
+
} {
|
|
49
|
+
const actions1 = new Set(listAllowedActions(role1));
|
|
50
|
+
const actions2 = new Set(listAllowedActions(role2));
|
|
51
|
+
|
|
52
|
+
const onlyInRole1: ActionCategory[] = [];
|
|
53
|
+
const onlyInRole2: ActionCategory[] = [];
|
|
54
|
+
const inBoth: ActionCategory[] = [];
|
|
55
|
+
|
|
56
|
+
for (const action of actions1) {
|
|
57
|
+
if (actions2.has(action)) {
|
|
58
|
+
inBoth.push(action);
|
|
59
|
+
} else {
|
|
60
|
+
onlyInRole1.push(action);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const action of actions2) {
|
|
65
|
+
if (!actions1.has(action)) {
|
|
66
|
+
onlyInRole2.push(action);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { onlyInRole1, onlyInRole2, inBoth };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get a summary of role hierarchy based on authority levels
|
|
75
|
+
*/
|
|
76
|
+
export function getRoleHierarchy(roles: Map<string, RoleDefinition>): string {
|
|
77
|
+
const sorted = Array.from(roles.values()).sort(
|
|
78
|
+
(a, b) => b.authority_level - a.authority_level
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
let currentLevel = -1;
|
|
83
|
+
|
|
84
|
+
for (const role of sorted) {
|
|
85
|
+
if (role.authority_level !== currentLevel) {
|
|
86
|
+
currentLevel = role.authority_level;
|
|
87
|
+
lines.push(`\nLevel ${currentLevel}:`);
|
|
88
|
+
}
|
|
89
|
+
lines.push(` - ${role.name} (${role.id})`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return lines.join('\n').trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a role can spawn a specific sub-role
|
|
97
|
+
*/
|
|
98
|
+
export function canSpawnRole(
|
|
99
|
+
role: RoleDefinition,
|
|
100
|
+
subRoleId: string
|
|
101
|
+
): boolean {
|
|
102
|
+
return role.sub_roles.some(sr => sr.role_id === subRoleId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get all roles that can spawn a specific role
|
|
107
|
+
*/
|
|
108
|
+
export function findSpawnersOfRole(
|
|
109
|
+
roles: Map<string, RoleDefinition>,
|
|
110
|
+
targetRoleId: string
|
|
111
|
+
): RoleDefinition[] {
|
|
112
|
+
const spawners: RoleDefinition[] = [];
|
|
113
|
+
|
|
114
|
+
for (const role of roles.values()) {
|
|
115
|
+
if (canSpawnRole(role, targetRoleId)) {
|
|
116
|
+
spawners.push(role);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return spawners;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validate role hierarchy (check for circular dependencies)
|
|
125
|
+
*/
|
|
126
|
+
export function validateRoleHierarchy(
|
|
127
|
+
roles: Map<string, RoleDefinition>
|
|
128
|
+
): { valid: boolean; errors: string[] } {
|
|
129
|
+
const errors: string[] = [];
|
|
130
|
+
|
|
131
|
+
for (const [id, role] of roles) {
|
|
132
|
+
// Check if sub-roles exist
|
|
133
|
+
for (const subRole of role.sub_roles) {
|
|
134
|
+
if (!roles.has(subRole.role_id)) {
|
|
135
|
+
errors.push(
|
|
136
|
+
`Role '${id}' references non-existent sub-role '${subRole.role_id}'`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check for self-spawning
|
|
141
|
+
if (subRole.role_id === id) {
|
|
142
|
+
errors.push(`Role '${id}' attempts to spawn itself`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check authority levels (parent should have higher authority)
|
|
146
|
+
const subRoleDef = roles.get(subRole.role_id);
|
|
147
|
+
if (subRoleDef && subRoleDef.authority_level > role.authority_level) {
|
|
148
|
+
errors.push(
|
|
149
|
+
`Role '${id}' (level ${role.authority_level}) attempts to spawn ` +
|
|
150
|
+
`'${subRole.role_id}' (level ${subRoleDef.authority_level}) with higher authority`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
valid: errors.length === 0,
|
|
158
|
+
errors,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get statistics about a role collection
|
|
164
|
+
*/
|
|
165
|
+
export function getRoleStats(roles: Map<string, RoleDefinition>): {
|
|
166
|
+
totalRoles: number;
|
|
167
|
+
averageAuthorityLevel: number;
|
|
168
|
+
totalTools: number;
|
|
169
|
+
totalKPIs: number;
|
|
170
|
+
rolesWithSubRoles: number;
|
|
171
|
+
authorityDistribution: Record<number, number>;
|
|
172
|
+
} {
|
|
173
|
+
let totalAuthority = 0;
|
|
174
|
+
let totalTools = 0;
|
|
175
|
+
let totalKPIs = 0;
|
|
176
|
+
let rolesWithSubRoles = 0;
|
|
177
|
+
const authorityDistribution: Record<number, number> = {};
|
|
178
|
+
|
|
179
|
+
for (const role of roles.values()) {
|
|
180
|
+
totalAuthority += role.authority_level;
|
|
181
|
+
totalTools += role.tools.length;
|
|
182
|
+
totalKPIs += role.kpis.length;
|
|
183
|
+
|
|
184
|
+
if (role.sub_roles.length > 0) {
|
|
185
|
+
rolesWithSubRoles++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
authorityDistribution[role.authority_level] =
|
|
189
|
+
(authorityDistribution[role.authority_level] || 0) + 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
totalRoles: roles.size,
|
|
194
|
+
averageAuthorityLevel: roles.size > 0 ? totalAuthority / roles.size : 0,
|
|
195
|
+
totalTools,
|
|
196
|
+
totalKPIs,
|
|
197
|
+
rolesWithSubRoles,
|
|
198
|
+
authorityDistribution,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 Setup Script
|
|
3
|
+
*
|
|
4
|
+
* Interactive CLI to authenticate JARVIS with Google APIs.
|
|
5
|
+
* Opens browser to Google consent screen, handles callback,
|
|
6
|
+
* and saves tokens to ~/.jarvis/google-tokens.json.
|
|
7
|
+
*
|
|
8
|
+
* Usage: bun run src/scripts/google-setup.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { GoogleAuth } from '../integrations/google-auth.ts';
|
|
12
|
+
import { loadConfig } from '../config/loader.ts';
|
|
13
|
+
|
|
14
|
+
const SCOPES = [
|
|
15
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
16
|
+
'https://www.googleapis.com/auth/calendar.readonly',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log('=== Google OAuth2 Setup for JARVIS ===');
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
// Load config to get client_id / client_secret
|
|
25
|
+
const config = await loadConfig();
|
|
26
|
+
|
|
27
|
+
let clientId = config.google?.client_id ?? '';
|
|
28
|
+
let clientSecret = config.google?.client_secret ?? '';
|
|
29
|
+
|
|
30
|
+
if (!clientId || !clientSecret) {
|
|
31
|
+
console.log('No Google OAuth credentials found in config.yaml.');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log('Add the following to your ~/.jarvis/config.yaml:');
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log('google:');
|
|
36
|
+
console.log(' client_id: "your-client-id.apps.googleusercontent.com"');
|
|
37
|
+
console.log(' client_secret: "your-client-secret"');
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log('To get credentials:');
|
|
40
|
+
console.log(' 1. Go to https://console.cloud.google.com/apis/credentials');
|
|
41
|
+
console.log(' 2. Create an OAuth2 client ID (Web application)');
|
|
42
|
+
console.log(' 3. Add http://localhost:3142/api/auth/google/callback as a redirect URI');
|
|
43
|
+
console.log(' 4. Copy client_id and client_secret into config.yaml');
|
|
44
|
+
console.log('');
|
|
45
|
+
|
|
46
|
+
// Try reading from stdin
|
|
47
|
+
const rl = require('readline').createInterface({
|
|
48
|
+
input: process.stdin,
|
|
49
|
+
output: process.stdout,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const ask = (q: string): Promise<string> =>
|
|
53
|
+
new Promise((resolve) => rl.question(q, resolve));
|
|
54
|
+
|
|
55
|
+
clientId = (await ask('Client ID (or press Enter to abort): ')).trim();
|
|
56
|
+
if (!clientId) {
|
|
57
|
+
console.log('Aborted.');
|
|
58
|
+
rl.close();
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
clientSecret = (await ask('Client Secret: ')).trim();
|
|
63
|
+
if (!clientSecret) {
|
|
64
|
+
console.log('Aborted.');
|
|
65
|
+
rl.close();
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rl.close();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const auth = new GoogleAuth(clientId, clientSecret);
|
|
73
|
+
|
|
74
|
+
// Check if already authenticated
|
|
75
|
+
if (auth.isAuthenticated()) {
|
|
76
|
+
console.log('Already authenticated! Tokens exist at ~/.jarvis/google-tokens.json');
|
|
77
|
+
console.log('To re-authenticate, delete that file and run this again.');
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const authUrl = auth.getAuthUrl(SCOPES);
|
|
82
|
+
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log('Opening browser for Google authorization...');
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log('If the browser does not open, visit this URL:');
|
|
87
|
+
console.log(authUrl);
|
|
88
|
+
console.log('');
|
|
89
|
+
|
|
90
|
+
// Try to open browser
|
|
91
|
+
try {
|
|
92
|
+
const opener = process.platform === 'darwin'
|
|
93
|
+
? 'open'
|
|
94
|
+
: process.platform === 'win32'
|
|
95
|
+
? 'start'
|
|
96
|
+
: 'xdg-open';
|
|
97
|
+
Bun.spawn([opener, authUrl], { stdout: 'ignore', stderr: 'ignore' });
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore — user can open manually
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Start temporary HTTP server to receive the callback
|
|
103
|
+
console.log('Waiting for authorization callback on port 3142...');
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
const server = Bun.serve({
|
|
107
|
+
port: 3142,
|
|
108
|
+
async fetch(req) {
|
|
109
|
+
const url = new URL(req.url);
|
|
110
|
+
|
|
111
|
+
if (url.pathname === '/api/auth/google/callback') {
|
|
112
|
+
const code = url.searchParams.get('code');
|
|
113
|
+
const error = url.searchParams.get('error');
|
|
114
|
+
|
|
115
|
+
if (error) {
|
|
116
|
+
console.error('Authorization denied:', error);
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
server.stop();
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}, 500);
|
|
121
|
+
return new Response(
|
|
122
|
+
'<html><body><h1>Authorization Denied</h1><p>You can close this tab.</p></body></html>',
|
|
123
|
+
{ headers: { 'Content-Type': 'text/html' } }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!code) {
|
|
128
|
+
return new Response('Missing code', { status: 400 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const tokens = await auth.exchangeCode(code);
|
|
133
|
+
console.log('Authorization successful!');
|
|
134
|
+
console.log(`Access token: ${tokens.access_token.slice(0, 20)}...`);
|
|
135
|
+
console.log(`Refresh token: ${tokens.refresh_token.slice(0, 20)}...`);
|
|
136
|
+
console.log(`Tokens saved to ~/.jarvis/google-tokens.json`);
|
|
137
|
+
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
server.stop();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}, 500);
|
|
142
|
+
|
|
143
|
+
return new Response(
|
|
144
|
+
'<html><body><h1>JARVIS Google Authorization Complete!</h1><p>Tokens saved. You can close this tab.</p></body></html>',
|
|
145
|
+
{ headers: { 'Content-Type': 'text/html' } }
|
|
146
|
+
);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('Token exchange failed:', err);
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
server.stop();
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}, 500);
|
|
153
|
+
return new Response(
|
|
154
|
+
`<html><body><h1>Token Exchange Failed</h1><pre>${err}</pre></body></html>`,
|
|
155
|
+
{ headers: { 'Content-Type': 'text/html' }, status: 500 }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return new Response('Not found', { status: 404 });
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
main().catch((err) => {
|
|
166
|
+
console.error('Setup failed:', err);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar Connection — Per-Sidecar WebSocket Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Manages a single sidecar's WebSocket connection, including message
|
|
5
|
+
* parsing, validation, binary frame correlation, and heartbeat.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ServerWebSocket } from 'bun';
|
|
9
|
+
import type { RPCRequest, SidecarEvent } from './protocol.ts';
|
|
10
|
+
import type { EventScheduler } from './scheduler.ts';
|
|
11
|
+
import { validateEvent, validateBinaryFrame, MAX_JSON_SIZE } from './validator.ts';
|
|
12
|
+
|
|
13
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
14
|
+
const MAX_MISSED_PONGS = 3;
|
|
15
|
+
const BINARY_WAIT_TIMEOUT_MS = 5_000;
|
|
16
|
+
|
|
17
|
+
interface PendingBinary {
|
|
18
|
+
resolve: (payload: Buffer) => void;
|
|
19
|
+
reject: (error: Error) => void;
|
|
20
|
+
timer: Timer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SidecarConnection {
|
|
24
|
+
readonly sidecarId: string;
|
|
25
|
+
private ws: ServerWebSocket<unknown>;
|
|
26
|
+
private scheduler: EventScheduler;
|
|
27
|
+
private pendingBinary = new Map<string, PendingBinary>();
|
|
28
|
+
private heartbeatTimer: Timer | null = null;
|
|
29
|
+
private missedPongs = 0;
|
|
30
|
+
private alive = true;
|
|
31
|
+
private onDisconnect: () => void;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
sidecarId: string,
|
|
35
|
+
ws: ServerWebSocket<unknown>,
|
|
36
|
+
scheduler: EventScheduler,
|
|
37
|
+
onDisconnect: () => void,
|
|
38
|
+
) {
|
|
39
|
+
this.sidecarId = sidecarId;
|
|
40
|
+
this.ws = ws;
|
|
41
|
+
this.scheduler = scheduler;
|
|
42
|
+
this.onDisconnect = onDisconnect;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Send an RPC request to the sidecar */
|
|
46
|
+
sendRPC(request: RPCRequest): void {
|
|
47
|
+
try {
|
|
48
|
+
this.ws.send(JSON.stringify(request));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(`[SidecarConnection:${this.sidecarId}] Failed to send RPC:`, err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Handle an inbound text (JSON) message */
|
|
55
|
+
async handleMessage(raw: string): Promise<void> {
|
|
56
|
+
if (raw.length > MAX_JSON_SIZE) {
|
|
57
|
+
console.warn(`[SidecarConnection:${this.sidecarId}] Message too large: ${raw.length} bytes`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let parsed: unknown;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(raw);
|
|
64
|
+
} catch {
|
|
65
|
+
console.warn(`[SidecarConnection:${this.sidecarId}] Invalid JSON`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = validateEvent(parsed);
|
|
70
|
+
if (!result.valid || !result.event) {
|
|
71
|
+
console.warn(`[SidecarConnection:${this.sidecarId}] Validation failed: ${result.error}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const event = result.event;
|
|
76
|
+
|
|
77
|
+
// If event references binary data via ref, wait for the binary frame
|
|
78
|
+
if (event.binary?.type === 'ref') {
|
|
79
|
+
const refId = event.binary.ref_id;
|
|
80
|
+
try {
|
|
81
|
+
const binaryPayload = await this.waitForBinary(refId);
|
|
82
|
+
// Attach resolved binary data to the event payload
|
|
83
|
+
(event.payload as Record<string, unknown>)._binary = binaryPayload;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.warn(`[SidecarConnection:${this.sidecarId}] Binary wait failed for ${refId}:`, err);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.scheduler.enqueue(this.sidecarId, event, event.priority);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Handle an inbound binary frame */
|
|
94
|
+
handleBinary(data: Buffer): void {
|
|
95
|
+
const result = validateBinaryFrame(data);
|
|
96
|
+
if (!result.valid || !result.refId) {
|
|
97
|
+
console.warn(`[SidecarConnection:${this.sidecarId}] Invalid binary frame: ${result.error}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pending = this.pendingBinary.get(result.refId);
|
|
102
|
+
if (pending) {
|
|
103
|
+
clearTimeout(pending.timer);
|
|
104
|
+
this.pendingBinary.delete(result.refId);
|
|
105
|
+
pending.resolve(result.payload!);
|
|
106
|
+
} else {
|
|
107
|
+
console.warn(`[SidecarConnection:${this.sidecarId}] Unexpected binary ref: ${result.refId}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Start heartbeat ping/pong */
|
|
112
|
+
startHeartbeat(): void {
|
|
113
|
+
this.missedPongs = 0;
|
|
114
|
+
this.alive = true;
|
|
115
|
+
|
|
116
|
+
this.heartbeatTimer = setInterval(() => {
|
|
117
|
+
if (!this.alive) {
|
|
118
|
+
this.missedPongs++;
|
|
119
|
+
if (this.missedPongs >= MAX_MISSED_PONGS) {
|
|
120
|
+
console.warn(`[SidecarConnection:${this.sidecarId}] ${MAX_MISSED_PONGS} missed pongs, disconnecting`);
|
|
121
|
+
this.close();
|
|
122
|
+
this.onDisconnect();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.alive = false;
|
|
128
|
+
try {
|
|
129
|
+
this.ws.ping();
|
|
130
|
+
} catch {
|
|
131
|
+
this.close();
|
|
132
|
+
this.onDisconnect();
|
|
133
|
+
}
|
|
134
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Called when a pong is received */
|
|
138
|
+
handlePong(): void {
|
|
139
|
+
this.alive = true;
|
|
140
|
+
this.missedPongs = 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Stop heartbeat */
|
|
144
|
+
stopHeartbeat(): void {
|
|
145
|
+
if (this.heartbeatTimer) {
|
|
146
|
+
clearInterval(this.heartbeatTimer);
|
|
147
|
+
this.heartbeatTimer = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Close connection and clean up */
|
|
152
|
+
close(): void {
|
|
153
|
+
this.stopHeartbeat();
|
|
154
|
+
|
|
155
|
+
// Reject all pending binary waits
|
|
156
|
+
for (const [refId, pending] of this.pendingBinary) {
|
|
157
|
+
clearTimeout(pending.timer);
|
|
158
|
+
pending.reject(new Error('Connection closed'));
|
|
159
|
+
}
|
|
160
|
+
this.pendingBinary.clear();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
this.ws.close();
|
|
164
|
+
} catch {
|
|
165
|
+
// Already closed
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private waitForBinary(refId: string): Promise<Buffer> {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
this.pendingBinary.delete(refId);
|
|
173
|
+
reject(new Error(`Binary frame timeout for ref ${refId}`));
|
|
174
|
+
}, BINARY_WAIT_TIMEOUT_MS);
|
|
175
|
+
|
|
176
|
+
this.pendingBinary.set(refId, { resolve, reject, timer });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|