@shykaruu/jarvis-brain 0.4.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.
Files changed (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "react-jsx",
4
+ "strict": true,
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "target": "esnext",
8
+ "types": ["bun-types"]
9
+ }
10
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Site Builder — Git Manager
3
+ *
4
+ * Wraps git CLI commands via Bun.spawn for project version control.
5
+ */
6
+
7
+ import type { GitCommit, GitBranch } from './types.ts';
8
+
9
+ export class GitManager {
10
+ /**
11
+ * Check if git is installed on the system.
12
+ */
13
+ static async isInstalled(): Promise<boolean> {
14
+ try {
15
+ const proc = Bun.spawn(['git', '--version'], { stdout: 'pipe', stderr: 'pipe' });
16
+ const stdout = await new Response(proc.stdout).text();
17
+ return (await proc.exited) === 0;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Get the effective git author config (global/system level).
25
+ */
26
+ static async getGlobalAuthor(): Promise<{ name: string | null; email: string | null }> {
27
+ let name: string | null = null;
28
+ let email: string | null = null;
29
+ try {
30
+ const proc = Bun.spawn(['git', 'config', '--global', 'user.name'], { stdout: 'pipe', stderr: 'pipe' });
31
+ const out = await new Response(proc.stdout).text();
32
+ if ((await proc.exited) === 0) name = out.trim() || null;
33
+ } catch {}
34
+ try {
35
+ const proc = Bun.spawn(['git', 'config', '--global', 'user.email'], { stdout: 'pipe', stderr: 'pipe' });
36
+ const out = await new Response(proc.stdout).text();
37
+ if ((await proc.exited) === 0) email = out.trim() || null;
38
+ } catch {}
39
+ return { name, email };
40
+ }
41
+
42
+ /**
43
+ * Initialize a new git repo in the project directory.
44
+ * If author config is provided, sets it before the initial commit.
45
+ */
46
+ async init(projectPath: string, author?: { name: string; email: string; global: boolean }): Promise<void> {
47
+ await this.run(projectPath, ['init']);
48
+
49
+ if (author) {
50
+ const scope = author.global ? '--global' : '--local';
51
+ await this.run(projectPath, ['config', scope, 'user.name', author.name]);
52
+ await this.run(projectPath, ['config', scope, 'user.email', author.email]);
53
+ }
54
+
55
+ // Create initial commit
56
+ await this.run(projectPath, ['add', '-A']);
57
+ await this.run(projectPath, ['commit', '-m', 'Initial commit', '--allow-empty']);
58
+ }
59
+
60
+ /**
61
+ * Stage all changes and commit with a descriptive message.
62
+ * Returns null if there are no changes to commit.
63
+ */
64
+ async autoCommit(projectPath: string, message: string): Promise<GitCommit | null> {
65
+ const dirty = await this.isDirty(projectPath);
66
+ if (!dirty) return null;
67
+
68
+ await this.run(projectPath, ['add', '-A']);
69
+ await this.run(projectPath, ['commit', '-m', message]);
70
+
71
+ const log = await this.getLog(projectPath, 1);
72
+ return log[0] ?? null;
73
+ }
74
+
75
+ /**
76
+ * List all local branches.
77
+ */
78
+ async getBranches(projectPath: string): Promise<GitBranch[]> {
79
+ const output = await this.run(projectPath, ['branch', '--no-color']);
80
+ if (!output.trim()) return [{ name: 'main', current: true }];
81
+
82
+ return output
83
+ .split('\n')
84
+ .filter(line => line.trim())
85
+ .map(line => ({
86
+ name: line.replace(/^\*?\s+/, '').trim(),
87
+ current: line.startsWith('*'),
88
+ }));
89
+ }
90
+
91
+ /**
92
+ * Get the current branch name.
93
+ */
94
+ async getCurrentBranch(projectPath: string): Promise<string> {
95
+ const output = await this.run(projectPath, ['branch', '--show-current']);
96
+ return output.trim() || 'main';
97
+ }
98
+
99
+ /**
100
+ * Create a new branch.
101
+ */
102
+ async createBranch(projectPath: string, name: string): Promise<void> {
103
+ await this.run(projectPath, ['checkout', '-b', name]);
104
+ }
105
+
106
+ /**
107
+ * Switch to an existing branch.
108
+ */
109
+ async switchBranch(projectPath: string, name: string): Promise<void> {
110
+ await this.run(projectPath, ['checkout', name]);
111
+ }
112
+
113
+ /**
114
+ * Get commit log.
115
+ */
116
+ async getLog(projectPath: string, limit: number = 50): Promise<GitCommit[]> {
117
+ try {
118
+ const output = await this.run(projectPath, [
119
+ 'log',
120
+ `--max-count=${limit}`,
121
+ '--format=%H|%h|%s|%an|%at',
122
+ ]);
123
+
124
+ if (!output.trim()) return [];
125
+
126
+ return output
127
+ .split('\n')
128
+ .filter(line => line.trim())
129
+ .map(line => {
130
+ const [hash, shortHash, message, author, dateStr] = line.split('|');
131
+ return {
132
+ hash: hash!,
133
+ shortHash: shortHash!,
134
+ message: message!,
135
+ author: author!,
136
+ date: parseInt(dateStr!, 10) * 1000,
137
+ };
138
+ });
139
+ } catch {
140
+ return [];
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Check if working tree has uncommitted changes.
146
+ */
147
+ async isDirty(projectPath: string): Promise<boolean> {
148
+ const output = await this.run(projectPath, ['status', '--porcelain']);
149
+ return output.trim().length > 0;
150
+ }
151
+
152
+ /**
153
+ * Get diff of uncommitted changes.
154
+ */
155
+ async getDiff(projectPath: string): Promise<string> {
156
+ const staged = await this.run(projectPath, ['diff', '--cached']);
157
+ const unstaged = await this.run(projectPath, ['diff']);
158
+ return (staged + '\n' + unstaged).trim();
159
+ }
160
+
161
+ /**
162
+ * Merge a branch into the current branch.
163
+ */
164
+ async merge(projectPath: string, branch: string): Promise<{ success: boolean; conflicts?: string[] }> {
165
+ try {
166
+ await this.run(projectPath, ['merge', branch]);
167
+ return { success: true };
168
+ } catch (err) {
169
+ // Check for merge conflicts
170
+ const status = await this.run(projectPath, ['status', '--porcelain']);
171
+ const conflicts = status
172
+ .split('\n')
173
+ .filter(line => line.startsWith('UU') || line.startsWith('AA'))
174
+ .map(line => line.slice(3).trim());
175
+
176
+ if (conflicts.length > 0) {
177
+ return { success: false, conflicts };
178
+ }
179
+
180
+ // Abort the failed merge
181
+ try { await this.run(projectPath, ['merge', '--abort']); } catch { /* ignore */ }
182
+ throw err;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Rebase current branch onto another branch.
188
+ */
189
+ async rebase(projectPath: string, ontoBranch: string): Promise<{ success: boolean; conflicts?: string[] }> {
190
+ try {
191
+ await this.run(projectPath, ['rebase', ontoBranch]);
192
+ return { success: true };
193
+ } catch {
194
+ const status = await this.run(projectPath, ['status', '--porcelain']);
195
+ const conflicts = status
196
+ .split('\n')
197
+ .filter(line => line.startsWith('UU') || line.startsWith('AA'))
198
+ .map(line => line.slice(3).trim());
199
+
200
+ if (conflicts.length > 0) {
201
+ return { success: false, conflicts };
202
+ }
203
+
204
+ try { await this.run(projectPath, ['rebase', '--abort']); } catch { /* ignore */ }
205
+ return { success: false };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Delete a branch.
211
+ */
212
+ async deleteBranch(projectPath: string, name: string): Promise<void> {
213
+ await this.run(projectPath, ['branch', '-d', name]);
214
+ }
215
+
216
+ /**
217
+ * Run a git command in the project directory.
218
+ */
219
+ private async run(cwd: string, args: string[]): Promise<string> {
220
+ const proc = Bun.spawn(['git', ...args], {
221
+ cwd,
222
+ stdout: 'pipe',
223
+ stderr: 'pipe',
224
+ env: {
225
+ ...process.env,
226
+ GIT_TERMINAL_PROMPT: '0',
227
+ },
228
+ });
229
+
230
+ const stdout = await new Response(proc.stdout).text();
231
+ const exitCode = await proc.exited;
232
+
233
+ if (exitCode !== 0) {
234
+ const stderr = await new Response(proc.stderr).text();
235
+ throw new Error(`git ${args[0]} failed: ${stderr.trim() || stdout.trim()}`);
236
+ }
237
+
238
+ return stdout;
239
+ }
240
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Site Builder — GitHub Manager
3
+ *
4
+ * Handles GitHub integration: token management (via encrypted keychain),
5
+ * GitHub REST API calls (create/list repos, validate token), and git
6
+ * remote operations (push, pull, fetch, ahead/behind status).
7
+ */
8
+
9
+ import { getSecret, setSecret, deleteSecret, hasSecret } from '../vault/keychain.ts';
10
+ import type { GitRemoteStatus, GitHubRepoOptions } from './types.ts';
11
+
12
+ const TOKEN_KEY = 'github.personal_access_token';
13
+ const API_BASE = 'https://api.github.com';
14
+
15
+ export class GitHubManager {
16
+
17
+ // ── Token Management ──
18
+
19
+ getToken(): string | null {
20
+ return process.env.JARVIS_GITHUB_TOKEN ?? getSecret(TOKEN_KEY);
21
+ }
22
+
23
+ setToken(token: string): void {
24
+ setSecret(TOKEN_KEY, token);
25
+ }
26
+
27
+ deleteToken(): void {
28
+ deleteSecret(TOKEN_KEY);
29
+ }
30
+
31
+ hasToken(): boolean {
32
+ return !!process.env.JARVIS_GITHUB_TOKEN || hasSecret(TOKEN_KEY);
33
+ }
34
+
35
+ /**
36
+ * Validate the stored token against GitHub API.
37
+ * Returns the authenticated username and granted scopes.
38
+ */
39
+ async validateToken(): Promise<{ valid: boolean; username: string | null; scopes: string[] }> {
40
+ try {
41
+ const res = await this.githubFetch('GET', '/user');
42
+ const data = await res.json() as { login: string };
43
+ const scopes = (res.headers.get('x-oauth-scopes') ?? '').split(',').map(s => s.trim()).filter(Boolean);
44
+ return { valid: true, username: data.login, scopes };
45
+ } catch {
46
+ return { valid: false, username: null, scopes: [] };
47
+ }
48
+ }
49
+
50
+ // ── Repository Operations ──
51
+
52
+ /**
53
+ * Create a new GitHub repository under the authenticated user.
54
+ */
55
+ async createRepo(options: GitHubRepoOptions): Promise<{
56
+ owner: string;
57
+ repo: string;
58
+ cloneUrl: string;
59
+ htmlUrl: string;
60
+ }> {
61
+ const res = await this.githubFetch('POST', '/user/repos', {
62
+ name: options.name,
63
+ description: options.description ?? '',
64
+ private: options.private,
65
+ auto_init: false,
66
+ });
67
+ const data = await res.json() as {
68
+ owner: { login: string };
69
+ name: string;
70
+ clone_url: string;
71
+ html_url: string;
72
+ };
73
+ return {
74
+ owner: data.owner.login,
75
+ repo: data.name,
76
+ cloneUrl: data.clone_url,
77
+ htmlUrl: data.html_url,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * List the authenticated user's repositories (sorted by most recently updated).
83
+ */
84
+ async listUserRepos(page = 1, perPage = 30): Promise<Array<{
85
+ owner: string;
86
+ name: string;
87
+ fullName: string;
88
+ private: boolean;
89
+ htmlUrl: string;
90
+ cloneUrl: string;
91
+ }>> {
92
+ const res = await this.githubFetch('GET', `/user/repos?sort=updated&per_page=${perPage}&page=${page}`);
93
+ const data = await res.json() as Array<{
94
+ owner: { login: string };
95
+ name: string;
96
+ full_name: string;
97
+ private: boolean;
98
+ html_url: string;
99
+ clone_url: string;
100
+ }>;
101
+ return data.map(r => ({
102
+ owner: r.owner.login,
103
+ name: r.name,
104
+ fullName: r.full_name,
105
+ private: r.private,
106
+ htmlUrl: r.html_url,
107
+ cloneUrl: r.clone_url,
108
+ }));
109
+ }
110
+
111
+ /**
112
+ * Get info about a specific repo (used when connecting to an existing repo).
113
+ */
114
+ async getRepo(owner: string, repo: string): Promise<{
115
+ owner: string;
116
+ repo: string;
117
+ cloneUrl: string;
118
+ htmlUrl: string;
119
+ }> {
120
+ const res = await this.githubFetch('GET', `/repos/${owner}/${repo}`);
121
+ const data = await res.json() as {
122
+ owner: { login: string };
123
+ name: string;
124
+ clone_url: string;
125
+ html_url: string;
126
+ };
127
+ return {
128
+ owner: data.owner.login,
129
+ repo: data.name,
130
+ cloneUrl: data.clone_url,
131
+ htmlUrl: data.html_url,
132
+ };
133
+ }
134
+
135
+ // ── Git Remote Operations ──
136
+
137
+ /**
138
+ * Add or update the 'origin' remote for a project.
139
+ */
140
+ async addRemote(projectPath: string, remoteUrl: string): Promise<void> {
141
+ const existing = await this.getRemoteUrl(projectPath);
142
+ if (existing) {
143
+ await this.git(projectPath, ['remote', 'set-url', 'origin', remoteUrl]);
144
+ } else {
145
+ await this.git(projectPath, ['remote', 'add', 'origin', remoteUrl]);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Remove the 'origin' remote.
151
+ */
152
+ async removeRemote(projectPath: string): Promise<void> {
153
+ try {
154
+ await this.git(projectPath, ['remote', 'remove', 'origin']);
155
+ } catch { /* already gone */ }
156
+ }
157
+
158
+ /**
159
+ * Get the current origin remote URL, or null if not set.
160
+ */
161
+ async getRemoteUrl(projectPath: string): Promise<string | null> {
162
+ try {
163
+ const url = await this.git(projectPath, ['remote', 'get-url', 'origin']);
164
+ return url.trim() || null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Push to the origin remote. Injects token into URL for authentication.
172
+ */
173
+ async push(projectPath: string, branch?: string, force = false): Promise<{ success: boolean; error?: string }> {
174
+ const token = this.getToken();
175
+ if (!token) return { success: false, error: 'GitHub token not configured' };
176
+
177
+ const remoteUrl = await this.getRemoteUrl(projectPath);
178
+ if (!remoteUrl) return { success: false, error: 'No remote origin configured' };
179
+
180
+ const authUrl = this.injectToken(remoteUrl, token);
181
+ const targetBranch = branch ?? await this.getCurrentBranch(projectPath);
182
+
183
+ const args = ['push', '-u', authUrl, targetBranch];
184
+ if (force) args.splice(1, 0, '--force');
185
+
186
+ try {
187
+ await this.git(projectPath, args);
188
+ return { success: true };
189
+ } catch (err) {
190
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Pull from the origin remote.
196
+ */
197
+ async pull(projectPath: string, branch?: string): Promise<{ success: boolean; conflicts?: string[]; error?: string }> {
198
+ const token = this.getToken();
199
+ if (!token) return { success: false, error: 'GitHub token not configured' };
200
+
201
+ const remoteUrl = await this.getRemoteUrl(projectPath);
202
+ if (!remoteUrl) return { success: false, error: 'No remote origin configured' };
203
+
204
+ const authUrl = this.injectToken(remoteUrl, token);
205
+ const targetBranch = branch ?? await this.getCurrentBranch(projectPath);
206
+
207
+ try {
208
+ await this.git(projectPath, ['pull', authUrl, targetBranch]);
209
+ return { success: true };
210
+ } catch (err) {
211
+ // Check for merge conflicts
212
+ try {
213
+ const status = await this.git(projectPath, ['status', '--porcelain']);
214
+ const conflicts = status
215
+ .split('\n')
216
+ .filter(line => line.startsWith('UU') || line.startsWith('AA'))
217
+ .map(line => line.slice(3).trim());
218
+
219
+ if (conflicts.length > 0) {
220
+ return { success: false, conflicts };
221
+ }
222
+ } catch { /* ignore */ }
223
+
224
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Fetch from origin and compute ahead/behind status.
230
+ */
231
+ async getRemoteStatus(projectPath: string): Promise<GitRemoteStatus> {
232
+ const remoteUrl = await this.getRemoteUrl(projectPath);
233
+ if (!remoteUrl) {
234
+ return { hasRemote: false, remoteUrl: null, owner: null, repo: null, ahead: 0, behind: 0, lastPushedAt: null };
235
+ }
236
+
237
+ const { owner, repo } = this.parseRemoteUrl(remoteUrl);
238
+ const token = this.getToken();
239
+
240
+ // Fetch latest refs from origin (requires auth)
241
+ if (token) {
242
+ const authUrl = this.injectToken(remoteUrl, token);
243
+ try {
244
+ await this.git(projectPath, ['fetch', authUrl, '--quiet']);
245
+ } catch { /* network error, show stale data */ }
246
+ }
247
+
248
+ const currentBranch = await this.getCurrentBranch(projectPath);
249
+ let ahead = 0;
250
+ let behind = 0;
251
+
252
+ try {
253
+ const behindStr = await this.git(projectPath, ['rev-list', '--count', `HEAD..origin/${currentBranch}`]);
254
+ behind = parseInt(behindStr.trim(), 10) || 0;
255
+ } catch { /* no tracking branch yet */ }
256
+
257
+ try {
258
+ const aheadStr = await this.git(projectPath, ['rev-list', '--count', `origin/${currentBranch}..HEAD`]);
259
+ ahead = parseInt(aheadStr.trim(), 10) || 0;
260
+ } catch { /* no tracking branch yet — all local commits are "ahead" */
261
+ try {
262
+ const totalStr = await this.git(projectPath, ['rev-list', '--count', 'HEAD']);
263
+ ahead = parseInt(totalStr.trim(), 10) || 0;
264
+ } catch { /* empty repo */ }
265
+ }
266
+
267
+ return { hasRemote: true, remoteUrl, owner, repo, ahead, behind, lastPushedAt: null };
268
+ }
269
+
270
+ // ── Private Helpers ──
271
+
272
+ /**
273
+ * Inject a token into an HTTPS GitHub URL for non-interactive auth.
274
+ * Converts https://github.com/owner/repo.git → https://<token>@github.com/owner/repo.git
275
+ */
276
+ private injectToken(remoteUrl: string, token: string): string {
277
+ try {
278
+ const url = new URL(remoteUrl);
279
+ url.username = token;
280
+ url.password = '';
281
+ return url.toString();
282
+ } catch {
283
+ // Fallback for non-standard URLs
284
+ return remoteUrl.replace('https://', `https://${token}@`);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Parse owner/repo from a GitHub remote URL.
290
+ */
291
+ private parseRemoteUrl(remoteUrl: string): { owner: string | null; repo: string | null } {
292
+ // Handles both https://github.com/owner/repo.git and git@github.com:owner/repo.git
293
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
294
+ if (!match) return { owner: null, repo: null };
295
+ return { owner: match[1]!, repo: match[2]! };
296
+ }
297
+
298
+ private async getCurrentBranch(projectPath: string): Promise<string> {
299
+ const output = await this.git(projectPath, ['branch', '--show-current']);
300
+ return output.trim() || 'main';
301
+ }
302
+
303
+ /**
304
+ * Run a git command via Bun.spawn.
305
+ */
306
+ private async git(cwd: string, args: string[]): Promise<string> {
307
+ const proc = Bun.spawn(['git', ...args], {
308
+ cwd,
309
+ stdout: 'pipe',
310
+ stderr: 'pipe',
311
+ env: {
312
+ ...process.env,
313
+ GIT_TERMINAL_PROMPT: '0',
314
+ },
315
+ });
316
+
317
+ const stdout = await new Response(proc.stdout).text();
318
+ const exitCode = await proc.exited;
319
+
320
+ if (exitCode !== 0) {
321
+ const stderr = await new Response(proc.stderr).text();
322
+ throw new Error(`git ${args[0]} failed: ${stderr.trim() || stdout.trim()}`);
323
+ }
324
+
325
+ return stdout;
326
+ }
327
+
328
+ /**
329
+ * Make an authenticated request to the GitHub REST API.
330
+ */
331
+ private async githubFetch(method: string, path: string, body?: unknown): Promise<Response> {
332
+ const token = this.getToken();
333
+ if (!token) throw new Error('GitHub token not configured');
334
+
335
+ const url = path.startsWith('http') ? path : `${API_BASE}${path}`;
336
+ const res = await fetch(url, {
337
+ method,
338
+ headers: {
339
+ 'Authorization': `Bearer ${token}`,
340
+ 'Accept': 'application/vnd.github+json',
341
+ 'X-GitHub-Api-Version': '2022-11-28',
342
+ 'User-Agent': 'JARVIS-SiteBuilder',
343
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
344
+ },
345
+ body: body ? JSON.stringify(body) : undefined,
346
+ });
347
+
348
+ if (!res.ok) {
349
+ const err = await res.json().catch(() => ({})) as { message?: string };
350
+ throw new Error(`GitHub API error (${res.status}): ${err.message ?? res.statusText}`);
351
+ }
352
+
353
+ return res;
354
+ }
355
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Site Builder — Public API
3
+ */
4
+
5
+ export { SiteBuilderService } from './service.ts';
6
+ export { ProjectManager } from './project-manager.ts';
7
+ export { GitManager } from './git-manager.ts';
8
+ export { GitHubManager } from './github-manager.ts';
9
+ export { DevServerManager } from './dev-server-manager.ts';
10
+ export { SiteProxy } from './proxy.ts';
11
+ export { TEMPLATES } from './templates.ts';
12
+ export { createSiteBuilderTools } from './builder-tools.ts';
13
+
14
+ export type {
15
+ Project,
16
+ ProjectMeta,
17
+ ProjectTemplate,
18
+ ProjectStatus,
19
+ FileEntry,
20
+ GitCommit,
21
+ GitBranch,
22
+ GitRemoteStatus,
23
+ GitHubRepoOptions,
24
+ SiteBuilderConfig,
25
+ } from './types.ts';