@iletai/nzb 1.4.9 → 1.5.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/copilot/orchestrator.js +35 -2
- package/dist/copilot/system-message.js +11 -0
- package/dist/copilot/tools.js +195 -0
- package/dist/store/db.js +75 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { approveAll } from "@github/copilot-sdk";
|
|
2
2
|
import { config, DEFAULT_MODEL } from "../config.js";
|
|
3
3
|
import { SESSIONS_DIR } from "../paths.js";
|
|
4
|
-
import { deleteState, getMemorySummary, getRecentConversation, getState, logConversation, setState, } from "../store/db.js";
|
|
4
|
+
import { completeTeam, deleteState, getMemorySummary, getRecentConversation, getState, logConversation, setState, updateTeamMemberResult, } from "../store/db.js";
|
|
5
5
|
import { resetClient } from "./client.js";
|
|
6
6
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
7
7
|
import { getSkillDirectories } from "./skills.js";
|
|
@@ -25,6 +25,7 @@ export function setWorkerNotify(fn) {
|
|
|
25
25
|
}
|
|
26
26
|
let copilotClient;
|
|
27
27
|
const workers = new Map();
|
|
28
|
+
const teams = new Map();
|
|
28
29
|
let healthCheckTimer;
|
|
29
30
|
// Persistent orchestrator session
|
|
30
31
|
let orchestratorSession;
|
|
@@ -48,6 +49,7 @@ function getSessionConfig() {
|
|
|
48
49
|
cachedTools = createTools({
|
|
49
50
|
client: copilotClient,
|
|
50
51
|
workers,
|
|
52
|
+
teams,
|
|
51
53
|
onWorkerComplete: feedBackgroundResult,
|
|
52
54
|
onWorkerEvent: (event) => {
|
|
53
55
|
const worker = workers.get(event.name);
|
|
@@ -67,7 +69,38 @@ function getSessionConfig() {
|
|
|
67
69
|
export function feedBackgroundResult(workerName, result) {
|
|
68
70
|
const worker = workers.get(workerName);
|
|
69
71
|
const channel = worker?.originChannel;
|
|
70
|
-
|
|
72
|
+
const teamId = worker?.teamId;
|
|
73
|
+
console.log(`[nzb] Feeding background result from worker '${workerName}' (channel: ${channel ?? "none"}, team: ${teamId ?? "none"})`);
|
|
74
|
+
// If this worker is part of a team, handle team aggregation
|
|
75
|
+
if (teamId) {
|
|
76
|
+
const team = teams.get(teamId);
|
|
77
|
+
if (team) {
|
|
78
|
+
const status = result.startsWith("Error:") ? "error" : "completed";
|
|
79
|
+
updateTeamMemberResult(teamId, workerName, result, status);
|
|
80
|
+
team.completedMembers.add(workerName);
|
|
81
|
+
team.memberResults.set(workerName, result);
|
|
82
|
+
// Check if all members completed
|
|
83
|
+
if (team.completedMembers.size >= team.members.length) {
|
|
84
|
+
const aggregated = Array.from(team.memberResults.entries())
|
|
85
|
+
.map(([name, res]) => `## ${name}\n${res}`)
|
|
86
|
+
.join("\n\n---\n\n");
|
|
87
|
+
completeTeam(teamId, aggregated);
|
|
88
|
+
const prompt = `[Agent Team Completed] Team '${teamId}' finished.\n\n` +
|
|
89
|
+
`Task: ${team.taskDescription}\n\n` +
|
|
90
|
+
`${team.members.length} members completed:\n\n${aggregated}\n\n` +
|
|
91
|
+
`Please synthesize these results into a coherent summary for the user.`;
|
|
92
|
+
sendToOrchestrator(prompt, { type: "background" }, (_text, done) => {
|
|
93
|
+
if (done && proactiveNotifyFn) {
|
|
94
|
+
proactiveNotifyFn(_text, team.originChannel);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Cleanup team from memory (DB record persists)
|
|
98
|
+
teams.delete(teamId);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Non-team worker: original behavior
|
|
71
104
|
const prompt = `[Background task completed] Worker '${workerName}' finished:\n\n${result}`;
|
|
72
105
|
sendToOrchestrator(prompt, { type: "background" }, (_text, done) => {
|
|
73
106
|
if (done && proactiveNotifyFn) {
|
|
@@ -48,6 +48,17 @@ Worker tools are **non-blocking** — dispatch and return immediately:
|
|
|
48
48
|
- Never do complex work yourself — delegate to workers.
|
|
49
49
|
- Only orchestrator turns block the queue.
|
|
50
50
|
|
|
51
|
+
## Agent Teams
|
|
52
|
+
|
|
53
|
+
Create agent teams for parallel collaborative work:
|
|
54
|
+
1. Use \`create_agent_team\` to spawn 2-5 specialized workers
|
|
55
|
+
2. Each member works independently with their own role
|
|
56
|
+
3. Results auto-aggregate when all members finish → you get \`[Agent Team Completed]\` → synthesize for user
|
|
57
|
+
4. Use \`get_team_status\` to check progress
|
|
58
|
+
5. Use \`send_team_message\` to broadcast updates
|
|
59
|
+
|
|
60
|
+
Best for: multi-angle code review, competing debug hypotheses, parallel feature implementation, research from different perspectives.
|
|
61
|
+
|
|
51
62
|
## Skills Workflow
|
|
52
63
|
|
|
53
64
|
1. Search skills.sh first for existing community skills.
|
package/dist/copilot/tools.js
CHANGED
|
@@ -33,6 +33,7 @@ const BLOCKED_WORKER_DIRS = [
|
|
|
33
33
|
".pypirc",
|
|
34
34
|
];
|
|
35
35
|
const MAX_CONCURRENT_WORKERS = 5;
|
|
36
|
+
const MAX_CONCURRENT_TEAMS = 3;
|
|
36
37
|
export function createTools(deps) {
|
|
37
38
|
return [
|
|
38
39
|
defineTool("create_worker_session", {
|
|
@@ -220,6 +221,200 @@ export function createTools(deps) {
|
|
|
220
221
|
return `Worker '${args.name}' terminated.`;
|
|
221
222
|
},
|
|
222
223
|
}),
|
|
224
|
+
// ── Agent Team Tools ──────────────────────────────────────────
|
|
225
|
+
defineTool("create_agent_team", {
|
|
226
|
+
description: "Create an agent team — multiple workers collaborating on a task in parallel. Each member gets a role " +
|
|
227
|
+
"and works independently. Results are automatically aggregated when all members complete. Use for tasks " +
|
|
228
|
+
"that benefit from parallel work: code review from multiple angles, investigating competing hypotheses, " +
|
|
229
|
+
"implementing independent modules, etc.",
|
|
230
|
+
parameters: z.object({
|
|
231
|
+
team_name: z.string().describe("Unique name for the team, e.g. 'pr-review-team'"),
|
|
232
|
+
task_description: z.string().describe("Overall task description shared with all members"),
|
|
233
|
+
members: z
|
|
234
|
+
.array(z.object({
|
|
235
|
+
name: z.string().describe("Unique worker name for this member, e.g. 'security-reviewer'"),
|
|
236
|
+
role: z.string().describe("Role description, e.g. 'Review code for security vulnerabilities'"),
|
|
237
|
+
prompt: z.string().describe("Specific prompt/instructions for this member"),
|
|
238
|
+
}))
|
|
239
|
+
.min(2)
|
|
240
|
+
.max(5)
|
|
241
|
+
.describe("Team members (2-5). Each gets their own worker session."),
|
|
242
|
+
working_dir: z.string().describe("Absolute path to working directory for all members"),
|
|
243
|
+
}),
|
|
244
|
+
handler: async (args) => {
|
|
245
|
+
const activeTeams = Array.from(deps.teams.values()).filter((t) => t.completedMembers.size < t.members.length);
|
|
246
|
+
if (activeTeams.length >= MAX_CONCURRENT_TEAMS) {
|
|
247
|
+
return `❌ Maximum ${MAX_CONCURRENT_TEAMS} concurrent teams reached. Wait for an active team to complete.`;
|
|
248
|
+
}
|
|
249
|
+
const totalWorkers = deps.workers.size + args.members.length;
|
|
250
|
+
if (totalWorkers > MAX_CONCURRENT_WORKERS) {
|
|
251
|
+
return `❌ Adding ${args.members.length} members would exceed max workers (${MAX_CONCURRENT_WORKERS}). Currently ${deps.workers.size} active. Kill some workers first.`;
|
|
252
|
+
}
|
|
253
|
+
const home = homedir();
|
|
254
|
+
const resolvedDir = resolve(args.working_dir);
|
|
255
|
+
for (const blocked of BLOCKED_WORKER_DIRS) {
|
|
256
|
+
const blockedPath = join(home, blocked);
|
|
257
|
+
if (resolvedDir === blockedPath || resolvedDir.startsWith(blockedPath + sep)) {
|
|
258
|
+
return `❌ Working directory '${args.working_dir}' is a sensitive directory. Workers cannot operate in ${blocked}.`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const member of args.members) {
|
|
262
|
+
if (deps.workers.has(member.name)) {
|
|
263
|
+
return `❌ Worker '${member.name}' already exists. Use unique names.`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const teamId = args.team_name;
|
|
267
|
+
const originChannel = getCurrentSourceChannel();
|
|
268
|
+
const { createTeam: dbCreateTeam, addTeamMember: dbAddTeamMember } = await import("../store/db.js");
|
|
269
|
+
dbCreateTeam(teamId, args.task_description, originChannel);
|
|
270
|
+
const teamInfo = {
|
|
271
|
+
id: teamId,
|
|
272
|
+
taskDescription: args.task_description,
|
|
273
|
+
members: args.members.map((m) => m.name),
|
|
274
|
+
originChannel,
|
|
275
|
+
completedMembers: new Set(),
|
|
276
|
+
memberResults: new Map(),
|
|
277
|
+
};
|
|
278
|
+
deps.teams.set(teamId, teamInfo);
|
|
279
|
+
const spawnResults = [];
|
|
280
|
+
for (const member of args.members) {
|
|
281
|
+
try {
|
|
282
|
+
const session = await deps.client.createSession({
|
|
283
|
+
model: config.copilotModel,
|
|
284
|
+
configDir: SESSIONS_DIR,
|
|
285
|
+
workingDirectory: args.working_dir,
|
|
286
|
+
onPermissionRequest: approveAll,
|
|
287
|
+
});
|
|
288
|
+
const worker = {
|
|
289
|
+
name: member.name,
|
|
290
|
+
session,
|
|
291
|
+
workingDir: resolvedDir,
|
|
292
|
+
status: "running",
|
|
293
|
+
startedAt: Date.now(),
|
|
294
|
+
originChannel,
|
|
295
|
+
teamId,
|
|
296
|
+
};
|
|
297
|
+
deps.workers.set(member.name, worker);
|
|
298
|
+
deps.onWorkerEvent?.({ type: "created", name: member.name, workingDir: resolvedDir });
|
|
299
|
+
const db = getDb();
|
|
300
|
+
db.prepare(`INSERT OR REPLACE INTO worker_sessions (name, copilot_session_id, working_dir, status) VALUES (?, ?, ?, 'running')`).run(member.name, session.sessionId, resolvedDir);
|
|
301
|
+
dbAddTeamMember(teamId, member.name, member.role);
|
|
302
|
+
const teamPrompt = `Working directory: ${args.working_dir}\n\n${member.prompt}\n\n` +
|
|
303
|
+
`Context: You are part of team '${teamId}'. Your role: ${member.role}\n` +
|
|
304
|
+
`Overall task: ${args.task_description}\n\nProvide your results clearly. Focus on your role.`;
|
|
305
|
+
const timeoutMs = config.workerTimeoutMs;
|
|
306
|
+
session
|
|
307
|
+
.sendAndWait({ prompt: teamPrompt }, timeoutMs)
|
|
308
|
+
.then((result) => {
|
|
309
|
+
worker.lastOutput = result?.data?.content || "No response";
|
|
310
|
+
worker.status = "idle";
|
|
311
|
+
deps.onWorkerEvent?.({ type: "completed", name: member.name });
|
|
312
|
+
deps.onWorkerComplete(member.name, worker.lastOutput);
|
|
313
|
+
})
|
|
314
|
+
.catch((err) => {
|
|
315
|
+
const errMsg = formatWorkerError(member.name, worker.startedAt, timeoutMs, err);
|
|
316
|
+
worker.lastOutput = errMsg;
|
|
317
|
+
worker.status = "error";
|
|
318
|
+
deps.onWorkerEvent?.({ type: "error", name: member.name, error: errMsg });
|
|
319
|
+
deps.onWorkerComplete(member.name, errMsg);
|
|
320
|
+
})
|
|
321
|
+
.finally(() => {
|
|
322
|
+
session.destroy().catch(() => { });
|
|
323
|
+
deps.workers.delete(member.name);
|
|
324
|
+
try {
|
|
325
|
+
getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(member.name);
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
});
|
|
329
|
+
spawnResults.push(`✅ ${member.name} (${member.role})`);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
333
|
+
spawnResults.push(`❌ ${member.name}: ${errMsg}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return (`🏗️ Agent team '${teamId}' created with ${args.members.length} members:\n` +
|
|
337
|
+
`${spawnResults.join("\n")}\n\n` +
|
|
338
|
+
`All agents dispatched in parallel. Results will be aggregated automatically when all members complete.`);
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
defineTool("get_team_status", {
|
|
342
|
+
description: "Get the status of agent teams — shows active teams, their members, and progress.",
|
|
343
|
+
parameters: z.object({
|
|
344
|
+
team_name: z
|
|
345
|
+
.string()
|
|
346
|
+
.optional()
|
|
347
|
+
.describe("Specific team name to check. Omit to list all active teams."),
|
|
348
|
+
}),
|
|
349
|
+
handler: async (args) => {
|
|
350
|
+
if (args.team_name) {
|
|
351
|
+
const team = deps.teams.get(args.team_name);
|
|
352
|
+
if (!team)
|
|
353
|
+
return `❌ Team '${args.team_name}' not found.`;
|
|
354
|
+
const lines = [
|
|
355
|
+
`🏗️ Team: ${team.id}`,
|
|
356
|
+
`📋 Task: ${team.taskDescription}`,
|
|
357
|
+
`📊 Progress: ${team.completedMembers.size}/${team.members.length}`,
|
|
358
|
+
"",
|
|
359
|
+
"Members:",
|
|
360
|
+
];
|
|
361
|
+
for (const memberName of team.members) {
|
|
362
|
+
const worker = deps.workers.get(memberName);
|
|
363
|
+
const completed = team.completedMembers.has(memberName);
|
|
364
|
+
const status = completed
|
|
365
|
+
? "✅ done"
|
|
366
|
+
: worker?.status === "running"
|
|
367
|
+
? "⏳ running"
|
|
368
|
+
: worker?.status === "error"
|
|
369
|
+
? "❌ error"
|
|
370
|
+
: "🔄 pending";
|
|
371
|
+
const elapsed = worker?.startedAt
|
|
372
|
+
? `${Math.round((Date.now() - worker.startedAt) / 1000)}s`
|
|
373
|
+
: "";
|
|
374
|
+
lines.push(` ${status} ${memberName} ${elapsed}`);
|
|
375
|
+
}
|
|
376
|
+
return lines.join("\n");
|
|
377
|
+
}
|
|
378
|
+
const activeTeams = Array.from(deps.teams.values());
|
|
379
|
+
if (activeTeams.length === 0)
|
|
380
|
+
return "No active agent teams.";
|
|
381
|
+
const lines = [`📋 Active teams (${activeTeams.length}):`];
|
|
382
|
+
for (const team of activeTeams) {
|
|
383
|
+
lines.push(` 🏗️ ${team.id}: ${team.completedMembers.size}/${team.members.length} done — ${team.taskDescription.slice(0, 80)}`);
|
|
384
|
+
}
|
|
385
|
+
return lines.join("\n");
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
defineTool("send_team_message", {
|
|
389
|
+
description: "Send a message to all active members of a team. Use to provide additional instructions, " +
|
|
390
|
+
"context, or redirect the team's work.",
|
|
391
|
+
parameters: z.object({
|
|
392
|
+
team_name: z.string().describe("Name of the team"),
|
|
393
|
+
message: z.string().describe("Message to send to all active team members"),
|
|
394
|
+
}),
|
|
395
|
+
handler: async (args) => {
|
|
396
|
+
const team = deps.teams.get(args.team_name);
|
|
397
|
+
if (!team)
|
|
398
|
+
return `❌ Team '${args.team_name}' not found.`;
|
|
399
|
+
const activeMembers = team.members.filter((name) => !team.completedMembers.has(name) && deps.workers.has(name));
|
|
400
|
+
if (activeMembers.length === 0)
|
|
401
|
+
return `❌ No active members in team '${args.team_name}'.`;
|
|
402
|
+
let sent = 0;
|
|
403
|
+
for (const memberName of activeMembers) {
|
|
404
|
+
const worker = deps.workers.get(memberName);
|
|
405
|
+
if (worker && worker.status !== "error") {
|
|
406
|
+
try {
|
|
407
|
+
worker.session
|
|
408
|
+
.sendAndWait({ prompt: `[Team message from coordinator]: ${args.message}` }, 60_000)
|
|
409
|
+
.catch(() => { });
|
|
410
|
+
sent++;
|
|
411
|
+
}
|
|
412
|
+
catch { }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return `📨 Message sent to ${sent}/${activeMembers.length} active members of team '${args.team_name}'.`;
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
223
418
|
defineTool("list_machine_sessions", {
|
|
224
419
|
description: "List ALL Copilot CLI sessions on this machine — including sessions started from VS Code, " +
|
|
225
420
|
"the terminal, or other tools. Shows session ID, summary, working directory. " +
|
package/dist/store/db.js
CHANGED
|
@@ -55,6 +55,32 @@ export function getDb() {
|
|
|
55
55
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
56
56
|
last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
57
57
|
)
|
|
58
|
+
`);
|
|
59
|
+
db.exec(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS agent_teams (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'error')),
|
|
63
|
+
task_description TEXT NOT NULL,
|
|
64
|
+
origin_channel TEXT,
|
|
65
|
+
member_count INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
completed_count INTEGER NOT NULL DEFAULT 0,
|
|
67
|
+
aggregated_result TEXT,
|
|
68
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
completed_at DATETIME
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS team_members (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
team_id TEXT NOT NULL,
|
|
76
|
+
worker_name TEXT NOT NULL,
|
|
77
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
78
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'error')),
|
|
79
|
+
result TEXT,
|
|
80
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
81
|
+
completed_at DATETIME,
|
|
82
|
+
FOREIGN KEY(team_id) REFERENCES agent_teams(id)
|
|
83
|
+
)
|
|
58
84
|
`);
|
|
59
85
|
// Migrate: if the table already existed with a stricter CHECK, recreate it
|
|
60
86
|
try {
|
|
@@ -266,6 +292,55 @@ export function getMemorySummary() {
|
|
|
266
292
|
});
|
|
267
293
|
return sections.join("\n");
|
|
268
294
|
}
|
|
295
|
+
// ── Agent Teams CRUD ──────────────────────────────────────────
|
|
296
|
+
export function createTeam(id, taskDescription, originChannel) {
|
|
297
|
+
const db = getDb();
|
|
298
|
+
db.prepare(`INSERT INTO agent_teams (id, task_description, origin_channel) VALUES (?, ?, ?)`).run(id, taskDescription, originChannel ?? null);
|
|
299
|
+
}
|
|
300
|
+
export function addTeamMember(teamId, workerName, role) {
|
|
301
|
+
const db = getDb();
|
|
302
|
+
db.prepare(`INSERT INTO team_members (team_id, worker_name, role, status) VALUES (?, ?, ?, 'pending')`).run(teamId, workerName, role);
|
|
303
|
+
db.prepare(`UPDATE agent_teams SET member_count = member_count + 1 WHERE id = ?`).run(teamId);
|
|
304
|
+
}
|
|
305
|
+
export function updateTeamMemberResult(teamId, workerName, result, status) {
|
|
306
|
+
const db = getDb();
|
|
307
|
+
db.prepare(`UPDATE team_members SET result = ?, status = ?, completed_at = CURRENT_TIMESTAMP WHERE team_id = ? AND worker_name = ?`).run(result, status, teamId, workerName);
|
|
308
|
+
if (status === "completed" || status === "error") {
|
|
309
|
+
db.prepare(`UPDATE agent_teams SET completed_count = completed_count + 1 WHERE id = ?`).run(teamId);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export function getTeam(id) {
|
|
313
|
+
const db = getDb();
|
|
314
|
+
return db.prepare(`SELECT * FROM agent_teams WHERE id = ?`).get(id);
|
|
315
|
+
}
|
|
316
|
+
export function getTeamMembers(teamId) {
|
|
317
|
+
const db = getDb();
|
|
318
|
+
return db
|
|
319
|
+
.prepare(`SELECT worker_name, role, status, result FROM team_members WHERE team_id = ? ORDER BY id`)
|
|
320
|
+
.all(teamId);
|
|
321
|
+
}
|
|
322
|
+
export function completeTeam(teamId, aggregatedResult, status = "completed") {
|
|
323
|
+
const db = getDb();
|
|
324
|
+
db.prepare(`UPDATE agent_teams SET status = ?, aggregated_result = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(status, aggregatedResult, teamId);
|
|
325
|
+
}
|
|
326
|
+
export function getActiveTeams() {
|
|
327
|
+
const db = getDb();
|
|
328
|
+
return db
|
|
329
|
+
.prepare(`SELECT id, status, task_description, member_count, completed_count, created_at FROM agent_teams WHERE status = 'active' ORDER BY created_at DESC`)
|
|
330
|
+
.all();
|
|
331
|
+
}
|
|
332
|
+
export function getTeamByWorkerName(workerName) {
|
|
333
|
+
const db = getDb();
|
|
334
|
+
const row = db
|
|
335
|
+
.prepare(`SELECT team_id FROM team_members WHERE worker_name = ? AND status IN ('pending', 'running') LIMIT 1`)
|
|
336
|
+
.get(workerName);
|
|
337
|
+
return row?.team_id;
|
|
338
|
+
}
|
|
339
|
+
export function cleanupTeam(teamId) {
|
|
340
|
+
const db = getDb();
|
|
341
|
+
db.prepare(`DELETE FROM team_members WHERE team_id = ?`).run(teamId);
|
|
342
|
+
db.prepare(`DELETE FROM agent_teams WHERE id = ?`).run(teamId);
|
|
343
|
+
}
|
|
269
344
|
export function closeDb() {
|
|
270
345
|
if (db) {
|
|
271
346
|
stmtCache = undefined;
|