@crowdlisten/harness 1.0.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/AGENTS.md +167 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/agent-proxy.d.ts +24 -0
- package/dist/agent-proxy.js +140 -0
- package/dist/agent-tools.d.ts +736 -0
- package/dist/agent-tools.js +409 -0
- package/dist/context/api.d.ts +5 -0
- package/dist/context/api.js +164 -0
- package/dist/context/cli.d.ts +19 -0
- package/dist/context/cli.js +108 -0
- package/dist/context/extractor.d.ts +12 -0
- package/dist/context/extractor.js +43 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +11 -0
- package/dist/context/matcher.d.ts +39 -0
- package/dist/context/matcher.js +246 -0
- package/dist/context/parser.d.ts +28 -0
- package/dist/context/parser.js +157 -0
- package/dist/context/pipeline.d.ts +26 -0
- package/dist/context/pipeline.js +56 -0
- package/dist/context/prompts.d.ts +6 -0
- package/dist/context/prompts.js +60 -0
- package/dist/context/providers.d.ts +6 -0
- package/dist/context/providers.js +106 -0
- package/dist/context/redactor.d.ts +10 -0
- package/dist/context/redactor.js +68 -0
- package/dist/context/server.d.ts +5 -0
- package/dist/context/server.js +134 -0
- package/dist/context/store.d.ts +12 -0
- package/dist/context/store.js +82 -0
- package/dist/context/types.d.ts +79 -0
- package/dist/context/types.js +4 -0
- package/dist/context/user-state.d.ts +40 -0
- package/dist/context/user-state.js +144 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +385 -0
- package/dist/insights/browser/BrowserPool.d.ts +87 -0
- package/dist/insights/browser/BrowserPool.js +266 -0
- package/dist/insights/browser/RequestInterceptor.d.ts +46 -0
- package/dist/insights/browser/RequestInterceptor.js +115 -0
- package/dist/insights/cli.d.ts +8 -0
- package/dist/insights/cli.js +206 -0
- package/dist/insights/core/base/BaseAdapter.d.ts +37 -0
- package/dist/insights/core/base/BaseAdapter.js +123 -0
- package/dist/insights/core/health/HealthMonitor.d.ts +75 -0
- package/dist/insights/core/health/HealthMonitor.js +171 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.d.ts +125 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.js +42 -0
- package/dist/insights/core/utils/DataNormalizer.d.ts +53 -0
- package/dist/insights/core/utils/DataNormalizer.js +349 -0
- package/dist/insights/core/utils/InstagramUrlUtils.d.ts +11 -0
- package/dist/insights/core/utils/InstagramUrlUtils.js +60 -0
- package/dist/insights/core/utils/TikTokUrlUtils.d.ts +10 -0
- package/dist/insights/core/utils/TikTokUrlUtils.js +57 -0
- package/dist/insights/handlers.d.ts +157 -0
- package/dist/insights/handlers.js +246 -0
- package/dist/insights/index.d.ts +437 -0
- package/dist/insights/index.js +426 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.d.ts +34 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.js +342 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.d.ts +31 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.js +227 -0
- package/dist/insights/platforms/reddit/RedditAdapter.d.ts +21 -0
- package/dist/insights/platforms/reddit/RedditAdapter.js +212 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.d.ts +34 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.js +269 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.d.ts +23 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.js +211 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.d.ts +35 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.js +258 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.d.ts +22 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.js +254 -0
- package/dist/insights/service-config.d.ts +7 -0
- package/dist/insights/service-config.js +60 -0
- package/dist/insights/services/UnifiedSocialMediaService.d.ts +94 -0
- package/dist/insights/services/UnifiedSocialMediaService.js +259 -0
- package/dist/insights/vision/VisionExtractor.d.ts +46 -0
- package/dist/insights/vision/VisionExtractor.js +236 -0
- package/dist/learnings.d.ts +50 -0
- package/dist/learnings.js +130 -0
- package/dist/openapi.d.ts +29 -0
- package/dist/openapi.js +169 -0
- package/dist/server-factory.d.ts +20 -0
- package/dist/server-factory.js +41 -0
- package/dist/suggestions.d.ts +16 -0
- package/dist/suggestions.js +72 -0
- package/dist/telemetry.d.ts +44 -0
- package/dist/telemetry.js +93 -0
- package/dist/tools/registry.d.ts +65 -0
- package/dist/tools/registry.js +256 -0
- package/dist/tools.d.ts +2433 -0
- package/dist/tools.js +2294 -0
- package/dist/transport/http.d.ts +15 -0
- package/dist/transport/http.js +154 -0
- package/package.json +76 -0
- package/skills/catalog.json +272 -0
- package/skills/community-catalog.json +4202 -0
- package/skills/competitive-analysis/SKILL.md +174 -0
- package/skills/content-creator/SKILL.md +256 -0
- package/skills/content-strategy/SKILL.md +222 -0
- package/skills/data-storytelling/SKILL.md +248 -0
- package/skills/heuristic-evaluation/SKILL.md +201 -0
- package/skills/market-research-reports/SKILL.md +184 -0
- package/skills/user-stories/SKILL.md +178 -0
- package/skills/ux-researcher/SKILL.md +239 -0
- package/web-dist/assets/index-B1b25lNd.css +1 -0
- package/web-dist/assets/index-CDWHwHbl.js +64 -0
- package/web-dist/index.html +16 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,2294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrowdListen Tasks — Planning and Delegation Business Logic
|
|
3
|
+
*
|
|
4
|
+
* All pure functions, tool handlers, and Supabase interaction logic.
|
|
5
|
+
* Routes executable work to coding agents with project context intact.
|
|
6
|
+
* Extracted from index.ts for testability.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { runPipeline } from "./context/pipeline.js";
|
|
13
|
+
import { getBlocks, addBlocks } from "./context/store.js";
|
|
14
|
+
import { matchSkills, discoverSkills, searchSkills, getFullCatalog } from "./context/matcher.js";
|
|
15
|
+
import { loadUserState, saveUserState, activatePack } from "./context/user-state.js";
|
|
16
|
+
import { listPacks, hasPack, getPack, getSkillMdContent, getPackTools } from "./tools/registry.js";
|
|
17
|
+
// logLearning/searchLearnings — kept in learnings.ts but no longer imported (consolidated into save/recall)
|
|
18
|
+
import { AGENT_TOOLS, isAgentTool, handleAgentTool } from "./agent-tools.js";
|
|
19
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
20
|
+
export const AUTH_DIR = path.join(os.homedir(), ".crowdlisten");
|
|
21
|
+
export const AUTH_FILE = path.join(AUTH_DIR, "auth.json");
|
|
22
|
+
export function loadAuth() {
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(AUTH_FILE))
|
|
25
|
+
return null;
|
|
26
|
+
const raw = fs.readFileSync(AUTH_FILE, "utf-8");
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function saveAuth(auth) {
|
|
34
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
35
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
|
|
36
|
+
}
|
|
37
|
+
export function clearAuth() {
|
|
38
|
+
try {
|
|
39
|
+
fs.unlinkSync(AUTH_FILE);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ─── Browser Helper ────────────────────────────────────────────────────────
|
|
46
|
+
export function openBrowser(url) {
|
|
47
|
+
try {
|
|
48
|
+
if (process.platform === "darwin") {
|
|
49
|
+
execSync(`open "${url}"`);
|
|
50
|
+
}
|
|
51
|
+
else if (process.platform === "win32") {
|
|
52
|
+
execSync(`start "" "${url}"`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null || echo ""`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Silent fail
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* HTML page shown in the browser after auth callback
|
|
64
|
+
*/
|
|
65
|
+
export function callbackHtml(success, error) {
|
|
66
|
+
if (success) {
|
|
67
|
+
return `<!DOCTYPE html>
|
|
68
|
+
<html><head><title>CrowdListen</title>
|
|
69
|
+
<style>
|
|
70
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #fafafa; }
|
|
71
|
+
.card { text-align: center; padding: 3rem; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); max-width: 400px; }
|
|
72
|
+
.check { font-size: 3rem; margin-bottom: 1rem; }
|
|
73
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #111; }
|
|
74
|
+
p { color: #666; font-size: 0.875rem; margin: 0; }
|
|
75
|
+
</style></head>
|
|
76
|
+
<body><div class="card">
|
|
77
|
+
<div class="check">✔</div>
|
|
78
|
+
<h1>You're connected!</h1>
|
|
79
|
+
<p>You can close this tab and go back to your terminal.</p>
|
|
80
|
+
</div></body></html>`;
|
|
81
|
+
}
|
|
82
|
+
return `<!DOCTYPE html>
|
|
83
|
+
<html><head><title>CrowdListen</title>
|
|
84
|
+
<style>
|
|
85
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #fafafa; }
|
|
86
|
+
.card { text-align: center; padding: 3rem; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); max-width: 400px; }
|
|
87
|
+
.icon { font-size: 3rem; margin-bottom: 1rem; }
|
|
88
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #111; }
|
|
89
|
+
p { color: #666; font-size: 0.875rem; margin: 0; }
|
|
90
|
+
</style></head>
|
|
91
|
+
<body><div class="card">
|
|
92
|
+
<div class="icon">❌</div>
|
|
93
|
+
<h1>Login failed</h1>
|
|
94
|
+
<p>${error || "Something went wrong. Please try again."}</p>
|
|
95
|
+
</div></body></html>`;
|
|
96
|
+
}
|
|
97
|
+
// ─── Auto-Install MCP Config ────────────────────────────────────────────────
|
|
98
|
+
export const MCP_ENTRY = {
|
|
99
|
+
command: "npx",
|
|
100
|
+
args: ["-y", "@crowdlisten/harness"],
|
|
101
|
+
};
|
|
102
|
+
export function getAgentConfigs() {
|
|
103
|
+
const home = os.homedir();
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
name: "Claude Code",
|
|
107
|
+
configPath: path.join(home, ".claude.json"),
|
|
108
|
+
mcpKey: "mcpServers",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "Cursor",
|
|
112
|
+
configPath: path.join(home, ".cursor", "mcp.json"),
|
|
113
|
+
mcpKey: "mcpServers",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "Gemini CLI",
|
|
117
|
+
configPath: path.join(home, ".gemini", "settings.json"),
|
|
118
|
+
mcpKey: "mcpServers",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "Codex",
|
|
122
|
+
configPath: path.join(home, ".codex", "config.json"),
|
|
123
|
+
mcpKey: "mcp_servers",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "OpenClaw",
|
|
127
|
+
configPath: path.join(home, ".openclaw", "openclaw.json"),
|
|
128
|
+
mcpKey: "mcpServers",
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
export async function autoInstallMcp() {
|
|
133
|
+
const installed = [];
|
|
134
|
+
for (const agent of getAgentConfigs()) {
|
|
135
|
+
try {
|
|
136
|
+
if (!fs.existsSync(agent.configPath))
|
|
137
|
+
continue;
|
|
138
|
+
let config = {};
|
|
139
|
+
try {
|
|
140
|
+
const raw = fs.readFileSync(agent.configPath, "utf-8");
|
|
141
|
+
config = JSON.parse(raw);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const keys = agent.mcpKey.split(".");
|
|
147
|
+
let target = config;
|
|
148
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
149
|
+
if (!target[keys[i]] || typeof target[keys[i]] !== "object") {
|
|
150
|
+
target[keys[i]] = {};
|
|
151
|
+
}
|
|
152
|
+
target = target[keys[i]];
|
|
153
|
+
}
|
|
154
|
+
const leafKey = keys[keys.length - 1];
|
|
155
|
+
if (!target[leafKey] || typeof target[leafKey] !== "object") {
|
|
156
|
+
target[leafKey] = {};
|
|
157
|
+
}
|
|
158
|
+
const servers = target[leafKey];
|
|
159
|
+
let changed = false;
|
|
160
|
+
// Unified server replaces the old two-server setup
|
|
161
|
+
if (!servers["crowdlisten"]) {
|
|
162
|
+
servers["crowdlisten"] = { ...MCP_ENTRY };
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
// Clean up old entries if present
|
|
166
|
+
if (servers["crowdlisten/harness"]) {
|
|
167
|
+
delete servers["crowdlisten/harness"];
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
if (servers["crowdlisten/insights"]) {
|
|
171
|
+
delete servers["crowdlisten/insights"];
|
|
172
|
+
changed = true;
|
|
173
|
+
}
|
|
174
|
+
if (!changed)
|
|
175
|
+
continue;
|
|
176
|
+
target[leafKey] = servers;
|
|
177
|
+
fs.writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
|
|
178
|
+
installed.push(agent.name);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Non-fatal
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return installed;
|
|
185
|
+
}
|
|
186
|
+
// ─── Tool Definitions ───────────────────────────────────────────────────────
|
|
187
|
+
export const TOOLS = [
|
|
188
|
+
{
|
|
189
|
+
name: "get_or_create_global_board",
|
|
190
|
+
description: "[Setup] Get (or create) your single global task board. Call once at start of session if you need the board_id. All tasks go here by default.",
|
|
191
|
+
inputSchema: { type: "object", properties: {} },
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "list_projects",
|
|
195
|
+
description: "[Setup] List all projects you have access to. Use to find project_id for scoping tasks and context.",
|
|
196
|
+
inputSchema: { type: "object", properties: {} },
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "list_boards",
|
|
200
|
+
description: "[Setup] List task boards for a project. Most users have one global board — use get_or_create_global_board instead.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
project_id: { type: "string", description: "Project UUID" },
|
|
205
|
+
},
|
|
206
|
+
required: ["project_id"],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "create_board",
|
|
211
|
+
description: "[Setup] Create a new task board for a project with default columns (To Do, In Progress, In Review, Done, Cancelled). Rarely needed — get_or_create_global_board handles this automatically.",
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
project_id: { type: "string", description: "Project UUID" },
|
|
216
|
+
name: { type: "string", description: "Board name (default: 'Tasks')" },
|
|
217
|
+
},
|
|
218
|
+
required: ["project_id"],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "list_tasks",
|
|
223
|
+
description: "List tasks on the board. Call this first to see what work is available. Uses global board by default. Filter by status: todo, inprogress, inreview, done, cancelled.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
board_id: { type: "string", description: "Optional: specific board (defaults to global board)" },
|
|
228
|
+
status: { type: "string", description: "Filter by status" },
|
|
229
|
+
limit: { type: "number", description: "Max results (default 50)" },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "get_task",
|
|
235
|
+
description: "Get full details of a task including description, status, priority, and labels.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
240
|
+
},
|
|
241
|
+
required: ["task_id"],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "create_task",
|
|
246
|
+
description: "Create a new task on the board. Uses global board by default. Optionally tag with a project_id for scoping.",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
title: { type: "string", description: "Task title" },
|
|
251
|
+
description: { type: "string", description: "Task description" },
|
|
252
|
+
priority: { type: "string", description: "low, medium, or high" },
|
|
253
|
+
project_id: { type: "string", description: "Optional: tag task with a project" },
|
|
254
|
+
board_id: { type: "string", description: "Optional: specific board (defaults to global board)" },
|
|
255
|
+
labels: {
|
|
256
|
+
type: "array",
|
|
257
|
+
items: { type: "object", properties: { name: { type: "string" }, color: { type: "string" } } },
|
|
258
|
+
description: "Label objects [{name, color}]",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
required: ["title"],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "update_task",
|
|
266
|
+
description: "Update a task's title, description, status, or priority. Pass only the fields you want to change.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
271
|
+
title: { type: "string" },
|
|
272
|
+
description: { type: "string" },
|
|
273
|
+
status: { type: "string", description: "todo, inprogress, inreview, done, cancelled" },
|
|
274
|
+
priority: { type: "string", description: "low, medium, high" },
|
|
275
|
+
},
|
|
276
|
+
required: ["task_id"],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "claim_task",
|
|
281
|
+
description: "Claim a task to start working on it. Call this after list_tasks to begin. Moves task to In Progress, creates workspace + session. Returns context (semantic map, knowledge base, existing plan) and branch name. Call query_context next to check existing decisions.",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
286
|
+
executor: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: "Coding agent name: CLAUDE_CODE, CURSOR, GEMINI, CODEX, AMP, OPENCLAW, OPENCODE, COPILOT, DROID, QWEN_CODE",
|
|
289
|
+
},
|
|
290
|
+
branch: { type: "string", description: "Custom branch name (auto-generated if omitted)" },
|
|
291
|
+
},
|
|
292
|
+
required: ["task_id"],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "complete_task",
|
|
297
|
+
description: "Mark task done. Call record_learning before this to capture what you learned. Optionally attach a summary of what was accomplished. Auto-completes the plan if one exists.",
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: "object",
|
|
300
|
+
properties: {
|
|
301
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
302
|
+
summary: { type: "string", description: "Summary of work completed" },
|
|
303
|
+
},
|
|
304
|
+
required: ["task_id"],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "log_progress",
|
|
309
|
+
description: "Log a progress note to the task's execution session. Call periodically during execution to track what you're doing. Useful for handoff between agents.",
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
314
|
+
message: { type: "string", description: "Progress message" },
|
|
315
|
+
session_id: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "Optional: specific session UUID (defaults to most recent active session)",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
required: ["task_id", "message"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "delete_task",
|
|
325
|
+
description: "Permanently delete a task. Cannot be undone.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
330
|
+
},
|
|
331
|
+
required: ["task_id"],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "migrate_to_global_board",
|
|
336
|
+
description: "[Setup] Migrate all tasks from all boards to the global board. Run once to consolidate if you have tasks spread across multiple boards.",
|
|
337
|
+
inputSchema: { type: "object", properties: {} },
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "start_session",
|
|
341
|
+
description: "[Advanced] Start a new parallel agent session for a task. Use when multiple agents need to work on different aspects of the same task simultaneously. claim_task already creates one session.",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: {
|
|
345
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
346
|
+
executor: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Agent: CLAUDE_CODE, CURSOR, GEMINI, CODEX, AMP, etc.",
|
|
349
|
+
},
|
|
350
|
+
focus: {
|
|
351
|
+
type: "string",
|
|
352
|
+
description: "What this session will work on (e.g., 'implement auth backend')",
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
required: ["task_id", "focus"],
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "list_sessions",
|
|
360
|
+
description: "[Advanced] List all agent sessions for a task, showing status and what each is working on. Useful for coordinating parallel agents.",
|
|
361
|
+
inputSchema: {
|
|
362
|
+
type: "object",
|
|
363
|
+
properties: {
|
|
364
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
365
|
+
status: {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: "Filter by status: idle, running, completed, failed, stopped",
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
required: ["task_id"],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: "update_session",
|
|
375
|
+
description: "[Advanced] Update a session's status or focus. Use to mark running/completed/stopped when coordinating parallel agents.",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: "object",
|
|
378
|
+
properties: {
|
|
379
|
+
session_id: { type: "string", description: "Session UUID" },
|
|
380
|
+
status: {
|
|
381
|
+
type: "string",
|
|
382
|
+
description: "idle, running, completed, failed, stopped",
|
|
383
|
+
},
|
|
384
|
+
focus: { type: "string", description: "Updated focus description" },
|
|
385
|
+
},
|
|
386
|
+
required: ["session_id"],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
// ── Planning & Context Tools ─────────────────────────────────────────────
|
|
390
|
+
{
|
|
391
|
+
name: "create_plan",
|
|
392
|
+
description: "Create an execution plan for a task. Call after claim_task and query_context. Plans go through draft → review → approved → executing → completed lifecycle. Submit for human review with update_plan(status='review').",
|
|
393
|
+
inputSchema: {
|
|
394
|
+
type: "object",
|
|
395
|
+
properties: {
|
|
396
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
397
|
+
approach: { type: "string", description: "How you plan to execute this task" },
|
|
398
|
+
assumptions: {
|
|
399
|
+
type: "array",
|
|
400
|
+
items: { type: "string" },
|
|
401
|
+
description: "Assumptions the plan relies on",
|
|
402
|
+
},
|
|
403
|
+
constraints: {
|
|
404
|
+
type: "array",
|
|
405
|
+
items: { type: "string" },
|
|
406
|
+
description: "Known constraints to work within",
|
|
407
|
+
},
|
|
408
|
+
success_criteria: {
|
|
409
|
+
type: "array",
|
|
410
|
+
items: { type: "string" },
|
|
411
|
+
description: "How to know the task is done correctly",
|
|
412
|
+
},
|
|
413
|
+
risks: {
|
|
414
|
+
type: "array",
|
|
415
|
+
items: { type: "string" },
|
|
416
|
+
description: "Potential risks or blockers",
|
|
417
|
+
},
|
|
418
|
+
estimated_steps: { type: "number", description: "Estimated number of steps" },
|
|
419
|
+
},
|
|
420
|
+
required: ["task_id", "approach"],
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "get_plan",
|
|
425
|
+
description: "Get the current plan for a task including version history and any pending human feedback. Check this after human review to see feedback.",
|
|
426
|
+
inputSchema: {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {
|
|
429
|
+
task_id: { type: "string", description: "Card/task UUID" },
|
|
430
|
+
},
|
|
431
|
+
required: ["task_id"],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: "update_plan",
|
|
436
|
+
description: "Iterate on a plan: update approach, change status, or add human feedback. Set status='review' to submit for review, status='executing' after approval. Content changes archive the current version. Setting feedback auto-reverts status to draft for revision.",
|
|
437
|
+
inputSchema: {
|
|
438
|
+
type: "object",
|
|
439
|
+
properties: {
|
|
440
|
+
plan_id: { type: "string", description: "Plan UUID (from create_plan or get_plan)" },
|
|
441
|
+
approach: { type: "string", description: "Updated approach" },
|
|
442
|
+
status: {
|
|
443
|
+
type: "string",
|
|
444
|
+
description: "draft, review, approved, executing, completed",
|
|
445
|
+
},
|
|
446
|
+
feedback: { type: "string", description: "Human feedback — auto-reverts plan to draft" },
|
|
447
|
+
assumptions: { type: "array", items: { type: "string" } },
|
|
448
|
+
constraints: { type: "array", items: { type: "string" } },
|
|
449
|
+
success_criteria: { type: "array", items: { type: "string" } },
|
|
450
|
+
risks: { type: "array", items: { type: "string" } },
|
|
451
|
+
},
|
|
452
|
+
required: ["plan_id"],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
// query_context, add_context, record_learning → consolidated into save/recall
|
|
456
|
+
// ─── Context Extraction Tools ──────────────────────────────────────────────
|
|
457
|
+
{
|
|
458
|
+
name: "process_transcript",
|
|
459
|
+
description: "[Context] Process text through the context extraction pipeline: PII redaction → LLM extraction → skill matching. Returns extracted context blocks and recommended skills. Requires LLM provider to be configured (run setup-context).",
|
|
460
|
+
inputSchema: {
|
|
461
|
+
type: "object",
|
|
462
|
+
properties: {
|
|
463
|
+
text: {
|
|
464
|
+
type: "string",
|
|
465
|
+
description: "The transcript/chat text to process. PII will be redacted before LLM sees it.",
|
|
466
|
+
},
|
|
467
|
+
source: {
|
|
468
|
+
type: "string",
|
|
469
|
+
description: "Label for the source (e.g. 'slack-export', 'chat-history'). Defaults to 'mcp'.",
|
|
470
|
+
},
|
|
471
|
+
is_chat: {
|
|
472
|
+
type: "boolean",
|
|
473
|
+
description: "Whether the text is chat history (uses 4-type extraction: style/insight/pattern/preference). Default true.",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
required: ["text"],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "get_context_blocks",
|
|
481
|
+
description: "[Context] Retrieve locally-stored context blocks from previous extractions. Blocks are stored in ~/.crowdlisten/context.json.",
|
|
482
|
+
inputSchema: { type: "object", properties: {} },
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "recommend_skills",
|
|
486
|
+
description: "[Context] Get CrowdListen skill recommendations based on stored context blocks. Matches block content against the skill catalog using keyword overlap scoring.",
|
|
487
|
+
inputSchema: { type: "object", properties: {} },
|
|
488
|
+
},
|
|
489
|
+
// ─── Skill Discovery Tools ──────────────────────────────────────────────
|
|
490
|
+
{
|
|
491
|
+
name: "discover_skills",
|
|
492
|
+
description: "[Context] Context-driven skill discovery — scores all skills (native CrowdListen + 146 community skills from 4 repos) against your extracted context blocks. Returns ranked skills with install instructions. Optionally process new context text first.",
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: {
|
|
496
|
+
context: {
|
|
497
|
+
type: "string",
|
|
498
|
+
description: "Optional: raw context text to process through extraction pipeline first. If omitted, uses stored blocks.",
|
|
499
|
+
},
|
|
500
|
+
category: {
|
|
501
|
+
type: "string",
|
|
502
|
+
description: "Filter by category: development, data, content, research, automation, design, business, productivity",
|
|
503
|
+
},
|
|
504
|
+
tier: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description: "Filter by tier: crowdlisten (native, need API key) or community (open source)",
|
|
507
|
+
},
|
|
508
|
+
limit: {
|
|
509
|
+
type: "number",
|
|
510
|
+
description: "Max results to return (default 10)",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "search_skills",
|
|
517
|
+
description: "[Context] Text search across all 154 skills (8 native + 146 community). Browse by category or search by name/keyword. Returns matching skills with descriptions and install methods.",
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
query: { type: "string", description: "Search query (name, keyword, or description text)" },
|
|
522
|
+
tier: { type: "string", description: "Filter: crowdlisten or community" },
|
|
523
|
+
category: {
|
|
524
|
+
type: "string",
|
|
525
|
+
description: "Filter: development, data, content, research, automation, design, business, productivity",
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
required: ["query"],
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: "install_skill",
|
|
533
|
+
description: "[Context] Install a skill by ID — copies SKILL.md content to .claude/commands/ for 'copy' type skills, or returns npx/git instructions for other install methods.",
|
|
534
|
+
inputSchema: {
|
|
535
|
+
type: "object",
|
|
536
|
+
properties: {
|
|
537
|
+
skill_id: { type: "string", description: "Skill ID (e.g., 'comm-typescript-expert' or 'competitive-analysis')" },
|
|
538
|
+
target_dir: {
|
|
539
|
+
type: "string",
|
|
540
|
+
description: "Target directory for SKILL.md (default: .claude/commands/)",
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
required: ["skill_id"],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
// ─── Core Always-On Tools (Skill Pack Discovery + Memory) ────────────────
|
|
547
|
+
{
|
|
548
|
+
name: "list_skill_packs",
|
|
549
|
+
description: "List all available skill packs with status (active/available). Skill packs group related tools — activate a pack to unlock its tools. Start here to see what capabilities are available.",
|
|
550
|
+
inputSchema: {
|
|
551
|
+
type: "object",
|
|
552
|
+
properties: {
|
|
553
|
+
include_virtual: {
|
|
554
|
+
type: "boolean",
|
|
555
|
+
description: "Include SKILL.md workflow packs (default true)",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "activate_skill_pack",
|
|
562
|
+
description: "Activate a skill pack to unlock its tools. After activation, the new tools appear in tools/list. For SKILL.md packs, returns the full workflow instructions. Call list_skill_packs first to see available packs.",
|
|
563
|
+
inputSchema: {
|
|
564
|
+
type: "object",
|
|
565
|
+
properties: {
|
|
566
|
+
pack_id: {
|
|
567
|
+
type: "string",
|
|
568
|
+
description: "Pack ID to activate (e.g., 'planning', 'social-listening', 'competitive-analysis')",
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
required: ["pack_id"],
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "save",
|
|
576
|
+
description: "Save context that persists across sessions. Use tags to categorize (e.g. 'decision', 'pattern', 'preference'). Embedding is auto-generated for semantic recall.",
|
|
577
|
+
inputSchema: {
|
|
578
|
+
type: "object",
|
|
579
|
+
properties: {
|
|
580
|
+
title: {
|
|
581
|
+
type: "string",
|
|
582
|
+
description: "Short title",
|
|
583
|
+
},
|
|
584
|
+
content: {
|
|
585
|
+
type: "string",
|
|
586
|
+
description: "The content to remember",
|
|
587
|
+
},
|
|
588
|
+
tags: {
|
|
589
|
+
type: "array",
|
|
590
|
+
items: { type: "string" },
|
|
591
|
+
description: "Freeform tags (e.g. ['decision', 'auth', 'pattern'])",
|
|
592
|
+
},
|
|
593
|
+
project_id: {
|
|
594
|
+
type: "string",
|
|
595
|
+
description: "Optional project scope",
|
|
596
|
+
},
|
|
597
|
+
task_id: {
|
|
598
|
+
type: "string",
|
|
599
|
+
description: "Optional task association",
|
|
600
|
+
},
|
|
601
|
+
confidence: {
|
|
602
|
+
type: "number",
|
|
603
|
+
description: "Confidence 0-1 (default 1.0)",
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
required: ["title", "content"],
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
name: "recall",
|
|
611
|
+
description: "Search saved memories using semantic similarity. Returns most relevant results ranked by meaning, not just keywords.",
|
|
612
|
+
inputSchema: {
|
|
613
|
+
type: "object",
|
|
614
|
+
properties: {
|
|
615
|
+
search: {
|
|
616
|
+
type: "string",
|
|
617
|
+
description: "Natural language search query",
|
|
618
|
+
},
|
|
619
|
+
tags: {
|
|
620
|
+
type: "array",
|
|
621
|
+
items: { type: "string" },
|
|
622
|
+
description: "Filter by tags",
|
|
623
|
+
},
|
|
624
|
+
project_id: {
|
|
625
|
+
type: "string",
|
|
626
|
+
description: "Filter by project",
|
|
627
|
+
},
|
|
628
|
+
limit: {
|
|
629
|
+
type: "number",
|
|
630
|
+
description: "Max results (default 20)",
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
// ── Spec Delivery ─────────────────────────────────────────────────
|
|
636
|
+
{
|
|
637
|
+
name: "get_specs",
|
|
638
|
+
description: "[Specs] List actionable specs generated from crowd feedback analysis. " +
|
|
639
|
+
"These are agent-consumable implementation specs with evidence, acceptance criteria, and priority. " +
|
|
640
|
+
"Filter by status (pending/claimed/in_progress/completed), type, priority, or minimum confidence.",
|
|
641
|
+
inputSchema: {
|
|
642
|
+
type: "object",
|
|
643
|
+
properties: {
|
|
644
|
+
project_id: {
|
|
645
|
+
type: "string",
|
|
646
|
+
description: "Filter by project UUID",
|
|
647
|
+
},
|
|
648
|
+
status: {
|
|
649
|
+
type: "string",
|
|
650
|
+
enum: ["pending", "claimed", "in_progress", "completed", "rejected"],
|
|
651
|
+
description: "Filter by lifecycle status (default: pending)",
|
|
652
|
+
},
|
|
653
|
+
spec_type: {
|
|
654
|
+
type: "string",
|
|
655
|
+
enum: ["feature", "bug_fix", "improvement", "investigation"],
|
|
656
|
+
description: "Filter by spec type",
|
|
657
|
+
},
|
|
658
|
+
min_confidence: {
|
|
659
|
+
type: "number",
|
|
660
|
+
description: "Minimum confidence threshold (0.0-1.0)",
|
|
661
|
+
},
|
|
662
|
+
priority: {
|
|
663
|
+
type: "string",
|
|
664
|
+
enum: ["critical", "high", "medium", "low"],
|
|
665
|
+
description: "Filter by priority level",
|
|
666
|
+
},
|
|
667
|
+
limit: {
|
|
668
|
+
type: "number",
|
|
669
|
+
description: "Max results (default 20)",
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: "get_spec_detail",
|
|
676
|
+
description: "[Specs] Get full spec details including evidence citations, acceptance criteria, " +
|
|
677
|
+
"and implementation context. Read this before starting implementation.",
|
|
678
|
+
inputSchema: {
|
|
679
|
+
type: "object",
|
|
680
|
+
properties: {
|
|
681
|
+
spec_id: {
|
|
682
|
+
type: "string",
|
|
683
|
+
description: "Spec UUID to retrieve",
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
required: ["spec_id"],
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: "start_spec",
|
|
691
|
+
description: "[Specs] Claim an actionable spec and begin implementation. " +
|
|
692
|
+
"Creates a kanban task from the spec, claims it (moves to In Progress), " +
|
|
693
|
+
"and returns workspace context for the coding agent. " +
|
|
694
|
+
"Composes create_task → claim_task internally.",
|
|
695
|
+
inputSchema: {
|
|
696
|
+
type: "object",
|
|
697
|
+
properties: {
|
|
698
|
+
spec_id: {
|
|
699
|
+
type: "string",
|
|
700
|
+
description: "Spec UUID to start working on",
|
|
701
|
+
},
|
|
702
|
+
executor: {
|
|
703
|
+
type: "string",
|
|
704
|
+
description: "Coding agent type (auto-detected if omitted)",
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
required: ["spec_id"],
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
// ── Preferences ─────────────────────────────────────────────────
|
|
711
|
+
{
|
|
712
|
+
name: "set_preferences",
|
|
713
|
+
description: "Set user preferences for telemetry, proactive suggestions, and cross-project learnings. " +
|
|
714
|
+
"Telemetry levels: off (no tracking), anonymous (local-only stats), community (anonymous aggregate stats). " +
|
|
715
|
+
"Pass only the fields you want to change.",
|
|
716
|
+
inputSchema: {
|
|
717
|
+
type: "object",
|
|
718
|
+
properties: {
|
|
719
|
+
telemetry: {
|
|
720
|
+
type: "string",
|
|
721
|
+
enum: ["off", "anonymous", "community"],
|
|
722
|
+
description: "Telemetry privacy level",
|
|
723
|
+
},
|
|
724
|
+
proactive_suggestions: {
|
|
725
|
+
type: "boolean",
|
|
726
|
+
description: "Enable/disable proactive skill pack suggestions",
|
|
727
|
+
},
|
|
728
|
+
cross_project_learnings: {
|
|
729
|
+
type: "boolean",
|
|
730
|
+
description: "Enable/disable cross-project learning persistence",
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
// log_learning, search_learnings → consolidated into save/recall
|
|
736
|
+
...AGENT_TOOLS,
|
|
737
|
+
];
|
|
738
|
+
// ─── Status <-> Column mapping ────────────────────────────────────────────────
|
|
739
|
+
export const STATUS_COLUMN = {
|
|
740
|
+
todo: "To Do",
|
|
741
|
+
inprogress: "In Progress",
|
|
742
|
+
inreview: "In Review",
|
|
743
|
+
done: "Done",
|
|
744
|
+
cancelled: "Cancelled",
|
|
745
|
+
};
|
|
746
|
+
export async function getColumnByStatus(sb, boardId, status) {
|
|
747
|
+
const colName = STATUS_COLUMN[status];
|
|
748
|
+
if (!colName)
|
|
749
|
+
return null;
|
|
750
|
+
const { data } = await sb
|
|
751
|
+
.from("kanban_columns")
|
|
752
|
+
.select("id")
|
|
753
|
+
.eq("board_id", boardId)
|
|
754
|
+
.eq("name", colName)
|
|
755
|
+
.single();
|
|
756
|
+
return data?.id || null;
|
|
757
|
+
}
|
|
758
|
+
export const GLOBAL_BOARD_NAME = "Global Tasks";
|
|
759
|
+
export async function getOrCreateGlobalBoard(sb, userId) {
|
|
760
|
+
// Look for existing global board
|
|
761
|
+
const { data: existing } = await sb
|
|
762
|
+
.from("kanban_boards")
|
|
763
|
+
.select("id, name")
|
|
764
|
+
.eq("user_id", userId)
|
|
765
|
+
.eq("name", GLOBAL_BOARD_NAME)
|
|
766
|
+
.single();
|
|
767
|
+
if (existing) {
|
|
768
|
+
return { id: existing.id, name: existing.name, created: false };
|
|
769
|
+
}
|
|
770
|
+
// Create a "global" project to house the board (required by schema)
|
|
771
|
+
let projectId;
|
|
772
|
+
const { data: globalProject } = await sb
|
|
773
|
+
.from("projects")
|
|
774
|
+
.select("id")
|
|
775
|
+
.eq("user_id", userId)
|
|
776
|
+
.eq("name", "Global Tasks")
|
|
777
|
+
.single();
|
|
778
|
+
if (globalProject) {
|
|
779
|
+
projectId = globalProject.id;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
const { data: newProject, error: projErr } = await sb
|
|
783
|
+
.from("projects")
|
|
784
|
+
.insert({
|
|
785
|
+
user_id: userId,
|
|
786
|
+
name: "Global Tasks",
|
|
787
|
+
description: "Container for your global task board",
|
|
788
|
+
})
|
|
789
|
+
.select("id")
|
|
790
|
+
.single();
|
|
791
|
+
if (projErr)
|
|
792
|
+
throw new Error(`Failed to create global project: ${projErr.message}`);
|
|
793
|
+
projectId = newProject.id;
|
|
794
|
+
}
|
|
795
|
+
// Create the global board
|
|
796
|
+
const { data: board, error: boardErr } = await sb
|
|
797
|
+
.from("kanban_boards")
|
|
798
|
+
.insert({
|
|
799
|
+
project_id: projectId,
|
|
800
|
+
name: GLOBAL_BOARD_NAME,
|
|
801
|
+
user_id: userId,
|
|
802
|
+
})
|
|
803
|
+
.select("id")
|
|
804
|
+
.single();
|
|
805
|
+
if (boardErr)
|
|
806
|
+
throw new Error(`Failed to create global board: ${boardErr.message}`);
|
|
807
|
+
// Create default columns
|
|
808
|
+
const defaultColumns = ["To Do", "In Progress", "In Review", "Done", "Cancelled"];
|
|
809
|
+
for (let i = 0; i < defaultColumns.length; i++) {
|
|
810
|
+
await sb.from("kanban_columns").insert({
|
|
811
|
+
board_id: board.id,
|
|
812
|
+
name: defaultColumns[i],
|
|
813
|
+
position: i,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return { id: board.id, name: GLOBAL_BOARD_NAME, created: true };
|
|
817
|
+
}
|
|
818
|
+
// ─── Tool Handlers ──────────────────────────────────────────────────────────
|
|
819
|
+
export async function handleTool(sb, userId, name, args) {
|
|
820
|
+
switch (name) {
|
|
821
|
+
// ── Global Board ─────────────────────────────────────────
|
|
822
|
+
case "get_or_create_global_board": {
|
|
823
|
+
const board = await getOrCreateGlobalBoard(sb, userId);
|
|
824
|
+
return json({
|
|
825
|
+
board_id: board.id,
|
|
826
|
+
name: board.name,
|
|
827
|
+
status: board.created ? "created" : "exists",
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
// ── Projects ──────────────────────────────────────────────
|
|
831
|
+
case "list_projects": {
|
|
832
|
+
const { data, error } = await sb
|
|
833
|
+
.from("projects")
|
|
834
|
+
.select("id, name, updated_at")
|
|
835
|
+
.order("updated_at", { ascending: false })
|
|
836
|
+
.limit(20);
|
|
837
|
+
if (error)
|
|
838
|
+
throw new Error(error.message);
|
|
839
|
+
const slim = (data || []).map((p) => ({ id: p.id, name: p.name }));
|
|
840
|
+
return json({ projects: slim, count: slim.length });
|
|
841
|
+
}
|
|
842
|
+
// ── Boards ────────────────────────────────────────────────
|
|
843
|
+
case "list_boards": {
|
|
844
|
+
const { data, error } = await sb
|
|
845
|
+
.from("kanban_boards")
|
|
846
|
+
.select("id, name, description, created_at")
|
|
847
|
+
.eq("project_id", args.project_id)
|
|
848
|
+
.order("created_at", { ascending: false });
|
|
849
|
+
if (error)
|
|
850
|
+
throw new Error(error.message);
|
|
851
|
+
return json({ boards: data, count: data?.length || 0 });
|
|
852
|
+
}
|
|
853
|
+
case "create_board": {
|
|
854
|
+
const projectId = args.project_id;
|
|
855
|
+
const boardName = args.name || "Tasks";
|
|
856
|
+
// Verify project exists
|
|
857
|
+
const { data: project, error: projErr } = await sb
|
|
858
|
+
.from("projects")
|
|
859
|
+
.select("id")
|
|
860
|
+
.eq("id", projectId)
|
|
861
|
+
.single();
|
|
862
|
+
if (projErr || !project)
|
|
863
|
+
throw new Error("Project not found");
|
|
864
|
+
// Create board
|
|
865
|
+
const { data: board, error: boardErr } = await sb
|
|
866
|
+
.from("kanban_boards")
|
|
867
|
+
.insert({
|
|
868
|
+
project_id: projectId,
|
|
869
|
+
name: boardName,
|
|
870
|
+
user_id: userId,
|
|
871
|
+
})
|
|
872
|
+
.select("id")
|
|
873
|
+
.single();
|
|
874
|
+
if (boardErr)
|
|
875
|
+
throw new Error(boardErr.message);
|
|
876
|
+
// Create default columns
|
|
877
|
+
const defaultColumns = ["To Do", "In Progress", "In Review", "Done", "Cancelled"];
|
|
878
|
+
for (let i = 0; i < defaultColumns.length; i++) {
|
|
879
|
+
const { error: colErr } = await sb.from("kanban_columns").insert({
|
|
880
|
+
board_id: board.id,
|
|
881
|
+
name: defaultColumns[i],
|
|
882
|
+
position: i,
|
|
883
|
+
});
|
|
884
|
+
if (colErr)
|
|
885
|
+
throw new Error(`Failed to create column '${defaultColumns[i]}': ${colErr.message}`);
|
|
886
|
+
}
|
|
887
|
+
return json({ board_id: board.id, name: boardName, status: "created", columns: defaultColumns });
|
|
888
|
+
}
|
|
889
|
+
// ── Tasks ─────────────────────────────────────────────────
|
|
890
|
+
case "list_tasks": {
|
|
891
|
+
// Use global board if no board_id specified
|
|
892
|
+
let boardId = args.board_id;
|
|
893
|
+
if (!boardId) {
|
|
894
|
+
const globalBoard = await getOrCreateGlobalBoard(sb, userId);
|
|
895
|
+
boardId = globalBoard.id;
|
|
896
|
+
}
|
|
897
|
+
let query = sb
|
|
898
|
+
.from("kanban_cards")
|
|
899
|
+
.select(`id, title, description, status, priority, labels, due_date, position, created_at, updated_at,
|
|
900
|
+
column:column_id(id, name)`)
|
|
901
|
+
.eq("board_id", boardId)
|
|
902
|
+
.order("position", { ascending: true });
|
|
903
|
+
if (args.status)
|
|
904
|
+
query = query.eq("status", args.status);
|
|
905
|
+
query = query.limit(args.limit || 50);
|
|
906
|
+
const { data, error } = await query;
|
|
907
|
+
if (error)
|
|
908
|
+
throw new Error(error.message);
|
|
909
|
+
return json({ tasks: data, count: data?.length || 0, board_id: boardId });
|
|
910
|
+
}
|
|
911
|
+
case "get_task": {
|
|
912
|
+
const { data, error } = await sb
|
|
913
|
+
.from("kanban_cards")
|
|
914
|
+
.select(`id, title, description, status, priority, labels, due_date, position, created_at, updated_at,
|
|
915
|
+
column:column_id(id, name),
|
|
916
|
+
board:board_id(id, name, project_id)`)
|
|
917
|
+
.eq("id", args.task_id)
|
|
918
|
+
.single();
|
|
919
|
+
if (error)
|
|
920
|
+
throw new Error(error.message);
|
|
921
|
+
return json({ task: data });
|
|
922
|
+
}
|
|
923
|
+
case "create_task": {
|
|
924
|
+
// Use global board if no board_id specified
|
|
925
|
+
let boardId = args.board_id;
|
|
926
|
+
if (!boardId) {
|
|
927
|
+
const globalBoard = await getOrCreateGlobalBoard(sb, userId);
|
|
928
|
+
boardId = globalBoard.id;
|
|
929
|
+
}
|
|
930
|
+
const colId = await getColumnByStatus(sb, boardId, "todo");
|
|
931
|
+
if (!colId)
|
|
932
|
+
throw new Error("Could not find 'To Do' column");
|
|
933
|
+
const { data: last } = await sb
|
|
934
|
+
.from("kanban_cards")
|
|
935
|
+
.select("position")
|
|
936
|
+
.eq("column_id", colId)
|
|
937
|
+
.order("position", { ascending: false })
|
|
938
|
+
.limit(1)
|
|
939
|
+
.single();
|
|
940
|
+
// Add project_id as a label if provided
|
|
941
|
+
const labels = args.labels || [];
|
|
942
|
+
const projectId = args.project_id;
|
|
943
|
+
if (projectId) {
|
|
944
|
+
labels.push({ name: `project:${projectId}`, color: "#6366f1" });
|
|
945
|
+
}
|
|
946
|
+
const { data, error } = await sb
|
|
947
|
+
.from("kanban_cards")
|
|
948
|
+
.insert({
|
|
949
|
+
board_id: boardId,
|
|
950
|
+
column_id: colId,
|
|
951
|
+
user_id: userId,
|
|
952
|
+
title: args.title,
|
|
953
|
+
description: args.description || null,
|
|
954
|
+
priority: args.priority || "medium",
|
|
955
|
+
labels,
|
|
956
|
+
status: "todo",
|
|
957
|
+
position: (last?.position || 0) + 1,
|
|
958
|
+
})
|
|
959
|
+
.select("id")
|
|
960
|
+
.single();
|
|
961
|
+
if (error)
|
|
962
|
+
throw new Error(error.message);
|
|
963
|
+
return json({ task_id: data.id, board_id: boardId, status: "created", project_id: projectId || null });
|
|
964
|
+
}
|
|
965
|
+
case "update_task": {
|
|
966
|
+
const taskId = args.task_id;
|
|
967
|
+
const updates = {};
|
|
968
|
+
if (args.title)
|
|
969
|
+
updates.title = args.title;
|
|
970
|
+
if (args.description !== undefined)
|
|
971
|
+
updates.description = args.description;
|
|
972
|
+
if (args.priority)
|
|
973
|
+
updates.priority = args.priority;
|
|
974
|
+
if (args.status) {
|
|
975
|
+
updates.status = args.status;
|
|
976
|
+
const { data: card } = await sb
|
|
977
|
+
.from("kanban_cards")
|
|
978
|
+
.select("board_id")
|
|
979
|
+
.eq("id", taskId)
|
|
980
|
+
.single();
|
|
981
|
+
if (card) {
|
|
982
|
+
const col = await getColumnByStatus(sb, card.board_id, args.status);
|
|
983
|
+
if (col)
|
|
984
|
+
updates.column_id = col;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const { data, error } = await sb
|
|
988
|
+
.from("kanban_cards")
|
|
989
|
+
.update(updates)
|
|
990
|
+
.eq("id", taskId)
|
|
991
|
+
.select("id, title, status, priority")
|
|
992
|
+
.single();
|
|
993
|
+
if (error)
|
|
994
|
+
throw new Error(error.message);
|
|
995
|
+
return json({ task: data, status: "updated" });
|
|
996
|
+
}
|
|
997
|
+
// ── Claim (start working) ─────────────────────────────────
|
|
998
|
+
case "claim_task": {
|
|
999
|
+
const taskId = args.task_id;
|
|
1000
|
+
const executor = args.executor || detectExecutor();
|
|
1001
|
+
// Get card
|
|
1002
|
+
const { data: card, error: cardErr } = await sb
|
|
1003
|
+
.from("kanban_cards")
|
|
1004
|
+
.select("id, board_id, title")
|
|
1005
|
+
.eq("id", taskId)
|
|
1006
|
+
.single();
|
|
1007
|
+
if (cardErr || !card)
|
|
1008
|
+
throw new Error(cardErr?.message || "Task not found");
|
|
1009
|
+
// Move to In Progress
|
|
1010
|
+
const col = await getColumnByStatus(sb, card.board_id, "inprogress");
|
|
1011
|
+
if (col) {
|
|
1012
|
+
await sb
|
|
1013
|
+
.from("kanban_cards")
|
|
1014
|
+
.update({ status: "inprogress", column_id: col })
|
|
1015
|
+
.eq("id", taskId);
|
|
1016
|
+
}
|
|
1017
|
+
// Create workspace
|
|
1018
|
+
const branch = args.branch ||
|
|
1019
|
+
`task/${slugify(card.title)}-${taskId.slice(0, 8)}`;
|
|
1020
|
+
const { data: ws, error: wsErr } = await sb
|
|
1021
|
+
.from("kanban_workspaces")
|
|
1022
|
+
.insert({ card_id: taskId, user_id: userId, branch })
|
|
1023
|
+
.select("id")
|
|
1024
|
+
.single();
|
|
1025
|
+
if (wsErr)
|
|
1026
|
+
throw new Error(wsErr.message);
|
|
1027
|
+
// Create session
|
|
1028
|
+
const { data: sess } = await sb
|
|
1029
|
+
.from("kanban_sessions")
|
|
1030
|
+
.insert({ workspace_id: ws.id, user_id: userId, executor })
|
|
1031
|
+
.select("id")
|
|
1032
|
+
.single();
|
|
1033
|
+
// Fetch project context if this board belongs to a project
|
|
1034
|
+
let projectContext = null;
|
|
1035
|
+
let contextEntries = [];
|
|
1036
|
+
let existingPlan = null;
|
|
1037
|
+
try {
|
|
1038
|
+
const { data: board } = await sb
|
|
1039
|
+
.from("kanban_boards")
|
|
1040
|
+
.select("project_id")
|
|
1041
|
+
.eq("id", card.board_id)
|
|
1042
|
+
.single();
|
|
1043
|
+
if (board?.project_id) {
|
|
1044
|
+
projectContext = await buildProjectContextMd(sb, board.project_id);
|
|
1045
|
+
// Fetch relevant context entries (active decisions, constraints, patterns, etc.)
|
|
1046
|
+
const { data: ctx } = await sb
|
|
1047
|
+
.from("planning_context")
|
|
1048
|
+
.select("id, type, title, body, tags, confidence")
|
|
1049
|
+
.eq("user_id", userId)
|
|
1050
|
+
.eq("project_id", board.project_id)
|
|
1051
|
+
.in("status", ["active", "approved", "executing"])
|
|
1052
|
+
.order("updated_at", { ascending: false })
|
|
1053
|
+
.limit(20);
|
|
1054
|
+
if (ctx)
|
|
1055
|
+
contextEntries = ctx;
|
|
1056
|
+
}
|
|
1057
|
+
// Fetch existing plan for this task
|
|
1058
|
+
const { data: plan } = await sb
|
|
1059
|
+
.from("planning_context")
|
|
1060
|
+
.select("id, title, body, metadata, status, version")
|
|
1061
|
+
.eq("task_id", taskId)
|
|
1062
|
+
.eq("type", "plan")
|
|
1063
|
+
.not("status", "in", '("completed","archived","superseded")')
|
|
1064
|
+
.single();
|
|
1065
|
+
if (plan)
|
|
1066
|
+
existingPlan = plan;
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
// Non-blocking — proceed without context
|
|
1070
|
+
}
|
|
1071
|
+
return json({
|
|
1072
|
+
task_id: taskId,
|
|
1073
|
+
workspace_id: ws.id,
|
|
1074
|
+
session_id: sess?.id,
|
|
1075
|
+
branch,
|
|
1076
|
+
executor,
|
|
1077
|
+
status: "claimed",
|
|
1078
|
+
project_context: projectContext,
|
|
1079
|
+
context_entries: contextEntries,
|
|
1080
|
+
existing_plan: existingPlan,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
// ── Complete ──────────────────────────────────────────────
|
|
1084
|
+
case "complete_task": {
|
|
1085
|
+
const taskId = args.task_id;
|
|
1086
|
+
const summary = args.summary || null;
|
|
1087
|
+
// Move to Done
|
|
1088
|
+
const { data: card } = await sb
|
|
1089
|
+
.from("kanban_cards")
|
|
1090
|
+
.select("board_id")
|
|
1091
|
+
.eq("id", taskId)
|
|
1092
|
+
.single();
|
|
1093
|
+
if (card) {
|
|
1094
|
+
const col = await getColumnByStatus(sb, card.board_id, "done");
|
|
1095
|
+
const updates = { status: "done" };
|
|
1096
|
+
if (col)
|
|
1097
|
+
updates.column_id = col;
|
|
1098
|
+
await sb.from("kanban_cards").update(updates).eq("id", taskId);
|
|
1099
|
+
}
|
|
1100
|
+
// Mark active plan as completed
|
|
1101
|
+
try {
|
|
1102
|
+
await sb
|
|
1103
|
+
.from("planning_context")
|
|
1104
|
+
.update({ status: "completed", updated_at: new Date().toISOString() })
|
|
1105
|
+
.eq("task_id", taskId)
|
|
1106
|
+
.eq("type", "plan")
|
|
1107
|
+
.in("status", ["draft", "review", "approved", "executing"]);
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
// Non-blocking
|
|
1111
|
+
}
|
|
1112
|
+
// Log summary if provided
|
|
1113
|
+
if (summary) {
|
|
1114
|
+
await logToSession(sb, userId, taskId, summary, true);
|
|
1115
|
+
}
|
|
1116
|
+
return json({ task_id: taskId, status: "done" });
|
|
1117
|
+
}
|
|
1118
|
+
// ── Log Progress ──────────────────────────────────────────
|
|
1119
|
+
case "log_progress": {
|
|
1120
|
+
const taskId = args.task_id;
|
|
1121
|
+
const message = args.message;
|
|
1122
|
+
const sessionId = args.session_id;
|
|
1123
|
+
await logToSession(sb, userId, taskId, message, false, sessionId);
|
|
1124
|
+
return json({ task_id: taskId, session_id: sessionId || null, status: "logged" });
|
|
1125
|
+
}
|
|
1126
|
+
// ── Delete ────────────────────────────────────────────────
|
|
1127
|
+
case "delete_task": {
|
|
1128
|
+
const { error } = await sb
|
|
1129
|
+
.from("kanban_cards")
|
|
1130
|
+
.delete()
|
|
1131
|
+
.eq("id", args.task_id);
|
|
1132
|
+
if (error)
|
|
1133
|
+
throw new Error(error.message);
|
|
1134
|
+
return json({ deleted_task_id: args.task_id, status: "deleted" });
|
|
1135
|
+
}
|
|
1136
|
+
// ── Migration ─────────────────────────────────────────────
|
|
1137
|
+
case "migrate_to_global_board": {
|
|
1138
|
+
// Get or create global board
|
|
1139
|
+
const globalBoard = await getOrCreateGlobalBoard(sb, userId);
|
|
1140
|
+
// Get all tasks from ALL boards (except global board)
|
|
1141
|
+
const { data: allTasks, error: tasksErr } = await sb
|
|
1142
|
+
.from("kanban_cards")
|
|
1143
|
+
.select("id, title, status, board_id")
|
|
1144
|
+
.eq("user_id", userId)
|
|
1145
|
+
.neq("board_id", globalBoard.id);
|
|
1146
|
+
if (tasksErr)
|
|
1147
|
+
throw new Error(tasksErr.message);
|
|
1148
|
+
if (!allTasks || allTasks.length === 0) {
|
|
1149
|
+
return json({ migrated: 0, message: "No tasks to migrate", global_board_id: globalBoard.id });
|
|
1150
|
+
}
|
|
1151
|
+
// Move each task to global board
|
|
1152
|
+
let migrated = 0;
|
|
1153
|
+
for (const task of allTasks) {
|
|
1154
|
+
const colId = await getColumnByStatus(sb, globalBoard.id, task.status || "todo");
|
|
1155
|
+
if (!colId)
|
|
1156
|
+
continue;
|
|
1157
|
+
const { error: updateErr } = await sb
|
|
1158
|
+
.from("kanban_cards")
|
|
1159
|
+
.update({ board_id: globalBoard.id, column_id: colId })
|
|
1160
|
+
.eq("id", task.id);
|
|
1161
|
+
if (!updateErr)
|
|
1162
|
+
migrated++;
|
|
1163
|
+
}
|
|
1164
|
+
return json({
|
|
1165
|
+
migrated,
|
|
1166
|
+
total_found: allTasks.length,
|
|
1167
|
+
global_board_id: globalBoard.id,
|
|
1168
|
+
status: "migration_complete",
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
// ── Start Session ─────────────────────────────────────────
|
|
1172
|
+
case "start_session": {
|
|
1173
|
+
const taskId = args.task_id;
|
|
1174
|
+
const executor = args.executor || detectExecutor();
|
|
1175
|
+
const focus = args.focus;
|
|
1176
|
+
// Find existing non-archived workspace for this task
|
|
1177
|
+
const { data: existingWs } = await sb
|
|
1178
|
+
.from("kanban_workspaces")
|
|
1179
|
+
.select("id, branch")
|
|
1180
|
+
.eq("card_id", taskId)
|
|
1181
|
+
.eq("archived", false)
|
|
1182
|
+
.order("created_at", { ascending: false })
|
|
1183
|
+
.limit(1)
|
|
1184
|
+
.single();
|
|
1185
|
+
let workspaceId;
|
|
1186
|
+
let branch;
|
|
1187
|
+
if (existingWs) {
|
|
1188
|
+
workspaceId = existingWs.id;
|
|
1189
|
+
branch = existingWs.branch;
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
const { data: card, error: cardErr } = await sb
|
|
1193
|
+
.from("kanban_cards")
|
|
1194
|
+
.select("id, board_id, title")
|
|
1195
|
+
.eq("id", taskId)
|
|
1196
|
+
.single();
|
|
1197
|
+
if (cardErr || !card)
|
|
1198
|
+
throw new Error(cardErr?.message || "Task not found");
|
|
1199
|
+
const col = await getColumnByStatus(sb, card.board_id, "inprogress");
|
|
1200
|
+
if (col) {
|
|
1201
|
+
await sb
|
|
1202
|
+
.from("kanban_cards")
|
|
1203
|
+
.update({ status: "inprogress", column_id: col })
|
|
1204
|
+
.eq("id", taskId);
|
|
1205
|
+
}
|
|
1206
|
+
branch = `task/${slugify(card.title)}-${taskId.slice(0, 8)}`;
|
|
1207
|
+
const { data: ws, error: wsErr } = await sb
|
|
1208
|
+
.from("kanban_workspaces")
|
|
1209
|
+
.insert({ card_id: taskId, user_id: userId, branch })
|
|
1210
|
+
.select("id")
|
|
1211
|
+
.single();
|
|
1212
|
+
if (wsErr)
|
|
1213
|
+
throw new Error(wsErr.message);
|
|
1214
|
+
workspaceId = ws.id;
|
|
1215
|
+
}
|
|
1216
|
+
const { data: sess, error: sessErr } = await sb
|
|
1217
|
+
.from("kanban_sessions")
|
|
1218
|
+
.insert({
|
|
1219
|
+
workspace_id: workspaceId,
|
|
1220
|
+
user_id: userId,
|
|
1221
|
+
executor,
|
|
1222
|
+
focus,
|
|
1223
|
+
status: "running",
|
|
1224
|
+
started_at: new Date().toISOString(),
|
|
1225
|
+
})
|
|
1226
|
+
.select("id, executor, focus, status, started_at")
|
|
1227
|
+
.single();
|
|
1228
|
+
if (sessErr)
|
|
1229
|
+
throw new Error(sessErr.message);
|
|
1230
|
+
return json({
|
|
1231
|
+
session_id: sess.id,
|
|
1232
|
+
workspace_id: workspaceId,
|
|
1233
|
+
executor: sess.executor,
|
|
1234
|
+
focus: sess.focus,
|
|
1235
|
+
status: sess.status,
|
|
1236
|
+
started_at: sess.started_at,
|
|
1237
|
+
branch,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
// ── List Sessions ─────────────────────────────────────────
|
|
1241
|
+
case "list_sessions": {
|
|
1242
|
+
const taskId = args.task_id;
|
|
1243
|
+
const statusFilter = args.status;
|
|
1244
|
+
const { data: workspaces, error: wsErr } = await sb
|
|
1245
|
+
.from("kanban_workspaces")
|
|
1246
|
+
.select("id, branch, archived, created_at")
|
|
1247
|
+
.eq("card_id", taskId)
|
|
1248
|
+
.order("created_at", { ascending: false });
|
|
1249
|
+
if (wsErr)
|
|
1250
|
+
throw new Error(wsErr.message);
|
|
1251
|
+
if (!workspaces || workspaces.length === 0) {
|
|
1252
|
+
return json({ sessions: [], count: 0, task_id: taskId });
|
|
1253
|
+
}
|
|
1254
|
+
const workspaceIds = workspaces.map((w) => w.id);
|
|
1255
|
+
let sessionQuery = sb
|
|
1256
|
+
.from("kanban_sessions")
|
|
1257
|
+
.select("id, workspace_id, executor, focus, status, started_at, completed_at, created_at")
|
|
1258
|
+
.in("workspace_id", workspaceIds)
|
|
1259
|
+
.order("created_at", { ascending: false });
|
|
1260
|
+
if (statusFilter) {
|
|
1261
|
+
sessionQuery = sessionQuery.eq("status", statusFilter);
|
|
1262
|
+
}
|
|
1263
|
+
const { data: sessions, error: sessErr } = await sessionQuery;
|
|
1264
|
+
if (sessErr)
|
|
1265
|
+
throw new Error(sessErr.message);
|
|
1266
|
+
const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));
|
|
1267
|
+
const enrichedSessions = (sessions || []).map((s) => {
|
|
1268
|
+
const ws = workspaceMap.get(s.workspace_id);
|
|
1269
|
+
return {
|
|
1270
|
+
session_id: s.id,
|
|
1271
|
+
workspace_id: s.workspace_id,
|
|
1272
|
+
branch: ws?.branch,
|
|
1273
|
+
workspace_archived: ws?.archived,
|
|
1274
|
+
executor: s.executor,
|
|
1275
|
+
focus: s.focus,
|
|
1276
|
+
status: s.status,
|
|
1277
|
+
started_at: s.started_at,
|
|
1278
|
+
completed_at: s.completed_at,
|
|
1279
|
+
created_at: s.created_at,
|
|
1280
|
+
};
|
|
1281
|
+
});
|
|
1282
|
+
return json({
|
|
1283
|
+
sessions: enrichedSessions,
|
|
1284
|
+
count: enrichedSessions.length,
|
|
1285
|
+
task_id: taskId,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
// ── Update Session ────────────────────────────────────────
|
|
1289
|
+
case "update_session": {
|
|
1290
|
+
const sessionId = args.session_id;
|
|
1291
|
+
const updates = {};
|
|
1292
|
+
if (args.status) {
|
|
1293
|
+
updates.status = args.status;
|
|
1294
|
+
if (args.status === "completed") {
|
|
1295
|
+
updates.completed_at = new Date().toISOString();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (args.focus !== undefined) {
|
|
1299
|
+
updates.focus = args.focus;
|
|
1300
|
+
}
|
|
1301
|
+
if (Object.keys(updates).length === 0) {
|
|
1302
|
+
throw new Error("No updates provided. Specify status or focus.");
|
|
1303
|
+
}
|
|
1304
|
+
const { data: sess, error: sessErr } = await sb
|
|
1305
|
+
.from("kanban_sessions")
|
|
1306
|
+
.update(updates)
|
|
1307
|
+
.eq("id", sessionId)
|
|
1308
|
+
.select("id, workspace_id, executor, focus, status, started_at, completed_at")
|
|
1309
|
+
.single();
|
|
1310
|
+
if (sessErr)
|
|
1311
|
+
throw new Error(sessErr.message);
|
|
1312
|
+
return json({
|
|
1313
|
+
session: sess,
|
|
1314
|
+
status: "updated",
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
// ── Planning & Context ────────────────────────────────────
|
|
1318
|
+
case "create_plan": {
|
|
1319
|
+
const taskId = args.task_id;
|
|
1320
|
+
const approach = args.approach;
|
|
1321
|
+
// Build metadata from optional structured fields
|
|
1322
|
+
const metadata = {};
|
|
1323
|
+
if (args.assumptions)
|
|
1324
|
+
metadata.assumptions = args.assumptions;
|
|
1325
|
+
if (args.constraints)
|
|
1326
|
+
metadata.constraints = args.constraints;
|
|
1327
|
+
if (args.success_criteria)
|
|
1328
|
+
metadata.success_criteria = args.success_criteria;
|
|
1329
|
+
if (args.risks)
|
|
1330
|
+
metadata.risks = args.risks;
|
|
1331
|
+
if (args.estimated_steps)
|
|
1332
|
+
metadata.estimated_steps = args.estimated_steps;
|
|
1333
|
+
// Look up project_id from the task's board
|
|
1334
|
+
let projectId = null;
|
|
1335
|
+
try {
|
|
1336
|
+
const { data: card } = await sb
|
|
1337
|
+
.from("kanban_cards")
|
|
1338
|
+
.select("board_id")
|
|
1339
|
+
.eq("id", taskId)
|
|
1340
|
+
.single();
|
|
1341
|
+
if (card) {
|
|
1342
|
+
const { data: board } = await sb
|
|
1343
|
+
.from("kanban_boards")
|
|
1344
|
+
.select("project_id")
|
|
1345
|
+
.eq("id", card.board_id)
|
|
1346
|
+
.single();
|
|
1347
|
+
if (board?.project_id)
|
|
1348
|
+
projectId = board.project_id;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
// Non-blocking
|
|
1353
|
+
}
|
|
1354
|
+
const { data: plan, error: planErr } = await sb
|
|
1355
|
+
.from("planning_context")
|
|
1356
|
+
.insert({
|
|
1357
|
+
user_id: userId,
|
|
1358
|
+
project_id: projectId,
|
|
1359
|
+
task_id: taskId,
|
|
1360
|
+
type: "plan",
|
|
1361
|
+
title: `Plan: ${approach.slice(0, 80)}`,
|
|
1362
|
+
body: approach,
|
|
1363
|
+
metadata,
|
|
1364
|
+
status: "draft",
|
|
1365
|
+
source: "agent",
|
|
1366
|
+
source_agent: detectExecutor(),
|
|
1367
|
+
})
|
|
1368
|
+
.select("id, status, version")
|
|
1369
|
+
.single();
|
|
1370
|
+
if (planErr)
|
|
1371
|
+
throw new Error(planErr.message);
|
|
1372
|
+
return json({ plan_id: plan.id, status: plan.status, version: plan.version });
|
|
1373
|
+
}
|
|
1374
|
+
case "get_plan": {
|
|
1375
|
+
const taskId = args.task_id;
|
|
1376
|
+
const { data: plan, error: planErr } = await sb
|
|
1377
|
+
.from("planning_context")
|
|
1378
|
+
.select("id, title, body, metadata, status, version, source, source_agent, confidence, created_at, updated_at")
|
|
1379
|
+
.eq("task_id", taskId)
|
|
1380
|
+
.eq("type", "plan")
|
|
1381
|
+
.not("status", "in", '("completed","archived","superseded")')
|
|
1382
|
+
.single();
|
|
1383
|
+
if (planErr || !plan) {
|
|
1384
|
+
return json({ plan: null, versions: [], message: "No active plan for this task" });
|
|
1385
|
+
}
|
|
1386
|
+
const { data: versions } = await sb
|
|
1387
|
+
.from("planning_context_versions")
|
|
1388
|
+
.select("version, title, body, metadata, status, feedback, created_at")
|
|
1389
|
+
.eq("context_id", plan.id)
|
|
1390
|
+
.order("version", { ascending: false });
|
|
1391
|
+
return json({ plan, versions: versions || [] });
|
|
1392
|
+
}
|
|
1393
|
+
case "update_plan": {
|
|
1394
|
+
const planId = args.plan_id;
|
|
1395
|
+
// Get current plan state
|
|
1396
|
+
const { data: current, error: getErr } = await sb
|
|
1397
|
+
.from("planning_context")
|
|
1398
|
+
.select("id, title, body, metadata, status, version")
|
|
1399
|
+
.eq("id", planId)
|
|
1400
|
+
.single();
|
|
1401
|
+
if (getErr || !current)
|
|
1402
|
+
throw new Error(getErr?.message || "Plan not found");
|
|
1403
|
+
const hasContentChange = args.approach || args.assumptions || args.constraints ||
|
|
1404
|
+
args.success_criteria || args.risks;
|
|
1405
|
+
const hasFeedback = !!args.feedback;
|
|
1406
|
+
// Archive current version if content is changing or feedback given
|
|
1407
|
+
if (hasContentChange || hasFeedback) {
|
|
1408
|
+
await sb.from("planning_context_versions").insert({
|
|
1409
|
+
context_id: planId,
|
|
1410
|
+
version: current.version,
|
|
1411
|
+
title: current.title,
|
|
1412
|
+
body: current.body,
|
|
1413
|
+
metadata: current.metadata,
|
|
1414
|
+
status: current.status,
|
|
1415
|
+
feedback: args.feedback || null,
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
// Build updates
|
|
1419
|
+
const updates = {
|
|
1420
|
+
updated_at: new Date().toISOString(),
|
|
1421
|
+
};
|
|
1422
|
+
if (args.approach) {
|
|
1423
|
+
updates.body = args.approach;
|
|
1424
|
+
updates.title = `Plan: ${args.approach.slice(0, 80)}`;
|
|
1425
|
+
}
|
|
1426
|
+
// Merge metadata fields
|
|
1427
|
+
const meta = { ...current.metadata };
|
|
1428
|
+
if (args.assumptions)
|
|
1429
|
+
meta.assumptions = args.assumptions;
|
|
1430
|
+
if (args.constraints)
|
|
1431
|
+
meta.constraints = args.constraints;
|
|
1432
|
+
if (args.success_criteria)
|
|
1433
|
+
meta.success_criteria = args.success_criteria;
|
|
1434
|
+
if (args.risks)
|
|
1435
|
+
meta.risks = args.risks;
|
|
1436
|
+
if (hasContentChange)
|
|
1437
|
+
updates.metadata = meta;
|
|
1438
|
+
// Feedback auto-reverts to draft
|
|
1439
|
+
if (hasFeedback) {
|
|
1440
|
+
updates.status = "draft";
|
|
1441
|
+
const feedbackMeta = { ...meta, feedback: args.feedback };
|
|
1442
|
+
updates.metadata = feedbackMeta;
|
|
1443
|
+
}
|
|
1444
|
+
else if (args.status) {
|
|
1445
|
+
updates.status = args.status;
|
|
1446
|
+
}
|
|
1447
|
+
if (hasContentChange || hasFeedback) {
|
|
1448
|
+
updates.version = current.version + 1;
|
|
1449
|
+
}
|
|
1450
|
+
const { data: updated, error: upErr } = await sb
|
|
1451
|
+
.from("planning_context")
|
|
1452
|
+
.update(updates)
|
|
1453
|
+
.eq("id", planId)
|
|
1454
|
+
.select("id, status, version")
|
|
1455
|
+
.single();
|
|
1456
|
+
if (upErr)
|
|
1457
|
+
throw new Error(upErr.message);
|
|
1458
|
+
return json({ plan_id: updated.id, version: updated.version, status: updated.status });
|
|
1459
|
+
}
|
|
1460
|
+
// query_context, add_context, record_learning → removed (consolidated into save/recall)
|
|
1461
|
+
// ─── Context Extraction Tools ────────────────────────────────────────────
|
|
1462
|
+
case "process_transcript": {
|
|
1463
|
+
const text = args.text;
|
|
1464
|
+
const source = args.source || "mcp";
|
|
1465
|
+
const isChat = args.is_chat !== false;
|
|
1466
|
+
const result = await runPipeline({ text, source, isChat });
|
|
1467
|
+
return json({
|
|
1468
|
+
blocks_extracted: result.blocks.length,
|
|
1469
|
+
blocks: result.blocks,
|
|
1470
|
+
skills: result.skills,
|
|
1471
|
+
redaction_stats: result.redactionStats,
|
|
1472
|
+
total_redactions: result.totalRedactions,
|
|
1473
|
+
chunks_processed: result.chunkCount,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
case "get_context_blocks": {
|
|
1477
|
+
const blocks = getBlocks();
|
|
1478
|
+
return json({ count: blocks.length, blocks });
|
|
1479
|
+
}
|
|
1480
|
+
case "recommend_skills": {
|
|
1481
|
+
const blocks = getBlocks();
|
|
1482
|
+
const skills = await matchSkills(blocks);
|
|
1483
|
+
return json({ skills });
|
|
1484
|
+
}
|
|
1485
|
+
// ─── Skill Discovery Tools ──────────────────────────────────────────────
|
|
1486
|
+
case "discover_skills": {
|
|
1487
|
+
let blocks = getBlocks();
|
|
1488
|
+
// If context text provided, process it first
|
|
1489
|
+
if (args.context) {
|
|
1490
|
+
const result = await runPipeline({
|
|
1491
|
+
text: args.context,
|
|
1492
|
+
source: "discover",
|
|
1493
|
+
isChat: true,
|
|
1494
|
+
});
|
|
1495
|
+
blocks = result.blocks;
|
|
1496
|
+
}
|
|
1497
|
+
if (blocks.length === 0) {
|
|
1498
|
+
return json({
|
|
1499
|
+
error: "No context blocks found. Process a transcript first with process_transcript, or provide context text.",
|
|
1500
|
+
skills: [],
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
const skills = await discoverSkills(blocks, {
|
|
1504
|
+
category: args.category,
|
|
1505
|
+
tier: args.tier,
|
|
1506
|
+
limit: args.limit || 10,
|
|
1507
|
+
});
|
|
1508
|
+
return json({
|
|
1509
|
+
total_available: 154,
|
|
1510
|
+
results: skills.length,
|
|
1511
|
+
skills: skills.map((s) => ({
|
|
1512
|
+
id: s.skillId,
|
|
1513
|
+
name: s.name,
|
|
1514
|
+
description: s.description,
|
|
1515
|
+
score: `${Math.round(s.score * 100)}%`,
|
|
1516
|
+
tier: s.tier,
|
|
1517
|
+
category: s.category,
|
|
1518
|
+
install: s.installMethod === "copy" ? `Copy SKILL.md from ${s.installTarget}` : s.installTarget,
|
|
1519
|
+
matched_keywords: s.matchedKeywords,
|
|
1520
|
+
})),
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
case "search_skills": {
|
|
1524
|
+
const query = args.query;
|
|
1525
|
+
if (!query)
|
|
1526
|
+
return json({ error: "Missing 'query' parameter" });
|
|
1527
|
+
const results = searchSkills(query, {
|
|
1528
|
+
tier: args.tier,
|
|
1529
|
+
category: args.category,
|
|
1530
|
+
});
|
|
1531
|
+
return json({
|
|
1532
|
+
query,
|
|
1533
|
+
results: results.length,
|
|
1534
|
+
skills: results.map((s) => ({
|
|
1535
|
+
id: s.skillId,
|
|
1536
|
+
name: s.name,
|
|
1537
|
+
description: s.description,
|
|
1538
|
+
score: `${Math.round(s.score * 100)}%`,
|
|
1539
|
+
tier: s.tier,
|
|
1540
|
+
category: s.category,
|
|
1541
|
+
install_method: s.installMethod,
|
|
1542
|
+
install_target: s.installTarget,
|
|
1543
|
+
})),
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
case "install_skill": {
|
|
1547
|
+
const skillId = args.skill_id;
|
|
1548
|
+
if (!skillId)
|
|
1549
|
+
return json({ error: "Missing 'skill_id' parameter" });
|
|
1550
|
+
// Search both catalogs for the skill
|
|
1551
|
+
const skill = getFullCatalog().find((s) => s.id === skillId);
|
|
1552
|
+
if (!skill) {
|
|
1553
|
+
return json({ error: `Skill '${skillId}' not found. Use search_skills to find available skills.` });
|
|
1554
|
+
}
|
|
1555
|
+
if (skill.installMethod === "copy") {
|
|
1556
|
+
const targetDir = args.target_dir || path.join(process.cwd(), ".claude", "commands");
|
|
1557
|
+
const skillName = skill.id.replace(/^comm-/, "");
|
|
1558
|
+
// For native CrowdListen skills, the installTarget is a crowdlisten: reference
|
|
1559
|
+
if (skill.installTarget.startsWith("crowdlisten:")) {
|
|
1560
|
+
return json({
|
|
1561
|
+
skill: skill.name,
|
|
1562
|
+
tier: skill.tier,
|
|
1563
|
+
instructions: `This is a native CrowdListen skill. It requires a CROWDLISTEN_API_KEY.`,
|
|
1564
|
+
install: `Add to your .claude/commands/ directory from the crowdlisten_tasks/skills/${skill.id}/ folder.`,
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
// Community skill — provide the raw URL
|
|
1568
|
+
return json({
|
|
1569
|
+
skill: skill.name,
|
|
1570
|
+
tier: skill.tier,
|
|
1571
|
+
install_method: "copy",
|
|
1572
|
+
instructions: [
|
|
1573
|
+
`1. Create directory: mkdir -p "${targetDir}"`,
|
|
1574
|
+
`2. Download: curl -o "${path.join(targetDir, skillName + ".md")}" "${skill.installTarget}"`,
|
|
1575
|
+
`3. Or copy the SKILL.md content from: ${skill.installTarget}`,
|
|
1576
|
+
],
|
|
1577
|
+
source: skill.source,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
return json({
|
|
1581
|
+
skill: skill.name,
|
|
1582
|
+
install_method: skill.installMethod,
|
|
1583
|
+
install_target: skill.installTarget,
|
|
1584
|
+
instructions: skill.installMethod === "npx"
|
|
1585
|
+
? `Run: npx ${skill.installTarget}`
|
|
1586
|
+
: `Clone: git clone ${skill.installTarget}`,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
// ── Core Always-On Tools ─────────────────────────────────────
|
|
1590
|
+
case "list_skill_packs": {
|
|
1591
|
+
const includeVirtual = args.include_virtual !== false;
|
|
1592
|
+
const state = loadUserState();
|
|
1593
|
+
let packList = listPacks(state.activePacks);
|
|
1594
|
+
if (!includeVirtual) {
|
|
1595
|
+
packList = packList.filter(p => !p.isVirtual);
|
|
1596
|
+
}
|
|
1597
|
+
return json({
|
|
1598
|
+
packs: packList,
|
|
1599
|
+
activePacks: state.activePacks.length,
|
|
1600
|
+
totalPacks: packList.length,
|
|
1601
|
+
hint: "Call activate_skill_pack with a pack_id to unlock its tools.",
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
case "activate_skill_pack": {
|
|
1605
|
+
const packId = args.pack_id;
|
|
1606
|
+
if (!packId)
|
|
1607
|
+
return json({ error: "Missing 'pack_id' parameter" });
|
|
1608
|
+
if (!hasPack(packId))
|
|
1609
|
+
return json({ error: `Pack '${packId}' not found. Use list_skill_packs to see available packs.` });
|
|
1610
|
+
const pack = getPack(packId);
|
|
1611
|
+
// Virtual SKILL.md packs — return content instead of activating tools
|
|
1612
|
+
if (pack.isVirtual) {
|
|
1613
|
+
const content = getSkillMdContent(packId);
|
|
1614
|
+
if (!content)
|
|
1615
|
+
return json({ error: `SKILL.md not found for pack '${packId}'` });
|
|
1616
|
+
return json({
|
|
1617
|
+
activated: packId,
|
|
1618
|
+
type: "skill_workflow",
|
|
1619
|
+
name: pack.name,
|
|
1620
|
+
description: pack.description,
|
|
1621
|
+
instructions: content,
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
// Regular tool pack — activate and signal tools/list_changed
|
|
1625
|
+
const state = activatePack(packId);
|
|
1626
|
+
const tools = getPackTools(packId).map(t => t.name);
|
|
1627
|
+
// Note: server.sendToolListChanged() is called by the index.ts dispatcher
|
|
1628
|
+
return json({
|
|
1629
|
+
activated: packId,
|
|
1630
|
+
type: "tool_pack",
|
|
1631
|
+
name: pack.name,
|
|
1632
|
+
tools,
|
|
1633
|
+
toolCount: tools.length,
|
|
1634
|
+
totalActivePacks: state.activePacks.length,
|
|
1635
|
+
_needsListChanged: true,
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
case "save": {
|
|
1639
|
+
const title = args.title;
|
|
1640
|
+
const content = args.content;
|
|
1641
|
+
if (!title || !content) {
|
|
1642
|
+
return json({ error: "Missing required parameters: title, content" });
|
|
1643
|
+
}
|
|
1644
|
+
const tags = args.tags || [];
|
|
1645
|
+
const projectId = args.project_id || null;
|
|
1646
|
+
const taskId = args.task_id || null;
|
|
1647
|
+
const confidence = args.confidence ?? 1.0;
|
|
1648
|
+
const sourceAgent = detectExecutor();
|
|
1649
|
+
// Primary: write to Supabase memories table
|
|
1650
|
+
let savedToSupabase = false;
|
|
1651
|
+
let memoryId = null;
|
|
1652
|
+
try {
|
|
1653
|
+
const { data: row, error: sbErr } = await sb
|
|
1654
|
+
.from("memories")
|
|
1655
|
+
.insert({
|
|
1656
|
+
user_id: userId,
|
|
1657
|
+
project_id: projectId,
|
|
1658
|
+
task_id: taskId,
|
|
1659
|
+
title,
|
|
1660
|
+
content,
|
|
1661
|
+
tags,
|
|
1662
|
+
source: "agent",
|
|
1663
|
+
source_agent: sourceAgent,
|
|
1664
|
+
confidence,
|
|
1665
|
+
})
|
|
1666
|
+
.select("id")
|
|
1667
|
+
.single();
|
|
1668
|
+
if (sbErr)
|
|
1669
|
+
throw sbErr;
|
|
1670
|
+
savedToSupabase = true;
|
|
1671
|
+
memoryId = row.id;
|
|
1672
|
+
// Fire-and-forget: generate embedding via agent backend
|
|
1673
|
+
const AGENT_BASE = process.env.CROWDLISTEN_AGENT_URL || "https://agent.crowdlisten.com";
|
|
1674
|
+
fetch(`${AGENT_BASE}/agent/v1/content/embed`, {
|
|
1675
|
+
method: "POST",
|
|
1676
|
+
headers: { "Content-Type": "application/json" },
|
|
1677
|
+
body: JSON.stringify({ text: `${title}\n${content}` }),
|
|
1678
|
+
})
|
|
1679
|
+
.then(res => res.ok ? res.json() : null)
|
|
1680
|
+
.then(data => {
|
|
1681
|
+
if (data?.embedding && memoryId) {
|
|
1682
|
+
sb.from("memories")
|
|
1683
|
+
.update({ embedding: data.embedding })
|
|
1684
|
+
.eq("id", memoryId)
|
|
1685
|
+
.then(() => { });
|
|
1686
|
+
}
|
|
1687
|
+
})
|
|
1688
|
+
.catch(() => {
|
|
1689
|
+
// Non-blocking — row exists without embedding, keyword fallback on recall
|
|
1690
|
+
});
|
|
1691
|
+
// Side effect: dual-write to project_insights for frontend visibility
|
|
1692
|
+
const knowledgeTags = ["decision", "pattern", "preference", "learning", "principle"];
|
|
1693
|
+
if (projectId && tags.some(t => knowledgeTags.includes(t))) {
|
|
1694
|
+
try {
|
|
1695
|
+
await sb.from("project_insights").insert({
|
|
1696
|
+
project_id: projectId,
|
|
1697
|
+
user_id: userId,
|
|
1698
|
+
title,
|
|
1699
|
+
content,
|
|
1700
|
+
source: "mcp_save",
|
|
1701
|
+
source_agent: sourceAgent,
|
|
1702
|
+
category: tags.find(t => knowledgeTags.includes(t)) || "insight",
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
catch {
|
|
1706
|
+
// Non-blocking
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
catch (err) {
|
|
1711
|
+
console.error(`[save] Supabase write failed: ${err?.message || err}`);
|
|
1712
|
+
// Fallback to local store
|
|
1713
|
+
const block = {
|
|
1714
|
+
type: (tags[0] || "insight"),
|
|
1715
|
+
title,
|
|
1716
|
+
content,
|
|
1717
|
+
source: "save",
|
|
1718
|
+
};
|
|
1719
|
+
addBlocks([block], "save");
|
|
1720
|
+
}
|
|
1721
|
+
return json({ saved: true, id: memoryId, title, tags, supabase: savedToSupabase });
|
|
1722
|
+
}
|
|
1723
|
+
case "recall": {
|
|
1724
|
+
const search = args.search;
|
|
1725
|
+
const tags = args.tags;
|
|
1726
|
+
const projectId = args.project_id;
|
|
1727
|
+
const limit = args.limit || 20;
|
|
1728
|
+
try {
|
|
1729
|
+
// Try semantic search first via agent embedding endpoint
|
|
1730
|
+
const AGENT_BASE = process.env.CROWDLISTEN_AGENT_URL || "https://agent.crowdlisten.com";
|
|
1731
|
+
let usedSemantic = false;
|
|
1732
|
+
if (search) {
|
|
1733
|
+
try {
|
|
1734
|
+
const embedRes = await fetch(`${AGENT_BASE}/agent/v1/content/embed`, {
|
|
1735
|
+
method: "POST",
|
|
1736
|
+
headers: { "Content-Type": "application/json" },
|
|
1737
|
+
body: JSON.stringify({ text: search }),
|
|
1738
|
+
});
|
|
1739
|
+
if (embedRes.ok) {
|
|
1740
|
+
const embedData = await embedRes.json();
|
|
1741
|
+
if (embedData.embedding) {
|
|
1742
|
+
// Use RPC for semantic search
|
|
1743
|
+
const rpcArgs = {
|
|
1744
|
+
p_user_id: userId,
|
|
1745
|
+
p_query_embedding: embedData.embedding,
|
|
1746
|
+
p_match_count: limit,
|
|
1747
|
+
};
|
|
1748
|
+
if (projectId)
|
|
1749
|
+
rpcArgs.p_project_id = projectId;
|
|
1750
|
+
if (tags && tags.length > 0)
|
|
1751
|
+
rpcArgs.p_tags = tags;
|
|
1752
|
+
const { data, error: rpcErr } = await sb.rpc("search_memories", rpcArgs);
|
|
1753
|
+
if (!rpcErr && data && data.length > 0) {
|
|
1754
|
+
usedSemantic = true;
|
|
1755
|
+
return json({
|
|
1756
|
+
memories: data,
|
|
1757
|
+
count: data.length,
|
|
1758
|
+
search_mode: "semantic",
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
catch {
|
|
1765
|
+
// Embedding API unavailable — fall through to keyword search
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
// Keyword fallback: ILIKE on title/content
|
|
1769
|
+
if (!usedSemantic) {
|
|
1770
|
+
let query = sb
|
|
1771
|
+
.from("memories")
|
|
1772
|
+
.select("id, title, content, tags, source, source_agent, task_id, project_id, confidence, metadata, created_at")
|
|
1773
|
+
.eq("user_id", userId)
|
|
1774
|
+
.order("created_at", { ascending: false })
|
|
1775
|
+
.limit(limit);
|
|
1776
|
+
if (projectId)
|
|
1777
|
+
query = query.eq("project_id", projectId);
|
|
1778
|
+
if (tags && tags.length > 0)
|
|
1779
|
+
query = query.overlaps("tags", tags);
|
|
1780
|
+
if (search)
|
|
1781
|
+
query = query.or(`title.ilike.%${search}%,content.ilike.%${search}%`);
|
|
1782
|
+
const { data, error: sbErr } = await query;
|
|
1783
|
+
if (sbErr)
|
|
1784
|
+
throw sbErr;
|
|
1785
|
+
return json({
|
|
1786
|
+
memories: data || [],
|
|
1787
|
+
count: (data || []).length,
|
|
1788
|
+
search_mode: "keyword",
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
// Fallback to local store
|
|
1794
|
+
let blocks = getBlocks();
|
|
1795
|
+
if (search) {
|
|
1796
|
+
const lower = search.toLowerCase();
|
|
1797
|
+
blocks = blocks.filter(b => b.title.toLowerCase().includes(lower) ||
|
|
1798
|
+
b.content.toLowerCase().includes(lower));
|
|
1799
|
+
}
|
|
1800
|
+
blocks = blocks.slice(-limit);
|
|
1801
|
+
return json({
|
|
1802
|
+
memories: blocks,
|
|
1803
|
+
count: blocks.length,
|
|
1804
|
+
search_mode: "local",
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
// Should not reach here, but return empty just in case
|
|
1808
|
+
return json({ memories: [], count: 0, search_mode: "none" });
|
|
1809
|
+
}
|
|
1810
|
+
// ── Spec Delivery ────────────────────────────────────────────────
|
|
1811
|
+
case "get_specs": {
|
|
1812
|
+
const statusFilter = args.status || "pending";
|
|
1813
|
+
const limit = args.limit || 20;
|
|
1814
|
+
let query = sb
|
|
1815
|
+
.from("actionable_specs")
|
|
1816
|
+
.select("id, spec_type, title, objective, priority, confidence, status, project_id, created_at")
|
|
1817
|
+
.eq("user_id", userId);
|
|
1818
|
+
if (args.project_id)
|
|
1819
|
+
query = query.eq("project_id", args.project_id);
|
|
1820
|
+
if (statusFilter)
|
|
1821
|
+
query = query.eq("status", statusFilter);
|
|
1822
|
+
if (args.spec_type)
|
|
1823
|
+
query = query.eq("spec_type", args.spec_type);
|
|
1824
|
+
if (args.min_confidence != null)
|
|
1825
|
+
query = query.gte("confidence", args.min_confidence);
|
|
1826
|
+
if (args.priority)
|
|
1827
|
+
query = query.eq("priority", args.priority);
|
|
1828
|
+
const { data, error } = await query
|
|
1829
|
+
.order("created_at", { ascending: false })
|
|
1830
|
+
.limit(limit);
|
|
1831
|
+
if (error)
|
|
1832
|
+
throw new Error(error.message);
|
|
1833
|
+
return json({ specs: data || [], count: (data || []).length, filters: { status: statusFilter } });
|
|
1834
|
+
}
|
|
1835
|
+
case "get_spec_detail": {
|
|
1836
|
+
const specId = args.spec_id;
|
|
1837
|
+
const { data, error } = await sb
|
|
1838
|
+
.from("actionable_specs")
|
|
1839
|
+
.select("*")
|
|
1840
|
+
.eq("id", specId)
|
|
1841
|
+
.eq("user_id", userId)
|
|
1842
|
+
.single();
|
|
1843
|
+
if (error || !data)
|
|
1844
|
+
throw new Error(error?.message || "Spec not found or access denied");
|
|
1845
|
+
// Format for agent consumption
|
|
1846
|
+
const spec = data;
|
|
1847
|
+
const markdown = [
|
|
1848
|
+
`# ${spec.title}`,
|
|
1849
|
+
`**Type:** ${spec.spec_type} | **Priority:** ${spec.priority} | **Confidence:** ${(spec.confidence * 100).toFixed(0)}%`,
|
|
1850
|
+
"",
|
|
1851
|
+
`## Objective`,
|
|
1852
|
+
spec.objective,
|
|
1853
|
+
"",
|
|
1854
|
+
];
|
|
1855
|
+
if (spec.description) {
|
|
1856
|
+
markdown.push("## Description", spec.description, "");
|
|
1857
|
+
}
|
|
1858
|
+
const evidence = spec.evidence;
|
|
1859
|
+
if (evidence && evidence.length > 0) {
|
|
1860
|
+
markdown.push("## Evidence");
|
|
1861
|
+
for (const e of evidence) {
|
|
1862
|
+
markdown.push(`- [${e.source}] "${e.excerpt}" (confidence: ${((e.confidence || 0) * 100).toFixed(0)}%)`);
|
|
1863
|
+
}
|
|
1864
|
+
markdown.push("");
|
|
1865
|
+
}
|
|
1866
|
+
const criteria = spec.acceptance_criteria;
|
|
1867
|
+
if (criteria && criteria.length > 0) {
|
|
1868
|
+
markdown.push("## Acceptance Criteria");
|
|
1869
|
+
for (const c of criteria) {
|
|
1870
|
+
markdown.push(`- [ ] ${c}`);
|
|
1871
|
+
}
|
|
1872
|
+
markdown.push("");
|
|
1873
|
+
}
|
|
1874
|
+
return json({
|
|
1875
|
+
spec: data,
|
|
1876
|
+
formatted: markdown.join("\n"),
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
case "start_spec": {
|
|
1880
|
+
const specId = args.spec_id;
|
|
1881
|
+
const executor = args.executor || detectExecutor();
|
|
1882
|
+
// 1. Fetch the spec
|
|
1883
|
+
const { data: spec, error: specErr } = await sb
|
|
1884
|
+
.from("actionable_specs")
|
|
1885
|
+
.select("*")
|
|
1886
|
+
.eq("id", specId)
|
|
1887
|
+
.eq("user_id", userId)
|
|
1888
|
+
.single();
|
|
1889
|
+
if (specErr || !spec)
|
|
1890
|
+
throw new Error(specErr?.message || "Spec not found or access denied");
|
|
1891
|
+
if (spec.status !== "pending") {
|
|
1892
|
+
return json({ error: `Spec is already ${spec.status}. Only pending specs can be started.` });
|
|
1893
|
+
}
|
|
1894
|
+
// 2. Create a kanban task from the spec
|
|
1895
|
+
const globalBoard = await getOrCreateGlobalBoard(sb, userId);
|
|
1896
|
+
const todoCol = await getColumnByStatus(sb, globalBoard.id, "todo");
|
|
1897
|
+
if (!todoCol)
|
|
1898
|
+
throw new Error("Could not find 'To Do' column");
|
|
1899
|
+
const { data: lastCard } = await sb
|
|
1900
|
+
.from("kanban_cards")
|
|
1901
|
+
.select("position")
|
|
1902
|
+
.eq("column_id", todoCol)
|
|
1903
|
+
.order("position", { ascending: false })
|
|
1904
|
+
.limit(1)
|
|
1905
|
+
.single();
|
|
1906
|
+
// Build task description from spec
|
|
1907
|
+
const criteria = spec.acceptance_criteria;
|
|
1908
|
+
const evidence = spec.evidence;
|
|
1909
|
+
const taskDesc = [
|
|
1910
|
+
`**Objective:** ${spec.objective}`,
|
|
1911
|
+
spec.description ? `\n${spec.description}` : "",
|
|
1912
|
+
criteria?.length ? `\n**Acceptance Criteria:**\n${criteria.map((c) => `- [ ] ${c}`).join("\n")}` : "",
|
|
1913
|
+
evidence?.length ? `\n**Evidence:**\n${evidence.map((e) => `- [${e.source}] "${e.excerpt}"`).join("\n")}` : "",
|
|
1914
|
+
`\n_Generated from spec ${specId} (${spec.spec_type}, ${spec.priority} priority, ${(spec.confidence * 100).toFixed(0)}% confidence)_`,
|
|
1915
|
+
].filter(Boolean).join("\n");
|
|
1916
|
+
const labels = [];
|
|
1917
|
+
if (spec.project_id) {
|
|
1918
|
+
labels.push({ name: `project:${spec.project_id}`, color: "#6366f1" });
|
|
1919
|
+
}
|
|
1920
|
+
labels.push({ name: `spec:${spec.spec_type}`, color: "#10b981" });
|
|
1921
|
+
labels.push({ name: `priority:${spec.priority}`, color: spec.priority === "critical" ? "#ef4444" : spec.priority === "high" ? "#f59e0b" : "#6b7280" });
|
|
1922
|
+
const { data: card, error: cardErr } = await sb
|
|
1923
|
+
.from("kanban_cards")
|
|
1924
|
+
.insert({
|
|
1925
|
+
board_id: globalBoard.id,
|
|
1926
|
+
column_id: todoCol,
|
|
1927
|
+
user_id: userId,
|
|
1928
|
+
title: spec.title,
|
|
1929
|
+
description: taskDesc,
|
|
1930
|
+
priority: spec.priority,
|
|
1931
|
+
labels,
|
|
1932
|
+
status: "todo",
|
|
1933
|
+
position: (lastCard?.position || 0) + 1,
|
|
1934
|
+
})
|
|
1935
|
+
.select("id")
|
|
1936
|
+
.single();
|
|
1937
|
+
if (cardErr || !card)
|
|
1938
|
+
throw new Error(cardErr?.message || "Failed to create task from spec");
|
|
1939
|
+
// 3. Claim the task (move to In Progress, create workspace + session)
|
|
1940
|
+
const inProgressCol = await getColumnByStatus(sb, globalBoard.id, "inprogress");
|
|
1941
|
+
if (inProgressCol) {
|
|
1942
|
+
await sb.from("kanban_cards")
|
|
1943
|
+
.update({ status: "inprogress", column_id: inProgressCol })
|
|
1944
|
+
.eq("id", card.id);
|
|
1945
|
+
}
|
|
1946
|
+
const branch = `spec/${slugify(spec.title)}-${card.id.slice(0, 8)}`;
|
|
1947
|
+
const { data: ws } = await sb
|
|
1948
|
+
.from("kanban_workspaces")
|
|
1949
|
+
.insert({ card_id: card.id, user_id: userId, branch })
|
|
1950
|
+
.select("id")
|
|
1951
|
+
.single();
|
|
1952
|
+
const { data: sess } = await sb
|
|
1953
|
+
.from("kanban_sessions")
|
|
1954
|
+
.insert({ workspace_id: ws.id, user_id: userId, executor })
|
|
1955
|
+
.select("id")
|
|
1956
|
+
.single();
|
|
1957
|
+
// 4. Update spec status to claimed
|
|
1958
|
+
await sb.from("actionable_specs")
|
|
1959
|
+
.update({ status: "claimed" })
|
|
1960
|
+
.eq("id", specId);
|
|
1961
|
+
return json({
|
|
1962
|
+
spec_id: specId,
|
|
1963
|
+
task_id: card.id,
|
|
1964
|
+
workspace_id: ws.id,
|
|
1965
|
+
session_id: sess?.id,
|
|
1966
|
+
branch,
|
|
1967
|
+
executor,
|
|
1968
|
+
status: "started",
|
|
1969
|
+
spec: {
|
|
1970
|
+
title: spec.title,
|
|
1971
|
+
spec_type: spec.spec_type,
|
|
1972
|
+
priority: spec.priority,
|
|
1973
|
+
objective: spec.objective,
|
|
1974
|
+
acceptance_criteria: spec.acceptance_criteria,
|
|
1975
|
+
},
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
// ── Preferences ─────────────────────────────────────────────────
|
|
1979
|
+
case "set_preferences": {
|
|
1980
|
+
const state = loadUserState();
|
|
1981
|
+
const changed = [];
|
|
1982
|
+
if (args.telemetry !== undefined) {
|
|
1983
|
+
const level = args.telemetry;
|
|
1984
|
+
if (!["off", "anonymous", "community"].includes(level)) {
|
|
1985
|
+
return json({ error: "Invalid telemetry level. Use: off, anonymous, community" });
|
|
1986
|
+
}
|
|
1987
|
+
state.preferences.telemetry = level;
|
|
1988
|
+
changed.push(`telemetry → ${level}`);
|
|
1989
|
+
// Mark telemetry onboarding complete
|
|
1990
|
+
if (!state.onboardingCompleted.includes("telemetry")) {
|
|
1991
|
+
state.onboardingCompleted.push("telemetry");
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (args.proactive_suggestions !== undefined) {
|
|
1995
|
+
state.preferences.proactiveSuggestions = !!args.proactive_suggestions;
|
|
1996
|
+
changed.push(`proactive_suggestions → ${state.preferences.proactiveSuggestions}`);
|
|
1997
|
+
if (!state.onboardingCompleted.includes("proactive")) {
|
|
1998
|
+
state.onboardingCompleted.push("proactive");
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
if (args.cross_project_learnings !== undefined) {
|
|
2002
|
+
state.preferences.crossProjectLearnings = !!args.cross_project_learnings;
|
|
2003
|
+
changed.push(`cross_project_learnings → ${state.preferences.crossProjectLearnings}`);
|
|
2004
|
+
if (!state.onboardingCompleted.includes("learnings")) {
|
|
2005
|
+
state.onboardingCompleted.push("learnings");
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (changed.length === 0) {
|
|
2009
|
+
return json({ message: "No preferences changed. Pass telemetry, proactive_suggestions, or cross_project_learnings." });
|
|
2010
|
+
}
|
|
2011
|
+
saveUserState(state);
|
|
2012
|
+
return json({
|
|
2013
|
+
updated: changed,
|
|
2014
|
+
preferences: {
|
|
2015
|
+
telemetry: state.preferences.telemetry,
|
|
2016
|
+
proactiveSuggestions: state.preferences.proactiveSuggestions,
|
|
2017
|
+
crossProjectLearnings: state.preferences.crossProjectLearnings,
|
|
2018
|
+
},
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
// log_learning, search_learnings → removed (consolidated into save/recall)
|
|
2022
|
+
default: {
|
|
2023
|
+
// Delegate to agent-proxied tools
|
|
2024
|
+
if (isAgentTool(name)) {
|
|
2025
|
+
return handleAgentTool(name, args);
|
|
2026
|
+
}
|
|
2027
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
2032
|
+
export function json(obj) {
|
|
2033
|
+
return JSON.stringify(obj, null, 2);
|
|
2034
|
+
}
|
|
2035
|
+
export function slugify(text) {
|
|
2036
|
+
return text
|
|
2037
|
+
.toLowerCase()
|
|
2038
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
2039
|
+
.replace(/^-|-$/g, "")
|
|
2040
|
+
.slice(0, 40);
|
|
2041
|
+
}
|
|
2042
|
+
export function detectExecutor() {
|
|
2043
|
+
if (process.env.OPENCLAW_SESSION || process.env.OPENCLAW_AGENT)
|
|
2044
|
+
return "OPENCLAW";
|
|
2045
|
+
if (process.env.CLAUDE_CODE === "1" || process.env.CLAUDE_SESSION_ID)
|
|
2046
|
+
return "CLAUDE_CODE";
|
|
2047
|
+
if (process.env.CURSOR_SESSION_ID || process.env.CURSOR_TRACE_ID)
|
|
2048
|
+
return "CURSOR";
|
|
2049
|
+
if (process.env.CODEX_SESSION_ID)
|
|
2050
|
+
return "CODEX";
|
|
2051
|
+
if (process.env.GEMINI_CLI)
|
|
2052
|
+
return "GEMINI";
|
|
2053
|
+
if (process.env.AMP_SESSION_ID)
|
|
2054
|
+
return "AMP";
|
|
2055
|
+
try {
|
|
2056
|
+
const ppid = process.ppid;
|
|
2057
|
+
if (ppid) {
|
|
2058
|
+
// Best effort
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
catch {
|
|
2062
|
+
// ignore
|
|
2063
|
+
}
|
|
2064
|
+
return "UNKNOWN";
|
|
2065
|
+
}
|
|
2066
|
+
export async function logToSession(sb, userId, taskId, message, complete, sessionId) {
|
|
2067
|
+
let sessId;
|
|
2068
|
+
let wsId;
|
|
2069
|
+
if (sessionId) {
|
|
2070
|
+
const { data: sess } = await sb
|
|
2071
|
+
.from("kanban_sessions")
|
|
2072
|
+
.select("id, workspace_id")
|
|
2073
|
+
.eq("id", sessionId)
|
|
2074
|
+
.single();
|
|
2075
|
+
if (!sess)
|
|
2076
|
+
return;
|
|
2077
|
+
sessId = sess.id;
|
|
2078
|
+
wsId = sess.workspace_id;
|
|
2079
|
+
}
|
|
2080
|
+
else {
|
|
2081
|
+
const { data: ws } = await sb
|
|
2082
|
+
.from("kanban_workspaces")
|
|
2083
|
+
.select("id")
|
|
2084
|
+
.eq("card_id", taskId)
|
|
2085
|
+
.eq("archived", false)
|
|
2086
|
+
.order("created_at", { ascending: false })
|
|
2087
|
+
.limit(1)
|
|
2088
|
+
.single();
|
|
2089
|
+
if (!ws)
|
|
2090
|
+
return;
|
|
2091
|
+
wsId = ws.id;
|
|
2092
|
+
const { data: sess } = await sb
|
|
2093
|
+
.from("kanban_sessions")
|
|
2094
|
+
.select("id")
|
|
2095
|
+
.eq("workspace_id", ws.id)
|
|
2096
|
+
.order("created_at", { ascending: false })
|
|
2097
|
+
.limit(1)
|
|
2098
|
+
.single();
|
|
2099
|
+
if (!sess)
|
|
2100
|
+
return;
|
|
2101
|
+
sessId = sess.id;
|
|
2102
|
+
}
|
|
2103
|
+
const { data: proc } = await sb
|
|
2104
|
+
.from("kanban_execution_processes")
|
|
2105
|
+
.insert({
|
|
2106
|
+
session_id: sessId,
|
|
2107
|
+
run_reason: "codingagent",
|
|
2108
|
+
status: complete ? "completed" : "running",
|
|
2109
|
+
...(complete ? { completed_at: new Date().toISOString() } : {}),
|
|
2110
|
+
})
|
|
2111
|
+
.select("id")
|
|
2112
|
+
.single();
|
|
2113
|
+
if (!proc)
|
|
2114
|
+
return;
|
|
2115
|
+
await sb.from("kanban_execution_process_logs").insert({
|
|
2116
|
+
execution_process_id: proc.id,
|
|
2117
|
+
log_type: "stdout",
|
|
2118
|
+
output: message,
|
|
2119
|
+
byte_size: Buffer.byteLength(message, "utf-8"),
|
|
2120
|
+
});
|
|
2121
|
+
if (complete) {
|
|
2122
|
+
await sb.from("kanban_coding_agent_turns").insert({
|
|
2123
|
+
execution_process_id: proc.id,
|
|
2124
|
+
summary: message,
|
|
2125
|
+
seen: false,
|
|
2126
|
+
});
|
|
2127
|
+
await sb
|
|
2128
|
+
.from("kanban_workspaces")
|
|
2129
|
+
.update({ archived: true })
|
|
2130
|
+
.eq("id", wsId);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
// ─── Project Context Builder ─────────────────────────────────────────────────
|
|
2134
|
+
/**
|
|
2135
|
+
* Build a project context markdown document from Supabase data.
|
|
2136
|
+
* No LLM call — pure data assembly with ~8K char cap.
|
|
2137
|
+
*/
|
|
2138
|
+
export async function buildProjectContextMd(sb, projectId) {
|
|
2139
|
+
const { data: project } = await sb
|
|
2140
|
+
.from("projects")
|
|
2141
|
+
.select("name, description")
|
|
2142
|
+
.eq("id", projectId)
|
|
2143
|
+
.single();
|
|
2144
|
+
if (!project)
|
|
2145
|
+
return null;
|
|
2146
|
+
const sections = [`# Project Context: ${project.name}\n`];
|
|
2147
|
+
if (project.description) {
|
|
2148
|
+
sections.push(`## Overview\n${project.description}\n`);
|
|
2149
|
+
}
|
|
2150
|
+
// PRD (most recent)
|
|
2151
|
+
const { data: prd } = await sb
|
|
2152
|
+
.from("project_documents")
|
|
2153
|
+
.select("content")
|
|
2154
|
+
.eq("project_id", projectId)
|
|
2155
|
+
.eq("doc_type", "prd")
|
|
2156
|
+
.order("updated_at", { ascending: false })
|
|
2157
|
+
.limit(1)
|
|
2158
|
+
.single();
|
|
2159
|
+
if (prd?.content) {
|
|
2160
|
+
const prdText = tiptapToText(prd.content);
|
|
2161
|
+
if (prdText) {
|
|
2162
|
+
const truncated = prdText.length > 4000 ? prdText.slice(0, 4000) + "\n...(truncated)" : prdText;
|
|
2163
|
+
sections.push(`## PRD\n${truncated}\n`);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
// Analyses
|
|
2167
|
+
const { data: analyses } = await sb
|
|
2168
|
+
.from("analyses")
|
|
2169
|
+
.select("question, takeaway, sentiment, themes")
|
|
2170
|
+
.eq("project_id", projectId)
|
|
2171
|
+
.eq("status", "completed")
|
|
2172
|
+
.order("created_at", { ascending: false })
|
|
2173
|
+
.limit(10);
|
|
2174
|
+
if (analyses && analyses.length > 0) {
|
|
2175
|
+
const parts = [`## Research Analyses (${analyses.length})\n`];
|
|
2176
|
+
for (const a of analyses) {
|
|
2177
|
+
let entry = `### ${a.question || "Untitled"}\n`;
|
|
2178
|
+
if (a.takeaway)
|
|
2179
|
+
entry += `${a.takeaway}\n`;
|
|
2180
|
+
const meta = [];
|
|
2181
|
+
if (a.sentiment) {
|
|
2182
|
+
const s = a.sentiment;
|
|
2183
|
+
const sp = Object.entries(s)
|
|
2184
|
+
.filter(([, v]) => v)
|
|
2185
|
+
.map(([k, v]) => `${k}: ${v}%`);
|
|
2186
|
+
if (sp.length)
|
|
2187
|
+
meta.push(`Sentiment: ${sp.join(", ")}`);
|
|
2188
|
+
}
|
|
2189
|
+
if (a.themes && Array.isArray(a.themes)) {
|
|
2190
|
+
const names = a.themes
|
|
2191
|
+
.slice(0, 5)
|
|
2192
|
+
.map((t) => (typeof t === "object" && t !== null && "name" in t ? t.name : String(t)));
|
|
2193
|
+
if (names.length)
|
|
2194
|
+
meta.push(`Themes: ${names.join(", ")}`);
|
|
2195
|
+
}
|
|
2196
|
+
if (meta.length)
|
|
2197
|
+
entry += meta.join(" | ") + "\n";
|
|
2198
|
+
parts.push(entry);
|
|
2199
|
+
}
|
|
2200
|
+
sections.push(parts.join("\n"));
|
|
2201
|
+
}
|
|
2202
|
+
// Documents
|
|
2203
|
+
const { data: docs } = await sb
|
|
2204
|
+
.from("project_documents")
|
|
2205
|
+
.select("title, doc_type, content")
|
|
2206
|
+
.eq("project_id", projectId)
|
|
2207
|
+
.order("position", { ascending: true });
|
|
2208
|
+
if (docs && docs.length > 0) {
|
|
2209
|
+
const parts = [`## Documents (${docs.length})\n`];
|
|
2210
|
+
for (const d of docs) {
|
|
2211
|
+
const text = d.content ? tiptapToText(d.content) : "";
|
|
2212
|
+
const preview = text.length > 500 ? text.slice(0, 500) + "..." : text;
|
|
2213
|
+
parts.push(`### ${d.title || "Untitled"} (${d.doc_type || "unknown"})\n${preview}\n`);
|
|
2214
|
+
}
|
|
2215
|
+
sections.push(parts.join("\n"));
|
|
2216
|
+
}
|
|
2217
|
+
// Insights
|
|
2218
|
+
const { data: insights } = await sb
|
|
2219
|
+
.from("project_insights")
|
|
2220
|
+
.select("title, content")
|
|
2221
|
+
.eq("project_id", projectId)
|
|
2222
|
+
.order("created_at", { ascending: false })
|
|
2223
|
+
.limit(20);
|
|
2224
|
+
if (insights && insights.length > 0) {
|
|
2225
|
+
const parts = [`## Insights (${insights.length})\n`];
|
|
2226
|
+
for (const i of insights) {
|
|
2227
|
+
parts.push(`- **${i.title || "Untitled"}**: ${i.content || ""}\n`);
|
|
2228
|
+
}
|
|
2229
|
+
sections.push(parts.join("\n"));
|
|
2230
|
+
}
|
|
2231
|
+
// Planning context (decisions, patterns, principles, learnings)
|
|
2232
|
+
try {
|
|
2233
|
+
const { data: contextEntries } = await sb
|
|
2234
|
+
.from("planning_context")
|
|
2235
|
+
.select("type, title, body, confidence")
|
|
2236
|
+
.eq("project_id", projectId)
|
|
2237
|
+
.in("status", ["active", "approved"])
|
|
2238
|
+
.in("type", ["decision", "constraint", "preference", "pattern", "learning", "principle"])
|
|
2239
|
+
.order("updated_at", { ascending: false })
|
|
2240
|
+
.limit(15);
|
|
2241
|
+
if (contextEntries && contextEntries.length > 0) {
|
|
2242
|
+
const parts = [`## Knowledge Base (${contextEntries.length})\n`];
|
|
2243
|
+
for (const e of contextEntries) {
|
|
2244
|
+
const conf = e.confidence < 1 ? ` (confidence: ${e.confidence})` : "";
|
|
2245
|
+
const preview = e.body.length > 200 ? e.body.slice(0, 200) + "..." : e.body;
|
|
2246
|
+
parts.push(`- **[${e.type}] ${e.title}**${conf}: ${preview}\n`);
|
|
2247
|
+
}
|
|
2248
|
+
sections.push(parts.join("\n"));
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
catch {
|
|
2252
|
+
// Non-blocking — planning_context table may not exist yet
|
|
2253
|
+
}
|
|
2254
|
+
let result = sections.join("\n");
|
|
2255
|
+
if (result.length > 8000) {
|
|
2256
|
+
result = result.slice(0, 7950) + "\n\n...(truncated for length)";
|
|
2257
|
+
}
|
|
2258
|
+
return result;
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Simple Tiptap JSON to plain text converter.
|
|
2262
|
+
*/
|
|
2263
|
+
function tiptapToText(content) {
|
|
2264
|
+
if (!content || typeof content !== "object")
|
|
2265
|
+
return "";
|
|
2266
|
+
function extract(node) {
|
|
2267
|
+
if (typeof node === "string")
|
|
2268
|
+
return node;
|
|
2269
|
+
if (!node || typeof node !== "object")
|
|
2270
|
+
return "";
|
|
2271
|
+
const n = node;
|
|
2272
|
+
const type = n.type;
|
|
2273
|
+
const children = n.content || [];
|
|
2274
|
+
if (type === "text")
|
|
2275
|
+
return n.text || "";
|
|
2276
|
+
if (type === "heading") {
|
|
2277
|
+
const level = (n.attrs?.level) || 1;
|
|
2278
|
+
return "\n" + "#".repeat(level) + " " + children.map(extract).join("") + "\n";
|
|
2279
|
+
}
|
|
2280
|
+
if (type === "paragraph")
|
|
2281
|
+
return children.map(extract).join("") + "\n";
|
|
2282
|
+
if (type === "bulletList" || type === "orderedList") {
|
|
2283
|
+
return children
|
|
2284
|
+
.map((item, i) => {
|
|
2285
|
+
const prefix = type === "bulletList" ? "- " : `${i + 1}. `;
|
|
2286
|
+
const inner = (item.content || []).map(extract).join("").trim();
|
|
2287
|
+
return prefix + inner;
|
|
2288
|
+
})
|
|
2289
|
+
.join("\n") + "\n";
|
|
2290
|
+
}
|
|
2291
|
+
return children.map(extract).join("");
|
|
2292
|
+
}
|
|
2293
|
+
return extract(content).trim();
|
|
2294
|
+
}
|