vibeops-tracker 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/AUTHORS +10 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/cli.mjs +75 -0
- package/lib/api.mjs +140 -0
- package/lib/data-dir.mjs +46 -0
- package/lib/prompt.mjs +96 -0
- package/lib/store.mjs +569 -0
- package/mcp-server.mjs +247 -0
- package/package.json +62 -0
- package/public/app.js +733 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.svg +56 -0
- package/public/help.html +214 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +56 -0
- package/public/index.html +33 -0
- package/public/manifest.webmanifest +12 -0
- package/public/styles.css +420 -0
- package/public/widget.js +554 -0
- package/server.mjs +75 -0
package/AUTHORS
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
VibeOps Tracker authors and maintainers
|
|
2
|
+
|
|
3
|
+
Created and maintained by:
|
|
4
|
+
Igor Gembitsky <https://github.com/igembitsky>
|
|
5
|
+
|
|
6
|
+
VibeOps Tracker is an original project by Igor Gembitsky. A local-first issue
|
|
7
|
+
tracker and triage board for solo builders juggling many projects at once.
|
|
8
|
+
|
|
9
|
+
Contributions are welcome; contributors who land changes will be listed here.
|
|
10
|
+
See CONTRIBUTING.md to get started.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Gembitsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# VibeOps Tracker
|
|
2
|
+
|
|
3
|
+
**A local-first issue tracker for builders juggling a dozen projects at once.**
|
|
4
|
+
Capture a bug or idea the moment you spot it, prioritize it on a Kanban board,
|
|
5
|
+
and hand it to AI coding agents. Everything runs on your machine, nothing in the
|
|
6
|
+
cloud.
|
|
7
|
+
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.npmjs.com/package/vibeops-tracker)
|
|
10
|
+
[](https://github.com/igembitsky/vibeops-tracker/actions/workflows/ci.yml)
|
|
11
|
+

|
|
12
|
+
[](CONTRIBUTING.md)
|
|
13
|
+
|
|
14
|
+
When you build several things at once, the work that breaks your flow isn't the
|
|
15
|
+
big feature. It's the steady drip of small ones. You're testing a side project,
|
|
16
|
+
you notice a misaligned button, a missing validation, a "wouldn't it be nice."
|
|
17
|
+
Stop to fix each one and you never finish anything; open a tab to track it and
|
|
18
|
+
the idea rots in a list you never reopen. VibeOps Tracker is the place to **put
|
|
19
|
+
it down and keep moving**, then pick it up, prioritize it, and ship it (or let
|
|
20
|
+
an agent ship it) when the time is right.
|
|
21
|
+
|
|
22
|
+
It runs entirely on your machine. No account, no telemetry, no cloud. Your
|
|
23
|
+
issues are plain markdown files in a folder you control.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx vibeops-tracker # board at http://localhost:4400
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it. No install, no config, no database. Prefer to clone it?
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/igembitsky/vibeops-tracker.git
|
|
37
|
+
cd vibeops-tracker
|
|
38
|
+
npm install
|
|
39
|
+
npm start # board at http://localhost:4400
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Node 20+. Your data lives in a local `data/` folder (when cloned) or
|
|
43
|
+
your OS app-data directory (when installed globally). Run `vibeops where` to
|
|
44
|
+
see exactly where.
|
|
45
|
+
|
|
46
|
+
## Three ways to capture
|
|
47
|
+
|
|
48
|
+
The whole point is to make capturing an issue cheaper than the urge to ignore
|
|
49
|
+
it. So there are three on-ramps, and you can mix them freely:
|
|
50
|
+
|
|
51
|
+
**1. From any running app, with the widget.** Drop one line into a project's HTML:
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<script src="http://localhost:4400/widget.js" data-project="my-app" defer></script>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A floating 🐞 button appears. Highlight the thing you're talking about, click
|
|
58
|
+
the button, and the issue is captured with the selected text plus a context
|
|
59
|
+
snapshot: the URL, the viewport, and (for bugs) an activity trail of your last
|
|
60
|
+
clicks, network calls, and JavaScript errors. The widget never breaks the host
|
|
61
|
+
app: if the tracker is down, it fails quietly and keeps your typed text.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
**2. From a Claude Code session, over MCP.** Register the tracker once as an
|
|
66
|
+
MCP server (below), and any agent session can file an issue mid-task when it
|
|
67
|
+
notices something out of scope. Or you can just say "add that to the backlog."
|
|
68
|
+
|
|
69
|
+
**3. From the board itself.** Click **+ New issue** to file something directly,
|
|
70
|
+
no widget required.
|
|
71
|
+
|
|
72
|
+
## The board
|
|
73
|
+
|
|
74
|
+
Open `http://localhost:4400` and you get a Kanban board across all your
|
|
75
|
+
projects, with a switcher to move between them:
|
|
76
|
+
|
|
77
|
+
- **Four columns:** Backlog → In Progress → In Review → Done.
|
|
78
|
+
- **Drag to prioritize.** Card order is saved to each issue's file, so agents
|
|
79
|
+
see the same priority you do.
|
|
80
|
+
- **Real-time search** filters cards as you type.
|
|
81
|
+
- **Click to edit** an issue's type or severity right on the card detail.
|
|
82
|
+
- **Sweep** finished work into a searchable **Archive**, and **delete** stale or
|
|
83
|
+
test issues outright (with a two-step confirm).
|
|
84
|
+
- **Copy Prompt** turns any issue into a paste-ready Claude Code prompt: the
|
|
85
|
+
full description, the captured context, the repo path, and the workflow.
|
|
86
|
+
|
|
87
|
+
## Let agents work the backlog
|
|
88
|
+
|
|
89
|
+
This is the part that makes it *ops*, not just a list. Register the tracker as
|
|
90
|
+
an MCP server, available in every Claude Code session:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
claude mcp add vibeops -- npx -y -p vibeops-tracker vibeops-mcp
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Now your agents can read and drive the board directly. The intended loop:
|
|
97
|
+
|
|
98
|
+
1. You stack and prioritize a backlog during the day.
|
|
99
|
+
2. When you step away, you point an agent at it: *"Work the top items in the
|
|
100
|
+
`my-app` backlog and stop for my review."*
|
|
101
|
+
3. The agent calls `list_issues`, picks the highest-priority one, marks it
|
|
102
|
+
**In Progress**, does the work, and moves it to **In Review** with a
|
|
103
|
+
summary of what it changed and which files it touched.
|
|
104
|
+
4. You come back to a board that shows exactly what got done, ready to verify.
|
|
105
|
+
|
|
106
|
+
Agents set every status except **Done**. That one is yours, so nothing ships
|
|
107
|
+
without your sign-off. The MCP server reads the markdown store directly, so it
|
|
108
|
+
works even when the web UI isn't running.
|
|
109
|
+
|
|
110
|
+
**Tools:** `get_tracker_instructions`, `list_projects`, `list_issues`,
|
|
111
|
+
`search_issues`, `get_issue`, `create_issue`, `update_issue`, `add_comment`,
|
|
112
|
+
`resolve_issue`, `delete_issue`.
|
|
113
|
+
|
|
114
|
+
## How it stores things
|
|
115
|
+
|
|
116
|
+
One issue is one markdown file under `data/<project>/issues/`:
|
|
117
|
+
|
|
118
|
+
```markdown
|
|
119
|
+
---
|
|
120
|
+
id: my-app-7
|
|
121
|
+
project: my-app
|
|
122
|
+
title: Login button does nothing on mobile
|
|
123
|
+
status: backlog
|
|
124
|
+
type: bug
|
|
125
|
+
severity: 4
|
|
126
|
+
tags: [auth, mobile]
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Seeing
|
|
130
|
+
Tapping "Log in" on iOS Safari does nothing.
|
|
131
|
+
|
|
132
|
+
## Expecting
|
|
133
|
+
It submits the form and signs me in.
|
|
134
|
+
|
|
135
|
+
## Context
|
|
136
|
+
The captured snapshot (URL, selected text, recent errors), stored as JSON.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Human-readable, AI-readable, greppable, and versionable. The store handles
|
|
140
|
+
concurrent writes from the web server and the MCP server with file locks, so the
|
|
141
|
+
board and your agents never corrupt each other's edits.
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
Everything has a sensible default; override only what you need.
|
|
146
|
+
|
|
147
|
+
| Setting | Default | How to set |
|
|
148
|
+
| ---------- | ------------------------------------ | ----------------------------------- |
|
|
149
|
+
| Port | `4400` | `--port 5000` or `PORT=5000` |
|
|
150
|
+
| Data dir | `./data` (clone) or OS app-data dir | `--data <dir>` or `DATA_DIR=<dir>` |
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
vibeops # start the board (default command)
|
|
154
|
+
vibeops --port 5000 # on a different port
|
|
155
|
+
vibeops where # print the data directory
|
|
156
|
+
vibeops mcp # run the MCP stdio server directly
|
|
157
|
+
vibeops --help
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## FAQ
|
|
161
|
+
|
|
162
|
+
**Is my data private?** Yes, completely. Everything lives on your machine in
|
|
163
|
+
local files. There's no account, no server you don't run, and no telemetry. The
|
|
164
|
+
board binds to `localhost` only, so don't expose its port to an untrusted network
|
|
165
|
+
(see [SECURITY.md](SECURITY.md)).
|
|
166
|
+
|
|
167
|
+
**Do I have to use AI agents?** No. The widget, the board, and the Copy Prompt
|
|
168
|
+
button are useful on their own. MCP is opt-in.
|
|
169
|
+
|
|
170
|
+
**Does it lock me into one project?** No. It's built for the opposite. One
|
|
171
|
+
tracker holds all your projects, each with its own board, and the widget installs
|
|
172
|
+
into any of them with a single line.
|
|
173
|
+
|
|
174
|
+
**What does a project need to integrate?** One `<script>` tag. That's the only
|
|
175
|
+
tracker code that ever lives in your project; everything else stays here.
|
|
176
|
+
|
|
177
|
+
**Why markdown files instead of a database?** So your backlog is readable,
|
|
178
|
+
greppable, diffable, and yours. No migration, no lock-in, no service to keep
|
|
179
|
+
alive.
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
184
|
+
for setup and the project layout, and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
185
|
+
for the ground rules. It's a small, dependency-light codebase (plain ESM, no
|
|
186
|
+
build step), meant to be easy to read and easy to change.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
[MIT](LICENSE) © Igor Gembitsky
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Why I built this
|
|
195
|
+
|
|
196
|
+
I build a lot of things at once. Testing them locally, I'd constantly trip over
|
|
197
|
+
small bugs and half-formed ideas at the worst possible moment, mid-flow on
|
|
198
|
+
something else. Fixing each one on the spot wrecked my focus. Spinning up a
|
|
199
|
+
separate session for every one wasn't worth it when I'm the only one driving.
|
|
200
|
+
What I wanted was somewhere to *triage*: drop the bug, the feature, the
|
|
201
|
+
"improve this later," keep building, and come back to a prioritized stack I could
|
|
202
|
+
work through myself or hand to an agent on a coffee break.
|
|
203
|
+
|
|
204
|
+
I looked at the existing tools and they were all built for teams, for the cloud,
|
|
205
|
+
or for ceremony I didn't want. So I built the small, local, private thing that
|
|
206
|
+
fit how I actually work, and figured other people building solo might want it
|
|
207
|
+
too. That's what this is: my "vibe ops," a lightweight backbone for managing
|
|
208
|
+
the work behind a pile of side projects. Take it, use it, change it, and tell me
|
|
209
|
+
how to make it better.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// VibeOps Tracker CLI — start the board/API server (default) or the MCP stdio server.
|
|
3
|
+
// A thin, dependency-free dispatcher.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { resolveDataDir, ROOT } from '../lib/data-dir.mjs';
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
function opt(names, takesValue) {
|
|
11
|
+
for (const name of names) {
|
|
12
|
+
const i = args.indexOf(name);
|
|
13
|
+
if (i !== -1) return takesValue ? args[i + 1] : true;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
19
|
+
|
|
20
|
+
function help() {
|
|
21
|
+
console.log(`VibeOps Tracker ${pkg.version} — local-first issue tracker
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
vibeops [start] Start the board + API + capture widget (default)
|
|
25
|
+
vibeops mcp Start the MCP stdio server (for AI agent hosts)
|
|
26
|
+
vibeops where Print the data directory and exit
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
-p, --port <n> Server port (default 4400, or $PORT)
|
|
30
|
+
-d, --data <dir> Data directory (default: ./data in a clone, otherwise your
|
|
31
|
+
OS app-data dir; or set $DATA_DIR)
|
|
32
|
+
-h, --help Show this help
|
|
33
|
+
-v, --version Print the version
|
|
34
|
+
|
|
35
|
+
Everything runs locally. Docs: ${pkg.homepage || 'https://github.com/igembitsky/vibeops-tracker'}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (opt(['-v', '--version'])) {
|
|
39
|
+
console.log(pkg.version);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
if (opt(['-h', '--help'])) {
|
|
43
|
+
help();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cmd = args.find((a) => !a.startsWith('-')) || 'start';
|
|
48
|
+
const dataDir = resolveDataDir({ flag: opt(['-d', '--data'], true) });
|
|
49
|
+
|
|
50
|
+
if (cmd === 'where') {
|
|
51
|
+
console.log(dataDir);
|
|
52
|
+
} else if (cmd === 'mcp') {
|
|
53
|
+
process.env.DATA_DIR = dataDir; // mcp-server.mjs resolves DATA_DIR at import time
|
|
54
|
+
await import('../mcp-server.mjs');
|
|
55
|
+
} else if (cmd === 'start') {
|
|
56
|
+
const port = Number(opt(['-p', '--port'], true) || process.env.PORT) || 4400;
|
|
57
|
+
const { startServer } = await import('../server.mjs');
|
|
58
|
+
try {
|
|
59
|
+
await startServer({ port, dataDir });
|
|
60
|
+
console.log('VibeOps Tracker is running.');
|
|
61
|
+
console.log(` Board: http://localhost:${port}`);
|
|
62
|
+
console.log(` Data: ${dataDir}`);
|
|
63
|
+
console.log('Press Ctrl+C to stop.');
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err && err.code === 'EADDRINUSE') {
|
|
66
|
+
console.error(`Port ${port} is already in use. Try: vibeops --port <other-port>`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
73
|
+
help();
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
package/lib/api.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// REST API router. handleApi returns true when the request was an /api route.
|
|
2
|
+
import {
|
|
3
|
+
listProjects,
|
|
4
|
+
ensureProject,
|
|
5
|
+
getProject,
|
|
6
|
+
createIssue,
|
|
7
|
+
getIssue,
|
|
8
|
+
listIssues,
|
|
9
|
+
updateIssue,
|
|
10
|
+
addComment,
|
|
11
|
+
resolveIssue,
|
|
12
|
+
sweepDone,
|
|
13
|
+
listClosed,
|
|
14
|
+
deleteIssue,
|
|
15
|
+
} from './store.mjs';
|
|
16
|
+
import { buildPrompt } from './prompt.mjs';
|
|
17
|
+
|
|
18
|
+
export function setCors(res) {
|
|
19
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
20
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
|
|
21
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// CSRF guard for destructive, board-only mutations. The capture widget POSTs
|
|
25
|
+
// cross-origin (that must stay allowed), but DELETE/PATCH are only ever issued
|
|
26
|
+
// by the same-origin board UI or by header-less callers (curl, MCP). A browser
|
|
27
|
+
// always attaches a truthful Origin on cross-origin unsafe requests, so any
|
|
28
|
+
// request whose Origin doesn't match this server is a drive-by and is refused.
|
|
29
|
+
// Header-less callers (no Origin) pass — they aren't the CSRF threat.
|
|
30
|
+
function isSameOrigin(req) {
|
|
31
|
+
const origin = req.headers.origin;
|
|
32
|
+
if (!origin) return true;
|
|
33
|
+
return origin === `http://${req.headers.host}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sendJson(res, status, body) {
|
|
37
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
38
|
+
res.end(JSON.stringify(body, null, 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sendText(res, status, body) {
|
|
42
|
+
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
43
|
+
res.end(body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readBody(req) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
req.on('data', (chunk) => {
|
|
50
|
+
data += chunk;
|
|
51
|
+
if (data.length > 5 * 1024 * 1024) {
|
|
52
|
+
const err = new Error('body too large');
|
|
53
|
+
err.statusCode = 400;
|
|
54
|
+
req.destroy(err);
|
|
55
|
+
reject(err);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
req.on('end', () => resolve(data));
|
|
59
|
+
req.on('error', reject);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readJsonBody(req) {
|
|
64
|
+
const raw = await readBody(req);
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(raw || '{}');
|
|
67
|
+
} catch {
|
|
68
|
+
const err = new Error('invalid JSON body');
|
|
69
|
+
err.statusCode = 400;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function handleApi(req, res, { dataDir }) {
|
|
75
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
76
|
+
const pathname = url.pathname;
|
|
77
|
+
if (!pathname.startsWith('/api/')) return false;
|
|
78
|
+
|
|
79
|
+
setCors(res);
|
|
80
|
+
if (req.method === 'OPTIONS') {
|
|
81
|
+
res.writeHead(204);
|
|
82
|
+
res.end();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ((req.method === 'DELETE' || req.method === 'PATCH') && !isSameOrigin(req)) {
|
|
87
|
+
sendJson(res, 403, { error: 'cross-origin mutation refused' });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const route = `${req.method} ${pathname}`;
|
|
92
|
+
let match;
|
|
93
|
+
try {
|
|
94
|
+
if (route === 'GET /api/projects') {
|
|
95
|
+
sendJson(res, 200, listProjects(dataDir));
|
|
96
|
+
} else if (route === 'POST /api/projects') {
|
|
97
|
+
const body = await readJsonBody(req);
|
|
98
|
+
sendJson(res, 201, ensureProject(dataDir, body.key, body.name, body.repo_path));
|
|
99
|
+
} else if ((match = /^GET \/api\/projects\/([^/]+)\/issues$/.exec(route))) {
|
|
100
|
+
const status = url.searchParams.get('status') || undefined;
|
|
101
|
+
sendJson(res, 200, listIssues(dataDir, decodeURIComponent(match[1]), { status }));
|
|
102
|
+
} else if ((match = /^GET \/api\/projects\/([^/]+)\/closed$/.exec(route))) {
|
|
103
|
+
sendJson(res, 200, listClosed(dataDir, decodeURIComponent(match[1])));
|
|
104
|
+
} else if ((match = /^POST \/api\/projects\/([^/]+)\/sweep$/.exec(route))) {
|
|
105
|
+
sendJson(res, 200, sweepDone(dataDir, decodeURIComponent(match[1])));
|
|
106
|
+
} else if (route === 'POST /api/issues') {
|
|
107
|
+
const body = await readJsonBody(req);
|
|
108
|
+
sendJson(res, 201, createIssue(dataDir, body));
|
|
109
|
+
} else if ((match = /^GET \/api\/issues\/([^/]+)\/prompt$/.exec(route))) {
|
|
110
|
+
const issue = getIssue(dataDir, decodeURIComponent(match[1]));
|
|
111
|
+
if (!issue) return sendJson(res, 404, { error: `Issue not found: ${match[1]}` }), true;
|
|
112
|
+
const project = getProject(dataDir, issue.project);
|
|
113
|
+
sendText(res, 200, buildPrompt(issue, { apiBase: `http://${req.headers.host}`, project }));
|
|
114
|
+
} else if ((match = /^POST \/api\/issues\/([^/]+)\/resolve$/.exec(route))) {
|
|
115
|
+
const body = await readJsonBody(req);
|
|
116
|
+
sendJson(res, 200, resolveIssue(dataDir, decodeURIComponent(match[1]), body));
|
|
117
|
+
} else if ((match = /^GET \/api\/issues\/([^/]+)$/.exec(route))) {
|
|
118
|
+
const issue = getIssue(dataDir, decodeURIComponent(match[1]));
|
|
119
|
+
if (!issue) return sendJson(res, 404, { error: `Issue not found: ${match[1]}` }), true;
|
|
120
|
+
sendJson(res, 200, issue);
|
|
121
|
+
} else if ((match = /^PATCH \/api\/issues\/([^/]+)$/.exec(route))) {
|
|
122
|
+
const body = await readJsonBody(req);
|
|
123
|
+
sendJson(res, 200, updateIssue(dataDir, decodeURIComponent(match[1]), body));
|
|
124
|
+
} else if ((match = /^DELETE \/api\/issues\/([^/]+)$/.exec(route))) {
|
|
125
|
+
sendJson(res, 200, deleteIssue(dataDir, decodeURIComponent(match[1])));
|
|
126
|
+
} else if ((match = /^POST \/api\/issues\/([^/]+)\/comments$/.exec(route))) {
|
|
127
|
+
const body = await readJsonBody(req);
|
|
128
|
+
sendJson(res, 200, addComment(dataDir, decodeURIComponent(match[1]), body));
|
|
129
|
+
} else {
|
|
130
|
+
sendJson(res, 404, { error: `No route: ${route}` });
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err.code === 'NOT_FOUND') sendJson(res, 404, { error: err.message });
|
|
134
|
+
else if (err.code === 'CLOSED') sendJson(res, 409, { error: err.message });
|
|
135
|
+
else if (err.statusCode) sendJson(res, err.statusCode, { error: err.message });
|
|
136
|
+
else if (err.name === 'Error') sendJson(res, 400, { error: err.message }); // validation throws are plain Errors
|
|
137
|
+
else sendJson(res, 500, { error: err.message });
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
package/lib/data-dir.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Resolve where VibeOps Tracker stores its data — used by the HTTP server, the CLI,
|
|
2
|
+
// and the MCP server alike, so a human (board UI) and an agent (MCP) always read
|
|
3
|
+
// and write the same store.
|
|
4
|
+
//
|
|
5
|
+
// Precedence:
|
|
6
|
+
// 1. an explicit --data flag (passed in by the CLI) — caller wins
|
|
7
|
+
// 2. the DATA_DIR environment variable — host / MCP config
|
|
8
|
+
// 3. running from a git clone → <repoRoot>/data — gitignored, local
|
|
9
|
+
// 4. installed globally or via npx (lives in node_modules) → OS app-data dir
|
|
10
|
+
//
|
|
11
|
+
// Rationale: a cloned repo keeps its data beside the code (familiar, easy to
|
|
12
|
+
// back up). A globally-installed tool must persist data across upgrades and keep
|
|
13
|
+
// it private to the user, which is exactly what the per-user OS app-data dir
|
|
14
|
+
// gives you — never inside node_modules, where a reinstall would wipe it.
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const APP = 'vibeops-tracker';
|
|
20
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); // <root>/lib
|
|
21
|
+
export const ROOT = path.dirname(MODULE_DIR); // package root
|
|
22
|
+
|
|
23
|
+
export function appDataDir() {
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
26
|
+
return path.join(base, APP);
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === 'darwin') {
|
|
29
|
+
return path.join(os.homedir(), 'Library', 'Application Support', APP);
|
|
30
|
+
}
|
|
31
|
+
const base = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
32
|
+
return path.join(base, APP);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// True when this package lives inside a node_modules tree (global install / npx
|
|
36
|
+
// cache) rather than a developer's clone.
|
|
37
|
+
function isInstalledPackage() {
|
|
38
|
+
return ROOT.split(path.sep).includes('node_modules');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveDataDir({ flag } = {}) {
|
|
42
|
+
if (flag) return path.resolve(flag);
|
|
43
|
+
if (process.env.DATA_DIR) return path.resolve(process.env.DATA_DIR);
|
|
44
|
+
if (!isInstalledPackage()) return path.join(ROOT, 'data'); // clone → ./data
|
|
45
|
+
return appDataDir();
|
|
46
|
+
}
|
package/lib/prompt.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Build a paste-ready Claude Code prompt for working an issue.
|
|
2
|
+
|
|
3
|
+
function selectionBlock(context) {
|
|
4
|
+
if (!context?.selectedText) return '';
|
|
5
|
+
const quoted = String(context.selectedText)
|
|
6
|
+
.trimEnd()
|
|
7
|
+
.split('\n')
|
|
8
|
+
.map((l) => `> ${l}`)
|
|
9
|
+
.join('\n');
|
|
10
|
+
return [
|
|
11
|
+
'',
|
|
12
|
+
'## What I highlighted',
|
|
13
|
+
'',
|
|
14
|
+
'I had this text selected on the page when I filed this — it is likely the exact thing I am referring to:',
|
|
15
|
+
'',
|
|
16
|
+
quoted,
|
|
17
|
+
'',
|
|
18
|
+
].join('\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function contextBlock(context) {
|
|
22
|
+
if (!context) return '';
|
|
23
|
+
const lines = ['', '## Captured context', ''];
|
|
24
|
+
if (context.url) lines.push(`- URL when reported: ${context.url}`);
|
|
25
|
+
if (context.viewport?.w) lines.push(`- Viewport: ${context.viewport.w}x${context.viewport.h}`);
|
|
26
|
+
if (context.userAgent) lines.push(`- User agent: ${context.userAgent}`);
|
|
27
|
+
const errors = context.recentErrors || [];
|
|
28
|
+
if (errors.length) {
|
|
29
|
+
lines.push('', `Recent JS errors (${errors.length}):`);
|
|
30
|
+
for (const e of errors) lines.push(`- ${e.message}`);
|
|
31
|
+
}
|
|
32
|
+
const failures = context.recentFetchFailures || [];
|
|
33
|
+
if (failures.length) {
|
|
34
|
+
lines.push('', `Recent failed requests (${failures.length}):`);
|
|
35
|
+
for (const f of failures) lines.push(`- ${f.method} ${f.url} -> ${f.status}`);
|
|
36
|
+
}
|
|
37
|
+
const git = context.git || context.app?.git;
|
|
38
|
+
if (git && (git.branch || git.commit || git.worktree)) {
|
|
39
|
+
lines.push('', 'Environment as of capture (may have changed since — verify against the current state of the repo before relying on it):');
|
|
40
|
+
if (git.branch) lines.push(`- Branch: ${git.branch}`);
|
|
41
|
+
if (git.commit) lines.push(`- Commit: ${git.commit}`);
|
|
42
|
+
if (git.worktree) lines.push(`- Worktree: ${git.worktree}`);
|
|
43
|
+
}
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function repoBlock(project) {
|
|
48
|
+
if (!project?.repo_path) return '';
|
|
49
|
+
return `
|
|
50
|
+
## Repository
|
|
51
|
+
|
|
52
|
+
This issue belongs to the "${project.name || project.key}" project:
|
|
53
|
+
${project.repo_path}
|
|
54
|
+
|
|
55
|
+
Work in that repository (check its current branch/worktree state first — the capture-time environment above may be stale).
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildPrompt(issue, { apiBase, project } = {}) {
|
|
60
|
+
const title = issue.title || issue.id;
|
|
61
|
+
const tags = (issue.tags || []).length ? ` | tags: ${issue.tags.join(', ')}` : '';
|
|
62
|
+
|
|
63
|
+
return `Work on this issue from my issue tracker.
|
|
64
|
+
|
|
65
|
+
# ${issue.id}: ${title}
|
|
66
|
+
|
|
67
|
+
Type: ${issue.type} | Severity: ${issue.severity}/5${tags} | Project: ${issue.project}
|
|
68
|
+
|
|
69
|
+
## What I'm seeing
|
|
70
|
+
|
|
71
|
+
${issue.seeing}
|
|
72
|
+
|
|
73
|
+
## What I expect
|
|
74
|
+
|
|
75
|
+
${issue.expecting}
|
|
76
|
+
${selectionBlock(issue.context)}${contextBlock(issue.context)}
|
|
77
|
+
${repoBlock(project)}
|
|
78
|
+
## Issue file
|
|
79
|
+
|
|
80
|
+
The full issue (including the complete context snapshot JSON and comments) lives at:
|
|
81
|
+
${issue.file}
|
|
82
|
+
|
|
83
|
+
## Tracker workflow
|
|
84
|
+
|
|
85
|
+
The tracker API runs at ${apiBase}. As you work:
|
|
86
|
+
|
|
87
|
+
1. Mark the issue in-progress when you start:
|
|
88
|
+
curl -X PATCH ${apiBase}/api/issues/${issue.id} -H 'Content-Type: application/json' -d '{"status": "in-progress"}'
|
|
89
|
+
2. Leave comments for anything notable along the way:
|
|
90
|
+
curl -X POST ${apiBase}/api/issues/${issue.id}/comments -H 'Content-Type: application/json' -d '{"author": "claude", "text": "<note>"}'
|
|
91
|
+
3. When the fix is ready, resolve it (this records your summary + touched files and moves it to in-review for me to verify):
|
|
92
|
+
curl -X POST ${apiBase}/api/issues/${issue.id}/resolve -H 'Content-Type: application/json' -d '{"resolution": "<what you changed, why, and how it was tested>", "modifiedFiles": ["path/one", "path/two"]}'
|
|
93
|
+
|
|
94
|
+
If this project's issue-tracker MCP server is connected, prefer its tools (update_issue, add_comment, resolve_issue) over curl. If the tracker server is down, you can edit the issue file's frontmatter directly (status: in-progress | in-review) and append to its ## Comments section (format: "### <author> — <ISO timestamp>").
|
|
95
|
+
`;
|
|
96
|
+
}
|