flyee 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.
Files changed (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/install.js +357 -0
  4. package/bridge/bridge.py +1780 -0
  5. package/bridge/local_tracker.py +722 -0
  6. package/core/agents/backend-specialist.md +266 -0
  7. package/core/agents/code-archaeologist.md +106 -0
  8. package/core/agents/database-architect.md +226 -0
  9. package/core/agents/debugger.md +225 -0
  10. package/core/agents/devops-engineer.md +323 -0
  11. package/core/agents/documentation-writer.md +104 -0
  12. package/core/agents/explorer-agent.md +73 -0
  13. package/core/agents/frontend-specialist.md +743 -0
  14. package/core/agents/game-developer.md +162 -0
  15. package/core/agents/mobile-developer.md +377 -0
  16. package/core/agents/orchestrator.md +416 -0
  17. package/core/agents/penetration-tester.md +188 -0
  18. package/core/agents/performance-optimizer.md +187 -0
  19. package/core/agents/product-manager.md +112 -0
  20. package/core/agents/product-owner.md +95 -0
  21. package/core/agents/project-planner.md +470 -0
  22. package/core/agents/qa-automation-engineer.md +103 -0
  23. package/core/agents/security-auditor.md +170 -0
  24. package/core/agents/seo-specialist.md +111 -0
  25. package/core/agents/stitch-designer.md +190 -0
  26. package/core/agents/tdd-reviewer.md +282 -0
  27. package/core/agents/test-engineer.md +158 -0
  28. package/core/scripts/auto_preview.py +148 -0
  29. package/core/scripts/checklist.py +243 -0
  30. package/core/scripts/cost_report.py +149 -0
  31. package/core/scripts/doc-sync-check.py +461 -0
  32. package/core/scripts/parse_user_stories.py +79 -0
  33. package/core/scripts/prepare_notion_updates.py +172 -0
  34. package/core/scripts/print_create_payload.py +18 -0
  35. package/core/scripts/session_manager.py +120 -0
  36. package/core/scripts/task_complete.py +127 -0
  37. package/core/scripts/verify_all.py +327 -0
  38. package/core/skills/analytics-strategy/SKILL.md +128 -0
  39. package/core/skills/api-patterns/SKILL.md +81 -0
  40. package/core/skills/api-patterns/api-style.md +42 -0
  41. package/core/skills/api-patterns/auth.md +24 -0
  42. package/core/skills/api-patterns/documentation.md +26 -0
  43. package/core/skills/api-patterns/graphql.md +41 -0
  44. package/core/skills/api-patterns/rate-limiting.md +31 -0
  45. package/core/skills/api-patterns/response.md +37 -0
  46. package/core/skills/api-patterns/rest.md +40 -0
  47. package/core/skills/api-patterns/scripts/api_validator.py +211 -0
  48. package/core/skills/api-patterns/security-testing.md +122 -0
  49. package/core/skills/api-patterns/trpc.md +41 -0
  50. package/core/skills/api-patterns/versioning.md +22 -0
  51. package/core/skills/app-builder/SKILL.md +75 -0
  52. package/core/skills/app-builder/agent-coordination.md +71 -0
  53. package/core/skills/app-builder/feature-building.md +53 -0
  54. package/core/skills/app-builder/project-detection.md +34 -0
  55. package/core/skills/app-builder/scaffolding.md +118 -0
  56. package/core/skills/app-builder/tech-stack.md +40 -0
  57. package/core/skills/app-builder/templates/SKILL.md +39 -0
  58. package/core/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
  59. package/core/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
  60. package/core/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  61. package/core/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
  62. package/core/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
  63. package/core/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
  64. package/core/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
  65. package/core/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +82 -0
  66. package/core/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +100 -0
  67. package/core/skills/app-builder/templates/nextjs-static/TEMPLATE.md +106 -0
  68. package/core/skills/app-builder/templates/nuxt-app/TEMPLATE.md +101 -0
  69. package/core/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
  70. package/core/skills/app-builder/templates/react-native-app/TEMPLATE.md +93 -0
  71. package/core/skills/architecture/SKILL.md +55 -0
  72. package/core/skills/architecture/context-discovery.md +43 -0
  73. package/core/skills/architecture/examples.md +94 -0
  74. package/core/skills/architecture/pattern-selection.md +68 -0
  75. package/core/skills/architecture/patterns-reference.md +50 -0
  76. package/core/skills/architecture/trade-off-analysis.md +77 -0
  77. package/core/skills/atomic-design/SKILL.md +282 -0
  78. package/core/skills/atomic-design/references/classification-guide.md +132 -0
  79. package/core/skills/atomic-design/references/quality-checklist.md +60 -0
  80. package/core/skills/atomic-design/references/stacks/stack-blade.md +254 -0
  81. package/core/skills/atomic-design/references/stacks/stack-nextjs.md +272 -0
  82. package/core/skills/atomic-design/references/stacks/stack-react.md +239 -0
  83. package/core/skills/atomic-design/references/stacks/stack-vue.md +224 -0
  84. package/core/skills/bash-linux/SKILL.md +199 -0
  85. package/core/skills/behavioral-modes/SKILL.md +242 -0
  86. package/core/skills/brainstorming/SKILL.md +163 -0
  87. package/core/skills/brainstorming/dynamic-questioning.md +373 -0
  88. package/core/skills/checkpointing-patterns/SKILL.md +163 -0
  89. package/core/skills/clean-code/SKILL.md +201 -0
  90. package/core/skills/code-review-checklist/SKILL.md +109 -0
  91. package/core/skills/code-truth-validation/SKILL.md +149 -0
  92. package/core/skills/component-library-discovery/SKILL.md +154 -0
  93. package/core/skills/content-strategy/SKILL.md +222 -0
  94. package/core/skills/context-budget/SKILL.md +155 -0
  95. package/core/skills/context-gathering-patterns/SKILL.md +278 -0
  96. package/core/skills/cost-tracking/SKILL.md +206 -0
  97. package/core/skills/database-design/SKILL.md +52 -0
  98. package/core/skills/database-design/database-selection.md +43 -0
  99. package/core/skills/database-design/indexing.md +39 -0
  100. package/core/skills/database-design/migrations.md +48 -0
  101. package/core/skills/database-design/optimization.md +36 -0
  102. package/core/skills/database-design/orm-selection.md +30 -0
  103. package/core/skills/database-design/schema-design.md +56 -0
  104. package/core/skills/database-design/scripts/schema_validator.py +172 -0
  105. package/core/skills/deployment-procedures/SKILL.md +295 -0
  106. package/core/skills/design-md/README.md +34 -0
  107. package/core/skills/design-md/SKILL.md +172 -0
  108. package/core/skills/design-md/examples/DESIGN.md +154 -0
  109. package/core/skills/design-system-enforcement/SKILL.md +339 -0
  110. package/core/skills/doc.md +177 -0
  111. package/core/skills/document-registry/SKILL.md +130 -0
  112. package/core/skills/documentation-publishing/SKILL.md +174 -0
  113. package/core/skills/documentation-templates/SKILL.md +194 -0
  114. package/core/skills/enhance-prompt/README.md +34 -0
  115. package/core/skills/enhance-prompt/SKILL.md +204 -0
  116. package/core/skills/enhance-prompt/references/KEYWORDS.md +114 -0
  117. package/core/skills/frontend-design/SKILL.md +430 -0
  118. package/core/skills/frontend-design/animation-guide.md +331 -0
  119. package/core/skills/frontend-design/color-system.md +311 -0
  120. package/core/skills/frontend-design/decision-trees.md +418 -0
  121. package/core/skills/frontend-design/motion-graphics.md +306 -0
  122. package/core/skills/frontend-design/scripts/accessibility_checker.py +183 -0
  123. package/core/skills/frontend-design/scripts/ux_audit.py +722 -0
  124. package/core/skills/frontend-design/typography-system.md +345 -0
  125. package/core/skills/frontend-design/ux-psychology.md +541 -0
  126. package/core/skills/frontend-design/visual-effects.md +383 -0
  127. package/core/skills/game-development/2d-games/SKILL.md +119 -0
  128. package/core/skills/game-development/3d-games/SKILL.md +135 -0
  129. package/core/skills/game-development/SKILL.md +167 -0
  130. package/core/skills/game-development/game-art/SKILL.md +185 -0
  131. package/core/skills/game-development/game-audio/SKILL.md +190 -0
  132. package/core/skills/game-development/game-design/SKILL.md +129 -0
  133. package/core/skills/game-development/mobile-games/SKILL.md +108 -0
  134. package/core/skills/game-development/multiplayer/SKILL.md +132 -0
  135. package/core/skills/game-development/pc-games/SKILL.md +144 -0
  136. package/core/skills/game-development/vr-ar/SKILL.md +123 -0
  137. package/core/skills/game-development/web-games/SKILL.md +150 -0
  138. package/core/skills/geo-fundamentals/SKILL.md +156 -0
  139. package/core/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
  140. package/core/skills/git-workflow/SKILL.md +263 -0
  141. package/core/skills/history-check-patterns/SKILL.md +125 -0
  142. package/core/skills/i18n-localization/SKILL.md +154 -0
  143. package/core/skills/i18n-localization/scripts/i18n_checker.py +241 -0
  144. package/core/skills/integration-completeness/SKILL.md +219 -0
  145. package/core/skills/intelligent-routing/SKILL.md +370 -0
  146. package/core/skills/lint-and-validate/SKILL.md +45 -0
  147. package/core/skills/lint-and-validate/scripts/lint_runner.py +173 -0
  148. package/core/skills/lint-and-validate/scripts/type_coverage.py +173 -0
  149. package/core/skills/local-verification/SKILL.md +195 -0
  150. package/core/skills/mcp-builder/SKILL.md +176 -0
  151. package/core/skills/mobile-design/SKILL.md +394 -0
  152. package/core/skills/mobile-design/decision-trees.md +516 -0
  153. package/core/skills/mobile-design/mobile-backend.md +491 -0
  154. package/core/skills/mobile-design/mobile-color-system.md +420 -0
  155. package/core/skills/mobile-design/mobile-debugging.md +122 -0
  156. package/core/skills/mobile-design/mobile-design-thinking.md +357 -0
  157. package/core/skills/mobile-design/mobile-navigation.md +458 -0
  158. package/core/skills/mobile-design/mobile-performance.md +767 -0
  159. package/core/skills/mobile-design/mobile-testing.md +356 -0
  160. package/core/skills/mobile-design/mobile-typography.md +433 -0
  161. package/core/skills/mobile-design/platform-android.md +666 -0
  162. package/core/skills/mobile-design/platform-ios.md +561 -0
  163. package/core/skills/mobile-design/scripts/mobile_audit.py +670 -0
  164. package/core/skills/mobile-design/touch-psychology.md +537 -0
  165. package/core/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +312 -0
  166. package/core/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
  167. package/core/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
  168. package/core/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
  169. package/core/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
  170. package/core/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
  171. package/core/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
  172. package/core/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
  173. package/core/skills/nextjs-react-expert/SKILL.md +267 -0
  174. package/core/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
  175. package/core/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -0
  176. package/core/skills/nodejs-best-practices/SKILL.md +333 -0
  177. package/core/skills/notion-task-patterns/SKILL.md +2529 -0
  178. package/core/skills/page-specifications/SKILL.md +367 -0
  179. package/core/skills/parallel-agents/SKILL.md +175 -0
  180. package/core/skills/performance-profiling/SKILL.md +143 -0
  181. package/core/skills/performance-profiling/scripts/lighthouse_audit.py +76 -0
  182. package/core/skills/plan-writing/SKILL.md +190 -0
  183. package/core/skills/powershell-windows/SKILL.md +167 -0
  184. package/core/skills/project-foundation/SKILL.md +117 -0
  185. package/core/skills/project-setup/SKILL.md +141 -0
  186. package/core/skills/project-tracking-patterns/SKILL.md +357 -0
  187. package/core/skills/project-type-discovery/SKILL.md +239 -0
  188. package/core/skills/python-patterns/SKILL.md +441 -0
  189. package/core/skills/qa-test-generation/SKILL.md +156 -0
  190. package/core/skills/react-components/README.md +36 -0
  191. package/core/skills/react-components/SKILL.md +47 -0
  192. package/core/skills/react-components/examples/gold-standard-card.tsx +80 -0
  193. package/core/skills/react-components/package-lock.json +231 -0
  194. package/core/skills/react-components/package.json +16 -0
  195. package/core/skills/react-components/resources/architecture-checklist.md +15 -0
  196. package/core/skills/react-components/resources/component-template.tsx +37 -0
  197. package/core/skills/react-components/resources/stitch-api-reference.md +14 -0
  198. package/core/skills/react-components/resources/style-guide.json +27 -0
  199. package/core/skills/react-components/scripts/fetch-stitch.sh +30 -0
  200. package/core/skills/react-components/scripts/validate.js +68 -0
  201. package/core/skills/red-team-tactics/SKILL.md +199 -0
  202. package/core/skills/remotion/README.md +105 -0
  203. package/core/skills/remotion/SKILL.md +393 -0
  204. package/core/skills/remotion/examples/WalkthroughComposition.tsx +78 -0
  205. package/core/skills/remotion/examples/screens.json +56 -0
  206. package/core/skills/remotion/resources/composition-checklist.md +124 -0
  207. package/core/skills/remotion/resources/screen-slide-template.tsx +123 -0
  208. package/core/skills/remotion/scripts/download-stitch-asset.sh +38 -0
  209. package/core/skills/seo-fundamentals/SKILL.md +129 -0
  210. package/core/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
  211. package/core/skills/server-management/SKILL.md +161 -0
  212. package/core/skills/session-resilience/SKILL.md +199 -0
  213. package/core/skills/shadcn-ui/README.md +248 -0
  214. package/core/skills/shadcn-ui/SKILL.md +326 -0
  215. package/core/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  216. package/core/skills/shadcn-ui/examples/data-table.tsx +313 -0
  217. package/core/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  218. package/core/skills/shadcn-ui/resources/component-catalog.md +481 -0
  219. package/core/skills/shadcn-ui/resources/customization-guide.md +516 -0
  220. package/core/skills/shadcn-ui/resources/migration-guide.md +463 -0
  221. package/core/skills/shadcn-ui/resources/setup-guide.md +412 -0
  222. package/core/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  223. package/core/skills/state-machine/SKILL.md +264 -0
  224. package/core/skills/stitch-loop/README.md +54 -0
  225. package/core/skills/stitch-loop/SKILL.md +203 -0
  226. package/core/skills/stitch-loop/examples/SITE.md +73 -0
  227. package/core/skills/stitch-loop/examples/next-prompt.md +25 -0
  228. package/core/skills/stitch-loop/resources/baton-schema.md +61 -0
  229. package/core/skills/stitch-loop/resources/site-template.md +104 -0
  230. package/core/skills/systematic-debugging/SKILL.md +109 -0
  231. package/core/skills/tailwind-patterns/SKILL.md +284 -0
  232. package/core/skills/tdd-validation/SKILL.md +243 -0
  233. package/core/skills/tdd-workflow/SKILL.md +284 -0
  234. package/core/skills/testing-patterns/SKILL.md +196 -0
  235. package/core/skills/testing-patterns/scripts/test_runner.py +219 -0
  236. package/core/skills/ui-ux-discovery/SKILL.md +329 -0
  237. package/core/skills/ui-validation/SKILL.md +190 -0
  238. package/core/skills/ui-validation/scripts/ui_antipattern_check.py +317 -0
  239. package/core/skills/verification-gate/SKILL.md +205 -0
  240. package/core/skills/vulnerability-scanner/SKILL.md +276 -0
  241. package/core/skills/vulnerability-scanner/checklists.md +121 -0
  242. package/core/skills/vulnerability-scanner/scripts/security_scan.py +458 -0
  243. package/core/skills/web-design-guidelines/SKILL.md +57 -0
  244. package/core/skills/webapp-testing/SKILL.md +187 -0
  245. package/core/skills/webapp-testing/scripts/playwright_runner.py +173 -0
  246. package/core/templates/ARCHITECTURE.template.md +407 -0
  247. package/core/templates/project-resources.example.json +71 -0
  248. package/core/workflows/atomic.md +182 -0
  249. package/core/workflows/brainstorm.md +134 -0
  250. package/core/workflows/check-task.md +242 -0
  251. package/core/workflows/copy-collect.md +306 -0
  252. package/core/workflows/create-agent.md +33 -0
  253. package/core/workflows/create-skill.md +39 -0
  254. package/core/workflows/create-workflow.md +33 -0
  255. package/core/workflows/create.md +92 -0
  256. package/core/workflows/debug.md +186 -0
  257. package/core/workflows/demand.md +443 -0
  258. package/core/workflows/deploy.md +260 -0
  259. package/core/workflows/discovery.md +267 -0
  260. package/core/workflows/document.md +272 -0
  261. package/core/workflows/ds-components.md +296 -0
  262. package/core/workflows/ds-init.md +58 -0
  263. package/core/workflows/ds-refactor.md +245 -0
  264. package/core/workflows/ds-references.md +197 -0
  265. package/core/workflows/ds-styleguide.md +237 -0
  266. package/core/workflows/ds-token-diff.md +103 -0
  267. package/core/workflows/ds-tokens.md +317 -0
  268. package/core/workflows/ds-validate.md +309 -0
  269. package/core/workflows/execute.md +483 -0
  270. package/core/workflows/extract-template.md +278 -0
  271. package/core/workflows/fix-failed-tests.md +160 -0
  272. package/core/workflows/init-project.md +386 -0
  273. package/core/workflows/legacy-project.md +849 -0
  274. package/core/workflows/log.md +97 -0
  275. package/core/workflows/new-project.md +610 -0
  276. package/core/workflows/new-project.md.bak +3292 -0
  277. package/core/workflows/new-task.md +404 -0
  278. package/core/workflows/orchestrate.md +237 -0
  279. package/core/workflows/page-build.md +296 -0
  280. package/core/workflows/plan.md +89 -0
  281. package/core/workflows/prd.md +255 -0
  282. package/core/workflows/preview.md +81 -0
  283. package/core/workflows/review-page.md +304 -0
  284. package/core/workflows/status.md +86 -0
  285. package/core/workflows/stitch.md +226 -0
  286. package/core/workflows/task-complete.md +473 -0
  287. package/core/workflows/task-update.md +163 -0
  288. package/core/workflows/tdd.md +344 -0
  289. package/core/workflows/test.md +251 -0
  290. package/core/workflows/ui-ux-pro-max.md +437 -0
  291. package/core/workflows/ux-mobile-optimize.md +262 -0
  292. package/core/workflows/ux-mobile-validate.md +297 -0
  293. package/engine-files/GEMINI.md +69 -0
  294. package/package.json +47 -0
  295. package/runtime-adapters/antigravity.js +26 -0
  296. package/runtime-adapters/claude.js +57 -0
  297. package/runtime-adapters/codex.js +51 -0
  298. package/runtime-adapters/copilot.js +51 -0
  299. package/runtime-adapters/cursor.js +51 -0
  300. package/runtime-adapters/gemini-cli.js +30 -0
  301. package/runtime-adapters/opencode.js +51 -0
  302. package/runtime-adapters/windsurf.js +51 -0
@@ -0,0 +1,722 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Local Tracker — Offline-first task and state management for @flyee.
4
+
5
+ Manages tasks, decisions, and events locally in .flyee/ directory.
6
+ No network required. Works 100% offline.
7
+
8
+ When Flyee SaaS is configured (flyee.json exists), operations are synced.
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import tempfile
14
+ import uuid
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any, Optional
18
+
19
+
20
+ FLYEE_DIR = ".flyee"
21
+ TASKS_FILE = "tasks.json"
22
+ DECISIONS_FILE = "DECISIONS.md"
23
+ EVENTS_FILE = "events.jsonl"
24
+ CONFIG_FILE = "config.json"
25
+
26
+
27
+ class LocalTracker:
28
+ """Offline-first task and state tracker using .flyee/ directory."""
29
+
30
+ def __init__(self, project_root: Optional[str] = None):
31
+ if project_root:
32
+ self.root = Path(project_root)
33
+ else:
34
+ self.root = self._find_project_root()
35
+ self.flyee_dir = self.root / FLYEE_DIR
36
+ self._ensure_dir()
37
+
38
+ def _find_project_root(self) -> Path:
39
+ """Walk up from CWD to find project root (has .git or flyee.json)."""
40
+ current = Path.cwd()
41
+ while current != current.parent:
42
+ if (current / ".git").exists() or (current / "flyee.json").exists():
43
+ return current
44
+ current = current.parent
45
+ return Path.cwd()
46
+
47
+ def _ensure_dir(self):
48
+ """Create .flyee/ if it doesn't exist."""
49
+ self.flyee_dir.mkdir(parents=True, exist_ok=True)
50
+ gitignore = self.flyee_dir / ".gitignore"
51
+ if not gitignore.exists():
52
+ gitignore.write_text(
53
+ "# Ephemeral state (not versioned)\n"
54
+ "STATE.md\n"
55
+ "session-lock.json\n"
56
+ "cost-log.jsonl\n"
57
+ "events.jsonl\n"
58
+ "tasks.json\n"
59
+ "**/continue.md\n"
60
+ )
61
+
62
+ def _atomic_write(self, filepath: Path, content: str):
63
+ """Write file atomically to prevent corruption."""
64
+ dir_path = filepath.parent
65
+ dir_path.mkdir(parents=True, exist_ok=True)
66
+ fd, tmp = tempfile.mkstemp(dir=str(dir_path), prefix="flyee-tmp-")
67
+ try:
68
+ os.write(fd, content.encode("utf-8"))
69
+ os.close(fd)
70
+ os.rename(tmp, str(filepath))
71
+ except Exception:
72
+ os.close(fd)
73
+ if os.path.exists(tmp):
74
+ os.unlink(tmp)
75
+ raise
76
+
77
+ # ─── Tasks ────────────────────────────────────────────────────────────
78
+
79
+ def _load_tasks(self) -> list:
80
+ """Load tasks from local storage."""
81
+ tasks_path = self.flyee_dir / TASKS_FILE
82
+ if not tasks_path.exists():
83
+ return []
84
+ try:
85
+ return json.loads(tasks_path.read_text())
86
+ except (json.JSONDecodeError, OSError):
87
+ return []
88
+
89
+ def _save_tasks(self, tasks: list):
90
+ """Save tasks to local storage."""
91
+ self._atomic_write(
92
+ self.flyee_dir / TASKS_FILE,
93
+ json.dumps(tasks, indent=2, ensure_ascii=False)
94
+ )
95
+
96
+ def create_task(
97
+ self,
98
+ name: str,
99
+ task_type: str = "implement_feature",
100
+ description: str = "",
101
+ priority: str = "normal",
102
+ parent_task_id: Optional[str] = None,
103
+ meta: Optional[dict] = None,
104
+ ) -> dict:
105
+ """Create a task locally. Returns the task dict."""
106
+ tasks = self._load_tasks()
107
+
108
+ task = {
109
+ "id": str(uuid.uuid4()),
110
+ "name": name,
111
+ "type": task_type,
112
+ "description": description,
113
+ "priority": priority,
114
+ "status": "pending",
115
+ "result_status": None,
116
+ "parent_task_id": parent_task_id,
117
+ "meta": meta or {},
118
+ "created_at": datetime.now(timezone.utc).isoformat(),
119
+ "updated_at": datetime.now(timezone.utc).isoformat(),
120
+ "synced": False, # Not yet synced with SaaS
121
+ }
122
+
123
+ tasks.append(task)
124
+ self._save_tasks(tasks)
125
+
126
+ print(f"✅ Task created locally: {name} (ID: {task['id'][:8]}...)")
127
+ return task
128
+
129
+ def update_task(
130
+ self,
131
+ task_id: str,
132
+ status: Optional[str] = None,
133
+ result_status: Optional[str] = None,
134
+ output: Optional[dict] = None,
135
+ meta: Optional[dict] = None,
136
+ ) -> Optional[dict]:
137
+ """Update a task locally."""
138
+ tasks = self._load_tasks()
139
+
140
+ # Find task by full ID or prefix
141
+ task = None
142
+ for t in tasks:
143
+ if t["id"] == task_id or t["id"].startswith(task_id):
144
+ task = t
145
+ break
146
+
147
+ if not task:
148
+ print(f"❌ Task not found: {task_id}")
149
+ return None
150
+
151
+ if status:
152
+ task["status"] = status
153
+ if result_status:
154
+ task["result_status"] = result_status
155
+ if output:
156
+ task["output"] = output
157
+ if meta:
158
+ task["meta"].update(meta)
159
+
160
+ task["updated_at"] = datetime.now(timezone.utc).isoformat()
161
+ task["synced"] = False # Mark as needing sync
162
+
163
+ self._save_tasks(tasks)
164
+ print(f"✅ Task updated: {task['name']} → {status or 'updated'}")
165
+ return task
166
+
167
+ def list_tasks(self, status: Optional[str] = None) -> list:
168
+ """List tasks, optionally filtered by status."""
169
+ tasks = self._load_tasks()
170
+ if status:
171
+ tasks = [t for t in tasks if t["status"] == status]
172
+ return tasks
173
+
174
+ def get_task(self, task_id: str) -> Optional[dict]:
175
+ """Get a single task by ID or prefix."""
176
+ tasks = self._load_tasks()
177
+ for t in tasks:
178
+ if t["id"] == task_id or t["id"].startswith(task_id):
179
+ return t
180
+ return None
181
+
182
+ def search_tasks(self, keywords: str) -> list:
183
+ """Search tasks by keywords in name and description."""
184
+ tasks = self._load_tasks()
185
+ kw = keywords.lower().split()
186
+ results = []
187
+ for t in tasks:
188
+ text = f"{t['name']} {t.get('description', '')}".lower()
189
+ if any(k in text for k in kw):
190
+ results.append(t)
191
+ return results
192
+
193
+ # ─── Decisions ────────────────────────────────────────────────────────
194
+
195
+ def create_decision(
196
+ self,
197
+ decision: str,
198
+ reason: Optional[str] = None,
199
+ impact: Optional[str] = None,
200
+ sprint: Optional[str] = None,
201
+ phase: Optional[str] = None,
202
+ task: Optional[str] = None,
203
+ ) -> dict:
204
+ """Append a decision to DECISIONS.md."""
205
+ decisions_path = self.flyee_dir / DECISIONS_FILE
206
+
207
+ # Count existing decisions to generate ID
208
+ count = 0
209
+ if decisions_path.exists():
210
+ content = decisions_path.read_text()
211
+ count = content.count("## D")
212
+
213
+ entry_id = f"D{count + 1:03d}"
214
+ now = datetime.now().strftime("%Y-%m-%d")
215
+
216
+ entry = f"\n## {entry_id} — {decision} ({now})\n\n"
217
+ if reason:
218
+ entry += f"**Context:** {reason}\n"
219
+ entry += f"**Decision:** {decision}\n"
220
+ if impact:
221
+ entry += f"**Impact:** {impact}\n"
222
+
223
+ location_parts = []
224
+ if sprint:
225
+ location_parts.append(f"**Sprint:** {sprint}")
226
+ if phase:
227
+ location_parts.append(f"**Phase:** {phase}")
228
+ if task:
229
+ location_parts.append(f"**Task:** {task}")
230
+ if location_parts:
231
+ entry += " | ".join(location_parts) + "\n"
232
+
233
+ entry += "\n---\n"
234
+
235
+ # Create or append
236
+ if not decisions_path.exists():
237
+ header = "# Decisions Register\n\n> Append-only log of technical decisions.\n\n---\n"
238
+ decisions_path.write_text(header + entry)
239
+ else:
240
+ with open(decisions_path, "a") as f:
241
+ f.write(entry)
242
+
243
+ result = {
244
+ "id": entry_id,
245
+ "decision": decision,
246
+ "created_at": now,
247
+ }
248
+ print(f"✅ Decision recorded: {entry_id} — {decision}")
249
+ return result
250
+
251
+ # ─── Events ───────────────────────────────────────────────────────────
252
+
253
+ def emit_event(self, event_type: str, payload: Optional[dict] = None) -> bool:
254
+ """Log an event locally to events.jsonl."""
255
+ events_path = self.flyee_dir / EVENTS_FILE
256
+
257
+ event = {
258
+ "event_type": event_type,
259
+ "payload": payload or {},
260
+ "timestamp": datetime.now(timezone.utc).isoformat(),
261
+ "synced": False,
262
+ }
263
+
264
+ with open(events_path, "a") as f:
265
+ f.write(json.dumps(event, ensure_ascii=False) + "\n")
266
+
267
+ return True
268
+
269
+ # ─── Cost Tracking ────────────────────────────────────────────────────
270
+
271
+ def log_cost(
272
+ self,
273
+ sprint: str,
274
+ phase: str,
275
+ task: str,
276
+ operation: str,
277
+ model: str = "unknown",
278
+ tokens_in: int = 0,
279
+ tokens_out: int = 0,
280
+ cost_usd: float = 0.0,
281
+ duration_ms: int = 0,
282
+ ):
283
+ """Append a cost entry to cost-log.jsonl."""
284
+ cost_path = self.flyee_dir / "cost-log.jsonl"
285
+
286
+ entry = {
287
+ "ts": datetime.now(timezone.utc).isoformat(),
288
+ "sprint": sprint,
289
+ "phase": phase,
290
+ "task": task,
291
+ "op": operation,
292
+ "model": model,
293
+ "tokens_in": tokens_in,
294
+ "tokens_out": tokens_out,
295
+ "cost_usd": cost_usd,
296
+ "duration_ms": duration_ms,
297
+ }
298
+
299
+ with open(cost_path, "a") as f:
300
+ f.write(json.dumps(entry) + "\n")
301
+
302
+ # ─── Session Lock ─────────────────────────────────────────────────────
303
+
304
+ def acquire_session_lock(self, runtime: str = "antigravity") -> bool:
305
+ """Acquire session lock. Returns False if another session is active."""
306
+ lock_path = self.flyee_dir / "session-lock.json"
307
+
308
+ if lock_path.exists():
309
+ try:
310
+ lock = json.loads(lock_path.read_text())
311
+ pid = lock.get("pid")
312
+ if pid and self._is_pid_alive(pid):
313
+ print(f"⚠️ Another session is active (PID {pid})")
314
+ return False
315
+ else:
316
+ # Orphan lock = crash detected
317
+ print(f"⚠️ Previous session crashed. Recovering...")
318
+ self._handle_crash(lock)
319
+ except (json.JSONDecodeError, OSError):
320
+ pass
321
+
322
+ lock_data = {
323
+ "pid": os.getpid(),
324
+ "startedAt": datetime.now(timezone.utc).isoformat(),
325
+ "runtime": runtime,
326
+ "lastUpdate": datetime.now(timezone.utc).isoformat(),
327
+ }
328
+
329
+ self._atomic_write(lock_path, json.dumps(lock_data, indent=2))
330
+ return True
331
+
332
+ def release_session_lock(self):
333
+ """Release session lock on clean exit."""
334
+ lock_path = self.flyee_dir / "session-lock.json"
335
+ if lock_path.exists():
336
+ lock_path.unlink()
337
+
338
+ def update_session_lock(self, **kwargs):
339
+ """Update session lock with current activity."""
340
+ lock_path = self.flyee_dir / "session-lock.json"
341
+ if not lock_path.exists():
342
+ return
343
+
344
+ try:
345
+ lock = json.loads(lock_path.read_text())
346
+ lock.update(kwargs)
347
+ lock["lastUpdate"] = datetime.now(timezone.utc).isoformat()
348
+ self._atomic_write(lock_path, json.dumps(lock, indent=2))
349
+ except (json.JSONDecodeError, OSError):
350
+ pass
351
+
352
+ def _is_pid_alive(self, pid: int) -> bool:
353
+ """Check if a PID is still running."""
354
+ try:
355
+ os.kill(pid, 0)
356
+ return True
357
+ except (OSError, ProcessLookupError):
358
+ return False
359
+
360
+ def _handle_crash(self, lock: dict):
361
+ """Handle a detected crash — show info and prepare for resume."""
362
+ sprint = lock.get("activeSprint", "?")
363
+ phase = lock.get("activePhase", "?")
364
+ task = lock.get("activeTask", "?")
365
+ action = lock.get("lastAction", "unknown")
366
+ crashed_at = lock.get("lastUpdate", "?")
367
+
368
+ print(f"\n⚠️ CRASH RECOVERY")
369
+ print(f" Sprint: {sprint} | Phase: {phase} | Task: {task}")
370
+ print(f" Last action: {action}")
371
+ print(f" Crashed at: {crashed_at}")
372
+ print()
373
+
374
+ # ─── Sync Status ──────────────────────────────────────────────────────
375
+
376
+ def get_unsynced_tasks(self) -> list:
377
+ """Get tasks that haven't been synced to SaaS."""
378
+ tasks = self._load_tasks()
379
+ return [t for t in tasks if not t.get("synced", False)]
380
+
381
+ def mark_synced(self, task_id: str, remote_id: Optional[str] = None):
382
+ """Mark a task as synced with SaaS."""
383
+ tasks = self._load_tasks()
384
+ for t in tasks:
385
+ if t["id"] == task_id or t["id"].startswith(task_id):
386
+ t["synced"] = True
387
+ if remote_id:
388
+ t["remote_id"] = remote_id
389
+ t["synced_at"] = datetime.now(timezone.utc).isoformat()
390
+ break
391
+ self._save_tasks(tasks)
392
+
393
+ # ─── Context Search ───────────────────────────────────────────────────
394
+
395
+ def search_context(self, keywords: str) -> dict:
396
+ """Search local state for context relevant to keywords."""
397
+ results = {
398
+ "tasks": self.search_tasks(keywords),
399
+ "decisions": self._search_decisions(keywords),
400
+ }
401
+ return results
402
+
403
+ def _search_decisions(self, keywords: str) -> list:
404
+ """Search DECISIONS.md for keywords."""
405
+ decisions_path = self.flyee_dir / DECISIONS_FILE
406
+ if not decisions_path.exists():
407
+ return []
408
+
409
+ content = decisions_path.read_text()
410
+ kw = keywords.lower().split()
411
+
412
+ results = []
413
+ for section in content.split("## D")[1:]:
414
+ if any(k in section.lower() for k in kw):
415
+ first_line = section.split("\n")[0]
416
+ results.append({"decision": f"D{first_line}"})
417
+
418
+ return results
419
+
420
+
421
+ class FlyeeSync:
422
+ """Optional sync layer for Flyee SaaS platform."""
423
+
424
+ def __init__(self, config: dict):
425
+ self.config = config
426
+ self.api_url = config.get("api_url", "")
427
+ self.api_key = config.get("api_key", "")
428
+ self.project_id = config.get("project_id", "")
429
+
430
+ @property
431
+ def is_available(self) -> bool:
432
+ return bool(self.api_url and self.api_key and self.project_id)
433
+
434
+ def sync_task(self, local_task: dict) -> Optional[str]:
435
+ """Sync a local task to Flyee SaaS. Returns remote ID."""
436
+ if not self.is_available:
437
+ return None
438
+
439
+ # Import bridge functions dynamically
440
+ try:
441
+ bridge_dir = Path(__file__).parent
442
+ import importlib.util
443
+ spec = importlib.util.spec_from_file_location(
444
+ "bridge", bridge_dir / "bridge.py"
445
+ )
446
+ bridge = importlib.util.module_from_spec(spec)
447
+ spec.loader.exec_module(bridge)
448
+
449
+ result = bridge.create_task(
450
+ api_url=self.api_url,
451
+ api_key=self.api_key,
452
+ project_id=self.project_id,
453
+ task_type=local_task.get("type", "implement_feature"),
454
+ name=local_task.get("name", ""),
455
+ description=local_task.get("description", ""),
456
+ priority=local_task.get("priority", "normal"),
457
+ meta=local_task.get("meta", {}),
458
+ )
459
+
460
+ if result and result.get("id"):
461
+ return str(result["id"])
462
+ except Exception as e:
463
+ print(f"⚠️ Flyee sync failed: {e}")
464
+
465
+ return None
466
+
467
+ def sync_event(self, event: dict) -> bool:
468
+ """Sync a local event to Flyee SaaS."""
469
+ if not self.is_available:
470
+ return False
471
+
472
+ try:
473
+ bridge_dir = Path(__file__).parent
474
+ import importlib.util
475
+ spec = importlib.util.spec_from_file_location(
476
+ "bridge", bridge_dir / "bridge.py"
477
+ )
478
+ bridge = importlib.util.module_from_spec(spec)
479
+ spec.loader.exec_module(bridge)
480
+
481
+ return bridge.emit_event(
482
+ event["event_type"],
483
+ event.get("payload"),
484
+ self.config,
485
+ )
486
+ except Exception as e:
487
+ print(f"⚠️ Event sync failed: {e}")
488
+ return False
489
+
490
+
491
+ class Bridge:
492
+ """Unified bridge — offline-first with optional SaaS sync.
493
+
494
+ Usage:
495
+ bridge = Bridge()
496
+ bridge.create_task(name="Build feature", type="implement_feature")
497
+ bridge.create_decision(decision="Use RSC", reason="Performance")
498
+ """
499
+
500
+ def __init__(self, project_root: Optional[str] = None):
501
+ self.local = LocalTracker(project_root)
502
+ self.remote = self._init_remote()
503
+
504
+ def _init_remote(self) -> Optional[FlyeeSync]:
505
+ """Initialize remote sync if flyee.json exists and is configured."""
506
+ config_path = self.local.root / "flyee.json"
507
+ if not config_path.exists():
508
+ return None
509
+
510
+ try:
511
+ config = json.loads(config_path.read_text())
512
+ if config.get("enabled") and config.get("api_key"):
513
+ sync = FlyeeSync(config)
514
+ if sync.is_available:
515
+ return sync
516
+ except (json.JSONDecodeError, OSError):
517
+ pass
518
+
519
+ return None
520
+
521
+ @property
522
+ def is_online(self) -> bool:
523
+ """Whether SaaS sync is available."""
524
+ return self.remote is not None and self.remote.is_available
525
+
526
+ def create_task(self, **kwargs) -> dict:
527
+ """Create task locally, sync to SaaS if available."""
528
+ task = self.local.create_task(**kwargs)
529
+
530
+ if self.remote:
531
+ remote_id = self.remote.sync_task(task)
532
+ if remote_id:
533
+ self.local.mark_synced(task["id"], remote_id)
534
+ print(f" ↳ Synced to Flyee SaaS (remote: {remote_id[:8]}...)")
535
+
536
+ return task
537
+
538
+ def update_task(self, task_id: str, **kwargs) -> Optional[dict]:
539
+ """Update task locally, sync to SaaS if available."""
540
+ task = self.local.update_task(task_id, **kwargs)
541
+ # Remote sync for updates would go here
542
+ return task
543
+
544
+ def list_tasks(self, **kwargs) -> list:
545
+ """List tasks from local storage."""
546
+ return self.local.list_tasks(**kwargs)
547
+
548
+ def create_decision(self, **kwargs) -> dict:
549
+ """Record decision locally, sync to SaaS if available."""
550
+ result = self.local.create_decision(**kwargs)
551
+
552
+ if self.remote:
553
+ try:
554
+ self.remote.sync_event({
555
+ "event_type": "dev.decision_detected",
556
+ "payload": {
557
+ "decision": kwargs.get("decision", ""),
558
+ "reason": kwargs.get("reason"),
559
+ }
560
+ })
561
+ except Exception:
562
+ pass
563
+
564
+ return result
565
+
566
+ def emit_event(self, event_type: str, payload: Optional[dict] = None) -> bool:
567
+ """Emit event locally, sync to SaaS if available."""
568
+ self.local.emit_event(event_type, payload)
569
+
570
+ if self.remote:
571
+ self.remote.sync_event({
572
+ "event_type": event_type,
573
+ "payload": payload or {},
574
+ })
575
+
576
+ return True
577
+
578
+ def search_context(self, keywords: str) -> dict:
579
+ """Search local context + SaaS if available."""
580
+ local_results = self.local.search_context(keywords)
581
+
582
+ # If SaaS available, could enhance with remote search
583
+ # For now, local-only
584
+ return local_results
585
+
586
+ def status(self) -> dict:
587
+ """Get bridge status."""
588
+ tasks = self.local.list_tasks()
589
+ unsynced = self.local.get_unsynced_tasks()
590
+
591
+ return {
592
+ "mode": "online" if self.is_online else "offline",
593
+ "project_root": str(self.local.root),
594
+ "flyee_dir": str(self.local.flyee_dir),
595
+ "total_tasks": len(tasks),
596
+ "unsynced_tasks": len(unsynced),
597
+ "saas_configured": self.is_online,
598
+ }
599
+
600
+
601
+ # ─── CLI Interface ───────────────────────────────────────────────────────
602
+
603
+ def main():
604
+ """CLI interface for the local tracker."""
605
+ args = sys.argv[1:]
606
+ bridge = Bridge()
607
+
608
+ if not args or "--help" in args:
609
+ print("""
610
+ @flyee Local Tracker — Offline-first task management
611
+
612
+ Usage:
613
+ python local_tracker.py --create-task --name "Task name" --type implement_feature
614
+ python local_tracker.py --update-task <id> --status completed
615
+ python local_tracker.py --list-tasks [--status pending]
616
+ python local_tracker.py --search-context "keywords"
617
+ python local_tracker.py --create-decision --decision "Use X" --reason "Because Y"
618
+ python local_tracker.py --status
619
+ python local_tracker.py --sync (sync unsynced tasks to SaaS)
620
+ """)
621
+ return
622
+
623
+ if "--status" in args:
624
+ s = bridge.status()
625
+ print(f"\n📊 @flyee Bridge Status")
626
+ print(f" Mode: {'🌐 Online' if s['saas_configured'] else '📴 Offline'}")
627
+ print(f" Root: {s['project_root']}")
628
+ print(f" Tasks: {s['total_tasks']} total, {s['unsynced_tasks']} unsynced")
629
+ return
630
+
631
+ if "--create-task" in args:
632
+ name = _get_arg(args, "--name", "Unnamed task")
633
+ task_type = _get_arg(args, "--type", "implement_feature")
634
+ description = _get_arg(args, "--description", "")
635
+ priority = _get_arg(args, "--priority", "normal")
636
+
637
+ task = bridge.create_task(
638
+ name=name,
639
+ task_type=task_type,
640
+ description=description,
641
+ priority=priority,
642
+ )
643
+ print(json.dumps(task, indent=2))
644
+ return
645
+
646
+ if "--update-task" in args:
647
+ idx = args.index("--update-task")
648
+ task_id = args[idx + 1] if idx + 1 < len(args) else ""
649
+ status = _get_arg(args, "--status")
650
+ result = _get_arg(args, "--result")
651
+
652
+ bridge.update_task(task_id, status=status, result_status=result)
653
+ return
654
+
655
+ if "--list-tasks" in args:
656
+ status = _get_arg(args, "--status")
657
+ tasks = bridge.list_tasks(status=status)
658
+
659
+ if not tasks:
660
+ print("No tasks found.")
661
+ return
662
+
663
+ print(f"\n{'ID':<10} {'Name':<35} {'Status':<12} {'Priority':<10}")
664
+ print("-" * 70)
665
+ for t in tasks:
666
+ tid = t["id"][:8]
667
+ print(f"{tid:<10} {t['name'][:33]:<35} {t['status']:<12} {t['priority']:<10}")
668
+ return
669
+
670
+ if "--search-context" in args:
671
+ idx = args.index("--search-context")
672
+ keywords = args[idx + 1] if idx + 1 < len(args) else ""
673
+ results = bridge.search_context(keywords)
674
+ print(json.dumps(results, indent=2, ensure_ascii=False))
675
+ return
676
+
677
+ if "--create-decision" in args:
678
+ decision = _get_arg(args, "--decision", "")
679
+ reason = _get_arg(args, "--reason")
680
+ impact = _get_arg(args, "--impact")
681
+
682
+ bridge.create_decision(decision=decision, reason=reason, impact=impact)
683
+ return
684
+
685
+ if "--sync" in args:
686
+ if not bridge.is_online:
687
+ print("❌ Flyee SaaS not configured. Run bridge.py --setup first.")
688
+ return
689
+
690
+ unsynced = bridge.local.get_unsynced_tasks()
691
+ if not unsynced:
692
+ print("✅ All tasks are synced.")
693
+ return
694
+
695
+ print(f"Syncing {len(unsynced)} tasks...")
696
+ for task in unsynced:
697
+ remote_id = bridge.remote.sync_task(task)
698
+ if remote_id:
699
+ bridge.local.mark_synced(task["id"], remote_id)
700
+ print(f" ✅ {task['name']} → {remote_id[:8]}")
701
+ else:
702
+ print(f" ❌ {task['name']} — sync failed")
703
+ return
704
+
705
+ print(f"Unknown command. Use --help for usage.")
706
+
707
+
708
+ def _get_arg(args: list, flag: str, default: str = None) -> Optional[str]:
709
+ """Extract a flag value from args list."""
710
+ try:
711
+ idx = args.index(flag)
712
+ if idx + 1 < len(args) and not args[idx + 1].startswith("--"):
713
+ return args[idx + 1]
714
+ except ValueError:
715
+ pass
716
+ return default
717
+
718
+
719
+ import sys
720
+
721
+ if __name__ == "__main__":
722
+ main()