fifony 0.1.25-next.227d159 → 0.1.26-next.f3fade3
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/README.md +64 -73
- package/app/dist/service-worker.js +1 -1
- package/package.json +21 -11
- package/app/public/dinofffaur.png +0 -0
- package/app/public/icon-maskable.svg +0 -8
- package/app/public/icon.svg +0 -7
- package/app/public/manifest.webmanifest +0 -49
- package/app/public/offline.html +0 -86
- package/app/public/service-worker.js +0 -145
- package/dist/agent/cli-wrapper.js.map +0 -1
- package/dist/agent/run-local.js.map +0 -1
- package/dist/chunk-2F3Q2MAG.js.map +0 -1
- package/dist/chunk-AMOGDOM7.js.map +0 -1
- package/dist/chunk-DVU3CXWA.js.map +0 -1
- package/dist/chunk-IA7IMQ5F.js.map +0 -1
- package/dist/chunk-NFHVAIPW.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/issue-runner-DA4IDLKX.js.map +0 -1
- package/dist/issue-state-machine-OWABY5S2.js.map +0 -1
- package/dist/mcp/server.js.map +0 -1
- package/dist/queue-workers-JIH5ZMNQ.js.map +0 -1
- package/dist/store-WN47MDT5.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**AI agents that actually ship code. You just watch.**
|
|
6
6
|
|
|
7
|
-
Point at a repo. Open the dashboard. AI plans, builds, and reviews — you approve.
|
|
7
|
+
Point at a repo. Open the dashboard. AI plans, builds, and reviews — you approve and merge.
|
|
8
8
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[]()
|
|
@@ -16,42 +16,60 @@ Point at a repo. Open the dashboard. AI plans, builds, and reviews — you appro
|
|
|
16
16
|
## Quick Start
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
npx -y fifony
|
|
19
|
+
npx -y fifony
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Open **http://localhost:
|
|
22
|
+
Open **http://localhost:4000**. The first run launches the onboarding wizard — it detects your CLIs, scans your project, and configures everything in six steps. State lives in `.fifony/`. No accounts, no cloud, no external database.
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
## How It Works
|
|
27
27
|
|
|
28
|
-
fifony
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
Plan Execute Review
|
|
32
|
-
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
33
|
-
│ Claude │─▶│ Codex │─▶│ Claude │
|
|
34
|
-
│ Opus 4.5 │ │ │ │ Sonnet 4.5 │
|
|
35
|
-
│ effort: high │ │ effort: med │ │ effort: med │
|
|
36
|
-
└──────────────┘ └──────────────┘ └──────────────┘
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
You set the provider, model, and reasoning effort for each stage. Claude plans, Codex executes, Claude reviews — or any combination you prefer. Configure it in the Settings UI or drop a `WORKFLOW.md` in your project root.
|
|
28
|
+
fifony auto-detects your installed CLI tools (Claude, Codex, Gemini) and routes each pipeline stage to the best available provider. Configure per-stage provider, model, and reasoning effort in the Settings UI or drop a `WORKFLOW.md` in your project root.
|
|
40
29
|
|
|
41
30
|
### Issue Lifecycle
|
|
42
31
|
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
```mermaid
|
|
33
|
+
stateDiagram-v2
|
|
34
|
+
[*] --> Planning
|
|
35
|
+
Planning --> Planned: PLANNED
|
|
36
|
+
Planned --> Queued: QUEUE
|
|
37
|
+
Queued --> Running: RUN
|
|
38
|
+
Running --> Reviewing: REVIEW
|
|
39
|
+
Reviewing --> Reviewed: REVIEWED
|
|
40
|
+
Reviewed --> Done: DONE
|
|
41
|
+
Done --> Merged: MERGE
|
|
42
|
+
|
|
43
|
+
Running --> Blocked: BLOCK
|
|
44
|
+
Reviewing --> Blocked: BLOCK
|
|
45
|
+
Blocked --> Queued: UNBLOCK
|
|
46
|
+
Blocked --> Planning: REPLAN
|
|
47
|
+
|
|
48
|
+
Reviewed --> Queued: REQUEUE (rework)
|
|
49
|
+
Reviewed --> Planning: REPLAN
|
|
50
|
+
|
|
51
|
+
Done --> Planning: REOPEN
|
|
52
|
+
Merged --> Planning: REOPEN
|
|
53
|
+
|
|
54
|
+
Planning --> Cancelled: CANCEL
|
|
55
|
+
Planned --> Cancelled: CANCEL
|
|
56
|
+
Reviewed --> Cancelled: CANCEL
|
|
57
|
+
Blocked --> Cancelled: CANCEL
|
|
58
|
+
Cancelled --> Planning: REOPEN
|
|
59
|
+
|
|
60
|
+
Merged --> [*]
|
|
61
|
+
Cancelled --> [*]
|
|
47
62
|
```
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
| Step | What happens |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| **Create** | Describe what you want done. |
|
|
67
|
+
| **Plan** | The planner agent generates a structured execution plan: phases, steps, target files, complexity, risks. |
|
|
68
|
+
| **Approve** | You review the plan. Optionally refine it with AI chat before approving. |
|
|
69
|
+
| **Execute** | Agents run in an isolated git worktree. Live output streams to the dashboard. |
|
|
70
|
+
| **Review** | The reviewer agent inspects the diff and either approves, requests rework, or blocks. |
|
|
71
|
+
| **Done** | Approved and waiting for merge. You review the diff in the dashboard. |
|
|
72
|
+
| **Merge** | You merge the worktree into your project. Analytics capture lines added/removed. |
|
|
55
73
|
|
|
56
74
|
Agents run as detached child processes, tracked by PID. If the server restarts mid-run, fifony recovers on the next boot.
|
|
57
75
|
|
|
@@ -63,7 +81,7 @@ The first run walks you through six steps:
|
|
|
63
81
|
|
|
64
82
|
| Step | What happens |
|
|
65
83
|
|------|-------------|
|
|
66
|
-
| CLI Detection | Finds `claude`, `codex`, `git`, `node`, `docker`, and other tools on your system |
|
|
84
|
+
| CLI Detection | Finds `claude`, `codex`, `gemini`, `git`, `node`, `docker`, and other tools on your system |
|
|
67
85
|
| Project Scan | Detects language, stack, and build system — 18+ ecosystems supported |
|
|
68
86
|
| AI Analysis | Uses the detected CLI to extract domain context from your codebase |
|
|
69
87
|
| Domains | 21 options across Technical / Industry / Role, pre-selected by the AI |
|
|
@@ -72,16 +90,14 @@ The first run walks you through six steps:
|
|
|
72
90
|
|
|
73
91
|
Settings are saved progressively and can be re-run from Settings at any time.
|
|
74
92
|
|
|
75
|
-
Supported build files include: `package.json`, `Cargo.toml`, `pyproject.toml`, `go.mod`, `build.gradle`, `Gemfile`, `mix.exs`, `pubspec.yaml`, `CMakeLists.txt`, `composer.json`, `Package.swift`, `deno.json`, `pom.xml`, `Dockerfile`, and more.
|
|
76
|
-
|
|
77
93
|
---
|
|
78
94
|
|
|
79
95
|
## Dashboard
|
|
80
96
|
|
|
81
97
|
| Route | What you see |
|
|
82
98
|
|-------|-------------|
|
|
83
|
-
| `/kanban` | Drag-and-drop board
|
|
84
|
-
| `/issues` | Searchable list with multi-state filters, sort options, and capability filters.
|
|
99
|
+
| `/kanban` | Drag-and-drop board with 5 columns: Planning, In Progress, Reviewing, Blocked, Done. |
|
|
100
|
+
| `/issues` | Searchable list with multi-state filters, sort options, and capability filters. |
|
|
85
101
|
| `/agents` | Live cockpit: worker slots, queue depth, real-time log tail, token sparklines per agent. |
|
|
86
102
|
| `/analytics` | Token usage trends, daily and weekly rollups, top issues by cost, per-model breakdown. |
|
|
87
103
|
| `/settings` | General, Workflow pipeline config, Notifications, Providers. |
|
|
@@ -125,20 +141,20 @@ Agents install to `.claude/agents/` and `.codex/agents/` during onboarding. Skil
|
|
|
125
141
|
## CLI Reference
|
|
126
142
|
|
|
127
143
|
```bash
|
|
128
|
-
# Dashboard + API
|
|
129
|
-
npx -y fifony
|
|
144
|
+
# Dashboard + API (default port 4000)
|
|
145
|
+
npx -y fifony
|
|
130
146
|
|
|
131
|
-
#
|
|
132
|
-
npx -y fifony --port
|
|
147
|
+
# Custom port
|
|
148
|
+
npx -y fifony --port 8080
|
|
133
149
|
|
|
134
|
-
#
|
|
135
|
-
npx -y fifony
|
|
150
|
+
# With Vite HMR for frontend development
|
|
151
|
+
npx -y fifony --dev
|
|
136
152
|
|
|
137
153
|
# MCP server (stdio)
|
|
138
154
|
npx -y fifony mcp
|
|
139
155
|
|
|
140
156
|
# Different workspace
|
|
141
|
-
npx -y fifony --workspace /path/to/repo
|
|
157
|
+
npx -y fifony --workspace /path/to/repo
|
|
142
158
|
|
|
143
159
|
# Run one scheduler cycle and exit
|
|
144
160
|
npx -y fifony --once
|
|
@@ -187,7 +203,7 @@ Add to `claude_desktop_config.json` or VS Code settings:
|
|
|
187
203
|
|
|
188
204
|
## REST API
|
|
189
205
|
|
|
190
|
-
Interactive docs at `http://localhost:
|
|
206
|
+
Interactive docs at `http://localhost:4000/docs`.
|
|
191
207
|
|
|
192
208
|
| Endpoint | Description |
|
|
193
209
|
|----------|-------------|
|
|
@@ -223,6 +239,7 @@ FIFONY_AGENT_PROVIDER=codex # codex | claude
|
|
|
223
239
|
FIFONY_WORKER_CONCURRENCY=2
|
|
224
240
|
FIFONY_MAX_ATTEMPTS=3
|
|
225
241
|
FIFONY_AGENT_MAX_TURNS=4
|
|
242
|
+
FIFONY_LOG_FILE=0 # set to 1 to also write .fifony/fifony-local.log
|
|
226
243
|
```
|
|
227
244
|
|
|
228
245
|
---
|
|
@@ -233,49 +250,23 @@ FIFONY_AGENT_MAX_TURNS=4
|
|
|
233
250
|
.fifony/
|
|
234
251
|
s3db/ ← durable database (issues, events, sessions, settings)
|
|
235
252
|
source/ ← project snapshot used for workspace seeding
|
|
236
|
-
workspaces/ ← isolated per-issue execution directories
|
|
253
|
+
workspaces/ ← isolated per-issue execution directories (git worktrees)
|
|
237
254
|
```
|
|
238
255
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- **
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
- **
|
|
245
|
-
|
|
246
|
-
- Metrics tracked include:
|
|
247
|
-
- `usage.tokens` and `tokenUsage` by phase/model,
|
|
248
|
-
- `eventsCount`,
|
|
249
|
-
- `linesAdded`, `linesRemoved`, `filesChanged`.
|
|
250
|
-
- We query this through `getLastNDays/getLastNHours` to feed `/api/analytics/lines` and `/api/analytics/tokens`.
|
|
251
|
-
- **Issue lifecycle in StateMachinePlugin (single source of truth):**
|
|
252
|
-
- States, guarded transitions, actions, and triggers are centralized in `issue-state-machine.ts`.
|
|
253
|
-
- Public commands (`approve`, `execute`, `queue`, `retry`, `merge`) only emit transitions; the workflow behavior is not duplicated in route handlers.
|
|
254
|
-
- The machine also owns interruption recovery, retry delays, terminal transitions, and rollback edges.
|
|
255
|
-
- **Queue and execution pool:**
|
|
256
|
-
- `s3queue` is the runtime queue adapter with polling workers.
|
|
257
|
-
- Workers consume planning/execution/review jobs and run concurrently respecting configured concurrency.
|
|
258
|
-
- **Agent abstraction:**
|
|
259
|
-
- fifony wraps local CLIs, not proprietary model logic.
|
|
260
|
-
- Per stage we configure `cli`, `model`, and `reasoningEffort` independently (`planning`, `execute`, `review`).
|
|
261
|
-
- This lets you swap providers by issue without changing core orchestration.
|
|
262
|
-
- **Per-issue isolation:**
|
|
263
|
-
- Each issue has its own execution workspace and Git worktree branch.
|
|
264
|
-
- That avoids file conflicts and enables parallel work on the same repo safely.
|
|
265
|
-
- **Review and execution observability:**
|
|
266
|
-
- Diff stats are computed from git `--stat` output (`linesAdded`, `linesRemoved`, `filesChanged`).
|
|
267
|
-
- Runtime events are emitted for state transitions, review path decisions, and merge outcomes.
|
|
268
|
-
- CLI output and audit trails are stored to make mid-run interruptions recoverable.
|
|
269
|
-
- **Capability routing:**
|
|
270
|
-
- Routing derives issue labels (`capability:<category>`, `overlay:<name>`) from issue text and inferred file paths.
|
|
271
|
-
- This improves queue triage and worker assignment.
|
|
256
|
+
- **State machine (single source of truth):** States, guarded transitions, actions, and triggers are centralized in `issue-state-machine.ts`. All side effects (events, field mutations, EC tracking) happen in FSM entry actions.
|
|
257
|
+
- **Persistence (s3db.js):** Issues, events, sessions, and settings are first-class resources. No external DB required.
|
|
258
|
+
- **Eventual consistency analytics:** `EventualConsistencyPlugin` tracks `linesAdded`, `linesRemoved`, `filesChanged`, token usage, and event counts with daily cohort rollups.
|
|
259
|
+
- **Queue workers:** `S3QueuePlugin` dispatches planning/execution/review jobs to concurrent workers.
|
|
260
|
+
- **Agent abstraction:** Wraps local CLIs (Claude, Codex, Gemini), not proprietary model logic. Per-stage provider/model/effort configuration.
|
|
261
|
+
- **Per-issue isolation:** Each issue gets its own git worktree branch. No file conflicts, safe parallel work.
|
|
262
|
+
- **Capability routing:** Issue labels derived from text and file paths drive automatic provider/agent selection.
|
|
272
263
|
|
|
273
264
|
---
|
|
274
265
|
|
|
275
266
|
## Requirements
|
|
276
267
|
|
|
277
268
|
- Node.js 23 or newer
|
|
278
|
-
- At least one of: `claude` CLI, `codex` CLI
|
|
269
|
+
- At least one of: `claude` CLI, `codex` CLI, `gemini` CLI
|
|
279
270
|
|
|
280
271
|
---
|
|
281
272
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CACHE_VERSION = "
|
|
1
|
+
const CACHE_VERSION = "1774027862578";
|
|
2
2
|
const CORE_CACHE = `fifony-core-${CACHE_VERSION}`;
|
|
3
3
|
const ASSET_CACHE = `fifony-assets-${CACHE_VERSION}`;
|
|
4
4
|
const APP_SHELL_ROUTES = ["/kanban", "/issues", "/agents", "/settings", "/onboarding"];
|
package/package.json
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fifony",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26-next.f3fade3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": "Filipe Forattini <filipe@forattini.com.br>",
|
|
8
|
+
"description": "Filesystem-backed local orchestrator with a TypeScript CLI, MCP mode, and multi-agent Codex or Claude workflows.",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"ai",
|
|
11
|
+
"agent",
|
|
12
|
+
"orchestrator",
|
|
13
|
+
"codex",
|
|
14
|
+
"claude",
|
|
15
|
+
"gemini",
|
|
16
|
+
"mcp",
|
|
17
|
+
"cli",
|
|
18
|
+
"automation"
|
|
19
|
+
],
|
|
7
20
|
"bin": {
|
|
8
21
|
"fifony": "./bin/fifony.js",
|
|
9
22
|
"fifony-wrap": "./bin/fifony-wrap.js"
|
|
10
23
|
},
|
|
11
24
|
"files": [
|
|
12
25
|
"bin/",
|
|
13
|
-
"dist
|
|
26
|
+
"dist/**/*.js",
|
|
14
27
|
"app/dist/",
|
|
15
|
-
"app/public/",
|
|
16
|
-
"src/fixtures/",
|
|
17
28
|
"README.md",
|
|
18
|
-
"FIFONY.md",
|
|
19
29
|
"LICENSE",
|
|
20
30
|
"NOTICE"
|
|
21
31
|
],
|
|
@@ -34,25 +44,25 @@
|
|
|
34
44
|
"node": ">=23"
|
|
35
45
|
},
|
|
36
46
|
"dependencies": {
|
|
37
|
-
"@tanstack/react-query": "^5.91.2",
|
|
38
|
-
"@tanstack/react-router": "^1.167.5",
|
|
39
47
|
"cli-args-parser": "^1.0.6",
|
|
40
|
-
"lucide-react": "^0.577.0",
|
|
41
48
|
"node-cron": "^4.2.1",
|
|
42
49
|
"pino": "^10.3.1",
|
|
43
50
|
"pino-pretty": "^13.1.3",
|
|
44
51
|
"raffel": "^1.0.18",
|
|
45
|
-
"react": "^19.2.4",
|
|
46
|
-
"react-dom": "^19.2.4",
|
|
47
52
|
"recker": "^1.0.86",
|
|
48
53
|
"s3db.js": "21.3.2",
|
|
49
54
|
"yaml": "^2.8.2"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
57
|
"@tailwindcss/vite": "^4.2.2",
|
|
58
|
+
"@tanstack/react-query": "^5.91.2",
|
|
59
|
+
"@tanstack/react-router": "^1.167.5",
|
|
53
60
|
"@tanstack/router-plugin": "^1.166.14",
|
|
54
61
|
"@vitejs/plugin-react": "^6.0.1",
|
|
55
62
|
"daisyui": "^5.5.19",
|
|
63
|
+
"lucide-react": "^0.577.0",
|
|
64
|
+
"react": "^19.2.4",
|
|
65
|
+
"react-dom": "^19.2.4",
|
|
56
66
|
"tailwindcss": "^4.2.2",
|
|
57
67
|
"tsup": "^8.5.1",
|
|
58
68
|
"tsx": "^4.21.0",
|
|
Binary file
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title">
|
|
2
|
-
<title id="title">Fifony</title>
|
|
3
|
-
<rect width="512" height="512" fill="#0f172a"/>
|
|
4
|
-
<rect x="88" y="88" width="336" height="336" rx="72" fill="#13223d"/>
|
|
5
|
-
<path fill="#4ade80" d="M154 160h204c11 0 20 9 20 20v26H134v-26c0-11 9-20 20-20z"/>
|
|
6
|
-
<path fill="#22d3ee" d="M130 212h252l-24 138c-3 15-16 26-31 26H185c-15 0-28-11-31-26L130 212z"/>
|
|
7
|
-
<circle cx="256" cy="326" r="24" fill="#0f172a"/>
|
|
8
|
-
</svg>
|
package/app/public/icon.svg
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title">
|
|
2
|
-
<title id="title">Fifony</title>
|
|
3
|
-
<rect width="512" height="512" rx="96" fill="#0f172a"/>
|
|
4
|
-
<path fill="#4ade80" d="M130 132h252c13 0 24 11 24 24v32H106v-32c0-13 11-24 24-24z"/>
|
|
5
|
-
<path fill="#22d3ee" d="M102 192h308l-30 170c-3 18-19 32-38 32H170c-19 0-35-14-38-32L102 192z"/>
|
|
6
|
-
<circle cx="256" cy="332" r="26" fill="#0f172a"/>
|
|
7
|
-
</svg>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "/",
|
|
3
|
-
"name": "fifony Dashboard",
|
|
4
|
-
"short_name": "fifony",
|
|
5
|
-
"description": "fifony local orchestrator dashboard (React + PWA)",
|
|
6
|
-
"lang": "en-US",
|
|
7
|
-
"start_url": "/kanban",
|
|
8
|
-
"scope": "/",
|
|
9
|
-
"display": "standalone",
|
|
10
|
-
"display_override": ["window-controls-overlay", "standalone", "browser"],
|
|
11
|
-
"background_color": "#020617",
|
|
12
|
-
"theme_color": "#0f172a",
|
|
13
|
-
"orientation": "portrait-primary",
|
|
14
|
-
"prefer_related_applications": false,
|
|
15
|
-
"categories": ["productivity", "developer", "business"],
|
|
16
|
-
"shortcuts": [
|
|
17
|
-
{
|
|
18
|
-
"name": "Kanban",
|
|
19
|
-
"short_name": "Kanban",
|
|
20
|
-
"url": "/kanban",
|
|
21
|
-
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"name": "Issues",
|
|
25
|
-
"short_name": "Issues",
|
|
26
|
-
"url": "/issues",
|
|
27
|
-
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"name": "Agents",
|
|
31
|
-
"short_name": "Agents",
|
|
32
|
-
"url": "/agents",
|
|
33
|
-
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
34
|
-
}
|
|
35
|
-
],
|
|
36
|
-
"icons": [
|
|
37
|
-
{
|
|
38
|
-
"src": "/icon.svg",
|
|
39
|
-
"type": "image/svg+xml",
|
|
40
|
-
"sizes": "any"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"src": "/icon-maskable.svg",
|
|
44
|
-
"type": "image/svg+xml",
|
|
45
|
-
"sizes": "any",
|
|
46
|
-
"purpose": "any maskable"
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
}
|
package/app/public/offline.html
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<meta name="theme-color" content="#0f172a" />
|
|
7
|
-
<title>Offline · fifony</title>
|
|
8
|
-
<style>
|
|
9
|
-
:root {
|
|
10
|
-
color-scheme: dark;
|
|
11
|
-
--bg: #020617;
|
|
12
|
-
--panel: #0f172a;
|
|
13
|
-
--line: rgba(148, 163, 184, 0.18);
|
|
14
|
-
--text: #e2e8f0;
|
|
15
|
-
--muted: #94a3b8;
|
|
16
|
-
--accent: #22d3ee;
|
|
17
|
-
--accent-2: #4ade80;
|
|
18
|
-
}
|
|
19
|
-
* { box-sizing: border-box; }
|
|
20
|
-
body {
|
|
21
|
-
margin: 0;
|
|
22
|
-
min-height: 100vh;
|
|
23
|
-
display: grid;
|
|
24
|
-
place-items: center;
|
|
25
|
-
background:
|
|
26
|
-
radial-gradient(circle at top, rgba(34, 211, 238, 0.14), transparent 42%),
|
|
27
|
-
radial-gradient(circle at bottom, rgba(74, 222, 128, 0.12), transparent 36%),
|
|
28
|
-
var(--bg);
|
|
29
|
-
color: var(--text);
|
|
30
|
-
font: 16px/1.5 ui-sans-serif, system-ui, sans-serif;
|
|
31
|
-
padding: 24px;
|
|
32
|
-
}
|
|
33
|
-
main {
|
|
34
|
-
width: min(100%, 520px);
|
|
35
|
-
padding: 28px;
|
|
36
|
-
border-radius: 24px;
|
|
37
|
-
border: 1px solid var(--line);
|
|
38
|
-
background: linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.84));
|
|
39
|
-
box-shadow: 0 20px 60px rgba(2, 6, 23, 0.45);
|
|
40
|
-
}
|
|
41
|
-
.eyebrow {
|
|
42
|
-
display: inline-flex;
|
|
43
|
-
padding: 6px 10px;
|
|
44
|
-
border-radius: 999px;
|
|
45
|
-
border: 1px solid var(--line);
|
|
46
|
-
color: var(--muted);
|
|
47
|
-
font-size: 12px;
|
|
48
|
-
letter-spacing: 0.08em;
|
|
49
|
-
text-transform: uppercase;
|
|
50
|
-
}
|
|
51
|
-
h1 { margin: 16px 0 8px; font-size: clamp(28px, 5vw, 40px); line-height: 1.05; }
|
|
52
|
-
p { margin: 0; color: var(--muted); }
|
|
53
|
-
.actions { display: flex; gap: 12px; margin-top: 24px; flex-wrap: wrap; }
|
|
54
|
-
a, button {
|
|
55
|
-
appearance: none;
|
|
56
|
-
border: 0;
|
|
57
|
-
border-radius: 999px;
|
|
58
|
-
padding: 12px 18px;
|
|
59
|
-
font: inherit;
|
|
60
|
-
text-decoration: none;
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
}
|
|
63
|
-
a {
|
|
64
|
-
color: #082f49;
|
|
65
|
-
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
66
|
-
font-weight: 700;
|
|
67
|
-
}
|
|
68
|
-
button {
|
|
69
|
-
color: var(--text);
|
|
70
|
-
background: rgba(148, 163, 184, 0.12);
|
|
71
|
-
border: 1px solid var(--line);
|
|
72
|
-
}
|
|
73
|
-
</style>
|
|
74
|
-
</head>
|
|
75
|
-
<body>
|
|
76
|
-
<main>
|
|
77
|
-
<div class="eyebrow">Offline</div>
|
|
78
|
-
<h1>fifony is temporarily disconnected.</h1>
|
|
79
|
-
<p>The dashboard shell is available, but live runtime data needs the local server connection to come back.</p>
|
|
80
|
-
<div class="actions">
|
|
81
|
-
<a href="/kanban">Try again</a>
|
|
82
|
-
<button type="button" onclick="location.reload()">Reload</button>
|
|
83
|
-
</div>
|
|
84
|
-
</main>
|
|
85
|
-
</body>
|
|
86
|
-
</html>
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
const CACHE_VERSION = "__BUILD_TIMESTAMP__";
|
|
2
|
-
const CORE_CACHE = `fifony-core-${CACHE_VERSION}`;
|
|
3
|
-
const ASSET_CACHE = `fifony-assets-${CACHE_VERSION}`;
|
|
4
|
-
const APP_SHELL_ROUTES = ["/kanban", "/issues", "/agents", "/settings", "/onboarding"];
|
|
5
|
-
const APP_SHELL_FILES = ["/offline.html", "/manifest.webmanifest", "/icon.svg", "/icon-maskable.svg"];
|
|
6
|
-
const API_PREFIXES = ["/api/", "/docs", "/ws"];
|
|
7
|
-
|
|
8
|
-
self.addEventListener("install", (event) => {
|
|
9
|
-
event.waitUntil((async () => {
|
|
10
|
-
const cache = await caches.open(CORE_CACHE);
|
|
11
|
-
// Cache each route individually so one failure doesn't block the rest
|
|
12
|
-
await Promise.allSettled(
|
|
13
|
-
[...APP_SHELL_ROUTES, ...APP_SHELL_FILES].map(async (url) => {
|
|
14
|
-
try {
|
|
15
|
-
const response = await fetch(url);
|
|
16
|
-
if (response.ok) await cache.put(url, response);
|
|
17
|
-
} catch {}
|
|
18
|
-
}),
|
|
19
|
-
);
|
|
20
|
-
await self.skipWaiting();
|
|
21
|
-
})());
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
self.addEventListener("activate", (event) => {
|
|
25
|
-
event.waitUntil((async () => {
|
|
26
|
-
const names = await caches.keys();
|
|
27
|
-
await Promise.all(
|
|
28
|
-
names
|
|
29
|
-
.filter((name) => name !== CORE_CACHE && name !== ASSET_CACHE)
|
|
30
|
-
.map((name) => caches.delete(name)),
|
|
31
|
-
);
|
|
32
|
-
await self.clients.claim();
|
|
33
|
-
|
|
34
|
-
// Notify all clients that a new version activated (they may show a refresh banner)
|
|
35
|
-
const clients = await self.clients.matchAll({ type: "window" });
|
|
36
|
-
for (const client of clients) {
|
|
37
|
-
client.postMessage({ type: "SW_UPDATED", version: CACHE_VERSION });
|
|
38
|
-
}
|
|
39
|
-
})());
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
self.addEventListener("message", (event) => {
|
|
43
|
-
if (event.data?.type === "SKIP_WAITING") {
|
|
44
|
-
self.skipWaiting();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (event.data?.type === "GET_OFFLINE_STATUS") {
|
|
48
|
-
const port = event.ports?.[0];
|
|
49
|
-
if (port) {
|
|
50
|
-
// Probe the API to determine online/offline state
|
|
51
|
-
fetch("/health", { signal: AbortSignal.timeout(3000) })
|
|
52
|
-
.then((res) => port.postMessage({ offline: !res.ok }))
|
|
53
|
-
.catch(() => port.postMessage({ offline: true }));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (event.data?.type === "GET_VERSION") {
|
|
58
|
-
const port = event.ports?.[0];
|
|
59
|
-
if (port) {
|
|
60
|
-
port.postMessage({ version: CACHE_VERSION });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
self.addEventListener("fetch", (event) => {
|
|
66
|
-
const request = event.request;
|
|
67
|
-
if (request.method !== "GET") return;
|
|
68
|
-
|
|
69
|
-
const url = new URL(request.url);
|
|
70
|
-
if (url.origin !== self.location.origin) return;
|
|
71
|
-
if (API_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))) return;
|
|
72
|
-
|
|
73
|
-
if (request.mode === "navigate") {
|
|
74
|
-
event.respondWith((async () => {
|
|
75
|
-
try {
|
|
76
|
-
const response = await fetch(request);
|
|
77
|
-
const cache = await caches.open(CORE_CACHE);
|
|
78
|
-
await cache.put(request, response.clone());
|
|
79
|
-
return response;
|
|
80
|
-
} catch {
|
|
81
|
-
// Offline: notify clients
|
|
82
|
-
notifyClientsOffline();
|
|
83
|
-
|
|
84
|
-
const cache = await caches.open(CORE_CACHE);
|
|
85
|
-
return (
|
|
86
|
-
await cache.match(request) ||
|
|
87
|
-
await cache.match("/kanban") ||
|
|
88
|
-
await cache.match("/offline.html")
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
})());
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const isStaticAsset =
|
|
96
|
-
url.pathname.startsWith("/assets/") ||
|
|
97
|
-
url.pathname.endsWith(".css") ||
|
|
98
|
-
url.pathname.endsWith(".js") ||
|
|
99
|
-
url.pathname.endsWith(".svg") ||
|
|
100
|
-
url.pathname.endsWith(".webmanifest");
|
|
101
|
-
|
|
102
|
-
if (!isStaticAsset) return;
|
|
103
|
-
|
|
104
|
-
event.respondWith((async () => {
|
|
105
|
-
const cache = await caches.open(ASSET_CACHE);
|
|
106
|
-
const cached = await cache.match(request);
|
|
107
|
-
|
|
108
|
-
if (cached) {
|
|
109
|
-
event.waitUntil((async () => {
|
|
110
|
-
try {
|
|
111
|
-
const fresh = await fetch(request);
|
|
112
|
-
if (fresh?.ok) {
|
|
113
|
-
await cache.put(request, fresh.clone());
|
|
114
|
-
}
|
|
115
|
-
} catch {}
|
|
116
|
-
})());
|
|
117
|
-
return cached;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const response = await fetch(request);
|
|
122
|
-
if (response?.ok) {
|
|
123
|
-
await cache.put(request, response.clone());
|
|
124
|
-
}
|
|
125
|
-
return response;
|
|
126
|
-
} catch {
|
|
127
|
-
if (cached) return cached;
|
|
128
|
-
return new Response("Offline", {
|
|
129
|
-
status: 503,
|
|
130
|
-
statusText: "Service Unavailable",
|
|
131
|
-
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
})());
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Broadcast offline status to all window clients
|
|
138
|
-
async function notifyClientsOffline() {
|
|
139
|
-
try {
|
|
140
|
-
const clients = await self.clients.matchAll({ type: "window" });
|
|
141
|
-
for (const client of clients) {
|
|
142
|
-
client.postMessage({ type: "OFFLINE" });
|
|
143
|
-
}
|
|
144
|
-
} catch {}
|
|
145
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agents/cli-wrapper.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * fifony-wrap — man-in-the-middle wrapper for CLI agents\n *\n * Protocol:\n * FIFONY_WRAP_COMMAND (required) real CLI command to run, e.g. \"claude -p $FIFONY_TURN_PROMPT_FILE\"\n * FIFONY_WRAP_BEFORE_HOOK (optional) shell script run before the CLI; can modify $FIFONY_TURN_PROMPT_FILE in place\n * FIFONY_WRAP_AFTER_HOOK (optional) shell script run after the CLI; receives FIFONY_WRAP_OUTPUT_FILE pointing\n * to a temp file with the captured output — modify it in place to change what fifony sees\n *\n * Configure via the API, MCP, or s3db — set agentCommand on the workflow config to:\n * FIFONY_WRAP_COMMAND=\"claude -p $FIFONY_TURN_PROMPT_FILE\" FIFONY_WRAP_BEFORE_HOOK=./hooks/before.sh fifony-wrap\n */\nimport { readFileSync, writeFileSync, rmSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { env, exit } from \"node:process\";\nimport { spawn } from \"node:child_process\";\n\nfunction runShell(command: string, extraEnv: Record<string, string> = {}): Promise<{ code: number }> {\n return new Promise((resolve) => {\n const child = spawn(command, {\n shell: true,\n stdio: \"inherit\",\n env: { ...env, ...extraEnv },\n });\n child.on(\"close\", (code) => resolve({ code: code ?? 1 }));\n child.on(\"error\", () => resolve({ code: 1 }));\n });\n}\n\nfunction runAndCapture(command: string): Promise<{ code: number | null; output: string }> {\n return new Promise((resolve) => {\n const child = spawn(command, { shell: true, stdio: [\"inherit\", \"pipe\", \"pipe\"] });\n let output = \"\";\n child.stdout?.on(\"data\", (chunk: Buffer) => { output += String(chunk); });\n child.stderr?.on(\"data\", (chunk: Buffer) => { output += String(chunk); });\n child.on(\"close\", (code) => resolve({ code, output }));\n child.on(\"error\", () => resolve({ code: null, output: `Command execution error: ${command}` }));\n });\n}\n\nasync function main() {\n const wrapCommand = env.FIFONY_WRAP_COMMAND?.trim();\n if (!wrapCommand) {\n process.stderr.write(\"fifony-wrap: FIFONY_WRAP_COMMAND is required\\n\");\n exit(1);\n }\n\n const workspacePath = env.FIFONY_WORKSPACE_PATH ?? process.cwd();\n const beforeHook = env.FIFONY_WRAP_BEFORE_HOOK?.trim();\n const afterHook = env.FIFONY_WRAP_AFTER_HOOK?.trim();\n const outputFile = join(workspacePath, \".wrap-output.txt\");\n\n if (beforeHook) {\n const { code } = await runShell(beforeHook);\n if (code !== 0) {\n process.stderr.write(`fifony-wrap: before hook exited ${code}\\n`);\n exit(code);\n }\n }\n\n const { code, output } = await runAndCapture(wrapCommand);\n\n writeFileSync(outputFile, output, \"utf8\");\n\n if (afterHook) {\n const { code: hookCode } = await runShell(afterHook, { FIFONY_WRAP_OUTPUT_FILE: outputFile });\n if (hookCode !== 0) {\n process.stderr.write(`fifony-wrap: after hook exited ${hookCode} — using original output\\n`);\n writeFileSync(outputFile, output, \"utf8\");\n }\n }\n\n let finalOutput = output;\n try {\n finalOutput = readFileSync(outputFile, \"utf8\");\n } catch {}\n try { rmSync(outputFile, { force: true }); } catch {}\n\n process.stdout.write(finalOutput);\n exit(code ?? 1);\n}\n\nmain().catch((err) => {\n process.stderr.write(`fifony-wrap: fatal: ${String(err)}\\n`);\n exit(1);\n});\n"],"mappings":";;;AAaA,SAAS,cAAc,eAAe,cAAc;AACpD,SAAS,YAAY;AACrB,SAAS,KAAK,YAAY;AAC1B,SAAS,aAAa;AAEtB,SAAS,SAAS,SAAiB,WAAmC,CAAC,GAA8B;AACnG,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,QAAQ,MAAM,SAAS;AAAA,MAC3B,OAAO;AAAA,MACP,OAAO;AAAA,MACP,KAAK,EAAE,GAAG,KAAK,GAAG,SAAS;AAAA,IAC7B,CAAC;AACD,UAAM,GAAG,SAAS,CAAC,SAAS,QAAQ,EAAE,MAAM,QAAQ,EAAE,CAAC,CAAC;AACxD,UAAM,GAAG,SAAS,MAAM,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;AAAA,EAC9C,CAAC;AACH;AAEA,SAAS,cAAc,SAAmE;AACxF,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,QAAQ,MAAM,SAAS,EAAE,OAAO,MAAM,OAAO,CAAC,WAAW,QAAQ,MAAM,EAAE,CAAC;AAChF,QAAI,SAAS;AACb,UAAM,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AAAE,gBAAU,OAAO,KAAK;AAAA,IAAG,CAAC;AACxE,UAAM,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AAAE,gBAAU,OAAO,KAAK;AAAA,IAAG,CAAC;AACxE,UAAM,GAAG,SAAS,CAAC,SAAS,QAAQ,EAAE,MAAM,OAAO,CAAC,CAAC;AACrD,UAAM,GAAG,SAAS,MAAM,QAAQ,EAAE,MAAM,MAAM,QAAQ,4BAA4B,OAAO,GAAG,CAAC,CAAC;AAAA,EAChG,CAAC;AACH;AAEA,eAAe,OAAO;AACpB,QAAM,cAAc,IAAI,qBAAqB,KAAK;AAClD,MAAI,CAAC,aAAa;AAChB,YAAQ,OAAO,MAAM,gDAAgD;AACrE,SAAK,CAAC;AAAA,EACR;AAEA,QAAM,gBAAgB,IAAI,yBAAyB,QAAQ,IAAI;AAC/D,QAAM,aAAa,IAAI,yBAAyB,KAAK;AACrD,QAAM,YAAY,IAAI,wBAAwB,KAAK;AACnD,QAAM,aAAa,KAAK,eAAe,kBAAkB;AAEzD,MAAI,YAAY;AACd,UAAM,EAAE,MAAAA,MAAK,IAAI,MAAM,SAAS,UAAU;AAC1C,QAAIA,UAAS,GAAG;AACd,cAAQ,OAAO,MAAM,mCAAmCA,KAAI;AAAA,CAAI;AAChE,WAAKA,KAAI;AAAA,IACX;AAAA,EACF;AAEA,QAAM,EAAE,MAAM,OAAO,IAAI,MAAM,cAAc,WAAW;AAExD,gBAAc,YAAY,QAAQ,MAAM;AAExC,MAAI,WAAW;AACb,UAAM,EAAE,MAAM,SAAS,IAAI,MAAM,SAAS,WAAW,EAAE,yBAAyB,WAAW,CAAC;AAC5F,QAAI,aAAa,GAAG;AAClB,cAAQ,OAAO,MAAM,kCAAkC,QAAQ;AAAA,CAA4B;AAC3F,oBAAc,YAAY,QAAQ,MAAM;AAAA,IAC1C;AAAA,EACF;AAEA,MAAI,cAAc;AAClB,MAAI;AACF,kBAAc,aAAa,YAAY,MAAM;AAAA,EAC/C,QAAQ;AAAA,EAAC;AACT,MAAI;AAAE,WAAO,YAAY,EAAE,OAAO,KAAK,CAAC;AAAA,EAAG,QAAQ;AAAA,EAAC;AAEpD,UAAQ,OAAO,MAAM,WAAW;AAChC,OAAK,QAAQ,CAAC;AAChB;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,OAAO,MAAM,uBAAuB,OAAO,GAAG,CAAC;AAAA,CAAI;AAC3D,OAAK,CAAC;AACR,CAAC;","names":["code"]}
|