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/mcp-server.mjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MCP stdio server for the issue tracker. Reads/writes the markdown store
|
|
3
|
+
// directly, so it works even when the web server is down.
|
|
4
|
+
// Register (published): claude mcp add vibeops -- npx -y -p vibeops-tracker vibeops-mcp
|
|
5
|
+
// Register (from clone): claude mcp add vibeops -- node /path/to/vibeops-tracker/mcp-server.mjs
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import {
|
|
10
|
+
STATUSES,
|
|
11
|
+
TYPES,
|
|
12
|
+
listProjects,
|
|
13
|
+
createIssue,
|
|
14
|
+
getIssue,
|
|
15
|
+
listIssues,
|
|
16
|
+
updateIssue,
|
|
17
|
+
addComment,
|
|
18
|
+
resolveIssue,
|
|
19
|
+
searchIssues,
|
|
20
|
+
deleteIssue,
|
|
21
|
+
} from './lib/store.mjs';
|
|
22
|
+
import { resolveDataDir } from './lib/data-dir.mjs';
|
|
23
|
+
|
|
24
|
+
const DATA_DIR = resolveDataDir();
|
|
25
|
+
const API_BASE = process.env.TRACKER_URL || 'http://localhost:4400';
|
|
26
|
+
|
|
27
|
+
const INSTRUCTIONS = `# Issue tracker — how agents should work with it
|
|
28
|
+
|
|
29
|
+
Issues are bug reports / feature requests captured from running apps (via an embedded
|
|
30
|
+
widget with a browser context snapshot) or filed by humans and agents. One markdown
|
|
31
|
+
file per issue lives under ${DATA_DIR}/<project>/issues/. NEVER edit those files
|
|
32
|
+
directly — always go through these MCP tools (or the REST API at ${API_BASE}/api).
|
|
33
|
+
delete_issue permanently removes an issue (open or archived). It is IRREVERSIBLE —
|
|
34
|
+
there is no archive copy — and requires confirm:true. Reserve it for stale, test,
|
|
35
|
+
duplicate, or clearly no-longer-relevant issues, and prefer to delete only when the
|
|
36
|
+
human asked or the issue is unambiguous junk; for real issues, prefer resolve_issue or
|
|
37
|
+
add_comment. Never move an issue to done yourself — a human verifies in-review work.
|
|
38
|
+
|
|
39
|
+
Workflow:
|
|
40
|
+
1. search_issues first when filing — avoid duplicates; link regressions with related_to.
|
|
41
|
+
2. To pick up work: list_issues {project, status: "backlog"} — the FIRST result is the
|
|
42
|
+
highest-priority item. Read it fully with get_issue; the captured ## Context
|
|
43
|
+
(URL, clicks, fetches, JS errors, capture-time git state) usually localizes the bug.
|
|
44
|
+
Capture-time branch/worktree info may be stale — verify against the repo's current state.
|
|
45
|
+
3. Before coding: update_issue {id, status: "in-progress"}. Reproduce from context first.
|
|
46
|
+
4. Leave add_comment notes (author: "claude") for anything notable mid-work.
|
|
47
|
+
5. Finish with resolve_issue {id, resolution, modified_files} — a PR-style summary of
|
|
48
|
+
what changed, why, and how it was tested. This sets status to in-review; a HUMAN
|
|
49
|
+
moves it to done after verifying. Never set done yourself.
|
|
50
|
+
6. File new issues you notice with create_issue (seeing/expecting required).
|
|
51
|
+
|
|
52
|
+
Statuses: ${STATUSES.join(' | ')}. Severity 1 (cosmetic) – 5 (blocker).`;
|
|
53
|
+
|
|
54
|
+
const TOOLS = [
|
|
55
|
+
{
|
|
56
|
+
name: 'list_projects',
|
|
57
|
+
description: 'List all projects registered in the issue tracker.',
|
|
58
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
59
|
+
handler: () => listProjects(DATA_DIR),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'list_issues',
|
|
63
|
+
description:
|
|
64
|
+
'List issues for a project, priority-ordered (by status, then board position). Optionally filter by status. Statuses: backlog, in-progress, in-review, done. The first backlog issue is the highest-priority unstarted work. The captured browser context is omitted for brevity (hasContext flags it); call get_issue for the full snapshot.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
project: { type: 'string', description: 'Project key, e.g. "platform"' },
|
|
69
|
+
status: { type: 'string', enum: STATUSES, description: 'Optional status filter' },
|
|
70
|
+
},
|
|
71
|
+
required: ['project'],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
handler: ({ project, status }) =>
|
|
75
|
+
listIssues(DATA_DIR, project, { status }).map(({ context, ...rest }) => ({
|
|
76
|
+
...rest,
|
|
77
|
+
hasContext: !!context,
|
|
78
|
+
})),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'get_issue',
|
|
82
|
+
description: 'Get one issue in full: fields, seeing/expecting, captured browser context, comments, and the path of its markdown file.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: { id: { type: 'string', description: 'Issue id, e.g. "platform-3"' } },
|
|
86
|
+
required: ['id'],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
handler: ({ id }) => {
|
|
90
|
+
const issue = getIssue(DATA_DIR, id);
|
|
91
|
+
if (!issue) {
|
|
92
|
+
const err = new Error(`Issue not found: ${id}`);
|
|
93
|
+
err.code = 'NOT_FOUND';
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
return issue;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'create_issue',
|
|
101
|
+
description: 'File a new issue into the tracker (e.g. something noticed while working in a project).',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
project: { type: 'string', description: 'Project key, e.g. "platform"' },
|
|
106
|
+
title: { type: 'string' },
|
|
107
|
+
type: { type: 'string', enum: TYPES, default: 'other' },
|
|
108
|
+
severity: { type: 'integer', minimum: 1, maximum: 5, default: 3 },
|
|
109
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
110
|
+
seeing: { type: 'string', description: 'What is wrong / current behavior' },
|
|
111
|
+
expecting: { type: 'string', description: 'Desired behavior / requirements' },
|
|
112
|
+
relatedTo: { type: 'string', description: 'Optional id of a related prior issue' },
|
|
113
|
+
},
|
|
114
|
+
required: ['project', 'seeing', 'expecting'],
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
},
|
|
117
|
+
handler: (args) => createIssue(DATA_DIR, args),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'update_issue',
|
|
121
|
+
description:
|
|
122
|
+
'Patch issue fields: status (set in-progress when you start working), title, type, severity, tags, related_to. Only the provided fields change. Board position (ordinal) is human-controlled and not patchable here.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
id: { type: 'string' },
|
|
127
|
+
status: { type: 'string', enum: STATUSES },
|
|
128
|
+
title: { type: 'string' },
|
|
129
|
+
type: { type: 'string', enum: TYPES },
|
|
130
|
+
severity: { type: 'integer', minimum: 1, maximum: 5 },
|
|
131
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
132
|
+
related_to: { type: 'string', description: 'Id of a related prior issue (regression chains)' },
|
|
133
|
+
},
|
|
134
|
+
required: ['id'],
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
},
|
|
137
|
+
handler: ({ id, related_to, ...rest }) => {
|
|
138
|
+
const patch = { ...rest };
|
|
139
|
+
if (related_to !== undefined) patch.relatedTo = related_to;
|
|
140
|
+
if (!Object.keys(patch).length) throw new Error('provide at least one field to update');
|
|
141
|
+
return updateIssue(DATA_DIR, id, patch);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'resolve_issue',
|
|
146
|
+
description:
|
|
147
|
+
'Finish working an issue: record a PR-style resolution (what changed, why, how it was tested) plus the list of modified files, and move it to in-review for human verification. Use this instead of setting status done.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
id: { type: 'string' },
|
|
152
|
+
resolution: { type: 'string', description: 'What you changed, why, and how it was verified' },
|
|
153
|
+
modified_files: { type: 'array', items: { type: 'string' }, description: 'Repo-relative paths you touched' },
|
|
154
|
+
},
|
|
155
|
+
required: ['id', 'resolution'],
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
},
|
|
158
|
+
handler: ({ id, resolution, modified_files }) => resolveIssue(DATA_DIR, id, { resolution, modifiedFiles: modified_files }),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'search_issues',
|
|
162
|
+
description:
|
|
163
|
+
'Case-insensitive substring search across issue ids, titles, tags, seeing/expecting, resolutions, and comments. Search before filing to avoid duplicates. Searches all projects unless one is given; closed (swept) issues excluded unless include_closed.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
query: { type: 'string' },
|
|
168
|
+
project: { type: 'string' },
|
|
169
|
+
status: { type: 'string', enum: STATUSES },
|
|
170
|
+
include_closed: { type: 'boolean', default: false },
|
|
171
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
172
|
+
},
|
|
173
|
+
required: ['query'],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
handler: ({ query, project, status, include_closed, limit }) =>
|
|
177
|
+
searchIssues(DATA_DIR, { query, project, status, includeClosed: include_closed, limit: limit || 20 }).map(
|
|
178
|
+
({ context, ...rest }) => ({ ...rest, hasContext: !!context })
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'get_tracker_instructions',
|
|
183
|
+
description: 'How to work with this issue tracker: the expected agent workflow, status semantics, and rules. Call this once before interacting with issues.',
|
|
184
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
185
|
+
handler: () => INSTRUCTIONS,
|
|
186
|
+
raw: true,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'add_comment',
|
|
190
|
+
description: 'Append a comment to an issue (progress notes, questions, resolution summary).',
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
id: { type: 'string' },
|
|
195
|
+
author: { type: 'string', description: 'Who is commenting, e.g. "claude"' },
|
|
196
|
+
text: { type: 'string' },
|
|
197
|
+
},
|
|
198
|
+
required: ['id', 'author', 'text'],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
},
|
|
201
|
+
handler: ({ id, author, text }) => addComment(DATA_DIR, id, { author, text }),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'delete_issue',
|
|
205
|
+
description:
|
|
206
|
+
'Permanently delete an issue (open or archived). IRREVERSIBLE — the markdown file is removed with no archive copy (only git history could recover it). Reserve for stale, test, duplicate, or clearly no-longer-relevant issues; for real issues prefer resolve_issue or add_comment. Requires confirm: true.',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
id: { type: 'string', description: 'Issue id, e.g. "platform-3"' },
|
|
211
|
+
confirm: { type: 'boolean', description: 'Must be true to proceed — a deliberate-action guard for an irreversible delete.' },
|
|
212
|
+
},
|
|
213
|
+
required: ['id', 'confirm'],
|
|
214
|
+
additionalProperties: false,
|
|
215
|
+
},
|
|
216
|
+
handler: ({ id, confirm }) => {
|
|
217
|
+
if (confirm !== true) throw new Error('refusing to delete without confirm: true (deletion is permanent and has no archive copy)');
|
|
218
|
+
return deleteIssue(DATA_DIR, id);
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const server = new Server(
|
|
224
|
+
{ name: 'issue-tracker', version: '0.1.0' },
|
|
225
|
+
{ capabilities: { tools: {} } }
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
229
|
+
tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
233
|
+
const tool = TOOLS.find((t) => t.name === req.params.name);
|
|
234
|
+
if (!tool) {
|
|
235
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }], isError: true };
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const result = tool.handler(req.params.arguments || {});
|
|
239
|
+
const text = tool.raw ? String(result) : JSON.stringify(result, null, 2);
|
|
240
|
+
return { content: [{ type: 'text', text }] };
|
|
241
|
+
} catch (err) {
|
|
242
|
+
const code = err.code ? ` [${err.code}]` : '';
|
|
243
|
+
return { content: [{ type: 'text', text: `Error${code}: ${err.message}` }], isError: true };
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibeops-tracker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VibeOps Tracker: a local-first issue tracker. Capture, triage, and ship work across all your side projects with an embeddable widget, a Kanban board, markdown storage, and an MCP server that lets AI coding agents work your backlog.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibeops": "bin/cli.mjs",
|
|
8
|
+
"vibeops-mcp": "mcp-server.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"lib/",
|
|
16
|
+
"public/",
|
|
17
|
+
"server.mjs",
|
|
18
|
+
"mcp-server.mjs",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"AUTHORS"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node server.mjs",
|
|
25
|
+
"start:mcp": "node mcp-server.mjs",
|
|
26
|
+
"test": "node --test"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"issue-tracker",
|
|
30
|
+
"bug-tracker",
|
|
31
|
+
"kanban",
|
|
32
|
+
"backlog",
|
|
33
|
+
"triage",
|
|
34
|
+
"mcp",
|
|
35
|
+
"model-context-protocol",
|
|
36
|
+
"ai-agents",
|
|
37
|
+
"claude",
|
|
38
|
+
"claude-code",
|
|
39
|
+
"markdown",
|
|
40
|
+
"local-first",
|
|
41
|
+
"self-hosted",
|
|
42
|
+
"vibe-coding",
|
|
43
|
+
"developer-tools"
|
|
44
|
+
],
|
|
45
|
+
"author": "Igor Gembitsky",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"homepage": "https://github.com/igembitsky/vibeops-tracker#readme",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/igembitsky/vibeops-tracker.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/igembitsky/vibeops-tracker/issues"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
57
|
+
"gray-matter": "^4.0.3"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"jsdom": "^25.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|