funolio-agent 0.12.2 → 0.13.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/dist/backfill.d.ts +71 -0
- package/dist/backfill.d.ts.map +1 -0
- package/dist/backfill.js +637 -0
- package/dist/backfill.js.map +1 -0
- package/dist/clerk-model.d.ts +28 -2
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +90 -29
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli.js +36 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-cleanup.d.ts +35 -0
- package/dist/config-cleanup.d.ts.map +1 -0
- package/dist/config-cleanup.js +167 -0
- package/dist/config-cleanup.js.map +1 -0
- package/dist/context-window.d.ts +79 -0
- package/dist/context-window.d.ts.map +1 -0
- package/dist/context-window.js +192 -0
- package/dist/context-window.js.map +1 -0
- package/dist/local-data.d.ts +110 -0
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +219 -3
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +116 -3
- package/dist/local-db.js.map +1 -1
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +567 -29
- package/dist/local-server.js.map +1 -1
- package/dist/mcp/claude-config-writer.d.ts +28 -0
- package/dist/mcp/claude-config-writer.d.ts.map +1 -0
- package/dist/mcp/claude-config-writer.js +125 -0
- package/dist/mcp/claude-config-writer.js.map +1 -0
- package/dist/mcp/local-memory-server.d.ts.map +1 -1
- package/dist/mcp/local-memory-server.js +27 -2
- package/dist/mcp/local-memory-server.js.map +1 -1
- package/dist/mcp/manager.d.ts +4 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +19 -0
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/marketplace.d.ts +52 -0
- package/dist/mcp/marketplace.d.ts.map +1 -0
- package/dist/mcp/marketplace.js +144 -0
- package/dist/mcp/marketplace.js.map +1 -0
- package/dist/orchestration/safeguards.d.ts +29 -0
- package/dist/orchestration/safeguards.d.ts.map +1 -0
- package/dist/orchestration/safeguards.js +32 -0
- package/dist/orchestration/safeguards.js.map +1 -0
- package/dist/orchestration/status-parser.d.ts +10 -6
- package/dist/orchestration/status-parser.d.ts.map +1 -1
- package/dist/orchestration/status-parser.js +35 -19
- package/dist/orchestration/status-parser.js.map +1 -1
- package/dist/orchestration/topic-normalizer.d.ts +15 -0
- package/dist/orchestration/topic-normalizer.d.ts.map +1 -0
- package/dist/orchestration/topic-normalizer.js +65 -0
- package/dist/orchestration/topic-normalizer.js.map +1 -0
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +8 -1
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/summarization-pipeline.d.ts +39 -0
- package/dist/summarization-pipeline.d.ts.map +1 -0
- package/dist/summarization-pipeline.js +280 -0
- package/dist/summarization-pipeline.js.map +1 -0
- package/dist/tools/search-local-memory.d.ts.map +1 -1
- package/dist/tools/search-local-memory.js +34 -2
- package/dist/tools/search-local-memory.js.map +1 -1
- package/dist/workflow-engine.d.ts +6 -0
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +174 -11
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +1 -1
package/dist/local-server.js
CHANGED
|
@@ -54,8 +54,17 @@ const local_funnel_1 = require("./local-funnel");
|
|
|
54
54
|
const local_import_worker_1 = require("./local-import-worker");
|
|
55
55
|
const clerk_model_1 = require("./clerk-model");
|
|
56
56
|
const workflow_engine_1 = require("./workflow-engine");
|
|
57
|
+
const context_window_1 = require("./context-window");
|
|
58
|
+
const summarization_pipeline_1 = require("./summarization-pipeline");
|
|
59
|
+
const backfill_1 = require("./backfill");
|
|
60
|
+
const config_cleanup_1 = require("./config-cleanup");
|
|
57
61
|
const status_parser_1 = require("./orchestration/status-parser");
|
|
58
62
|
const guided_actions_1 = require("./orchestration/guided-actions");
|
|
63
|
+
const topic_normalizer_1 = require("./orchestration/topic-normalizer");
|
|
64
|
+
const manager_1 = require("./mcp/manager");
|
|
65
|
+
const registry_1 = require("./mcp/registry");
|
|
66
|
+
const marketplace_1 = require("./mcp/marketplace");
|
|
67
|
+
const claude_config_writer_1 = require("./mcp/claude-config-writer");
|
|
59
68
|
const chalk_1 = __importDefault(require("chalk"));
|
|
60
69
|
const path = __importStar(require("path"));
|
|
61
70
|
const fs = __importStar(require("fs"));
|
|
@@ -128,6 +137,16 @@ function startLocalServer(opts) {
|
|
|
128
137
|
catch (err) {
|
|
129
138
|
console.error(chalk_1.default.yellow(` Failed to seed agent profiles: ${err}`));
|
|
130
139
|
}
|
|
140
|
+
// Ensure there is always an "unassigned" project and no NULL project_id rows.
|
|
141
|
+
try {
|
|
142
|
+
const moved = data.moveNullConversationsToUnassigned();
|
|
143
|
+
if (moved.moved > 0) {
|
|
144
|
+
console.log(chalk_1.default.gray(` Assigned ${moved.moved} conversation(s) to "${data.UNASSIGNED_PROJECT_NAME}"`));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(chalk_1.default.yellow(` Failed to normalize unassigned conversations: ${err}`));
|
|
149
|
+
}
|
|
131
150
|
// ─── Health ──────────────────────────────────────────────────
|
|
132
151
|
app.get('/api/health', (_req, res) => {
|
|
133
152
|
try {
|
|
@@ -294,6 +313,7 @@ function startLocalServer(opts) {
|
|
|
294
313
|
// ─── Projects ──────────────────────────────────────────────
|
|
295
314
|
app.get('/api/projects', (req, res) => {
|
|
296
315
|
try {
|
|
316
|
+
data.moveNullConversationsToUnassigned();
|
|
297
317
|
const projects = data.listProjects();
|
|
298
318
|
if (req.query.include === 'conversations') {
|
|
299
319
|
const result = projects.map(p => {
|
|
@@ -337,6 +357,7 @@ function startLocalServer(opts) {
|
|
|
337
357
|
const deleted = data.deleteProject(req.params.id);
|
|
338
358
|
if (!deleted)
|
|
339
359
|
return res.status(404).json({ error: 'Not found' });
|
|
360
|
+
data.moveNullConversationsToUnassigned();
|
|
340
361
|
res.json({ ok: true });
|
|
341
362
|
}
|
|
342
363
|
catch (err) {
|
|
@@ -481,6 +502,67 @@ function startLocalServer(opts) {
|
|
|
481
502
|
res.status(500).json({ error: err.message });
|
|
482
503
|
}
|
|
483
504
|
});
|
|
505
|
+
// ─── Topic Operations (Phase 4) ────────────────────────────
|
|
506
|
+
/**
|
|
507
|
+
* GET /api/topics/:id/conversations — conversations linked via topic_segment
|
|
508
|
+
*/
|
|
509
|
+
app.get('/api/topics/:id/conversations', (req, res) => {
|
|
510
|
+
try {
|
|
511
|
+
const limit = parseInt(req.query.limit, 10) || 50;
|
|
512
|
+
const offset = parseInt(req.query.offset, 10) || 0;
|
|
513
|
+
const convs = data.listTopicConversations(req.params.id, { limit, offset });
|
|
514
|
+
res.json(convs);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
res.status(500).json({ error: err.message });
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
/**
|
|
521
|
+
* POST /api/topics/:id/move — move topic to a different project
|
|
522
|
+
*/
|
|
523
|
+
app.post('/api/topics/:id/move', (req, res) => {
|
|
524
|
+
try {
|
|
525
|
+
const { toProjectId } = req.body;
|
|
526
|
+
if (!toProjectId)
|
|
527
|
+
return res.status(400).json({ error: 'toProjectId required' });
|
|
528
|
+
const updated = data.moveTopic(req.params.id, toProjectId);
|
|
529
|
+
if (!updated)
|
|
530
|
+
return res.status(404).json({ error: 'Topic not found' });
|
|
531
|
+
res.json(updated);
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
res.status(500).json({ error: err.message });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
/**
|
|
538
|
+
* POST /api/topics/:id/merge — merge source topic into target
|
|
539
|
+
*/
|
|
540
|
+
app.post('/api/topics/:id/merge', (req, res) => {
|
|
541
|
+
try {
|
|
542
|
+
const { targetTopicId } = req.body;
|
|
543
|
+
if (!targetTopicId)
|
|
544
|
+
return res.status(400).json({ error: 'targetTopicId required' });
|
|
545
|
+
data.mergeTopics(req.params.id, targetTopicId);
|
|
546
|
+
res.json({ ok: true });
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
res.status(500).json({ error: err.message });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
/**
|
|
553
|
+
* GET /api/conversations/:id/messages — paginated messages for a conversation
|
|
554
|
+
*/
|
|
555
|
+
app.get('/api/conversations/:id/messages', (req, res) => {
|
|
556
|
+
try {
|
|
557
|
+
const limit = parseInt(req.query.limit, 10) || 25;
|
|
558
|
+
const offset = parseInt(req.query.offset, 10) || 0;
|
|
559
|
+
const msgs = data.getMessages(req.params.id, { limit, offset });
|
|
560
|
+
res.json(msgs);
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
res.status(500).json({ error: err.message });
|
|
564
|
+
}
|
|
565
|
+
});
|
|
484
566
|
// ─── File Upload ───────────────────────────────────────────
|
|
485
567
|
app.post('/api/chat/upload', async (req, res) => {
|
|
486
568
|
try {
|
|
@@ -565,6 +647,7 @@ function startLocalServer(opts) {
|
|
|
565
647
|
// Save user message (skip if multi-bot call where first bot already saved it)
|
|
566
648
|
if (!skipUserMessage) {
|
|
567
649
|
data.addMessage(convId, 'user', message);
|
|
650
|
+
(0, context_window_1.incrementTurnCount)(convId);
|
|
568
651
|
}
|
|
569
652
|
// Load conversation history
|
|
570
653
|
const history = data.getMessages(convId, { limit: 100 });
|
|
@@ -577,9 +660,14 @@ function startLocalServer(opts) {
|
|
|
577
660
|
let systemPrompt;
|
|
578
661
|
const clerk = (0, clerk_model_1.getClerk)();
|
|
579
662
|
if (clerk) {
|
|
580
|
-
const
|
|
663
|
+
const conv = data.getConversation(convId);
|
|
664
|
+
const built = clerk.buildPrompt(message, profile.id, profile, {
|
|
665
|
+
targetModel: profile.model,
|
|
666
|
+
conversationId: convId,
|
|
667
|
+
projectName: conv?.project_name || undefined,
|
|
668
|
+
});
|
|
581
669
|
systemPrompt = built.systemPrompt;
|
|
582
|
-
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedFacts} facts, ${built.injectedDecisions} decisions (${built.contextTokensUsed} tokens)`));
|
|
670
|
+
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedFacts} facts, ${built.injectedDecisions} decisions, ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
|
|
583
671
|
}
|
|
584
672
|
else {
|
|
585
673
|
// Fallback: manual prompt building
|
|
@@ -710,6 +798,10 @@ function startLocalServer(opts) {
|
|
|
710
798
|
}
|
|
711
799
|
}
|
|
712
800
|
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
801
|
+
// Summarization: trigger after every 5-turn boundary (fire-and-forget)
|
|
802
|
+
if ((0, context_window_1.shouldSummarize)(convId)) {
|
|
803
|
+
(0, summarization_pipeline_1.runSummarization)(convId).catch(err => console.error(chalk_1.default.yellow(` [summarization] ${err.message}`)));
|
|
804
|
+
}
|
|
713
805
|
}
|
|
714
806
|
catch (err) {
|
|
715
807
|
console.error(chalk_1.default.red(`Chat error: ${err.message}`));
|
|
@@ -728,16 +820,49 @@ function startLocalServer(opts) {
|
|
|
728
820
|
// ─── Memory Facts ───────────────────────────────────────────
|
|
729
821
|
app.get('/api/memory/facts', (req, res) => {
|
|
730
822
|
try {
|
|
731
|
-
const { agentId, factType, limit, offset, search } = req.query;
|
|
823
|
+
const { agentId, factType, limit, offset, search, groupByProject } = req.query;
|
|
824
|
+
const limitN = limit ? parseInt(limit) : 100;
|
|
825
|
+
const offsetN = offset ? parseInt(offset) : 0;
|
|
826
|
+
const grouped = groupByProject === '1' || groupByProject === 'true';
|
|
827
|
+
if (grouped) {
|
|
828
|
+
const db = data.getDb();
|
|
829
|
+
const wheres = [];
|
|
830
|
+
const params = [];
|
|
831
|
+
if (agentId) {
|
|
832
|
+
wheres.push('f.agent_id = ?');
|
|
833
|
+
params.push(agentId);
|
|
834
|
+
}
|
|
835
|
+
if (factType) {
|
|
836
|
+
wheres.push('f.fact_type = ?');
|
|
837
|
+
params.push(factType);
|
|
838
|
+
}
|
|
839
|
+
if (search) {
|
|
840
|
+
wheres.push('(f.content LIKE ? OR COALESCE(c.title, \'\') LIKE ?)');
|
|
841
|
+
params.push(`%${search}%`, `%${search}%`);
|
|
842
|
+
}
|
|
843
|
+
let sql = `
|
|
844
|
+
SELECT f.*, c.title as conversation_title, c.project_id,
|
|
845
|
+
COALESCE(p.name, '${data.UNASSIGNED_PROJECT_NAME}') as project_name
|
|
846
|
+
FROM memory_fact f
|
|
847
|
+
LEFT JOIN conversation c ON f.conversation_id = c.id
|
|
848
|
+
LEFT JOIN project p ON c.project_id = p.id
|
|
849
|
+
`;
|
|
850
|
+
if (wheres.length > 0)
|
|
851
|
+
sql += ` WHERE ${wheres.join(' AND ')}`;
|
|
852
|
+
sql += ' ORDER BY f.created_at DESC LIMIT ? OFFSET ?';
|
|
853
|
+
params.push(limitN, offsetN);
|
|
854
|
+
const rows = db.prepare(sql).all(...params);
|
|
855
|
+
return res.json(rows);
|
|
856
|
+
}
|
|
732
857
|
if (search && agentId) {
|
|
733
|
-
const facts = data.searchMemoryFacts(search, agentId,
|
|
858
|
+
const facts = data.searchMemoryFacts(search, agentId, limitN);
|
|
734
859
|
return res.json(facts);
|
|
735
860
|
}
|
|
736
861
|
const facts = data.listMemoryFacts({
|
|
737
862
|
agentId: agentId || undefined,
|
|
738
863
|
factType: factType || undefined,
|
|
739
|
-
limit:
|
|
740
|
-
offset:
|
|
864
|
+
limit: limitN,
|
|
865
|
+
offset: offsetN,
|
|
741
866
|
});
|
|
742
867
|
res.json(facts);
|
|
743
868
|
}
|
|
@@ -770,7 +895,27 @@ function startLocalServer(opts) {
|
|
|
770
895
|
});
|
|
771
896
|
app.get('/api/memory/decisions', (req, res) => {
|
|
772
897
|
try {
|
|
773
|
-
const { conversationId, limit } = req.query;
|
|
898
|
+
const { conversationId, limit, groupByProject } = req.query;
|
|
899
|
+
const grouped = groupByProject === '1' || groupByProject === 'true';
|
|
900
|
+
if (grouped) {
|
|
901
|
+
const db = data.getDb();
|
|
902
|
+
const params = [];
|
|
903
|
+
let sql = `
|
|
904
|
+
SELECT d.*, c.title as conversation_title, c.project_id,
|
|
905
|
+
COALESCE(p.name, '${data.UNASSIGNED_PROJECT_NAME}') as project_name
|
|
906
|
+
FROM decision d
|
|
907
|
+
LEFT JOIN conversation c ON d.conversation_id = c.id
|
|
908
|
+
LEFT JOIN project p ON c.project_id = p.id
|
|
909
|
+
`;
|
|
910
|
+
if (conversationId) {
|
|
911
|
+
sql += ' WHERE d.conversation_id = ?';
|
|
912
|
+
params.push(conversationId);
|
|
913
|
+
}
|
|
914
|
+
sql += ' ORDER BY d.created_at DESC LIMIT ?';
|
|
915
|
+
params.push(limit ? parseInt(limit) : 200);
|
|
916
|
+
const rows = db.prepare(sql).all(...params);
|
|
917
|
+
return res.json(rows);
|
|
918
|
+
}
|
|
774
919
|
const decisions = data.listDecisions({
|
|
775
920
|
conversationId: conversationId || undefined,
|
|
776
921
|
limit: limit ? parseInt(limit) : undefined,
|
|
@@ -781,9 +926,44 @@ function startLocalServer(opts) {
|
|
|
781
926
|
res.status(500).json({ error: err.message });
|
|
782
927
|
}
|
|
783
928
|
});
|
|
929
|
+
app.delete('/api/memory/decisions/:id', (req, res) => {
|
|
930
|
+
try {
|
|
931
|
+
const deleted = data.deleteDecision(req.params.id);
|
|
932
|
+
if (!deleted)
|
|
933
|
+
return res.status(404).json({ error: 'Not found' });
|
|
934
|
+
res.json({ ok: true });
|
|
935
|
+
}
|
|
936
|
+
catch (err) {
|
|
937
|
+
res.status(500).json({ error: err.message });
|
|
938
|
+
}
|
|
939
|
+
});
|
|
784
940
|
app.get('/api/memory/action-items', (req, res) => {
|
|
785
941
|
try {
|
|
786
|
-
const { status, limit } = req.query;
|
|
942
|
+
const { status, limit, groupByProject } = req.query;
|
|
943
|
+
const grouped = groupByProject === '1' || groupByProject === 'true';
|
|
944
|
+
if (grouped) {
|
|
945
|
+
const db = data.getDb();
|
|
946
|
+
const params = [];
|
|
947
|
+
const wheres = [];
|
|
948
|
+
if (status) {
|
|
949
|
+
wheres.push('a.status = ?');
|
|
950
|
+
params.push(status);
|
|
951
|
+
}
|
|
952
|
+
let sql = `
|
|
953
|
+
SELECT a.*, c.title as conversation_title, c.project_id,
|
|
954
|
+
COALESCE(p.name, '${data.UNASSIGNED_PROJECT_NAME}') as project_name
|
|
955
|
+
FROM action_item a
|
|
956
|
+
LEFT JOIN conversation c ON a.conversation_id = c.id
|
|
957
|
+
LEFT JOIN project p ON c.project_id = p.id
|
|
958
|
+
`;
|
|
959
|
+
if (wheres.length > 0) {
|
|
960
|
+
sql += ` WHERE ${wheres.join(' AND ')}`;
|
|
961
|
+
}
|
|
962
|
+
sql += ' ORDER BY a.created_at DESC LIMIT ?';
|
|
963
|
+
params.push(limit ? parseInt(limit) : 200);
|
|
964
|
+
const rows = db.prepare(sql).all(...params);
|
|
965
|
+
return res.json(rows);
|
|
966
|
+
}
|
|
787
967
|
const items = data.listActionItems({
|
|
788
968
|
status: status || undefined,
|
|
789
969
|
limit: limit ? parseInt(limit) : undefined,
|
|
@@ -794,6 +974,17 @@ function startLocalServer(opts) {
|
|
|
794
974
|
res.status(500).json({ error: err.message });
|
|
795
975
|
}
|
|
796
976
|
});
|
|
977
|
+
app.delete('/api/memory/action-items/:id', (req, res) => {
|
|
978
|
+
try {
|
|
979
|
+
const deleted = data.deleteActionItem(req.params.id);
|
|
980
|
+
if (!deleted)
|
|
981
|
+
return res.status(404).json({ error: 'Not found' });
|
|
982
|
+
res.json({ ok: true });
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
res.status(500).json({ error: err.message });
|
|
986
|
+
}
|
|
987
|
+
});
|
|
797
988
|
// ─── Settings ───────────────────────────────────────────────
|
|
798
989
|
app.get('/api/settings', (_req, res) => {
|
|
799
990
|
try {
|
|
@@ -986,6 +1177,9 @@ function startLocalServer(opts) {
|
|
|
986
1177
|
agentId: s.agentId,
|
|
987
1178
|
instruction: s.instruction,
|
|
988
1179
|
onStatus: s.onStatus,
|
|
1180
|
+
isCheckpoint: s.isCheckpoint,
|
|
1181
|
+
onFailStepIndex: s.onFailStepIndex,
|
|
1182
|
+
maxLoops: s.maxLoops,
|
|
989
1183
|
});
|
|
990
1184
|
}
|
|
991
1185
|
}
|
|
@@ -1031,7 +1225,7 @@ function startLocalServer(opts) {
|
|
|
1031
1225
|
});
|
|
1032
1226
|
app.post('/api/workflow-templates/:id/steps', (req, res) => {
|
|
1033
1227
|
try {
|
|
1034
|
-
const { agentId, instruction, onStatus, orderIndex } = req.body;
|
|
1228
|
+
const { agentId, instruction, onStatus, orderIndex, isCheckpoint, onFailStepIndex, maxLoops } = req.body;
|
|
1035
1229
|
if (!agentId)
|
|
1036
1230
|
return res.status(400).json({ error: 'agentId is required' });
|
|
1037
1231
|
const step = data.addWorkflowTemplateStep({
|
|
@@ -1040,6 +1234,9 @@ function startLocalServer(opts) {
|
|
|
1040
1234
|
agentId,
|
|
1041
1235
|
instruction,
|
|
1042
1236
|
onStatus,
|
|
1237
|
+
isCheckpoint,
|
|
1238
|
+
onFailStepIndex,
|
|
1239
|
+
maxLoops,
|
|
1043
1240
|
});
|
|
1044
1241
|
res.status(201).json(step);
|
|
1045
1242
|
}
|
|
@@ -1082,6 +1279,259 @@ function startLocalServer(opts) {
|
|
|
1082
1279
|
res.status(500).json({ error: err.message });
|
|
1083
1280
|
}
|
|
1084
1281
|
});
|
|
1282
|
+
// ─── Project Extractions (decisions, facts, action items) ──
|
|
1283
|
+
app.get('/api/projects/:id/extractions', (req, res) => {
|
|
1284
|
+
try {
|
|
1285
|
+
const projectId = req.params.id;
|
|
1286
|
+
const { type } = req.query;
|
|
1287
|
+
// Get all conversations for this project
|
|
1288
|
+
const db = data.getDb();
|
|
1289
|
+
const convIds = db.prepare('SELECT id FROM conversation WHERE project_id = ?').all(projectId);
|
|
1290
|
+
if (convIds.length === 0) {
|
|
1291
|
+
return res.json({ decisions: [], actionItems: [], facts: [], summaries: [] });
|
|
1292
|
+
}
|
|
1293
|
+
const ids = convIds.map(c => c.id);
|
|
1294
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
1295
|
+
const result = {};
|
|
1296
|
+
if (!type || type === 'decisions') {
|
|
1297
|
+
result.decisions = db.prepare(`SELECT d.*, c.title as conversation_title FROM decision d
|
|
1298
|
+
LEFT JOIN conversation c ON d.conversation_id = c.id
|
|
1299
|
+
WHERE d.conversation_id IN (${placeholders})
|
|
1300
|
+
ORDER BY d.created_at DESC`).all(...ids);
|
|
1301
|
+
}
|
|
1302
|
+
if (!type || type === 'actionItems') {
|
|
1303
|
+
result.actionItems = db.prepare(`SELECT a.*, c.title as conversation_title FROM action_item a
|
|
1304
|
+
LEFT JOIN conversation c ON a.conversation_id = c.id
|
|
1305
|
+
WHERE a.conversation_id IN (${placeholders})
|
|
1306
|
+
ORDER BY a.created_at DESC`).all(...ids);
|
|
1307
|
+
}
|
|
1308
|
+
if (!type || type === 'facts') {
|
|
1309
|
+
result.facts = db.prepare(`SELECT f.*, c.title as conversation_title FROM memory_fact f
|
|
1310
|
+
LEFT JOIN conversation c ON f.conversation_id = c.id
|
|
1311
|
+
WHERE f.conversation_id IN (${placeholders})
|
|
1312
|
+
ORDER BY f.created_at DESC`).all(...ids);
|
|
1313
|
+
}
|
|
1314
|
+
if (!type || type === 'summaries') {
|
|
1315
|
+
result.summaries = db.prepare(`SELECT s.*, c.title as conversation_title FROM conversation_summary s
|
|
1316
|
+
LEFT JOIN conversation c ON s.conversation_id = c.id
|
|
1317
|
+
WHERE s.conversation_id IN (${placeholders})
|
|
1318
|
+
ORDER BY s.created_at DESC`).all(...ids);
|
|
1319
|
+
}
|
|
1320
|
+
res.json(result);
|
|
1321
|
+
}
|
|
1322
|
+
catch (err) {
|
|
1323
|
+
res.status(500).json({ error: err.message });
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
// ─── Config Cleanup ─────────────────────────────────────────
|
|
1327
|
+
app.post('/api/projects/:id/cleanup', async (req, res) => {
|
|
1328
|
+
try {
|
|
1329
|
+
const project = data.getProject(req.params.id);
|
|
1330
|
+
if (!project)
|
|
1331
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1332
|
+
if (!project.folder)
|
|
1333
|
+
return res.status(400).json({ error: 'Project has no folder configured' });
|
|
1334
|
+
const result = await (0, config_cleanup_1.runConfigCleanup)(project.id, project.folder, { force: req.body?.force });
|
|
1335
|
+
res.json(result);
|
|
1336
|
+
}
|
|
1337
|
+
catch (err) {
|
|
1338
|
+
res.status(500).json({ error: err.message });
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
// ─── Workflow Executions ────────────────────────────────────
|
|
1342
|
+
app.get('/api/workflows/:id', (req, res) => {
|
|
1343
|
+
try {
|
|
1344
|
+
const exec = data.getWorkflowExecution(req.params.id);
|
|
1345
|
+
if (!exec)
|
|
1346
|
+
return res.status(404).json({ error: 'Workflow execution not found' });
|
|
1347
|
+
res.json(exec);
|
|
1348
|
+
}
|
|
1349
|
+
catch (err) {
|
|
1350
|
+
res.status(500).json({ error: err.message });
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
app.get('/api/conversations/:id/workflows', (req, res) => {
|
|
1354
|
+
try {
|
|
1355
|
+
const executions = data.listWorkflowExecutions({ conversationId: req.params.id, limit: 20 });
|
|
1356
|
+
res.json(executions);
|
|
1357
|
+
}
|
|
1358
|
+
catch (err) {
|
|
1359
|
+
res.status(500).json({ error: err.message });
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
// ─── Backfill ──────────────────────────────────────────────
|
|
1363
|
+
// Backfill runs detached from the HTTP request so it survives page navigation.
|
|
1364
|
+
// The SSE endpoint streams progress while connected, but the backfill job
|
|
1365
|
+
// continues in the background regardless.
|
|
1366
|
+
let backfillJob = {
|
|
1367
|
+
running: false, progress: null, result: null, error: null,
|
|
1368
|
+
};
|
|
1369
|
+
const launchBackfill = (projectHints, options) => {
|
|
1370
|
+
if (backfillJob.running)
|
|
1371
|
+
return;
|
|
1372
|
+
backfillJob = { running: true, progress: null, result: null, error: null };
|
|
1373
|
+
(0, backfill_1.backfillAll)((progress) => {
|
|
1374
|
+
backfillJob.progress = progress;
|
|
1375
|
+
}, projectHints, {
|
|
1376
|
+
includeSummaries: options?.includeSummaries,
|
|
1377
|
+
includeProjectCatalog: options?.includeProjectCatalog,
|
|
1378
|
+
}).then((result) => {
|
|
1379
|
+
backfillJob.result = result;
|
|
1380
|
+
backfillJob.running = false;
|
|
1381
|
+
}).catch((err) => {
|
|
1382
|
+
backfillJob.error = err.message;
|
|
1383
|
+
backfillJob.running = false;
|
|
1384
|
+
});
|
|
1385
|
+
};
|
|
1386
|
+
// JSON status endpoint used by desktop polling fallback (Tauri HTTP can buffer SSE).
|
|
1387
|
+
app.get('/api/backfill/status', (_req, res) => {
|
|
1388
|
+
res.json(backfillJob);
|
|
1389
|
+
});
|
|
1390
|
+
// JSON start endpoint for polling clients.
|
|
1391
|
+
app.post('/api/backfill/start', async (req, res) => {
|
|
1392
|
+
try {
|
|
1393
|
+
if (!(0, summarization_pipeline_1.isClerkConfigured)()) {
|
|
1394
|
+
return res.status(400).json({
|
|
1395
|
+
error: 'Configure a clerk model in Settings → Orchestration before running backfill'
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
const projectHints = Array.isArray(req.body?.projectHints) ? req.body.projectHints : undefined;
|
|
1399
|
+
launchBackfill(projectHints, {
|
|
1400
|
+
includeSummaries: req.body?.includeSummaries === true,
|
|
1401
|
+
includeProjectCatalog: req.body?.includeProjectCatalog !== false,
|
|
1402
|
+
});
|
|
1403
|
+
res.status(202).json({ ok: true, running: true });
|
|
1404
|
+
}
|
|
1405
|
+
catch (err) {
|
|
1406
|
+
res.status(500).json({ error: err.message });
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
app.post('/api/backfill', async (req, res) => {
|
|
1410
|
+
try {
|
|
1411
|
+
if (!(0, summarization_pipeline_1.isClerkConfigured)()) {
|
|
1412
|
+
return res.status(400).json({
|
|
1413
|
+
error: 'Configure a clerk model in Settings → Orchestration before running backfill'
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
if (backfillJob.running) {
|
|
1417
|
+
// Already running — stream existing progress
|
|
1418
|
+
res.writeHead(200, {
|
|
1419
|
+
'Content-Type': 'text/event-stream',
|
|
1420
|
+
'Cache-Control': 'no-cache',
|
|
1421
|
+
'Connection': 'keep-alive',
|
|
1422
|
+
});
|
|
1423
|
+
if (backfillJob.progress) {
|
|
1424
|
+
res.write(`event: progress\ndata: ${JSON.stringify(backfillJob.progress)}\n\n`);
|
|
1425
|
+
}
|
|
1426
|
+
// Don't end — keep SSE open for live updates
|
|
1427
|
+
const interval = setInterval(() => {
|
|
1428
|
+
try {
|
|
1429
|
+
if (!backfillJob.running) {
|
|
1430
|
+
if (backfillJob.result) {
|
|
1431
|
+
res.write(`event: done\ndata: ${JSON.stringify(backfillJob.result)}\n\n`);
|
|
1432
|
+
}
|
|
1433
|
+
else if (backfillJob.error) {
|
|
1434
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: backfillJob.error })}\n\n`);
|
|
1435
|
+
}
|
|
1436
|
+
clearInterval(interval);
|
|
1437
|
+
res.end();
|
|
1438
|
+
}
|
|
1439
|
+
else if (backfillJob.progress) {
|
|
1440
|
+
res.write(`event: progress\ndata: ${JSON.stringify(backfillJob.progress)}\n\n`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
clearInterval(interval);
|
|
1445
|
+
}
|
|
1446
|
+
}, 2000);
|
|
1447
|
+
req.on('close', () => clearInterval(interval));
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const projectHints = Array.isArray(req.body?.projectHints) ? req.body.projectHints : undefined;
|
|
1451
|
+
launchBackfill(projectHints, {
|
|
1452
|
+
includeSummaries: req.body?.includeSummaries === true,
|
|
1453
|
+
includeProjectCatalog: req.body?.includeProjectCatalog !== false,
|
|
1454
|
+
});
|
|
1455
|
+
// SSE for progress
|
|
1456
|
+
res.writeHead(200, {
|
|
1457
|
+
'Content-Type': 'text/event-stream',
|
|
1458
|
+
'Cache-Control': 'no-cache',
|
|
1459
|
+
'Connection': 'keep-alive',
|
|
1460
|
+
});
|
|
1461
|
+
let connected = true;
|
|
1462
|
+
req.on('close', () => { connected = false; });
|
|
1463
|
+
const sendEvent = (event, payload) => {
|
|
1464
|
+
if (connected) {
|
|
1465
|
+
try {
|
|
1466
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
connected = false;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
if (backfillJob.progress) {
|
|
1474
|
+
sendEvent('progress', backfillJob.progress);
|
|
1475
|
+
}
|
|
1476
|
+
const interval = setInterval(() => {
|
|
1477
|
+
try {
|
|
1478
|
+
if (!backfillJob.running) {
|
|
1479
|
+
if (backfillJob.result) {
|
|
1480
|
+
sendEvent('done', backfillJob.result);
|
|
1481
|
+
}
|
|
1482
|
+
else if (backfillJob.error) {
|
|
1483
|
+
sendEvent('error', { error: backfillJob.error });
|
|
1484
|
+
}
|
|
1485
|
+
clearInterval(interval);
|
|
1486
|
+
if (connected) {
|
|
1487
|
+
try {
|
|
1488
|
+
res.end();
|
|
1489
|
+
}
|
|
1490
|
+
catch { }
|
|
1491
|
+
}
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (backfillJob.progress) {
|
|
1495
|
+
sendEvent('progress', backfillJob.progress);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
clearInterval(interval);
|
|
1500
|
+
}
|
|
1501
|
+
}, 1000);
|
|
1502
|
+
req.on('close', () => clearInterval(interval));
|
|
1503
|
+
}
|
|
1504
|
+
catch (err) {
|
|
1505
|
+
backfillJob.running = false;
|
|
1506
|
+
if (!res.headersSent) {
|
|
1507
|
+
res.status(500).json({ error: err.message });
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
app.post('/api/backfill/test-one', async (req, res) => {
|
|
1512
|
+
try {
|
|
1513
|
+
if (!(0, summarization_pipeline_1.isClerkConfigured)()) {
|
|
1514
|
+
return res.status(400).json({
|
|
1515
|
+
error: 'Configure a clerk model in Settings -> Orchestration before running backfill',
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
if (backfillJob.running) {
|
|
1519
|
+
return res.status(409).json({ error: 'Backfill is currently running. Wait for completion before test-one.' });
|
|
1520
|
+
}
|
|
1521
|
+
const projectHints = Array.isArray(req.body?.projectHints) ? req.body.projectHints : undefined;
|
|
1522
|
+
const conversationId = typeof req.body?.conversationId === 'string' && req.body.conversationId.trim()
|
|
1523
|
+
? req.body.conversationId.trim()
|
|
1524
|
+
: undefined;
|
|
1525
|
+
const result = await (0, backfill_1.backfillOne)(projectHints, conversationId, {
|
|
1526
|
+
includeSummaries: req.body?.includeSummaries === true,
|
|
1527
|
+
includeProjectCatalog: req.body?.includeProjectCatalog !== false,
|
|
1528
|
+
});
|
|
1529
|
+
res.json(result);
|
|
1530
|
+
}
|
|
1531
|
+
catch (err) {
|
|
1532
|
+
res.status(500).json({ error: err.message });
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1085
1535
|
// ─── Import ─────────────────────────────────────────────────
|
|
1086
1536
|
app.post('/api/import/start', async (req, res) => {
|
|
1087
1537
|
try {
|
|
@@ -1218,6 +1668,100 @@ function startLocalServer(opts) {
|
|
|
1218
1668
|
res.status(500).json({ error: err.message });
|
|
1219
1669
|
}
|
|
1220
1670
|
});
|
|
1671
|
+
// ─── MCP Marketplace ────────────────────────────────────────────
|
|
1672
|
+
const mcpManager = new manager_1.MCPManager();
|
|
1673
|
+
// Auto-launch persisted always-on servers
|
|
1674
|
+
mcpManager.autoLaunch().catch((err) => {
|
|
1675
|
+
console.error(chalk_1.default.yellow(`[MCP] Auto-launch error: ${err.message}`));
|
|
1676
|
+
});
|
|
1677
|
+
/**
|
|
1678
|
+
* GET /api/mcp/catalog — full marketplace catalog with install status
|
|
1679
|
+
*/
|
|
1680
|
+
app.get('/api/mcp/catalog', (_req, res) => {
|
|
1681
|
+
try {
|
|
1682
|
+
const catalog = (0, marketplace_1.buildMarketplaceCatalog)();
|
|
1683
|
+
const running = new Set(mcpManager.getRunningServers());
|
|
1684
|
+
const persisted = new Set(mcpManager.loadPersistedConfigs().map((c) => c.id));
|
|
1685
|
+
const tools = catalog.map(({ entry, info }) => {
|
|
1686
|
+
let status = 'not_installed';
|
|
1687
|
+
if (running.has(entry.id)) {
|
|
1688
|
+
status = 'running';
|
|
1689
|
+
}
|
|
1690
|
+
else if (persisted.has(entry.id)) {
|
|
1691
|
+
status = 'installed';
|
|
1692
|
+
}
|
|
1693
|
+
return { entry, info, status, inClaudeConfig: (0, claude_config_writer_1.isInClaudeConfig)(entry.id) };
|
|
1694
|
+
});
|
|
1695
|
+
res.json({ tools, categories: marketplace_1.MARKETPLACE_CATEGORIES });
|
|
1696
|
+
}
|
|
1697
|
+
catch (err) {
|
|
1698
|
+
res.status(500).json({ error: err.message });
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
/**
|
|
1702
|
+
* POST /api/mcp/install — install + launch + configure Claude Code
|
|
1703
|
+
*/
|
|
1704
|
+
app.post('/api/mcp/install', async (req, res) => {
|
|
1705
|
+
try {
|
|
1706
|
+
const { toolId, envVars, alwaysOn } = req.body;
|
|
1707
|
+
if (!toolId)
|
|
1708
|
+
return res.status(400).json({ error: 'toolId required' });
|
|
1709
|
+
const entry = registry_1.BUILTIN_REGISTRY.find((e) => e.id === toolId);
|
|
1710
|
+
if (!entry)
|
|
1711
|
+
return res.status(404).json({ error: `Unknown tool: ${toolId}` });
|
|
1712
|
+
// Install and launch via MCPManager
|
|
1713
|
+
const tools = await mcpManager.installAndLaunch(entry, envVars || {}, alwaysOn ?? true);
|
|
1714
|
+
// Configure Claude Code
|
|
1715
|
+
(0, claude_config_writer_1.addMcpToClaudeConfig)(entry, envVars || {});
|
|
1716
|
+
res.json({
|
|
1717
|
+
ok: true,
|
|
1718
|
+
toolId,
|
|
1719
|
+
toolsDiscovered: tools.length,
|
|
1720
|
+
inClaudeConfig: true,
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
catch (err) {
|
|
1724
|
+
res.status(500).json({ error: err.message });
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
/**
|
|
1728
|
+
* POST /api/mcp/uninstall — stop + remove persisted config + remove from Claude Code
|
|
1729
|
+
*/
|
|
1730
|
+
app.post('/api/mcp/uninstall', async (req, res) => {
|
|
1731
|
+
try {
|
|
1732
|
+
const { toolId } = req.body;
|
|
1733
|
+
if (!toolId)
|
|
1734
|
+
return res.status(400).json({ error: 'toolId required' });
|
|
1735
|
+
await mcpManager.uninstallServer(toolId);
|
|
1736
|
+
(0, claude_config_writer_1.removeMcpFromClaudeConfig)(toolId);
|
|
1737
|
+
res.json({ ok: true, toolId });
|
|
1738
|
+
}
|
|
1739
|
+
catch (err) {
|
|
1740
|
+
res.status(500).json({ error: err.message });
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
/**
|
|
1744
|
+
* GET /api/mcp/status — status map of all servers
|
|
1745
|
+
*/
|
|
1746
|
+
app.get('/api/mcp/status', (_req, res) => {
|
|
1747
|
+
try {
|
|
1748
|
+
const running = mcpManager.getRunningServers();
|
|
1749
|
+
const persisted = mcpManager.loadPersistedConfigs();
|
|
1750
|
+
const status = {};
|
|
1751
|
+
for (const config of persisted) {
|
|
1752
|
+
const info = mcpManager.getServerInfo(config.id);
|
|
1753
|
+
status[config.id] = {
|
|
1754
|
+
running: running.includes(config.id),
|
|
1755
|
+
alwaysOn: config.alwaysOn,
|
|
1756
|
+
toolCount: info?.tools.length ?? 0,
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
res.json({ status });
|
|
1760
|
+
}
|
|
1761
|
+
catch (err) {
|
|
1762
|
+
res.status(500).json({ error: err.message });
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1221
1765
|
// Initialize workflow engine
|
|
1222
1766
|
(0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
|
|
1223
1767
|
// Start server
|
|
@@ -1281,44 +1825,38 @@ async function autoSuggestTopic(convId, projectId) {
|
|
|
1281
1825
|
if (msgs.length < 3)
|
|
1282
1826
|
return;
|
|
1283
1827
|
const clerk = (0, clerk_model_1.getClerk)();
|
|
1284
|
-
let
|
|
1828
|
+
let rawTitle;
|
|
1285
1829
|
if (clerk) {
|
|
1286
1830
|
const transcript = msgs.slice(0, 4).map(m => `${m.role}: ${m.content.slice(0, 150)}`).join('\n');
|
|
1287
1831
|
const clerkLlm = clerk.llm;
|
|
1288
1832
|
const resp = await clerkLlm.chat({
|
|
1289
|
-
messages: [{ role: 'user', content: `Generate a short topic name (2-
|
|
1290
|
-
system: 'You generate short topic names. Return ONLY the topic name, nothing else.',
|
|
1833
|
+
messages: [{ role: 'user', content: `Generate a short topic name (2-3 words, no quotes) for this conversation:\n\n${transcript}` }],
|
|
1834
|
+
system: 'You generate short topic names (2-3 words, title case). Return ONLY the topic name, nothing else.',
|
|
1291
1835
|
stream: false,
|
|
1292
1836
|
});
|
|
1293
|
-
|
|
1837
|
+
rawTitle = resp?.content?.trim().slice(0, 60) || 'General';
|
|
1294
1838
|
}
|
|
1295
1839
|
else {
|
|
1296
1840
|
// Heuristic: use first user message
|
|
1297
1841
|
const firstUser = msgs.find(m => m.role === 'user');
|
|
1298
|
-
|
|
1842
|
+
rawTitle = firstUser ? firstUser.content.slice(0, 40).replace(/\n/g, ' ').trim() : 'General';
|
|
1299
1843
|
}
|
|
1844
|
+
// Normalize for quality constraints
|
|
1845
|
+
const topicTitle = (0, topic_normalizer_1.normalizeTopicTitle)(rawTitle) || rawTitle;
|
|
1300
1846
|
// Check if a similar topic exists for this project
|
|
1301
1847
|
const topics = data.listTopics({ projectId, limit: 100 });
|
|
1302
|
-
const matchingTopic = topics.find(t => t.title.toLowerCase() === topicTitle.toLowerCase()
|
|
1848
|
+
const matchingTopic = topics.find(t => t.title.toLowerCase() === topicTitle.toLowerCase() ||
|
|
1849
|
+
(t.normalized_title && t.normalized_title === topicTitle.toLowerCase()));
|
|
1303
1850
|
if (matchingTopic) {
|
|
1304
|
-
//
|
|
1305
|
-
data.
|
|
1306
|
-
topicId: matchingTopic.id,
|
|
1307
|
-
conversationId: convId,
|
|
1308
|
-
startSeq: 1,
|
|
1309
|
-
endSeq: 999999,
|
|
1310
|
-
});
|
|
1851
|
+
// Idempotent link to existing topic
|
|
1852
|
+
data.upsertConversationTopicSegment(convId, matchingTopic.id);
|
|
1311
1853
|
data.updateTopic(matchingTopic.id, { messageCount: matchingTopic.message_count + msgs.length, lastActivityAt: new Date().toISOString() });
|
|
1312
1854
|
}
|
|
1313
1855
|
else {
|
|
1314
|
-
// Create new topic
|
|
1856
|
+
// Create new topic with normalized title
|
|
1315
1857
|
const topic = data.createTopic({ title: topicTitle, projectId, isAuto: true });
|
|
1316
|
-
data.
|
|
1317
|
-
|
|
1318
|
-
conversationId: convId,
|
|
1319
|
-
startSeq: 1,
|
|
1320
|
-
endSeq: 999999,
|
|
1321
|
-
});
|
|
1858
|
+
data.updateTopic(topic.id, { normalizedTitle: topicTitle.toLowerCase() });
|
|
1859
|
+
data.upsertConversationTopicSegment(convId, topic.id);
|
|
1322
1860
|
}
|
|
1323
1861
|
console.log(chalk_1.default.gray(` [topic] Auto-suggested: "${topicTitle}" for conv ${convId.slice(0, 8)}`));
|
|
1324
1862
|
}
|