@leejungkiin/awkit 1.1.7 → 1.2.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/README.md +36 -4
- package/bin/awk.js +2 -2
- package/package.json +6 -3
- package/skill-packs/neural-memory/skills/nm-memory-sync/SKILL.md +14 -1
- package/skills/gitnexus-intelligence/SKILL.md +224 -0
- package/skills/orchestrator/SKILL.md +9 -0
- package/skills/symphony-orchestrator/SKILL.md +9 -7
- package/workflows/gitnexus.md +123 -0
- package/symphony/LICENSE +0 -21
- package/symphony/README.md +0 -178
- package/symphony/app/api/agents/route.js +0 -152
- package/symphony/app/api/events/route.js +0 -22
- package/symphony/app/api/knowledge/route.js +0 -253
- package/symphony/app/api/locks/route.js +0 -29
- package/symphony/app/api/notes/route.js +0 -125
- package/symphony/app/api/preflight/route.js +0 -23
- package/symphony/app/api/projects/route.js +0 -116
- package/symphony/app/api/roles/route.js +0 -134
- package/symphony/app/api/skills/route.js +0 -82
- package/symphony/app/api/status/route.js +0 -18
- package/symphony/app/api/tasks/route.js +0 -157
- package/symphony/app/api/workflows/route.js +0 -61
- package/symphony/app/api/workspaces/route.js +0 -15
- package/symphony/app/globals.css +0 -2605
- package/symphony/app/layout.js +0 -20
- package/symphony/app/page.js +0 -2122
- package/symphony/cli/index.js +0 -1060
- package/symphony/core/agent-manager.js +0 -357
- package/symphony/core/context-bus.js +0 -100
- package/symphony/core/db.js +0 -223
- package/symphony/core/file-lock-manager.js +0 -154
- package/symphony/core/merge-pipeline.js +0 -234
- package/symphony/core/orchestrator.js +0 -236
- package/symphony/core/task-manager.js +0 -335
- package/symphony/core/workspace-manager.js +0 -168
- package/symphony/jsconfig.json +0 -7
- package/symphony/lib/core.mjs +0 -1034
- package/symphony/mcp/index.js +0 -29
- package/symphony/mcp/server.js +0 -110
- package/symphony/mcp/tools/context.js +0 -80
- package/symphony/mcp/tools/locks.js +0 -99
- package/symphony/mcp/tools/status.js +0 -82
- package/symphony/mcp/tools/tasks.js +0 -216
- package/symphony/mcp/tools/workspace.js +0 -143
- package/symphony/next.config.mjs +0 -7
- package/symphony/package.json +0 -53
- package/symphony/scripts/postinstall.js +0 -49
- package/symphony/symphony.config.js +0 -41
package/symphony/README.md
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
# 🎼 AWKit Symphony
|
|
2
|
-
|
|
3
|
-
> Multi-Agent Orchestration for AI Coding Assistants
|
|
4
|
-
|
|
5
|
-
Symphony coordinates multiple AI agents working on the same codebase — managing tasks, preventing file conflicts, and providing real-time visibility through a dashboard.
|
|
6
|
-
|
|
7
|
-
## ✨ Features
|
|
8
|
-
|
|
9
|
-
| Feature | Description |
|
|
10
|
-
|---------|-------------|
|
|
11
|
-
| **Task Management** | Create, assign, track tasks with priority & acceptance criteria |
|
|
12
|
-
| **Git Isolation** | Auto worktree/clone per task — agents work on separate branches |
|
|
13
|
-
| **File Locking** | Pessimistic locking prevents two agents from editing the same file |
|
|
14
|
-
| **Merge Pipeline** | Auto-rebase + fast-forward merge when tasks complete |
|
|
15
|
-
| **MCP Server** | 14 tools for IDE integration via Model Context Protocol |
|
|
16
|
-
| **Dashboard** | Real-time Kanban board, agent status, events feed |
|
|
17
|
-
| **Context Bus** | Event pub/sub — agents notify each other of changes |
|
|
18
|
-
|
|
19
|
-
## 🚀 Quick Start
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
# Install
|
|
23
|
-
cd symphony
|
|
24
|
-
npm install
|
|
25
|
-
|
|
26
|
-
# Start dashboard (port 3100)
|
|
27
|
-
npm run dev
|
|
28
|
-
|
|
29
|
-
# Or use CLI
|
|
30
|
-
node cli/index.js status
|
|
31
|
-
node cli/index.js task list
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## 📋 CLI Commands
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# System
|
|
38
|
-
symphony status # Show system status
|
|
39
|
-
symphony start # Start dashboard server
|
|
40
|
-
symphony dashboard # Open dashboard in browser
|
|
41
|
-
|
|
42
|
-
# Tasks
|
|
43
|
-
symphony task list [-s ready] # List tasks (filter by status)
|
|
44
|
-
symphony task create "Feature X" -p 1 # Create task (priority 1-3)
|
|
45
|
-
symphony task show <id> # Show task details
|
|
46
|
-
|
|
47
|
-
# Workspaces
|
|
48
|
-
symphony workspace list # List active workspaces
|
|
49
|
-
symphony workspace create <task-id> # Create workspace for task
|
|
50
|
-
symphony workspace merge <task-id> # Auto-merge completed task
|
|
51
|
-
symphony workspace clean # Remove merged workspaces
|
|
52
|
-
|
|
53
|
-
# File Locks
|
|
54
|
-
symphony lock list # Show active locks
|
|
55
|
-
symphony lock release <file> # Force-release stuck lock
|
|
56
|
-
|
|
57
|
-
# MCP Server
|
|
58
|
-
symphony mcp-serve [-n "Agent Name"] # Start MCP server (stdio)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## 🔌 MCP Integration
|
|
62
|
-
|
|
63
|
-
Add to your IDE's MCP config:
|
|
64
|
-
|
|
65
|
-
```json
|
|
66
|
-
{
|
|
67
|
-
"mcpServers": {
|
|
68
|
-
"symphony": {
|
|
69
|
-
"command": "node",
|
|
70
|
-
"args": ["/path/to/symphony/mcp/server.js"],
|
|
71
|
-
"env": {
|
|
72
|
-
"SYMPHONY_AGENT_NAME": "my-agent"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Available MCP Tools (14)
|
|
80
|
-
|
|
81
|
-
| Tool | Description |
|
|
82
|
-
|------|-------------|
|
|
83
|
-
| `symphony_available_tasks` | List available tasks |
|
|
84
|
-
| `symphony_claim_task` | Claim a task (get workspace + branch) |
|
|
85
|
-
| `symphony_report_progress` | Report progress (0-100%) |
|
|
86
|
-
| `symphony_complete_task` | Complete task + trigger merge |
|
|
87
|
-
| `symphony_abandon_task` | Abandon a task |
|
|
88
|
-
| `symphony_check_files` | Check file lock status |
|
|
89
|
-
| `symphony_lock_files` | Lock files for editing |
|
|
90
|
-
| `symphony_unlock_files` | Release file locks |
|
|
91
|
-
| `symphony_broadcast` | Broadcast event to other agents |
|
|
92
|
-
| `symphony_events` | Query context bus events |
|
|
93
|
-
| `symphony_status` | Get system status |
|
|
94
|
-
| `symphony_create_task` | Create a new task |
|
|
95
|
-
| `symphony_workspace_status` | Get workspace info + diff stats |
|
|
96
|
-
| `symphony_merge_task` | Run auto-merge pipeline |
|
|
97
|
-
|
|
98
|
-
## 🏗️ Architecture
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
symphony/
|
|
102
|
-
├── core/ # Engine
|
|
103
|
-
│ ├── db.js # SQLite (WAL mode)
|
|
104
|
-
│ ├── task-manager.js # Task CRUD + state machine
|
|
105
|
-
│ ├── workspace-manager.js # Git worktree lifecycle
|
|
106
|
-
│ ├── merge-pipeline.js # Auto-rebase + merge
|
|
107
|
-
│ ├── file-lock-manager.js # Pessimistic file locking
|
|
108
|
-
│ ├── context-bus.js # Event pub/sub
|
|
109
|
-
│ └── orchestrator.js # Agent dispatch + state
|
|
110
|
-
├── mcp/ # MCP Server
|
|
111
|
-
│ ├── server.js # stdio transport
|
|
112
|
-
│ ├── index.js # Tool registry (14 tools)
|
|
113
|
-
│ └── tools/ # Tool implementations
|
|
114
|
-
├── cli/ # CLI
|
|
115
|
-
│ └── index.js # Commander.js commands
|
|
116
|
-
├── app/ # Dashboard (Next.js)
|
|
117
|
-
│ ├── page.js # Kanban + status + events
|
|
118
|
-
│ └── api/ # REST API routes
|
|
119
|
-
└── lib/
|
|
120
|
-
└── core.mjs # ESM bridge for Turbopack
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## ⚙️ Configuration
|
|
124
|
-
|
|
125
|
-
Edit `symphony.config.js`:
|
|
126
|
-
|
|
127
|
-
```js
|
|
128
|
-
module.exports = {
|
|
129
|
-
port: 3100,
|
|
130
|
-
maxAgents: 3,
|
|
131
|
-
workspace: {
|
|
132
|
-
type: 'hybrid', // 'worktree' | 'clone' | 'hybrid'
|
|
133
|
-
cloneThreshold: 30, // files > 30 → full clone
|
|
134
|
-
},
|
|
135
|
-
git: {
|
|
136
|
-
autoMerge: true,
|
|
137
|
-
targetBranch: 'main',
|
|
138
|
-
branchPrefix: 'symphony/',
|
|
139
|
-
},
|
|
140
|
-
locks: {
|
|
141
|
-
strategy: 'pessimistic',
|
|
142
|
-
autoRelease: 3600, // seconds
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## 📊 Dashboard
|
|
148
|
-
|
|
149
|
-
The dashboard runs on `http://localhost:3100` and provides:
|
|
150
|
-
|
|
151
|
-
- **Kanban Board** — Tasks by status (Ready → In Progress → Review → Done)
|
|
152
|
-
- **Agent Panel** — Connected agents with live status
|
|
153
|
-
- **Events Feed** — Real-time context bus events
|
|
154
|
-
- **Lock Panel** — Active file locks with force-release
|
|
155
|
-
- **Stats Bar** — Task counts and system health
|
|
156
|
-
|
|
157
|
-
## 🔄 Task Lifecycle
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
Ready → Claimed → In Progress → Review → Done
|
|
161
|
-
│ │
|
|
162
|
-
└─── Abandoned ◀────────────────────────┘
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
1. **Create** task via CLI or dashboard
|
|
166
|
-
2. **Agent claims** via MCP → gets isolated workspace + branch
|
|
167
|
-
3. **Agent works** → reports progress, locks files, broadcasts events
|
|
168
|
-
4. **Agent completes** → triggers auto-merge pipeline
|
|
169
|
-
5. **Merge** → rebase onto main, fast-forward, cleanup workspace
|
|
170
|
-
|
|
171
|
-
## 📦 Tech Stack
|
|
172
|
-
|
|
173
|
-
- **Runtime:** Node.js
|
|
174
|
-
- **Database:** SQLite (via better-sqlite3, WAL mode)
|
|
175
|
-
- **Dashboard:** Next.js 16 + React 19
|
|
176
|
-
- **MCP:** @modelcontextprotocol/sdk
|
|
177
|
-
- **CLI:** Commander.js
|
|
178
|
-
- **Git:** Native git commands (worktree, rebase, merge)
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
listAllAgents, registerAgent, updateAgentProfile, removeAgent, dispatchTask,
|
|
4
|
-
listProjectAgents, getProjectAgent, createProjectAgent, updateProjectAgent,
|
|
5
|
-
removeProjectAgent, attachProjectAgentSession, detachProjectAgentSession,
|
|
6
|
-
} from '../../../lib/core.mjs';
|
|
7
|
-
|
|
8
|
-
// GET /api/agents — List agents (supports ?project=X for project-scoped filtering)
|
|
9
|
-
export async function GET(request) {
|
|
10
|
-
try {
|
|
11
|
-
const { searchParams } = new URL(request.url);
|
|
12
|
-
const project = searchParams.get('project');
|
|
13
|
-
const type = searchParams.get('type') || 'project'; // 'project' (default) or 'legacy'
|
|
14
|
-
|
|
15
|
-
if (type === 'legacy') {
|
|
16
|
-
const agents = listAllAgents();
|
|
17
|
-
return NextResponse.json({ agents, type: 'legacy' });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const agents = listProjectAgents(project || undefined);
|
|
21
|
-
return NextResponse.json({ agents, type: 'project' });
|
|
22
|
-
} catch (error) {
|
|
23
|
-
return NextResponse.json(
|
|
24
|
-
{ error: error.message },
|
|
25
|
-
{ status: 500 }
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// POST /api/agents — Create agent (project-scoped or legacy)
|
|
31
|
-
export async function POST(request) {
|
|
32
|
-
try {
|
|
33
|
-
const body = await request.json();
|
|
34
|
-
if (!body.id) {
|
|
35
|
-
return NextResponse.json({ error: 'Agent ID is required' }, { status: 400 });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Project-scoped agent (new)
|
|
39
|
-
if (body.projectId || body.project_id) {
|
|
40
|
-
const agent = createProjectAgent({
|
|
41
|
-
id: body.id,
|
|
42
|
-
projectId: body.projectId || body.project_id,
|
|
43
|
-
name: body.name || body.id,
|
|
44
|
-
skills: body.skills || [],
|
|
45
|
-
icon: body.icon,
|
|
46
|
-
color: body.color,
|
|
47
|
-
});
|
|
48
|
-
return NextResponse.json({ agent, type: 'project' }, { status: 201 });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Legacy agent
|
|
52
|
-
const agent = registerAgent(body.id, body.name);
|
|
53
|
-
if (body.specialties || body.color || body.max_concurrent) {
|
|
54
|
-
updateAgentProfile(body.id, {
|
|
55
|
-
specialties: body.specialties,
|
|
56
|
-
color: body.color,
|
|
57
|
-
max_concurrent: body.max_concurrent,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
const updated = listAllAgents().find(a => a.id === body.id);
|
|
61
|
-
return NextResponse.json({ agent: updated, type: 'legacy' }, { status: 201 });
|
|
62
|
-
} catch (error) {
|
|
63
|
-
return NextResponse.json(
|
|
64
|
-
{ error: error.message },
|
|
65
|
-
{ status: 500 }
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// PATCH /api/agents — Update agent, assign task, attach, or detach
|
|
71
|
-
export async function PATCH(request) {
|
|
72
|
-
try {
|
|
73
|
-
const body = await request.json();
|
|
74
|
-
if (!body.id) {
|
|
75
|
-
return NextResponse.json({ error: 'Agent ID is required' }, { status: 400 });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Check if this is a project agent
|
|
79
|
-
const projectAgent = getProjectAgent(body.id);
|
|
80
|
-
|
|
81
|
-
if (projectAgent) {
|
|
82
|
-
// Project agent actions
|
|
83
|
-
if (body.action === 'attach' && body.sessionId) {
|
|
84
|
-
const agent = attachProjectAgentSession(body.id, body.sessionId);
|
|
85
|
-
return NextResponse.json({ agent, action: 'attached' });
|
|
86
|
-
}
|
|
87
|
-
if (body.action === 'detach') {
|
|
88
|
-
const agent = detachProjectAgentSession(body.id);
|
|
89
|
-
return NextResponse.json({ agent, action: 'detached' });
|
|
90
|
-
}
|
|
91
|
-
if (body.action === 'assign' && body.taskId) {
|
|
92
|
-
const task = dispatchTask(body.id, body.taskId);
|
|
93
|
-
return NextResponse.json({ task, action: 'assigned' });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Update project agent fields
|
|
97
|
-
const agent = updateProjectAgent(body.id, {
|
|
98
|
-
name: body.name,
|
|
99
|
-
skills: body.skills,
|
|
100
|
-
icon: body.icon,
|
|
101
|
-
color: body.color,
|
|
102
|
-
});
|
|
103
|
-
return NextResponse.json({ agent, type: 'project' });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Legacy agent fallback
|
|
107
|
-
if (body.action === 'assign' && body.taskId) {
|
|
108
|
-
const task = dispatchTask(body.id, body.taskId);
|
|
109
|
-
return NextResponse.json({ task });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const agent = updateAgentProfile(body.id, {
|
|
113
|
-
name: body.name,
|
|
114
|
-
specialties: body.specialties,
|
|
115
|
-
color: body.color,
|
|
116
|
-
max_concurrent: body.max_concurrent,
|
|
117
|
-
});
|
|
118
|
-
const updated = listAllAgents().find(a => a.id === body.id);
|
|
119
|
-
return NextResponse.json({ agent: updated, type: 'legacy' });
|
|
120
|
-
} catch (error) {
|
|
121
|
-
return NextResponse.json(
|
|
122
|
-
{ error: error.message },
|
|
123
|
-
{ status: 500 }
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// DELETE /api/agents — Remove agent
|
|
129
|
-
export async function DELETE(request) {
|
|
130
|
-
try {
|
|
131
|
-
const body = await request.json();
|
|
132
|
-
if (!body.id) {
|
|
133
|
-
return NextResponse.json({ error: 'Agent ID is required' }, { status: 400 });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Try project agent first
|
|
137
|
-
const projectAgent = getProjectAgent(body.id);
|
|
138
|
-
if (projectAgent) {
|
|
139
|
-
removeProjectAgent(body.id);
|
|
140
|
-
return NextResponse.json({ success: true, type: 'project' });
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Legacy fallback
|
|
144
|
-
removeAgent(body.id);
|
|
145
|
-
return NextResponse.json({ success: true, type: 'legacy' });
|
|
146
|
-
} catch (error) {
|
|
147
|
-
return NextResponse.json(
|
|
148
|
-
{ error: error.message },
|
|
149
|
-
{ status: 500 }
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { queryEvents } from '../../../lib/core.mjs';
|
|
3
|
-
|
|
4
|
-
// GET /api/events — List recent events (scoped by project)
|
|
5
|
-
export async function GET(request) {
|
|
6
|
-
try {
|
|
7
|
-
const { searchParams } = new URL(request.url);
|
|
8
|
-
const since = searchParams.get('since') || undefined;
|
|
9
|
-
const eventType = searchParams.get('type') || undefined;
|
|
10
|
-
const project = searchParams.get('project') || undefined;
|
|
11
|
-
const limit = parseInt(searchParams.get('limit') || '30');
|
|
12
|
-
|
|
13
|
-
const events = queryEvents({ since, eventType, project, limit });
|
|
14
|
-
|
|
15
|
-
return NextResponse.json({ events });
|
|
16
|
-
} catch (error) {
|
|
17
|
-
return NextResponse.json(
|
|
18
|
-
{ error: error.message },
|
|
19
|
-
{ status: 500 }
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const KNOWLEDGE_DIR = path.join(
|
|
6
|
-
process.env.HOME || process.env.USERPROFILE,
|
|
7
|
-
'.gemini', 'antigravity', 'knowledge'
|
|
8
|
-
);
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* GET /api/knowledge
|
|
12
|
-
*
|
|
13
|
-
* Query params:
|
|
14
|
-
* - (none) → List all KI summaries
|
|
15
|
-
* - id=<folder> → Get KI detail + artifact list
|
|
16
|
-
* - id=<folder>&file=<path> → Read artifact file content
|
|
17
|
-
*/
|
|
18
|
-
export async function GET(request) {
|
|
19
|
-
try {
|
|
20
|
-
const { searchParams } = new URL(request.url);
|
|
21
|
-
const id = searchParams.get('id');
|
|
22
|
-
const file = searchParams.get('file');
|
|
23
|
-
|
|
24
|
-
// Read a specific artifact file
|
|
25
|
-
if (id && file) {
|
|
26
|
-
const filePath = path.join(KNOWLEDGE_DIR, id, file);
|
|
27
|
-
if (!filePath.startsWith(KNOWLEDGE_DIR)) {
|
|
28
|
-
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
|
29
|
-
}
|
|
30
|
-
if (!fs.existsSync(filePath)) {
|
|
31
|
-
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
|
32
|
-
}
|
|
33
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
-
return NextResponse.json({ content, path: file });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Get detail of a specific KI
|
|
38
|
-
if (id) {
|
|
39
|
-
const kiDir = path.join(KNOWLEDGE_DIR, id);
|
|
40
|
-
if (!fs.existsSync(kiDir)) {
|
|
41
|
-
return NextResponse.json({ error: 'KI not found' }, { status: 404 });
|
|
42
|
-
}
|
|
43
|
-
const metadata = readMetadata(kiDir);
|
|
44
|
-
const artifacts = listArtifacts(kiDir);
|
|
45
|
-
return NextResponse.json({ item: { id, ...metadata, artifacts } });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// List all KIs
|
|
49
|
-
if (!fs.existsSync(KNOWLEDGE_DIR)) {
|
|
50
|
-
return NextResponse.json({ items: [] });
|
|
51
|
-
}
|
|
52
|
-
const entries = fs.readdirSync(KNOWLEDGE_DIR, { withFileTypes: true });
|
|
53
|
-
const items = [];
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
if (!entry.isDirectory()) continue;
|
|
56
|
-
const kiDir = path.join(KNOWLEDGE_DIR, entry.name);
|
|
57
|
-
const metaPath = path.join(kiDir, 'metadata.json');
|
|
58
|
-
if (!fs.existsSync(metaPath)) continue;
|
|
59
|
-
const metadata = readMetadata(kiDir);
|
|
60
|
-
const artifactCount = countArtifacts(kiDir);
|
|
61
|
-
const stat = fs.statSync(metaPath);
|
|
62
|
-
items.push({
|
|
63
|
-
id: entry.name,
|
|
64
|
-
title: metadata.title || entry.name,
|
|
65
|
-
summary: metadata.summary || '',
|
|
66
|
-
referenceCount: (metadata.references || []).length,
|
|
67
|
-
artifactCount,
|
|
68
|
-
updatedAt: stat.mtime.toISOString(),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
// Sort by updatedAt desc
|
|
72
|
-
items.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
73
|
-
return NextResponse.json({ items });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* POST /api/knowledge — Create a new KI
|
|
81
|
-
* Body: { id, title, summary, references? }
|
|
82
|
-
*/
|
|
83
|
-
export async function POST(request) {
|
|
84
|
-
try {
|
|
85
|
-
const body = await request.json();
|
|
86
|
-
if (!body.id || !body.title) {
|
|
87
|
-
return NextResponse.json({ error: 'id and title are required' }, { status: 400 });
|
|
88
|
-
}
|
|
89
|
-
// Sanitize id
|
|
90
|
-
const safeId = body.id.replace(/[^a-z0-9_-]/gi, '_').toLowerCase();
|
|
91
|
-
const kiDir = path.join(KNOWLEDGE_DIR, safeId);
|
|
92
|
-
if (fs.existsSync(kiDir)) {
|
|
93
|
-
return NextResponse.json({ error: 'KI already exists' }, { status: 409 });
|
|
94
|
-
}
|
|
95
|
-
const artifactsDir = path.join(kiDir, 'artifacts');
|
|
96
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
97
|
-
|
|
98
|
-
const metadata = {
|
|
99
|
-
title: body.title,
|
|
100
|
-
summary: body.summary || '',
|
|
101
|
-
references: body.references || [],
|
|
102
|
-
};
|
|
103
|
-
fs.writeFileSync(
|
|
104
|
-
path.join(kiDir, 'metadata.json'),
|
|
105
|
-
JSON.stringify(metadata, null, 2),
|
|
106
|
-
'utf-8'
|
|
107
|
-
);
|
|
108
|
-
// Create overview.md stub
|
|
109
|
-
fs.writeFileSync(
|
|
110
|
-
path.join(artifactsDir, 'overview.md'),
|
|
111
|
-
`# ${body.title}\n\n${body.summary || 'TODO: Add content'}\n`,
|
|
112
|
-
'utf-8'
|
|
113
|
-
);
|
|
114
|
-
return NextResponse.json({
|
|
115
|
-
item: { id: safeId, ...metadata, artifacts: ['artifacts/overview.md'] }
|
|
116
|
-
}, { status: 201 });
|
|
117
|
-
} catch (error) {
|
|
118
|
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* PATCH /api/knowledge — Update metadata or file content
|
|
124
|
-
* Body: { id, file?, content?, metadata? }
|
|
125
|
-
*/
|
|
126
|
-
export async function PATCH(request) {
|
|
127
|
-
try {
|
|
128
|
-
const body = await request.json();
|
|
129
|
-
if (!body.id) {
|
|
130
|
-
return NextResponse.json({ error: 'id is required' }, { status: 400 });
|
|
131
|
-
}
|
|
132
|
-
const kiDir = path.join(KNOWLEDGE_DIR, body.id);
|
|
133
|
-
if (!fs.existsSync(kiDir)) {
|
|
134
|
-
return NextResponse.json({ error: 'KI not found' }, { status: 404 });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Update file content
|
|
138
|
-
if (body.file && body.content !== undefined) {
|
|
139
|
-
const filePath = path.join(kiDir, body.file);
|
|
140
|
-
if (!filePath.startsWith(KNOWLEDGE_DIR)) {
|
|
141
|
-
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
|
142
|
-
}
|
|
143
|
-
// Ensure parent dir exists
|
|
144
|
-
const parentDir = path.dirname(filePath);
|
|
145
|
-
if (!fs.existsSync(parentDir)) {
|
|
146
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
147
|
-
}
|
|
148
|
-
fs.writeFileSync(filePath, body.content, 'utf-8');
|
|
149
|
-
return NextResponse.json({ success: true, file: body.file });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Update metadata
|
|
153
|
-
if (body.metadata) {
|
|
154
|
-
const metaPath = path.join(kiDir, 'metadata.json');
|
|
155
|
-
const existing = readMetadata(kiDir);
|
|
156
|
-
const updated = {
|
|
157
|
-
...existing,
|
|
158
|
-
...body.metadata,
|
|
159
|
-
};
|
|
160
|
-
fs.writeFileSync(metaPath, JSON.stringify(updated, null, 2), 'utf-8');
|
|
161
|
-
return NextResponse.json({ success: true, metadata: updated });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return NextResponse.json({ error: 'No update specified (need file+content or metadata)' }, { status: 400 });
|
|
165
|
-
} catch (error) {
|
|
166
|
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* DELETE /api/knowledge — Delete an artifact file
|
|
172
|
-
* Body: { id, file }
|
|
173
|
-
*/
|
|
174
|
-
export async function DELETE(request) {
|
|
175
|
-
try {
|
|
176
|
-
const body = await request.json();
|
|
177
|
-
if (!body.id || !body.file) {
|
|
178
|
-
return NextResponse.json({ error: 'id and file are required' }, { status: 400 });
|
|
179
|
-
}
|
|
180
|
-
// Prevent deleting metadata.json
|
|
181
|
-
if (body.file === 'metadata.json') {
|
|
182
|
-
return NextResponse.json({ error: 'Cannot delete metadata.json' }, { status: 400 });
|
|
183
|
-
}
|
|
184
|
-
const filePath = path.join(KNOWLEDGE_DIR, body.id, body.file);
|
|
185
|
-
if (!filePath.startsWith(KNOWLEDGE_DIR)) {
|
|
186
|
-
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
|
187
|
-
}
|
|
188
|
-
if (!fs.existsSync(filePath)) {
|
|
189
|
-
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
|
190
|
-
}
|
|
191
|
-
fs.unlinkSync(filePath);
|
|
192
|
-
return NextResponse.json({ success: true });
|
|
193
|
-
} catch (error) {
|
|
194
|
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
199
|
-
|
|
200
|
-
function readMetadata(kiDir) {
|
|
201
|
-
const metaPath = path.join(kiDir, 'metadata.json');
|
|
202
|
-
if (!fs.existsSync(metaPath)) return { title: '', summary: '', references: [] };
|
|
203
|
-
try {
|
|
204
|
-
return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
205
|
-
} catch {
|
|
206
|
-
return { title: '', summary: '', references: [] };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function listArtifacts(kiDir) {
|
|
211
|
-
const results = [];
|
|
212
|
-
function walk(dir, prefix) {
|
|
213
|
-
if (!fs.existsSync(dir)) return;
|
|
214
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
215
|
-
for (const entry of entries) {
|
|
216
|
-
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
217
|
-
if (entry.name === 'metadata.json') continue;
|
|
218
|
-
if (entry.name === '.DS_Store') continue;
|
|
219
|
-
if (entry.isDirectory()) {
|
|
220
|
-
walk(path.join(dir, entry.name), relPath);
|
|
221
|
-
} else {
|
|
222
|
-
const stat = fs.statSync(path.join(dir, entry.name));
|
|
223
|
-
results.push({
|
|
224
|
-
path: relPath,
|
|
225
|
-
name: entry.name,
|
|
226
|
-
size: stat.size,
|
|
227
|
-
updatedAt: stat.mtime.toISOString(),
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
walk(kiDir, '');
|
|
233
|
-
return results;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function countArtifacts(kiDir) {
|
|
237
|
-
const artifactsDir = path.join(kiDir, 'artifacts');
|
|
238
|
-
if (!fs.existsSync(artifactsDir)) return 0;
|
|
239
|
-
let count = 0;
|
|
240
|
-
function walk(dir) {
|
|
241
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
242
|
-
for (const entry of entries) {
|
|
243
|
-
if (entry.name === '.DS_Store') continue;
|
|
244
|
-
if (entry.isDirectory()) {
|
|
245
|
-
walk(path.join(dir, entry.name));
|
|
246
|
-
} else {
|
|
247
|
-
count++;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
walk(artifactsDir);
|
|
252
|
-
return count;
|
|
253
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { forceReleaseLock } from '../../../lib/core.mjs';
|
|
3
|
-
|
|
4
|
-
// DELETE /api/locks — Force-release a file lock
|
|
5
|
-
export async function DELETE(request) {
|
|
6
|
-
try {
|
|
7
|
-
const body = await request.json();
|
|
8
|
-
|
|
9
|
-
if (!body.file) {
|
|
10
|
-
return NextResponse.json(
|
|
11
|
-
{ error: 'File path is required' },
|
|
12
|
-
{ status: 400 }
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const released = forceReleaseLock(body.file);
|
|
17
|
-
|
|
18
|
-
return NextResponse.json({
|
|
19
|
-
released,
|
|
20
|
-
file: body.file,
|
|
21
|
-
message: released ? 'Lock released' : 'No lock found',
|
|
22
|
-
});
|
|
23
|
-
} catch (error) {
|
|
24
|
-
return NextResponse.json(
|
|
25
|
-
{ error: error.message },
|
|
26
|
-
{ status: 500 }
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
}
|