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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/bin/install.js +357 -0
- package/bridge/bridge.py +1780 -0
- package/bridge/local_tracker.py +722 -0
- package/core/agents/backend-specialist.md +266 -0
- package/core/agents/code-archaeologist.md +106 -0
- package/core/agents/database-architect.md +226 -0
- package/core/agents/debugger.md +225 -0
- package/core/agents/devops-engineer.md +323 -0
- package/core/agents/documentation-writer.md +104 -0
- package/core/agents/explorer-agent.md +73 -0
- package/core/agents/frontend-specialist.md +743 -0
- package/core/agents/game-developer.md +162 -0
- package/core/agents/mobile-developer.md +377 -0
- package/core/agents/orchestrator.md +416 -0
- package/core/agents/penetration-tester.md +188 -0
- package/core/agents/performance-optimizer.md +187 -0
- package/core/agents/product-manager.md +112 -0
- package/core/agents/product-owner.md +95 -0
- package/core/agents/project-planner.md +470 -0
- package/core/agents/qa-automation-engineer.md +103 -0
- package/core/agents/security-auditor.md +170 -0
- package/core/agents/seo-specialist.md +111 -0
- package/core/agents/stitch-designer.md +190 -0
- package/core/agents/tdd-reviewer.md +282 -0
- package/core/agents/test-engineer.md +158 -0
- package/core/scripts/auto_preview.py +148 -0
- package/core/scripts/checklist.py +243 -0
- package/core/scripts/cost_report.py +149 -0
- package/core/scripts/doc-sync-check.py +461 -0
- package/core/scripts/parse_user_stories.py +79 -0
- package/core/scripts/prepare_notion_updates.py +172 -0
- package/core/scripts/print_create_payload.py +18 -0
- package/core/scripts/session_manager.py +120 -0
- package/core/scripts/task_complete.py +127 -0
- package/core/scripts/verify_all.py +327 -0
- package/core/skills/analytics-strategy/SKILL.md +128 -0
- package/core/skills/api-patterns/SKILL.md +81 -0
- package/core/skills/api-patterns/api-style.md +42 -0
- package/core/skills/api-patterns/auth.md +24 -0
- package/core/skills/api-patterns/documentation.md +26 -0
- package/core/skills/api-patterns/graphql.md +41 -0
- package/core/skills/api-patterns/rate-limiting.md +31 -0
- package/core/skills/api-patterns/response.md +37 -0
- package/core/skills/api-patterns/rest.md +40 -0
- package/core/skills/api-patterns/scripts/api_validator.py +211 -0
- package/core/skills/api-patterns/security-testing.md +122 -0
- package/core/skills/api-patterns/trpc.md +41 -0
- package/core/skills/api-patterns/versioning.md +22 -0
- package/core/skills/app-builder/SKILL.md +75 -0
- package/core/skills/app-builder/agent-coordination.md +71 -0
- package/core/skills/app-builder/feature-building.md +53 -0
- package/core/skills/app-builder/project-detection.md +34 -0
- package/core/skills/app-builder/scaffolding.md +118 -0
- package/core/skills/app-builder/tech-stack.md +40 -0
- package/core/skills/app-builder/templates/SKILL.md +39 -0
- package/core/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
- package/core/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
- package/core/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
- package/core/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
- package/core/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
- package/core/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
- package/core/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
- package/core/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +82 -0
- package/core/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +100 -0
- package/core/skills/app-builder/templates/nextjs-static/TEMPLATE.md +106 -0
- package/core/skills/app-builder/templates/nuxt-app/TEMPLATE.md +101 -0
- package/core/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
- package/core/skills/app-builder/templates/react-native-app/TEMPLATE.md +93 -0
- package/core/skills/architecture/SKILL.md +55 -0
- package/core/skills/architecture/context-discovery.md +43 -0
- package/core/skills/architecture/examples.md +94 -0
- package/core/skills/architecture/pattern-selection.md +68 -0
- package/core/skills/architecture/patterns-reference.md +50 -0
- package/core/skills/architecture/trade-off-analysis.md +77 -0
- package/core/skills/atomic-design/SKILL.md +282 -0
- package/core/skills/atomic-design/references/classification-guide.md +132 -0
- package/core/skills/atomic-design/references/quality-checklist.md +60 -0
- package/core/skills/atomic-design/references/stacks/stack-blade.md +254 -0
- package/core/skills/atomic-design/references/stacks/stack-nextjs.md +272 -0
- package/core/skills/atomic-design/references/stacks/stack-react.md +239 -0
- package/core/skills/atomic-design/references/stacks/stack-vue.md +224 -0
- package/core/skills/bash-linux/SKILL.md +199 -0
- package/core/skills/behavioral-modes/SKILL.md +242 -0
- package/core/skills/brainstorming/SKILL.md +163 -0
- package/core/skills/brainstorming/dynamic-questioning.md +373 -0
- package/core/skills/checkpointing-patterns/SKILL.md +163 -0
- package/core/skills/clean-code/SKILL.md +201 -0
- package/core/skills/code-review-checklist/SKILL.md +109 -0
- package/core/skills/code-truth-validation/SKILL.md +149 -0
- package/core/skills/component-library-discovery/SKILL.md +154 -0
- package/core/skills/content-strategy/SKILL.md +222 -0
- package/core/skills/context-budget/SKILL.md +155 -0
- package/core/skills/context-gathering-patterns/SKILL.md +278 -0
- package/core/skills/cost-tracking/SKILL.md +206 -0
- package/core/skills/database-design/SKILL.md +52 -0
- package/core/skills/database-design/database-selection.md +43 -0
- package/core/skills/database-design/indexing.md +39 -0
- package/core/skills/database-design/migrations.md +48 -0
- package/core/skills/database-design/optimization.md +36 -0
- package/core/skills/database-design/orm-selection.md +30 -0
- package/core/skills/database-design/schema-design.md +56 -0
- package/core/skills/database-design/scripts/schema_validator.py +172 -0
- package/core/skills/deployment-procedures/SKILL.md +295 -0
- package/core/skills/design-md/README.md +34 -0
- package/core/skills/design-md/SKILL.md +172 -0
- package/core/skills/design-md/examples/DESIGN.md +154 -0
- package/core/skills/design-system-enforcement/SKILL.md +339 -0
- package/core/skills/doc.md +177 -0
- package/core/skills/document-registry/SKILL.md +130 -0
- package/core/skills/documentation-publishing/SKILL.md +174 -0
- package/core/skills/documentation-templates/SKILL.md +194 -0
- package/core/skills/enhance-prompt/README.md +34 -0
- package/core/skills/enhance-prompt/SKILL.md +204 -0
- package/core/skills/enhance-prompt/references/KEYWORDS.md +114 -0
- package/core/skills/frontend-design/SKILL.md +430 -0
- package/core/skills/frontend-design/animation-guide.md +331 -0
- package/core/skills/frontend-design/color-system.md +311 -0
- package/core/skills/frontend-design/decision-trees.md +418 -0
- package/core/skills/frontend-design/motion-graphics.md +306 -0
- package/core/skills/frontend-design/scripts/accessibility_checker.py +183 -0
- package/core/skills/frontend-design/scripts/ux_audit.py +722 -0
- package/core/skills/frontend-design/typography-system.md +345 -0
- package/core/skills/frontend-design/ux-psychology.md +541 -0
- package/core/skills/frontend-design/visual-effects.md +383 -0
- package/core/skills/game-development/2d-games/SKILL.md +119 -0
- package/core/skills/game-development/3d-games/SKILL.md +135 -0
- package/core/skills/game-development/SKILL.md +167 -0
- package/core/skills/game-development/game-art/SKILL.md +185 -0
- package/core/skills/game-development/game-audio/SKILL.md +190 -0
- package/core/skills/game-development/game-design/SKILL.md +129 -0
- package/core/skills/game-development/mobile-games/SKILL.md +108 -0
- package/core/skills/game-development/multiplayer/SKILL.md +132 -0
- package/core/skills/game-development/pc-games/SKILL.md +144 -0
- package/core/skills/game-development/vr-ar/SKILL.md +123 -0
- package/core/skills/game-development/web-games/SKILL.md +150 -0
- package/core/skills/geo-fundamentals/SKILL.md +156 -0
- package/core/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
- package/core/skills/git-workflow/SKILL.md +263 -0
- package/core/skills/history-check-patterns/SKILL.md +125 -0
- package/core/skills/i18n-localization/SKILL.md +154 -0
- package/core/skills/i18n-localization/scripts/i18n_checker.py +241 -0
- package/core/skills/integration-completeness/SKILL.md +219 -0
- package/core/skills/intelligent-routing/SKILL.md +370 -0
- package/core/skills/lint-and-validate/SKILL.md +45 -0
- package/core/skills/lint-and-validate/scripts/lint_runner.py +173 -0
- package/core/skills/lint-and-validate/scripts/type_coverage.py +173 -0
- package/core/skills/local-verification/SKILL.md +195 -0
- package/core/skills/mcp-builder/SKILL.md +176 -0
- package/core/skills/mobile-design/SKILL.md +394 -0
- package/core/skills/mobile-design/decision-trees.md +516 -0
- package/core/skills/mobile-design/mobile-backend.md +491 -0
- package/core/skills/mobile-design/mobile-color-system.md +420 -0
- package/core/skills/mobile-design/mobile-debugging.md +122 -0
- package/core/skills/mobile-design/mobile-design-thinking.md +357 -0
- package/core/skills/mobile-design/mobile-navigation.md +458 -0
- package/core/skills/mobile-design/mobile-performance.md +767 -0
- package/core/skills/mobile-design/mobile-testing.md +356 -0
- package/core/skills/mobile-design/mobile-typography.md +433 -0
- package/core/skills/mobile-design/platform-android.md +666 -0
- package/core/skills/mobile-design/platform-ios.md +561 -0
- package/core/skills/mobile-design/scripts/mobile_audit.py +670 -0
- package/core/skills/mobile-design/touch-psychology.md +537 -0
- package/core/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +312 -0
- package/core/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
- package/core/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
- package/core/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
- package/core/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
- package/core/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
- package/core/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
- package/core/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
- package/core/skills/nextjs-react-expert/SKILL.md +267 -0
- package/core/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
- package/core/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -0
- package/core/skills/nodejs-best-practices/SKILL.md +333 -0
- package/core/skills/notion-task-patterns/SKILL.md +2529 -0
- package/core/skills/page-specifications/SKILL.md +367 -0
- package/core/skills/parallel-agents/SKILL.md +175 -0
- package/core/skills/performance-profiling/SKILL.md +143 -0
- package/core/skills/performance-profiling/scripts/lighthouse_audit.py +76 -0
- package/core/skills/plan-writing/SKILL.md +190 -0
- package/core/skills/powershell-windows/SKILL.md +167 -0
- package/core/skills/project-foundation/SKILL.md +117 -0
- package/core/skills/project-setup/SKILL.md +141 -0
- package/core/skills/project-tracking-patterns/SKILL.md +357 -0
- package/core/skills/project-type-discovery/SKILL.md +239 -0
- package/core/skills/python-patterns/SKILL.md +441 -0
- package/core/skills/qa-test-generation/SKILL.md +156 -0
- package/core/skills/react-components/README.md +36 -0
- package/core/skills/react-components/SKILL.md +47 -0
- package/core/skills/react-components/examples/gold-standard-card.tsx +80 -0
- package/core/skills/react-components/package-lock.json +231 -0
- package/core/skills/react-components/package.json +16 -0
- package/core/skills/react-components/resources/architecture-checklist.md +15 -0
- package/core/skills/react-components/resources/component-template.tsx +37 -0
- package/core/skills/react-components/resources/stitch-api-reference.md +14 -0
- package/core/skills/react-components/resources/style-guide.json +27 -0
- package/core/skills/react-components/scripts/fetch-stitch.sh +30 -0
- package/core/skills/react-components/scripts/validate.js +68 -0
- package/core/skills/red-team-tactics/SKILL.md +199 -0
- package/core/skills/remotion/README.md +105 -0
- package/core/skills/remotion/SKILL.md +393 -0
- package/core/skills/remotion/examples/WalkthroughComposition.tsx +78 -0
- package/core/skills/remotion/examples/screens.json +56 -0
- package/core/skills/remotion/resources/composition-checklist.md +124 -0
- package/core/skills/remotion/resources/screen-slide-template.tsx +123 -0
- package/core/skills/remotion/scripts/download-stitch-asset.sh +38 -0
- package/core/skills/seo-fundamentals/SKILL.md +129 -0
- package/core/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
- package/core/skills/server-management/SKILL.md +161 -0
- package/core/skills/session-resilience/SKILL.md +199 -0
- package/core/skills/shadcn-ui/README.md +248 -0
- package/core/skills/shadcn-ui/SKILL.md +326 -0
- package/core/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/core/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/core/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/core/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/core/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/core/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/core/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/core/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/core/skills/state-machine/SKILL.md +264 -0
- package/core/skills/stitch-loop/README.md +54 -0
- package/core/skills/stitch-loop/SKILL.md +203 -0
- package/core/skills/stitch-loop/examples/SITE.md +73 -0
- package/core/skills/stitch-loop/examples/next-prompt.md +25 -0
- package/core/skills/stitch-loop/resources/baton-schema.md +61 -0
- package/core/skills/stitch-loop/resources/site-template.md +104 -0
- package/core/skills/systematic-debugging/SKILL.md +109 -0
- package/core/skills/tailwind-patterns/SKILL.md +284 -0
- package/core/skills/tdd-validation/SKILL.md +243 -0
- package/core/skills/tdd-workflow/SKILL.md +284 -0
- package/core/skills/testing-patterns/SKILL.md +196 -0
- package/core/skills/testing-patterns/scripts/test_runner.py +219 -0
- package/core/skills/ui-ux-discovery/SKILL.md +329 -0
- package/core/skills/ui-validation/SKILL.md +190 -0
- package/core/skills/ui-validation/scripts/ui_antipattern_check.py +317 -0
- package/core/skills/verification-gate/SKILL.md +205 -0
- package/core/skills/vulnerability-scanner/SKILL.md +276 -0
- package/core/skills/vulnerability-scanner/checklists.md +121 -0
- package/core/skills/vulnerability-scanner/scripts/security_scan.py +458 -0
- package/core/skills/web-design-guidelines/SKILL.md +57 -0
- package/core/skills/webapp-testing/SKILL.md +187 -0
- package/core/skills/webapp-testing/scripts/playwright_runner.py +173 -0
- package/core/templates/ARCHITECTURE.template.md +407 -0
- package/core/templates/project-resources.example.json +71 -0
- package/core/workflows/atomic.md +182 -0
- package/core/workflows/brainstorm.md +134 -0
- package/core/workflows/check-task.md +242 -0
- package/core/workflows/copy-collect.md +306 -0
- package/core/workflows/create-agent.md +33 -0
- package/core/workflows/create-skill.md +39 -0
- package/core/workflows/create-workflow.md +33 -0
- package/core/workflows/create.md +92 -0
- package/core/workflows/debug.md +186 -0
- package/core/workflows/demand.md +443 -0
- package/core/workflows/deploy.md +260 -0
- package/core/workflows/discovery.md +267 -0
- package/core/workflows/document.md +272 -0
- package/core/workflows/ds-components.md +296 -0
- package/core/workflows/ds-init.md +58 -0
- package/core/workflows/ds-refactor.md +245 -0
- package/core/workflows/ds-references.md +197 -0
- package/core/workflows/ds-styleguide.md +237 -0
- package/core/workflows/ds-token-diff.md +103 -0
- package/core/workflows/ds-tokens.md +317 -0
- package/core/workflows/ds-validate.md +309 -0
- package/core/workflows/execute.md +483 -0
- package/core/workflows/extract-template.md +278 -0
- package/core/workflows/fix-failed-tests.md +160 -0
- package/core/workflows/init-project.md +386 -0
- package/core/workflows/legacy-project.md +849 -0
- package/core/workflows/log.md +97 -0
- package/core/workflows/new-project.md +610 -0
- package/core/workflows/new-project.md.bak +3292 -0
- package/core/workflows/new-task.md +404 -0
- package/core/workflows/orchestrate.md +237 -0
- package/core/workflows/page-build.md +296 -0
- package/core/workflows/plan.md +89 -0
- package/core/workflows/prd.md +255 -0
- package/core/workflows/preview.md +81 -0
- package/core/workflows/review-page.md +304 -0
- package/core/workflows/status.md +86 -0
- package/core/workflows/stitch.md +226 -0
- package/core/workflows/task-complete.md +473 -0
- package/core/workflows/task-update.md +163 -0
- package/core/workflows/tdd.md +344 -0
- package/core/workflows/test.md +251 -0
- package/core/workflows/ui-ux-pro-max.md +437 -0
- package/core/workflows/ux-mobile-optimize.md +262 -0
- package/core/workflows/ux-mobile-validate.md +297 -0
- package/engine-files/GEMINI.md +69 -0
- package/package.json +47 -0
- package/runtime-adapters/antigravity.js +26 -0
- package/runtime-adapters/claude.js +57 -0
- package/runtime-adapters/codex.js +51 -0
- package/runtime-adapters/copilot.js +51 -0
- package/runtime-adapters/cursor.js +51 -0
- package/runtime-adapters/gemini-cli.js +30 -0
- package/runtime-adapters/opencode.js +51 -0
- 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()
|