let-them-talk 3.3.3 → 3.4.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/CHANGELOG.md +48 -0
- package/LICENSE +1 -1
- package/README.md +17 -9
- package/cli.js +103 -2
- package/dashboard.html +640 -124
- package/dashboard.js +342 -3
- package/package.json +2 -1
- package/server.js +1 -1
package/dashboard.js
CHANGED
|
@@ -228,6 +228,74 @@ function apiStatus(query) {
|
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
function apiStats(query) {
|
|
232
|
+
const projectPath = query.get('project') || null;
|
|
233
|
+
const history = readJsonl(filePath('history.jsonl', projectPath));
|
|
234
|
+
const agents = readJson(filePath('agents.json', projectPath));
|
|
235
|
+
|
|
236
|
+
// Per-agent stats
|
|
237
|
+
const perAgent = {};
|
|
238
|
+
let totalMessages = history.length;
|
|
239
|
+
const hourBuckets = new Array(24).fill(0);
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < history.length; i++) {
|
|
242
|
+
const m = history[i];
|
|
243
|
+
const from = m.from || 'unknown';
|
|
244
|
+
if (!perAgent[from]) {
|
|
245
|
+
perAgent[from] = { messages: 0, responseTimes: [], hours: new Array(24).fill(0) };
|
|
246
|
+
}
|
|
247
|
+
perAgent[from].messages++;
|
|
248
|
+
const ts = new Date(m.timestamp);
|
|
249
|
+
const hour = ts.getHours();
|
|
250
|
+
perAgent[from].hours[hour]++;
|
|
251
|
+
hourBuckets[hour]++;
|
|
252
|
+
|
|
253
|
+
// Compute response time if this is a reply
|
|
254
|
+
if (m.reply_to) {
|
|
255
|
+
for (let j = i - 1; j >= Math.max(0, i - 50); j--) {
|
|
256
|
+
if (history[j].id === m.reply_to) {
|
|
257
|
+
const delta = ts.getTime() - new Date(history[j].timestamp).getTime();
|
|
258
|
+
if (delta > 0 && delta < 3600000) perAgent[from].responseTimes.push(delta);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Build per-agent summary
|
|
266
|
+
const agentStats = {};
|
|
267
|
+
let busiestAgent = null;
|
|
268
|
+
let busiestCount = 0;
|
|
269
|
+
for (const [name, data] of Object.entries(perAgent)) {
|
|
270
|
+
const avgResponseMs = data.responseTimes.length
|
|
271
|
+
? Math.round(data.responseTimes.reduce((a, b) => a + b, 0) / data.responseTimes.length)
|
|
272
|
+
: null;
|
|
273
|
+
const peakHour = data.hours.indexOf(Math.max(...data.hours));
|
|
274
|
+
agentStats[name] = {
|
|
275
|
+
messages: data.messages,
|
|
276
|
+
avg_response_ms: avgResponseMs,
|
|
277
|
+
peak_hour: peakHour,
|
|
278
|
+
};
|
|
279
|
+
if (data.messages > busiestCount) {
|
|
280
|
+
busiestCount = data.messages;
|
|
281
|
+
busiestAgent = name;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Conversation velocity (messages per minute over last 10 minutes)
|
|
286
|
+
const tenMinAgo = Date.now() - 600000;
|
|
287
|
+
const recentCount = history.filter(m => new Date(m.timestamp).getTime() > tenMinAgo).length;
|
|
288
|
+
const velocity = Math.round((recentCount / 10) * 10) / 10;
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
total_messages: totalMessages,
|
|
292
|
+
busiest_agent: busiestAgent,
|
|
293
|
+
velocity_per_min: velocity,
|
|
294
|
+
hour_distribution: hourBuckets,
|
|
295
|
+
agents: agentStats,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
231
299
|
function apiReset(query) {
|
|
232
300
|
const projectPath = query.get('project') || null;
|
|
233
301
|
const dataDir = resolveDataDir(projectPath);
|
|
@@ -528,9 +596,12 @@ function apiUpdateTask(body, query) {
|
|
|
528
596
|
const task = tasks.find(t => t.id === body.task_id);
|
|
529
597
|
if (!task) return { error: 'Task not found' };
|
|
530
598
|
|
|
599
|
+
const validStatuses = ['pending', 'in_progress', 'done', 'blocked'];
|
|
600
|
+
if (!validStatuses.includes(body.status)) return { error: 'Invalid status. Must be: ' + validStatuses.join(', ') };
|
|
531
601
|
task.status = body.status;
|
|
532
602
|
task.updated_at = new Date().toISOString();
|
|
533
603
|
if (body.notes) {
|
|
604
|
+
if (!Array.isArray(task.notes)) task.notes = [];
|
|
534
605
|
task.notes.push({ by: 'Dashboard', text: body.notes, at: new Date().toISOString() });
|
|
535
606
|
}
|
|
536
607
|
|
|
@@ -662,6 +733,233 @@ function apiLaunchAgent(body) {
|
|
|
662
733
|
};
|
|
663
734
|
}
|
|
664
735
|
|
|
736
|
+
// --- v3.4: Message Edit ---
|
|
737
|
+
function apiEditMessage(body, query) {
|
|
738
|
+
const projectPath = query.get('project') || null;
|
|
739
|
+
const { id, content } = body;
|
|
740
|
+
if (!id || !content) return { error: 'Missing "id" and/or "content" fields' };
|
|
741
|
+
if (content.length > 50000) return { error: 'Content too long (max 50000 chars)' };
|
|
742
|
+
|
|
743
|
+
const dataDir = resolveDataDir(projectPath);
|
|
744
|
+
const historyFile = path.join(dataDir, 'history.jsonl');
|
|
745
|
+
const messagesFile = path.join(dataDir, 'messages.jsonl');
|
|
746
|
+
|
|
747
|
+
let found = false;
|
|
748
|
+
const now = new Date().toISOString();
|
|
749
|
+
|
|
750
|
+
// Update in history.jsonl
|
|
751
|
+
if (fs.existsSync(historyFile)) {
|
|
752
|
+
const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
753
|
+
const updated = lines.map(line => {
|
|
754
|
+
try {
|
|
755
|
+
const msg = JSON.parse(line);
|
|
756
|
+
if (msg.id === id) {
|
|
757
|
+
found = true;
|
|
758
|
+
if (!msg.edit_history) msg.edit_history = [];
|
|
759
|
+
msg.edit_history.push({ content: msg.content, edited_at: now });
|
|
760
|
+
msg.content = content;
|
|
761
|
+
msg.edited = true;
|
|
762
|
+
msg.edited_at = now;
|
|
763
|
+
return JSON.stringify(msg);
|
|
764
|
+
}
|
|
765
|
+
return line;
|
|
766
|
+
} catch { return line; }
|
|
767
|
+
});
|
|
768
|
+
if (found) fs.writeFileSync(historyFile, updated.join('\n') + '\n');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Also update in messages.jsonl (for agents that haven't consumed yet)
|
|
772
|
+
if (found && fs.existsSync(messagesFile)) {
|
|
773
|
+
const raw = fs.readFileSync(messagesFile, 'utf8').trim();
|
|
774
|
+
if (raw) {
|
|
775
|
+
const lines = raw.split('\n');
|
|
776
|
+
const updated = lines.map(line => {
|
|
777
|
+
try {
|
|
778
|
+
const msg = JSON.parse(line);
|
|
779
|
+
if (msg.id === id) {
|
|
780
|
+
msg.content = content;
|
|
781
|
+
msg.edited = true;
|
|
782
|
+
msg.edited_at = now;
|
|
783
|
+
return JSON.stringify(msg);
|
|
784
|
+
}
|
|
785
|
+
return line;
|
|
786
|
+
} catch { return line; }
|
|
787
|
+
});
|
|
788
|
+
fs.writeFileSync(messagesFile, updated.join('\n') + '\n');
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (!found) return { error: 'Message not found' };
|
|
793
|
+
return { success: true, id, edited_at: now };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// --- v3.4: Message Delete ---
|
|
797
|
+
function apiDeleteMessage(body, query) {
|
|
798
|
+
const projectPath = query.get('project') || null;
|
|
799
|
+
const { id } = body;
|
|
800
|
+
if (!id) return { error: 'Missing "id" field' };
|
|
801
|
+
|
|
802
|
+
const dataDir = resolveDataDir(projectPath);
|
|
803
|
+
const historyFile = path.join(dataDir, 'history.jsonl');
|
|
804
|
+
const messagesFile = path.join(dataDir, 'messages.jsonl');
|
|
805
|
+
|
|
806
|
+
let found = false;
|
|
807
|
+
let msgFrom = null;
|
|
808
|
+
|
|
809
|
+
// Find the message first to check permissions
|
|
810
|
+
if (fs.existsSync(historyFile)) {
|
|
811
|
+
const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
|
|
812
|
+
for (const line of lines) {
|
|
813
|
+
try {
|
|
814
|
+
const msg = JSON.parse(line);
|
|
815
|
+
if (msg.id === id) { found = true; msgFrom = msg.from; break; }
|
|
816
|
+
} catch {}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (!found) return { error: 'Message not found' };
|
|
821
|
+
|
|
822
|
+
// Only allow deleting dashboard-injected or system messages
|
|
823
|
+
const allowed = ['Dashboard', 'dashboard', 'system', '__system__'];
|
|
824
|
+
if (!allowed.includes(msgFrom)) {
|
|
825
|
+
return { error: 'Can only delete messages sent from Dashboard or system' };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Remove from history.jsonl
|
|
829
|
+
if (fs.existsSync(historyFile)) {
|
|
830
|
+
const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
|
|
831
|
+
const filtered = lines.filter(line => {
|
|
832
|
+
try { return JSON.parse(line).id !== id; } catch { return true; }
|
|
833
|
+
});
|
|
834
|
+
fs.writeFileSync(historyFile, filtered.join('\n') + (filtered.length ? '\n' : ''));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Remove from messages.jsonl
|
|
838
|
+
if (fs.existsSync(messagesFile)) {
|
|
839
|
+
const lines = fs.readFileSync(messagesFile, 'utf8').trim().split('\n');
|
|
840
|
+
const filtered = lines.filter(line => {
|
|
841
|
+
try { return JSON.parse(line).id !== id; } catch { return true; }
|
|
842
|
+
});
|
|
843
|
+
fs.writeFileSync(messagesFile, filtered.join('\n') + (filtered.length ? '\n' : ''));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return { success: true, id };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// --- v3.4: Conversation Templates ---
|
|
850
|
+
function apiGetConversationTemplates() {
|
|
851
|
+
const templatesDir = path.join(__dirname, 'conversation-templates');
|
|
852
|
+
if (!fs.existsSync(templatesDir)) {
|
|
853
|
+
// Return built-in templates
|
|
854
|
+
return getBuiltInConversationTemplates();
|
|
855
|
+
}
|
|
856
|
+
const custom = fs.readdirSync(templatesDir)
|
|
857
|
+
.filter(f => f.endsWith('.json'))
|
|
858
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(templatesDir, f), 'utf8')); } catch { return null; } })
|
|
859
|
+
.filter(Boolean);
|
|
860
|
+
return [...getBuiltInConversationTemplates(), ...custom];
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function getBuiltInConversationTemplates() {
|
|
864
|
+
return [
|
|
865
|
+
{
|
|
866
|
+
id: 'code-review',
|
|
867
|
+
name: 'Code Review Pipeline',
|
|
868
|
+
description: 'Developer writes code, Reviewer checks it, Tester validates',
|
|
869
|
+
agents: [
|
|
870
|
+
{ name: 'Developer', role: 'Developer', prompt: 'You are a developer. Write code as instructed. After completing, send your code to Reviewer for review.' },
|
|
871
|
+
{ name: 'Reviewer', role: 'Code Reviewer', prompt: 'You are a code reviewer. Wait for code from Developer. Review it for bugs, style, and best practices. Send feedback back to Developer or approve and forward to Tester.' },
|
|
872
|
+
{ name: 'Tester', role: 'QA Tester', prompt: 'You are a QA tester. Wait for approved code from Reviewer. Write and run tests. Report results back to the team.' }
|
|
873
|
+
],
|
|
874
|
+
workflow: { name: 'Code Review', steps: ['Write Code', 'Review', 'Test', 'Approve'] }
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
id: 'debug-squad',
|
|
878
|
+
name: 'Debug Squad',
|
|
879
|
+
description: 'Investigator finds the bug, Fixer patches it, Verifier confirms the fix',
|
|
880
|
+
agents: [
|
|
881
|
+
{ name: 'Investigator', role: 'Bug Investigator', prompt: 'You investigate bugs. Analyze error logs, trace code paths, and identify root causes. Send findings to Fixer.' },
|
|
882
|
+
{ name: 'Fixer', role: 'Bug Fixer', prompt: 'You fix bugs. Wait for findings from Investigator. Implement fixes and send to Verifier for confirmation.' },
|
|
883
|
+
{ name: 'Verifier', role: 'Fix Verifier', prompt: 'You verify bug fixes. Wait for patches from Fixer. Test the fix and confirm resolution or send back for more work.' }
|
|
884
|
+
],
|
|
885
|
+
workflow: { name: 'Bug Fix', steps: ['Investigate', 'Fix', 'Verify', 'Close'] }
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
id: 'feature-build',
|
|
889
|
+
name: 'Feature Development',
|
|
890
|
+
description: 'Architect designs, Builder implements, Reviewer approves',
|
|
891
|
+
agents: [
|
|
892
|
+
{ name: 'Architect', role: 'Software Architect', prompt: 'You are a software architect. Design the feature architecture, define interfaces, and create the implementation plan. Send the plan to Builder.' },
|
|
893
|
+
{ name: 'Builder', role: 'Developer', prompt: 'You are a developer. Wait for architecture plans from Architect. Implement the feature following the design. Send completed code to Reviewer.' },
|
|
894
|
+
{ name: 'Reviewer', role: 'Senior Reviewer', prompt: 'You are a senior reviewer. Review implementations from Builder against the architecture from Architect. Approve or request changes.' }
|
|
895
|
+
],
|
|
896
|
+
workflow: { name: 'Feature Dev', steps: ['Design', 'Implement', 'Review', 'Ship'] }
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
id: 'research-write',
|
|
900
|
+
name: 'Research & Write',
|
|
901
|
+
description: 'Researcher gathers info, Writer creates content, Editor polishes',
|
|
902
|
+
agents: [
|
|
903
|
+
{ name: 'Researcher', role: 'Researcher', prompt: 'You are a researcher. Gather information on the given topic. Organize findings and send a research brief to Writer.' },
|
|
904
|
+
{ name: 'Writer', role: 'Writer', prompt: 'You are a writer. Wait for research from Researcher. Write clear, well-structured content based on the findings. Send to Editor.' },
|
|
905
|
+
{ name: 'Editor', role: 'Editor', prompt: 'You are an editor. Review and polish content from Writer. Check for clarity, accuracy, and style. Send back final version or request revisions.' }
|
|
906
|
+
],
|
|
907
|
+
workflow: { name: 'Content Pipeline', steps: ['Research', 'Draft', 'Edit', 'Publish'] }
|
|
908
|
+
}
|
|
909
|
+
];
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function apiLaunchConversationTemplate(body, query) {
|
|
913
|
+
const projectPath = query.get('project') || null;
|
|
914
|
+
const { template_id } = body;
|
|
915
|
+
if (!template_id) return { error: 'Missing template_id' };
|
|
916
|
+
|
|
917
|
+
const templates = apiGetConversationTemplates();
|
|
918
|
+
const template = templates.find(t => t.id === template_id);
|
|
919
|
+
if (!template) return { error: 'Template not found: ' + template_id };
|
|
920
|
+
|
|
921
|
+
// Return the template config for the frontend to display launch instructions
|
|
922
|
+
return {
|
|
923
|
+
success: true,
|
|
924
|
+
template,
|
|
925
|
+
instructions: template.agents.map(a => ({
|
|
926
|
+
agent_name: a.name,
|
|
927
|
+
role: a.role,
|
|
928
|
+
prompt: `You are "${a.name}" with role "${a.role}". ${a.prompt}\n\nFirst register yourself with: register(name="${a.name}"), then update_profile(role="${a.role}"). Then enter listen mode.`
|
|
929
|
+
}))
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// --- v3.4: Agent Permissions ---
|
|
934
|
+
function apiUpdatePermissions(body, query) {
|
|
935
|
+
const projectPath = query.get('project') || null;
|
|
936
|
+
const dataDir = resolveDataDir(projectPath);
|
|
937
|
+
const permFile = path.join(dataDir, 'permissions.json');
|
|
938
|
+
|
|
939
|
+
const { agent, permissions } = body;
|
|
940
|
+
if (!agent || !permissions) return { error: 'Missing "agent" and/or "permissions" fields' };
|
|
941
|
+
|
|
942
|
+
let perms = {};
|
|
943
|
+
if (fs.existsSync(permFile)) {
|
|
944
|
+
try { perms = JSON.parse(fs.readFileSync(permFile, 'utf8')); } catch {}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// permissions: { can_read: [agents...] or "*", can_write_to: [agents...] or "*", is_admin: bool }
|
|
948
|
+
const allowed = {};
|
|
949
|
+
if (permissions.can_read !== undefined) allowed.can_read = permissions.can_read;
|
|
950
|
+
if (permissions.can_write_to !== undefined) allowed.can_write_to = permissions.can_write_to;
|
|
951
|
+
if (permissions.is_admin !== undefined) allowed.is_admin = !!permissions.is_admin;
|
|
952
|
+
perms[agent] = {
|
|
953
|
+
...perms[agent],
|
|
954
|
+
...allowed,
|
|
955
|
+
updated_at: new Date().toISOString()
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
959
|
+
fs.writeFileSync(permFile, JSON.stringify(perms, null, 2));
|
|
960
|
+
return { success: true, agent, permissions: perms[agent] };
|
|
961
|
+
}
|
|
962
|
+
|
|
665
963
|
// --- HTTP Server ---
|
|
666
964
|
|
|
667
965
|
// Load HTML at startup (re-read on each request in dev for hot-reload)
|
|
@@ -696,7 +994,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
696
994
|
} else if (reqOrigin === allowedOrigin || reqOrigin === `http://127.0.0.1:${PORT}`) {
|
|
697
995
|
res.setHeader('Access-Control-Allow-Origin', reqOrigin);
|
|
698
996
|
}
|
|
699
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
997
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
700
998
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
701
999
|
|
|
702
1000
|
if (req.method === 'OPTIONS') {
|
|
@@ -706,7 +1004,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
706
1004
|
}
|
|
707
1005
|
|
|
708
1006
|
// CSRF + DNS rebinding protection: validate Host and Origin on mutating requests
|
|
709
|
-
if (req.method === 'POST' || req.method === 'DELETE') {
|
|
1007
|
+
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') {
|
|
710
1008
|
// Check Host header to block DNS rebinding attacks
|
|
711
1009
|
const host = (req.headers.host || '').replace(/:\d+$/, '');
|
|
712
1010
|
const validHosts = ['localhost', '127.0.0.1'];
|
|
@@ -775,6 +1073,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
775
1073
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
776
1074
|
res.end(JSON.stringify(apiStatus(url.searchParams)));
|
|
777
1075
|
}
|
|
1076
|
+
else if (url.pathname === '/api/stats' && req.method === 'GET') {
|
|
1077
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1078
|
+
res.end(JSON.stringify(apiStats(url.searchParams)));
|
|
1079
|
+
}
|
|
778
1080
|
else if (url.pathname === '/api/reset' && req.method === 'POST') {
|
|
779
1081
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
780
1082
|
res.end(JSON.stringify(apiReset(url.searchParams)));
|
|
@@ -953,6 +1255,43 @@ const server = http.createServer(async (req, res) => {
|
|
|
953
1255
|
res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
|
|
954
1256
|
res.end(JSON.stringify(result));
|
|
955
1257
|
}
|
|
1258
|
+
// --- v3.4: Message Edit ---
|
|
1259
|
+
else if (url.pathname === '/api/message' && req.method === 'PUT') {
|
|
1260
|
+
const body = await parseBody(req);
|
|
1261
|
+
const result = apiEditMessage(body, url.searchParams);
|
|
1262
|
+
res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
|
|
1263
|
+
res.end(JSON.stringify(result));
|
|
1264
|
+
}
|
|
1265
|
+
// --- v3.4: Message Delete ---
|
|
1266
|
+
else if (url.pathname === '/api/message' && req.method === 'DELETE') {
|
|
1267
|
+
const body = await parseBody(req);
|
|
1268
|
+
const result = apiDeleteMessage(body, url.searchParams);
|
|
1269
|
+
res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
|
|
1270
|
+
res.end(JSON.stringify(result));
|
|
1271
|
+
}
|
|
1272
|
+
// --- v3.4: Conversation Templates ---
|
|
1273
|
+
else if (url.pathname === '/api/conversation-templates' && req.method === 'GET') {
|
|
1274
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1275
|
+
res.end(JSON.stringify(apiGetConversationTemplates()));
|
|
1276
|
+
}
|
|
1277
|
+
else if (url.pathname === '/api/conversation-templates/launch' && req.method === 'POST') {
|
|
1278
|
+
const body = await parseBody(req);
|
|
1279
|
+
const result = apiLaunchConversationTemplate(body, url.searchParams);
|
|
1280
|
+
res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
|
|
1281
|
+
res.end(JSON.stringify(result));
|
|
1282
|
+
}
|
|
1283
|
+
// --- v3.4: Agent Permissions ---
|
|
1284
|
+
else if (url.pathname === '/api/permissions' && req.method === 'GET') {
|
|
1285
|
+
const projectPath = url.searchParams.get('project') || null;
|
|
1286
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1287
|
+
res.end(JSON.stringify(readJson(filePath('permissions.json', projectPath))));
|
|
1288
|
+
}
|
|
1289
|
+
else if (url.pathname === '/api/permissions' && req.method === 'POST') {
|
|
1290
|
+
const body = await parseBody(req);
|
|
1291
|
+
const result = apiUpdatePermissions(body, url.searchParams);
|
|
1292
|
+
res.writeHead(result.error ? 400 : 200, { 'Content-Type': 'application/json' });
|
|
1293
|
+
res.end(JSON.stringify(result));
|
|
1294
|
+
}
|
|
956
1295
|
// Server info (LAN mode detection for frontend)
|
|
957
1296
|
else if (url.pathname === '/api/server-info' && req.method === 'GET') {
|
|
958
1297
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -1081,7 +1420,7 @@ server.listen(PORT, LAN_MODE ? '0.0.0.0' : '127.0.0.1', () => {
|
|
|
1081
1420
|
const dataDir = resolveDataDir();
|
|
1082
1421
|
const lanIP = getLanIP();
|
|
1083
1422
|
console.log('');
|
|
1084
|
-
console.log(' Let Them Talk - Agent Bridge Dashboard v3.
|
|
1423
|
+
console.log(' Let Them Talk - Agent Bridge Dashboard v3.4.0');
|
|
1085
1424
|
console.log(' ============================================');
|
|
1086
1425
|
console.log(' Dashboard: http://localhost:' + PORT);
|
|
1087
1426
|
if (LAN_MODE && lanIP) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "let-them-talk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dashboard.html",
|
|
22
22
|
"cli.js",
|
|
23
23
|
"templates/",
|
|
24
|
+
"conversation-templates/",
|
|
24
25
|
"logo.png",
|
|
25
26
|
"LICENSE",
|
|
26
27
|
"SECURITY.md",
|
package/server.js
CHANGED
|
@@ -2021,7 +2021,7 @@ async function main() {
|
|
|
2021
2021
|
loadPlugins();
|
|
2022
2022
|
const transport = new StdioServerTransport();
|
|
2023
2023
|
await server.connect(transport);
|
|
2024
|
-
console.error('Agent Bridge MCP server v3.
|
|
2024
|
+
console.error('Agent Bridge MCP server v3.4.0 running (' + (27 + loadedPlugins.length) + ' tools)');
|
|
2025
2025
|
}
|
|
2026
2026
|
|
|
2027
2027
|
main().catch(console.error);
|