session-collab-mcp 0.8.8 → 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.
Files changed (3) hide show
  1. package/dist/cli.js +478 -3820
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4203,13 +4203,6 @@ var DEFAULT_SESSION_CONFIG = {
4203
4203
  auto_release_immediate: false,
4204
4204
  auto_release_delay_minutes: 5
4205
4205
  };
4206
- var SCOPE_WAIT_MINUTES = {
4207
- small: 30,
4208
- medium: 120,
4209
- // 2 hours
4210
- large: 480
4211
- // 8 hours
4212
- };
4213
4206
 
4214
4207
  // src/db/queries.ts
4215
4208
  async function createSession(db, params) {
@@ -4256,66 +4249,6 @@ async function listSessions(db, params = {}) {
4256
4249
  const result = await db.prepare(query).bind(...bindings).all();
4257
4250
  return result.results;
4258
4251
  }
4259
- async function updateSessionHeartbeat(db, id, statusUpdate) {
4260
- const now = (/* @__PURE__ */ new Date()).toISOString();
4261
- let progress = null;
4262
- let todosJson = null;
4263
- if (statusUpdate?.todos) {
4264
- const total = statusUpdate.todos.length;
4265
- const completed = statusUpdate.todos.filter((t) => t.status === "completed").length;
4266
- progress = {
4267
- completed,
4268
- total,
4269
- percentage: total > 0 ? Math.round(completed / total * 100) : 0
4270
- };
4271
- todosJson = JSON.stringify(statusUpdate.todos);
4272
- }
4273
- let query = "UPDATE sessions SET last_heartbeat = ?";
4274
- const bindings = [now];
4275
- if (statusUpdate?.current_task !== void 0) {
4276
- query += ", current_task = ?";
4277
- bindings.push(statusUpdate.current_task);
4278
- }
4279
- if (progress) {
4280
- query += ", progress = ?";
4281
- bindings.push(JSON.stringify(progress));
4282
- }
4283
- if (todosJson) {
4284
- query += ", todos = ?";
4285
- bindings.push(todosJson);
4286
- }
4287
- query += " WHERE id = ? AND status = 'active'";
4288
- bindings.push(id);
4289
- const result = await db.prepare(query).bind(...bindings).run();
4290
- return result.meta.changes > 0;
4291
- }
4292
- async function updateSessionStatus(db, id, params) {
4293
- const now = (/* @__PURE__ */ new Date()).toISOString();
4294
- let progress = null;
4295
- let todosJson = null;
4296
- if (params.todos) {
4297
- const total = params.todos.length;
4298
- const completed = params.todos.filter((t) => t.status === "completed").length;
4299
- progress = {
4300
- completed,
4301
- total,
4302
- percentage: total > 0 ? Math.round(completed / total * 100) : 0
4303
- };
4304
- todosJson = JSON.stringify(params.todos);
4305
- }
4306
- const result = await db.prepare(
4307
- `UPDATE sessions
4308
- SET current_task = ?, progress = ?, todos = ?, last_heartbeat = ?
4309
- WHERE id = ? AND status = 'active'`
4310
- ).bind(
4311
- params.current_task ?? null,
4312
- progress ? JSON.stringify(progress) : null,
4313
- todosJson,
4314
- now,
4315
- id
4316
- ).run();
4317
- return result.meta.changes > 0;
4318
- }
4319
4252
  async function endSession(db, id, release_claims = "abandon") {
4320
4253
  const claimStatus = release_claims === "complete" ? "completed" : "abandoned";
4321
4254
  await db.prepare("UPDATE claims SET status = ?, updated_at = ? WHERE session_id = ? AND status = 'active'").bind(claimStatus, (/* @__PURE__ */ new Date()).toISOString(), id).run();
@@ -4423,11 +4356,6 @@ async function createClaim(db, params) {
4423
4356
  symbols: params.symbols
4424
4357
  };
4425
4358
  }
4426
- async function updateClaimPriority(db, claimId, priority) {
4427
- const now = (/* @__PURE__ */ new Date()).toISOString();
4428
- const result = await db.prepare("UPDATE claims SET priority = ?, updated_at = ? WHERE id = ? AND status = ?").bind(priority, now, claimId, "active").run();
4429
- return result.meta.changes > 0;
4430
- }
4431
4359
  async function getClaim(db, id) {
4432
4360
  const result = await db.prepare(
4433
4361
  `SELECT
@@ -4617,209 +4545,6 @@ async function releaseClaim(db, id, params) {
4617
4545
  const result = await db.prepare("UPDATE claims SET status = ?, updated_at = ?, completed_summary = ? WHERE id = ?").bind(params.status, now, params.summary ?? null, id).run();
4618
4546
  return result.meta.changes > 0;
4619
4547
  }
4620
- async function getClaimInfoByFile(db, sessionId, filePath) {
4621
- const claim = await db.prepare(
4622
- `SELECT c.id, c.scope
4623
- FROM claims c
4624
- JOIN claim_files cf ON c.id = cf.claim_id
4625
- WHERE c.session_id = ? AND c.status = 'active' AND cf.file_path = ?`
4626
- ).bind(sessionId, filePath).first();
4627
- if (!claim) {
4628
- return null;
4629
- }
4630
- const filesResult = await db.prepare("SELECT file_path FROM claim_files WHERE claim_id = ?").bind(claim.id).all();
4631
- const files = (filesResult.results ?? []).map((f) => f.file_path);
4632
- return {
4633
- claim_id: claim.id,
4634
- scope: claim.scope,
4635
- file_count: files.length,
4636
- files
4637
- };
4638
- }
4639
- async function releaseClaimByFile(db, sessionId, filePath) {
4640
- const claim = await db.prepare(
4641
- `SELECT c.id, c.scope
4642
- FROM claims c
4643
- JOIN claim_files cf ON c.id = cf.claim_id
4644
- WHERE c.session_id = ? AND c.status = 'active' AND cf.file_path = ?`
4645
- ).bind(sessionId, filePath).first();
4646
- if (!claim) {
4647
- return { released: false };
4648
- }
4649
- const fileCount = await db.prepare("SELECT COUNT(*) as count FROM claim_files WHERE claim_id = ?").bind(claim.id).first();
4650
- const count = fileCount?.count ?? 0;
4651
- if (count <= 1) {
4652
- await releaseClaim(db, claim.id, { status: "completed" });
4653
- return { released: true, claim_id: claim.id, scope: claim.scope, partial: false };
4654
- } else {
4655
- await db.prepare("DELETE FROM claim_files WHERE claim_id = ? AND file_path = ?").bind(claim.id, filePath).run();
4656
- await db.prepare("DELETE FROM claim_symbols WHERE claim_id = ? AND file_path = ?").bind(claim.id, filePath).run();
4657
- await db.prepare("UPDATE claims SET updated_at = ? WHERE id = ?").bind((/* @__PURE__ */ new Date()).toISOString(), claim.id).run();
4658
- return {
4659
- released: true,
4660
- claim_id: claim.id,
4661
- scope: claim.scope,
4662
- partial: true,
4663
- files_remaining: count - 1
4664
- };
4665
- }
4666
- }
4667
- async function sendMessage(db, params) {
4668
- const id = generateId();
4669
- const now = (/* @__PURE__ */ new Date()).toISOString();
4670
- await db.prepare(
4671
- `INSERT INTO messages (id, from_session_id, to_session_id, content, created_at)
4672
- VALUES (?, ?, ?, ?, ?)`
4673
- ).bind(id, params.from_session_id, params.to_session_id ?? null, params.content, now).run();
4674
- return {
4675
- id,
4676
- from_session_id: params.from_session_id,
4677
- to_session_id: params.to_session_id ?? null,
4678
- content: params.content,
4679
- read_at: null,
4680
- created_at: now
4681
- };
4682
- }
4683
- async function listMessages(db, params) {
4684
- let query = `
4685
- SELECT * FROM messages
4686
- WHERE (to_session_id = ? OR to_session_id IS NULL)
4687
- `;
4688
- const bindings = [params.session_id];
4689
- if (params.unread_only) {
4690
- query += " AND read_at IS NULL";
4691
- }
4692
- query += " ORDER BY created_at DESC";
4693
- const messages = await db.prepare(query).bind(...bindings).all();
4694
- if (params.mark_as_read && messages.results.length > 0) {
4695
- const now = (/* @__PURE__ */ new Date()).toISOString();
4696
- const ids = messages.results.map((m) => m.id);
4697
- const placeholders = ids.map(() => "?").join(",");
4698
- await db.prepare(`UPDATE messages SET read_at = ? WHERE id IN (${placeholders}) AND read_at IS NULL`).bind(now, ...ids).run();
4699
- }
4700
- return messages.results;
4701
- }
4702
- async function addDecision(db, params) {
4703
- const id = generateId();
4704
- const now = (/* @__PURE__ */ new Date()).toISOString();
4705
- await db.prepare(
4706
- `INSERT INTO decisions (id, session_id, category, title, description, created_at)
4707
- VALUES (?, ?, ?, ?, ?, ?)`
4708
- ).bind(id, params.session_id, params.category ?? null, params.title, params.description, now).run();
4709
- return {
4710
- id,
4711
- session_id: params.session_id,
4712
- category: params.category ?? null,
4713
- title: params.title,
4714
- description: params.description,
4715
- created_at: now
4716
- };
4717
- }
4718
- async function listDecisions(db, params = {}) {
4719
- let query = "SELECT * FROM decisions WHERE 1=1";
4720
- const bindings = [];
4721
- if (params.category) {
4722
- query += " AND category = ?";
4723
- bindings.push(params.category);
4724
- }
4725
- query += " ORDER BY created_at DESC LIMIT ?";
4726
- bindings.push(params.limit ?? 20);
4727
- const result = await db.prepare(query).bind(...bindings).all();
4728
- return result.results;
4729
- }
4730
- async function storeReferences(db, sessionId, references) {
4731
- const now = (/* @__PURE__ */ new Date()).toISOString();
4732
- const statements = [];
4733
- for (const ref of references) {
4734
- for (const r of ref.references) {
4735
- statements.push(
4736
- db.prepare(
4737
- `INSERT OR IGNORE INTO symbol_references
4738
- (source_file, source_symbol, ref_file, ref_line, ref_context, session_id, created_at)
4739
- VALUES (?, ?, ?, ?, ?, ?, ?)`
4740
- ).bind(ref.source_file, ref.source_symbol, r.file, r.line, r.context ?? null, sessionId, now)
4741
- );
4742
- }
4743
- }
4744
- if (statements.length === 0) {
4745
- return { stored: 0, skipped: 0 };
4746
- }
4747
- try {
4748
- const results = await db.batch(statements);
4749
- const stored = results.reduce((acc, r) => acc + r.meta.changes, 0);
4750
- return { stored, skipped: statements.length - stored };
4751
- } catch (err) {
4752
- console.error("[storeReferences] Batch insert failed:", err);
4753
- return { stored: 0, skipped: statements.length };
4754
- }
4755
- }
4756
- async function getReferencesForSymbol(db, sourceFile, sourceSymbol) {
4757
- const result = await db.prepare(
4758
- `SELECT * FROM symbol_references
4759
- WHERE source_file = ? AND source_symbol = ?
4760
- ORDER BY ref_file, ref_line`
4761
- ).bind(sourceFile, sourceSymbol).all();
4762
- return result.results;
4763
- }
4764
- async function analyzeClaimImpact(db, sourceFile, sourceSymbol, excludeSessionId) {
4765
- const refs = await getReferencesForSymbol(db, sourceFile, sourceSymbol);
4766
- const affectedFiles = [...new Set(refs.map((r) => r.ref_file))];
4767
- const affectedClaims = [];
4768
- if (affectedFiles.length > 0) {
4769
- const placeholders = affectedFiles.map(() => "?").join(",");
4770
- let query = `
4771
- SELECT DISTINCT
4772
- c.id as claim_id,
4773
- s.name as session_name,
4774
- c.intent,
4775
- cf.file_path
4776
- FROM claim_files cf
4777
- JOIN claims c ON cf.claim_id = c.id
4778
- JOIN sessions s ON c.session_id = s.id
4779
- WHERE c.status = 'active'
4780
- AND s.status = 'active'
4781
- AND cf.file_path IN (${placeholders})
4782
- `;
4783
- const bindings = [...affectedFiles];
4784
- if (excludeSessionId) {
4785
- query += " AND c.session_id != ?";
4786
- bindings.push(excludeSessionId);
4787
- }
4788
- const claimResults = await db.prepare(query).bind(...bindings).all();
4789
- const claimMap = /* @__PURE__ */ new Map();
4790
- for (const r of claimResults.results) {
4791
- const existing = claimMap.get(r.claim_id);
4792
- if (existing) {
4793
- existing.files.push(r.file_path);
4794
- } else {
4795
- claimMap.set(r.claim_id, {
4796
- session_name: r.session_name,
4797
- intent: r.intent,
4798
- files: [r.file_path]
4799
- });
4800
- }
4801
- }
4802
- for (const [claimId, data] of claimMap) {
4803
- affectedClaims.push({
4804
- claim_id: claimId,
4805
- session_name: data.session_name,
4806
- intent: data.intent,
4807
- affected_symbols: data.files
4808
- });
4809
- }
4810
- }
4811
- return {
4812
- symbol: sourceSymbol,
4813
- file: sourceFile,
4814
- affected_claims: affectedClaims,
4815
- reference_count: refs.length,
4816
- affected_files: affectedFiles
4817
- };
4818
- }
4819
- async function clearSessionReferences(db, sessionId) {
4820
- const result = await db.prepare("DELETE FROM symbol_references WHERE session_id = ?").bind(sessionId).run();
4821
- return result.meta.changes;
4822
- }
4823
4548
  async function logAuditEvent(db, params) {
4824
4549
  const id = generateId();
4825
4550
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -4838,226 +4563,10 @@ async function logAuditEvent(db, params) {
4838
4563
  created_at: now
4839
4564
  };
4840
4565
  }
4841
- async function listAuditHistory(db, params = {}) {
4842
- let query = `
4843
- SELECT h.*, s.name as session_name
4844
- FROM audit_history h
4845
- LEFT JOIN sessions s ON h.session_id = s.id
4846
- WHERE 1=1
4847
- `;
4848
- const bindings = [];
4849
- if (params.session_id) {
4850
- query += " AND h.session_id = ?";
4851
- bindings.push(params.session_id);
4852
- }
4853
- if (params.action) {
4854
- query += " AND h.action = ?";
4855
- bindings.push(params.action);
4856
- }
4857
- if (params.entity_type) {
4858
- query += " AND h.entity_type = ?";
4859
- bindings.push(params.entity_type);
4860
- }
4861
- if (params.entity_id) {
4862
- query += " AND h.entity_id = ?";
4863
- bindings.push(params.entity_id);
4864
- }
4865
- if (params.from_date) {
4866
- query += " AND h.created_at >= ?";
4867
- bindings.push(params.from_date);
4868
- }
4869
- if (params.to_date) {
4870
- query += " AND h.created_at <= ?";
4871
- bindings.push(params.to_date);
4872
- }
4873
- query += " ORDER BY h.created_at DESC LIMIT ?";
4874
- bindings.push(params.limit ?? 50);
4875
- const result = await db.prepare(query).bind(...bindings).all();
4876
- return result.results;
4877
- }
4878
- async function cleanupOldAuditHistory(db, retentionDays = 7) {
4879
- const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1e3).toISOString();
4880
- const result = await db.prepare("DELETE FROM audit_history WHERE created_at < ?").bind(cutoff).run();
4881
- return result.meta.changes;
4882
- }
4883
- async function getNextQueuePosition(db, claimId) {
4884
- const result = await db.prepare("SELECT MAX(position) as max_pos FROM claim_queue WHERE claim_id = ?").bind(claimId).first();
4885
- return (result?.max_pos ?? 0) + 1;
4886
- }
4887
- async function calculateEstimatedWait(db, claimId, position) {
4888
- const result = await db.prepare(
4889
- `SELECT scope FROM claim_queue
4890
- WHERE claim_id = ? AND position < ?
4891
- ORDER BY priority DESC, position ASC`
4892
- ).bind(claimId, position).all();
4893
- let totalMinutes = 0;
4894
- for (const entry of result.results) {
4895
- totalMinutes += SCOPE_WAIT_MINUTES[entry.scope] ?? SCOPE_WAIT_MINUTES.medium;
4896
- }
4897
- const claim = await getClaim(db, claimId);
4898
- if (claim) {
4899
- totalMinutes += Math.round(SCOPE_WAIT_MINUTES[claim.scope] / 2);
4900
- }
4901
- return totalMinutes;
4902
- }
4903
- async function joinQueue(db, params) {
4904
- const id = generateId();
4905
- const now = (/* @__PURE__ */ new Date()).toISOString();
4906
- const priority = params.priority ?? 50;
4907
- const scope = params.scope ?? "medium";
4908
- const position = await getNextQueuePosition(db, params.claim_id);
4909
- const estimatedWait = await calculateEstimatedWait(db, params.claim_id, position);
4910
- await db.prepare(
4911
- `INSERT INTO claim_queue (id, claim_id, session_id, intent, position, priority, scope, estimated_wait_minutes, created_at)
4912
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
4913
- ).bind(id, params.claim_id, params.session_id, params.intent, position, priority, scope, estimatedWait, now).run();
4914
- return {
4915
- id,
4916
- claim_id: params.claim_id,
4917
- session_id: params.session_id,
4918
- intent: params.intent,
4919
- position,
4920
- priority,
4921
- scope,
4922
- estimated_wait_minutes: estimatedWait,
4923
- created_at: now
4924
- };
4925
- }
4926
- async function leaveQueue(db, queueId) {
4927
- const result = await db.prepare("DELETE FROM claim_queue WHERE id = ?").bind(queueId).run();
4928
- return result.meta.changes > 0;
4929
- }
4930
- async function getQueueEntry(db, queueId) {
4931
- const result = await db.prepare("SELECT * FROM claim_queue WHERE id = ?").bind(queueId).first();
4932
- return result ?? null;
4933
- }
4934
- async function listQueue(db, params = {}) {
4935
- let query = `
4936
- SELECT
4937
- q.*,
4938
- s.name as session_name,
4939
- c.intent as claim_intent,
4940
- cs.name as claim_session_name,
4941
- GROUP_CONCAT(cf.file_path, '|||') as claim_files_concat
4942
- FROM claim_queue q
4943
- JOIN sessions s ON q.session_id = s.id
4944
- JOIN claims c ON q.claim_id = c.id
4945
- JOIN sessions cs ON c.session_id = cs.id
4946
- LEFT JOIN claim_files cf ON c.id = cf.claim_id
4947
- WHERE 1=1
4948
- `;
4949
- const bindings = [];
4950
- if (params.claim_id) {
4951
- query += " AND q.claim_id = ?";
4952
- bindings.push(params.claim_id);
4953
- }
4954
- if (params.session_id) {
4955
- query += " AND q.session_id = ?";
4956
- bindings.push(params.session_id);
4957
- }
4958
- query += " GROUP BY q.id ORDER BY q.priority DESC, q.position ASC";
4959
- const result = await db.prepare(query).bind(...bindings).all();
4960
- return result.results.map((r) => ({
4961
- ...r,
4962
- claim_files: r.claim_files_concat ? r.claim_files_concat.split("|||") : []
4963
- }));
4964
- }
4965
4566
  async function removeSessionFromAllQueues(db, sessionId) {
4966
4567
  const result = await db.prepare("DELETE FROM claim_queue WHERE session_id = ?").bind(sessionId).run();
4967
4568
  return result.meta.changes;
4968
4569
  }
4969
- async function getQueuedSessionsForClaim(db, claimId) {
4970
- const result = await db.prepare(
4971
- `SELECT q.session_id, s.name as session_name, q.position
4972
- FROM claim_queue q
4973
- JOIN sessions s ON q.session_id = s.id
4974
- WHERE q.claim_id = ?
4975
- ORDER BY q.priority DESC, q.position ASC`
4976
- ).bind(claimId).all();
4977
- return result.results;
4978
- }
4979
- async function createNotification(db, params) {
4980
- const id = generateId();
4981
- const now = (/* @__PURE__ */ new Date()).toISOString();
4982
- const metadataJson = params.metadata ? JSON.stringify(params.metadata) : null;
4983
- await db.prepare(
4984
- `INSERT INTO notifications (id, session_id, type, title, message, reference_type, reference_id, metadata, created_at)
4985
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
4986
- ).bind(
4987
- id,
4988
- params.session_id,
4989
- params.type,
4990
- params.title,
4991
- params.message,
4992
- params.reference_type ?? null,
4993
- params.reference_id ?? null,
4994
- metadataJson,
4995
- now
4996
- ).run();
4997
- return {
4998
- id,
4999
- session_id: params.session_id,
5000
- type: params.type,
5001
- title: params.title,
5002
- message: params.message,
5003
- reference_type: params.reference_type ?? null,
5004
- reference_id: params.reference_id ?? null,
5005
- metadata: metadataJson,
5006
- read_at: null,
5007
- created_at: now
5008
- };
5009
- }
5010
- async function listNotifications(db, params) {
5011
- let query = "SELECT * FROM notifications WHERE session_id = ?";
5012
- const bindings = [params.session_id];
5013
- if (params.unread_only) {
5014
- query += " AND read_at IS NULL";
5015
- }
5016
- if (params.type) {
5017
- query += " AND type = ?";
5018
- bindings.push(params.type);
5019
- }
5020
- query += " ORDER BY created_at DESC LIMIT ?";
5021
- bindings.push(params.limit ?? 50);
5022
- const result = await db.prepare(query).bind(...bindings).all();
5023
- return result.results;
5024
- }
5025
- async function markNotificationsRead(db, notificationIds) {
5026
- if (notificationIds.length === 0) return 0;
5027
- const now = (/* @__PURE__ */ new Date()).toISOString();
5028
- const placeholders = notificationIds.map(() => "?").join(",");
5029
- const result = await db.prepare(`UPDATE notifications SET read_at = ? WHERE id IN (${placeholders}) AND read_at IS NULL`).bind(now, ...notificationIds).run();
5030
- return result.meta.changes;
5031
- }
5032
- async function getNotification(db, id) {
5033
- const result = await db.prepare("SELECT * FROM notifications WHERE id = ?").bind(id).first();
5034
- return result ?? null;
5035
- }
5036
- async function notifyQueueOnClaimRelease(db, claimId, releasedBy, files) {
5037
- const queuedSessions = await getQueuedSessionsForClaim(db, claimId);
5038
- if (queuedSessions.length === 0) return 0;
5039
- let notified = 0;
5040
- for (let i = 0; i < queuedSessions.length; i++) {
5041
- const entry = queuedSessions[i];
5042
- const isFirst = i === 0;
5043
- await createNotification(db, {
5044
- session_id: entry.session_id,
5045
- type: isFirst ? "queue_ready" : "claim_released",
5046
- title: isFirst ? "You are next in queue!" : "Claim released",
5047
- message: isFirst ? `The claim for ${files.join(", ")} has been released. You can now claim these files.` : `A claim you were waiting for has been released. Position: ${i + 1}`,
5048
- reference_type: "claim",
5049
- reference_id: claimId,
5050
- metadata: {
5051
- claim_id: claimId,
5052
- files,
5053
- released_by: releasedBy,
5054
- queue_position: i + 1
5055
- }
5056
- });
5057
- notified++;
5058
- }
5059
- return notified;
5060
- }
5061
4570
  async function saveMemory(db, sessionId, input) {
5062
4571
  const now = (/* @__PURE__ */ new Date()).toISOString();
5063
4572
  const metadataJson = input.metadata ? JSON.stringify(input.metadata) : null;
@@ -5117,34 +4626,6 @@ async function recallMemory(db, sessionId, params = {}) {
5117
4626
  const result = await db.prepare(query).bind(...bindings).all();
5118
4627
  return result.results;
5119
4628
  }
5120
- async function updateMemory(db, sessionId, key, updates) {
5121
- const now = (/* @__PURE__ */ new Date()).toISOString();
5122
- const setParts = ["updated_at = ?"];
5123
- const bindings = [now];
5124
- if (updates.content !== void 0) {
5125
- setParts.push("content = ?");
5126
- bindings.push(updates.content);
5127
- }
5128
- if (updates.priority !== void 0) {
5129
- setParts.push("priority = ?");
5130
- bindings.push(updates.priority);
5131
- }
5132
- if (updates.pinned !== void 0) {
5133
- setParts.push("pinned = ?");
5134
- bindings.push(updates.pinned ? 1 : 0);
5135
- }
5136
- if (updates.expires_at !== void 0) {
5137
- setParts.push("expires_at = ?");
5138
- bindings.push(updates.expires_at);
5139
- }
5140
- if (updates.metadata !== void 0) {
5141
- setParts.push("metadata = ?");
5142
- bindings.push(JSON.stringify(updates.metadata));
5143
- }
5144
- bindings.push(sessionId, key);
5145
- const result = await db.prepare(`UPDATE working_memory SET ${setParts.join(", ")} WHERE session_id = ? AND key = ?`).bind(...bindings).run();
5146
- return result.meta.changes > 0;
5147
- }
5148
4629
  async function clearMemory(db, sessionId, params = {}) {
5149
4630
  if (params.key) {
5150
4631
  const result = await db.prepare("DELETE FROM working_memory WHERE session_id = ? AND key = ?").bind(sessionId, params.key).run();
@@ -5160,30 +4641,6 @@ async function clearMemory(db, sessionId, params = {}) {
5160
4641
  }
5161
4642
  return 0;
5162
4643
  }
5163
- async function pinMemory(db, sessionId, key, pinned) {
5164
- const now = (/* @__PURE__ */ new Date()).toISOString();
5165
- const result = await db.prepare("UPDATE working_memory SET pinned = ?, updated_at = ? WHERE session_id = ? AND key = ?").bind(pinned ? 1 : 0, now, sessionId, key).run();
5166
- return result.meta.changes > 0;
5167
- }
5168
- async function getMemoryStats(db, sessionId) {
5169
- const totalResult = await db.prepare("SELECT COUNT(*) as count FROM working_memory WHERE session_id = ?").bind(sessionId).first();
5170
- const categoryResult = await db.prepare(
5171
- `SELECT category, COUNT(*) as count
5172
- FROM working_memory
5173
- WHERE session_id = ?
5174
- GROUP BY category`
5175
- ).bind(sessionId).all();
5176
- const pinnedResult = await db.prepare("SELECT COUNT(*) as count FROM working_memory WHERE session_id = ? AND pinned = 1").bind(sessionId).first();
5177
- const byCategory = {};
5178
- for (const row of categoryResult.results) {
5179
- byCategory[row.category] = row.count;
5180
- }
5181
- return {
5182
- total: totalResult?.count ?? 0,
5183
- by_category: byCategory,
5184
- pinned_count: pinnedResult?.count ?? 0
5185
- };
5186
- }
5187
4644
  async function getActiveMemories(db, sessionId, params = {}) {
5188
4645
  const threshold = params.priority_threshold ?? 70;
5189
4646
  const limit = params.max_items ?? 20;
@@ -5219,40 +4676,6 @@ ${params.content_summary}`,
5219
4676
  }
5220
4677
  });
5221
4678
  }
5222
- async function updatePlanStatus(db, sessionId, filePath, newStatus, summary) {
5223
- const key = `plan:${filePath}`;
5224
- const memories = await recallMemory(db, sessionId, { key });
5225
- if (memories.length === 0) return false;
5226
- const current = memories[0];
5227
- let metadata = {};
5228
- try {
5229
- metadata = current.metadata ? JSON.parse(current.metadata) : {};
5230
- } catch {
5231
- }
5232
- metadata.status = newStatus;
5233
- if (newStatus === "completed") {
5234
- metadata.completed_at = (/* @__PURE__ */ new Date()).toISOString();
5235
- }
5236
- let priority = 95;
5237
- let pinned = true;
5238
- if (newStatus === "completed") {
5239
- priority = 50;
5240
- pinned = false;
5241
- } else if (newStatus === "archived") {
5242
- priority = 30;
5243
- pinned = false;
5244
- }
5245
- const updatedContent = summary ? `[PLAN] ${metadata.title}
5246
- Status: ${newStatus}
5247
-
5248
- ${summary}` : current.content.replace(/Status: \w+/, `Status: ${newStatus}`);
5249
- return updateMemory(db, sessionId, key, {
5250
- content: updatedContent,
5251
- priority,
5252
- pinned,
5253
- metadata
5254
- });
5255
- }
5256
4679
  async function getPlan(db, sessionId, filePath) {
5257
4680
  const key = `plan:${filePath}`;
5258
4681
  const memories = await recallMemory(db, sessionId, { key });
@@ -5401,7 +4824,10 @@ async function getProtectedFiles(db, sessionId) {
5401
4824
  // src/mcp/schemas.ts
5402
4825
  var sessionIdSchema = external_exports.string().min(1, "session_id is required");
5403
4826
  var claimIdSchema = external_exports.string().min(1, "claim_id is required");
5404
- var filePathSchema = external_exports.string().min(1);
4827
+ var filePathSchema = external_exports.string().min(1).refine(
4828
+ (path) => !path.includes("..") && !path.includes("\0"),
4829
+ { message: "Path cannot contain path traversal sequences (..) or null bytes" }
4830
+ );
5405
4831
  var filesArraySchema = external_exports.array(filePathSchema).min(1, "At least one file is required");
5406
4832
  var symbolClaimSchema = external_exports.object({
5407
4833
  file: external_exports.string().min(1),
@@ -5676,7 +5102,7 @@ function validationError(message) {
5676
5102
  var sessionTools = [
5677
5103
  {
5678
5104
  name: "collab_session_start",
5679
- description: "Register a new collaboration session. Call this when starting work to enable coordination with other sessions.",
5105
+ description: "Start a new session. Call this when starting work.",
5680
5106
  inputSchema: {
5681
5107
  type: "object",
5682
5108
  properties: {
@@ -5687,10 +5113,6 @@ var sessionTools = [
5687
5113
  project_root: {
5688
5114
  type: "string",
5689
5115
  description: "Project root directory path"
5690
- },
5691
- machine_id: {
5692
- type: "string",
5693
- description: "Optional machine identifier for multi-machine setups"
5694
5116
  }
5695
5117
  },
5696
5118
  required: ["project_root"]
@@ -5698,7 +5120,7 @@ var sessionTools = [
5698
5120
  },
5699
5121
  {
5700
5122
  name: "collab_session_end",
5701
- description: "End a session and release all its claims.",
5123
+ description: "End session and release all claims.",
5702
5124
  inputSchema: {
5703
5125
  type: "object",
5704
5126
  properties: {
@@ -5709,7 +5131,7 @@ var sessionTools = [
5709
5131
  release_claims: {
5710
5132
  type: "string",
5711
5133
  enum: ["complete", "abandon"],
5712
- description: "How to handle unreleased claims: complete (mark as done) or abandon"
5134
+ description: "How to handle unreleased claims"
5713
5135
  }
5714
5136
  },
5715
5137
  required: ["session_id"]
@@ -5717,13 +5139,13 @@ var sessionTools = [
5717
5139
  },
5718
5140
  {
5719
5141
  name: "collab_session_list",
5720
- description: "List all active sessions. Use to see who else is working.",
5142
+ description: "List all active sessions.",
5721
5143
  inputSchema: {
5722
5144
  type: "object",
5723
5145
  properties: {
5724
5146
  include_inactive: {
5725
5147
  type: "boolean",
5726
- description: "Include inactive/terminated sessions"
5148
+ description: "Include inactive sessions"
5727
5149
  },
5728
5150
  project_root: {
5729
5151
  type: "string",
@@ -5732,68 +5154,9 @@ var sessionTools = [
5732
5154
  }
5733
5155
  }
5734
5156
  },
5735
- {
5736
- name: "collab_session_heartbeat",
5737
- description: "Update session heartbeat to indicate the session is still active.",
5738
- inputSchema: {
5739
- type: "object",
5740
- properties: {
5741
- session_id: {
5742
- type: "string",
5743
- description: "Session ID to update"
5744
- },
5745
- current_task: {
5746
- type: "string",
5747
- description: "Optional: Current task being worked on"
5748
- },
5749
- todos: {
5750
- type: "array",
5751
- description: "Optional: Current todo list to sync",
5752
- items: {
5753
- type: "object",
5754
- properties: {
5755
- content: { type: "string" },
5756
- status: { type: "string", enum: ["pending", "in_progress", "completed"] }
5757
- }
5758
- }
5759
- }
5760
- },
5761
- required: ["session_id"]
5762
- }
5763
- },
5764
- {
5765
- name: "collab_status_update",
5766
- description: "Update session work status. Use this to share what you are currently working on with other sessions.",
5767
- inputSchema: {
5768
- type: "object",
5769
- properties: {
5770
- session_id: {
5771
- type: "string",
5772
- description: "Your session ID"
5773
- },
5774
- current_task: {
5775
- type: "string",
5776
- description: 'Description of current task (e.g., "Refactoring auth module")'
5777
- },
5778
- todos: {
5779
- type: "array",
5780
- description: "Your current todo list",
5781
- items: {
5782
- type: "object",
5783
- properties: {
5784
- content: { type: "string", description: "Task description" },
5785
- status: { type: "string", enum: ["pending", "in_progress", "completed"] }
5786
- },
5787
- required: ["content", "status"]
5788
- }
5789
- }
5790
- },
5791
- required: ["session_id"]
5792
- }
5793
- },
5794
5157
  {
5795
5158
  name: "collab_config",
5796
- description: "Configure session behavior for conflict handling. Settings persist for the session duration.",
5159
+ description: "Configure session behavior.",
5797
5160
  inputSchema: {
5798
5161
  type: "object",
5799
5162
  properties: {
@@ -5804,19 +5167,11 @@ var sessionTools = [
5804
5167
  mode: {
5805
5168
  type: "string",
5806
5169
  enum: ["strict", "smart", "bypass"],
5807
- description: "Conflict handling mode: strict (always ask), smart (ask but suggest for stale), bypass (warn only)"
5170
+ description: "Conflict handling mode"
5808
5171
  },
5809
5172
  allow_release_others: {
5810
5173
  type: "boolean",
5811
- description: "Allow releasing claims from other sessions (default: false)"
5812
- },
5813
- auto_release_stale: {
5814
- type: "boolean",
5815
- description: "Automatically release stale claims (default: false)"
5816
- },
5817
- stale_threshold_hours: {
5818
- type: "number",
5819
- description: "Hours before a claim is considered stale (default: 2)"
5174
+ description: "Allow releasing other sessions claims"
5820
5175
  }
5821
5176
  },
5822
5177
  required: ["session_id"]
@@ -5855,54 +5210,19 @@ async function handleSessionTool(db, name, args, userId) {
5855
5210
  });
5856
5211
  for (const mem of memories) {
5857
5212
  allMemories.push({
5858
- session_name: otherSession.name,
5859
5213
  category: mem.category,
5860
5214
  key: mem.key,
5861
- content: mem.content,
5862
- priority: mem.priority,
5863
- pinned: mem.pinned === 1
5215
+ content: mem.content
5864
5216
  });
5865
5217
  }
5866
5218
  }
5867
- allMemories.sort((a, b) => {
5868
- if (a.pinned !== b.pinned) return b.pinned ? 1 : -1;
5869
- return b.priority - a.priority;
5219
+ return successResponse({
5220
+ session_id: session.id,
5221
+ name: session.name,
5222
+ active_sessions: activeSessions.length,
5223
+ restored_context: allMemories.length > 0 ? allMemories.slice(0, 15) : null,
5224
+ message: `Session started. ${activeSessions.length} active session(s).`
5870
5225
  });
5871
- const topMemories = allMemories.slice(0, 20);
5872
- const memoriesByCategory = {};
5873
- for (const mem of topMemories) {
5874
- if (!memoriesByCategory[mem.category]) {
5875
- memoriesByCategory[mem.category] = [];
5876
- }
5877
- memoriesByCategory[mem.category].push({
5878
- key: mem.key,
5879
- content: mem.content,
5880
- session: mem.session_name
5881
- });
5882
- }
5883
- return createToolResult(
5884
- JSON.stringify(
5885
- {
5886
- session_id: session.id,
5887
- name: session.name,
5888
- message: `Session registered. ${activeSessions.length} active session(s) in this project.`,
5889
- active_sessions: activeSessions.map((s) => ({
5890
- id: s.id,
5891
- name: s.name,
5892
- last_heartbeat: s.last_heartbeat
5893
- })),
5894
- // AUTO-LOADED CONTEXT
5895
- restored_context: topMemories.length > 0 ? {
5896
- count: topMemories.length,
5897
- by_category: memoriesByCategory,
5898
- note: "These memories were automatically loaded from previous sessions. Use them to maintain context continuity."
5899
- } : null,
5900
- tip: "Use collab_memory_save to persist important context. Plans and files are auto-protected when registered."
5901
- },
5902
- null,
5903
- 2
5904
- )
5905
- );
5906
5226
  }
5907
5227
  case "collab_session_end": {
5908
5228
  const validation = validateInput(sessionEndSchema, args);
@@ -5915,26 +5235,10 @@ async function handleSessionTool(db, name, args, userId) {
5915
5235
  return sessionResult.error;
5916
5236
  }
5917
5237
  const activeClaims = await listClaims(db, { session_id: input.session_id, status: "active" });
5918
- const claimsToRelease = activeClaims.map((claim) => ({
5919
- id: claim.id,
5920
- files: claim.files,
5921
- intent: claim.intent,
5922
- scope: claim.scope,
5923
- created_at: claim.created_at
5924
- }));
5925
5238
  const memories = await recallMemory(db, input.session_id, {});
5926
- let memorySummary = null;
5927
5239
  if (memories.length > 0) {
5928
5240
  const findings = memories.filter((m) => m.category === "finding").map((m) => m.content);
5929
5241
  const decisions = memories.filter((m) => m.category === "decision").map((m) => m.content);
5930
- const important = memories.filter((m) => m.category === "important" || m.pinned === 1).map((m) => m.content);
5931
- memorySummary = {
5932
- total: memories.length,
5933
- findings: findings.slice(0, 5),
5934
- // Top 5
5935
- decisions: decisions.slice(0, 5),
5936
- important: important.slice(0, 5)
5937
- };
5938
5242
  const summaryContent = [
5939
5243
  `Session: ${sessionResult.session.name || input.session_id}`,
5940
5244
  `Ended: ${(/* @__PURE__ */ new Date()).toISOString()}`,
@@ -5943,43 +5247,29 @@ Findings:
5943
5247
  - ${findings.slice(0, 5).join("\n- ")}` : "",
5944
5248
  decisions.length > 0 ? `
5945
5249
  Decisions:
5946
- - ${decisions.slice(0, 5).join("\n- ")}` : "",
5947
- important.length > 0 ? `
5948
- Important:
5949
- - ${important.slice(0, 5).join("\n- ")}` : ""
5250
+ - ${decisions.slice(0, 5).join("\n- ")}` : ""
5950
5251
  ].filter(Boolean).join("\n");
5951
5252
  await saveMemory(db, input.session_id, {
5952
5253
  category: "context",
5953
5254
  key: "session_summary",
5954
5255
  content: summaryContent,
5955
5256
  priority: 80,
5956
- pinned: true,
5957
- metadata: {
5958
- session_name: sessionResult.session.name,
5959
- ended_at: (/* @__PURE__ */ new Date()).toISOString(),
5960
- memory_count: memories.length
5961
- }
5257
+ pinned: true
5962
5258
  });
5963
5259
  }
5964
- const removedFromQueues = await removeSessionFromAllQueues(db, input.session_id);
5260
+ await removeSessionFromAllQueues(db, input.session_id);
5965
5261
  await endSession(db, input.session_id, input.release_claims);
5966
- const claimStatus = input.release_claims === "complete" ? "completed" : "abandoned";
5967
5262
  await logAuditEvent(db, {
5968
5263
  session_id: input.session_id,
5969
5264
  action: "session_ended",
5970
5265
  entity_type: "session",
5971
- entity_id: input.session_id,
5972
- metadata: { status: claimStatus, memory_count: memories.length }
5266
+ entity_id: input.session_id
5973
5267
  });
5974
5268
  return successResponse({
5975
5269
  success: true,
5976
- message: `Session ended. ${claimsToRelease.length} claim(s) marked as ${input.release_claims === "complete" ? "completed" : "abandoned"}.${removedFromQueues > 0 ? ` Removed from ${removedFromQueues} queue(s).` : ""}`,
5977
- claims_released: claimsToRelease.length > 0 ? {
5978
- count: claimsToRelease.length,
5979
- status: input.release_claims === "complete" ? "completed" : "abandoned",
5980
- details: claimsToRelease
5981
- } : null,
5982
- memory_summary: memorySummary
5270
+ claims_released: activeClaims.length,
5271
+ memories_saved: memories.length,
5272
+ message: `Session ended. ${activeClaims.length} claim(s) released.`
5983
5273
  });
5984
5274
  }
5985
5275
  case "collab_session_list": {
@@ -5992,102 +5282,32 @@ Important:
5992
5282
  include_inactive: input.include_inactive,
5993
5283
  project_root: input.project_root
5994
5284
  });
5995
- const sessionsWithDetails = await Promise.all(
5285
+ const sessionsWithClaims = await Promise.all(
5996
5286
  sessions.map(async (session) => {
5997
5287
  const claims = await listClaims(db, { session_id: session.id, status: "active" });
5998
- let progress = null;
5999
- let todos = null;
6000
- try {
6001
- if (session.progress) progress = JSON.parse(session.progress);
6002
- if (session.todos) todos = JSON.parse(session.todos);
6003
- } catch {
6004
- }
6005
5288
  return {
6006
5289
  id: session.id,
6007
5290
  name: session.name,
6008
- project_root: session.project_root,
6009
5291
  status: session.status,
6010
5292
  active_claims: claims.length,
6011
- last_heartbeat: session.last_heartbeat,
6012
- current_task: session.current_task,
6013
- progress,
6014
- todos
5293
+ last_heartbeat: session.last_heartbeat
6015
5294
  };
6016
5295
  })
6017
5296
  );
6018
- return createToolResult(
6019
- JSON.stringify(
6020
- {
6021
- sessions: sessionsWithDetails,
6022
- total: sessionsWithDetails.length
6023
- },
6024
- null,
6025
- 2
6026
- )
6027
- );
5297
+ return successResponse({
5298
+ sessions: sessionsWithClaims,
5299
+ total: sessionsWithClaims.length
5300
+ });
6028
5301
  }
6029
- case "collab_session_heartbeat": {
6030
- const validation = validateInput(sessionHeartbeatSchema, args);
5302
+ case "collab_config": {
5303
+ const validation = validateInput(configSchema, args);
6031
5304
  if (!validation.success) {
6032
5305
  return validationError(validation.error);
6033
5306
  }
6034
5307
  const input = validation.data;
6035
- const updated = await updateSessionHeartbeat(db, input.session_id, {
6036
- current_task: input.current_task,
6037
- todos: input.todos
6038
- });
6039
- if (!updated) {
6040
- return errorResponse(
6041
- ERROR_CODES.SESSION_NOT_FOUND,
6042
- "Session not found or inactive. Please start a new session."
6043
- );
6044
- }
6045
- return successResponse({
6046
- success: true,
6047
- message: "Heartbeat updated",
6048
- status_synced: !!(input.current_task || input.todos)
6049
- });
6050
- }
6051
- case "collab_status_update": {
6052
- const validation = validateInput(statusUpdateSchema, args);
6053
- if (!validation.success) {
6054
- return validationError(validation.error);
6055
- }
6056
- const input = validation.data;
6057
- const todos = input.todos;
6058
- const sessionResult = await validateActiveSession(db, input.session_id);
6059
- if (!sessionResult.valid) {
6060
- return sessionResult.error;
6061
- }
6062
- await updateSessionStatus(db, input.session_id, {
6063
- current_task: input.current_task,
6064
- todos
6065
- });
6066
- let progress = null;
6067
- if (todos && todos.length > 0) {
6068
- const completed = todos.filter((t) => t.status === "completed").length;
6069
- progress = {
6070
- completed,
6071
- total: todos.length,
6072
- percentage: Math.round(completed / todos.length * 100)
6073
- };
6074
- }
6075
- return successResponse({
6076
- success: true,
6077
- message: "Status updated successfully.",
6078
- current_task: input.current_task ?? null,
6079
- progress
6080
- });
6081
- }
6082
- case "collab_config": {
6083
- const validation = validateInput(configSchema, args);
6084
- if (!validation.success) {
6085
- return validationError(validation.error);
6086
- }
6087
- const input = validation.data;
6088
- const sessionResult = await validateActiveSession(db, input.session_id);
6089
- if (!sessionResult.valid) {
6090
- return sessionResult.error;
5308
+ const sessionResult = await validateActiveSession(db, input.session_id);
5309
+ if (!sessionResult.valid) {
5310
+ return sessionResult.error;
6091
5311
  }
6092
5312
  const { session } = sessionResult;
6093
5313
  let currentConfig = DEFAULT_SESSION_CONFIG;
@@ -6099,17 +5319,45 @@ Important:
6099
5319
  }
6100
5320
  const newConfig = {
6101
5321
  mode: input.mode ?? currentConfig.mode,
6102
- allow_release_others: input.allow_release_others !== void 0 ? input.allow_release_others : currentConfig.allow_release_others,
6103
- auto_release_stale: input.auto_release_stale !== void 0 ? input.auto_release_stale : currentConfig.auto_release_stale,
5322
+ allow_release_others: input.allow_release_others ?? currentConfig.allow_release_others,
5323
+ auto_release_stale: input.auto_release_stale ?? currentConfig.auto_release_stale,
6104
5324
  stale_threshold_hours: input.stale_threshold_hours ?? currentConfig.stale_threshold_hours,
6105
- auto_release_immediate: input.auto_release_immediate !== void 0 ? input.auto_release_immediate : currentConfig.auto_release_immediate,
5325
+ auto_release_immediate: input.auto_release_immediate ?? currentConfig.auto_release_immediate,
6106
5326
  auto_release_delay_minutes: input.auto_release_delay_minutes ?? currentConfig.auto_release_delay_minutes
6107
5327
  };
6108
5328
  await updateSessionConfig(db, input.session_id, newConfig);
6109
5329
  return successResponse({
6110
5330
  success: true,
6111
- message: "Configuration updated.",
6112
- config: newConfig
5331
+ config: newConfig,
5332
+ message: "Configuration updated."
5333
+ });
5334
+ }
5335
+ case "collab_status": {
5336
+ const sessionId = args.session_id;
5337
+ if (!sessionId) {
5338
+ return validationError("session_id is required");
5339
+ }
5340
+ const session = await getSession(db, sessionId);
5341
+ if (!session) {
5342
+ return errorResponse(ERROR_CODES.SESSION_NOT_FOUND, "Session not found");
5343
+ }
5344
+ const claims = await listClaims(db, { session_id: sessionId, status: "active" });
5345
+ const memories = await getActiveMemories(db, sessionId, { priority_threshold: 70, max_items: 10 });
5346
+ const allSessions = await listSessions(db, { project_root: session.project_root });
5347
+ return successResponse({
5348
+ session: {
5349
+ id: session.id,
5350
+ name: session.name,
5351
+ status: session.status
5352
+ },
5353
+ claims: claims.map((c) => ({
5354
+ id: c.id,
5355
+ files: c.files,
5356
+ intent: c.intent
5357
+ })),
5358
+ active_memories: memories.length,
5359
+ other_sessions: allSessions.filter((s) => s.id !== sessionId).length,
5360
+ message: `Session active. ${claims.length} claim(s), ${memories.length} memories.`
6113
5361
  });
6114
5362
  }
6115
5363
  default:
@@ -6121,10 +5369,19 @@ Important:
6121
5369
  var claimTools = [
6122
5370
  {
6123
5371
  name: "collab_claim",
6124
- description: "Declare files or specific symbols (functions/classes) you are about to modify. Use symbols for fine-grained claims that allow other sessions to work on different parts of the same file.",
5372
+ description: `Unified tool for file/symbol claims. Use action parameter to:
5373
+ - "create": Declare files you're about to modify
5374
+ - "check": Check if files are being worked on (ALWAYS call before editing)
5375
+ - "release": Release a claim when done
5376
+ - "list": List all active claims`,
6125
5377
  inputSchema: {
6126
5378
  type: "object",
6127
5379
  properties: {
5380
+ action: {
5381
+ type: "string",
5382
+ enum: ["create", "check", "release", "list"],
5383
+ description: "Action to perform"
5384
+ },
6128
5385
  session_id: {
6129
5386
  type: "string",
6130
5387
  description: "Your session ID"
@@ -6132,476 +5389,141 @@ var claimTools = [
6132
5389
  files: {
6133
5390
  type: "array",
6134
5391
  items: { type: "string" },
6135
- description: "File paths to claim. Supports glob patterns like 'src/api/*'. Use this for whole-file claims."
6136
- },
6137
- symbols: {
6138
- type: "array",
6139
- items: {
6140
- type: "object",
6141
- properties: {
6142
- file: { type: "string", description: "File path containing the symbols" },
6143
- symbols: {
6144
- type: "array",
6145
- items: { type: "string" },
6146
- description: "Symbol names (function, class, method names) to claim"
6147
- },
6148
- symbol_type: {
6149
- type: "string",
6150
- enum: ["function", "class", "method", "variable", "block", "other"],
6151
- description: "Type of symbols being claimed (default: function)"
6152
- }
6153
- },
6154
- required: ["file", "symbols"]
6155
- },
6156
- description: "Symbol-level claims for fine-grained conflict detection. Use this instead of files when you only need to modify specific functions/classes."
5392
+ description: "File paths (for create/check actions)"
6157
5393
  },
6158
5394
  intent: {
6159
5395
  type: "string",
6160
- description: "What you plan to do with these files/symbols"
6161
- },
6162
- scope: {
6163
- type: "string",
6164
- enum: ["small", "medium", "large"],
6165
- description: "Estimated scope: small(<30min), medium(30min-2hr), large(>2hr)"
6166
- },
6167
- priority: {
6168
- type: "number",
6169
- description: "Priority (0-100). Levels: critical (90-100), high (70-89), normal (40-69, default: 50), low (0-39)"
6170
- }
6171
- },
6172
- required: ["session_id", "intent"]
6173
- }
6174
- },
6175
- {
6176
- name: "collab_check",
6177
- description: "Check if files or symbols are being worked on by other sessions. ALWAYS call this before modifying files. Supports symbol-level checking for fine-grained conflict detection.",
6178
- inputSchema: {
6179
- type: "object",
6180
- properties: {
6181
- files: {
6182
- type: "array",
6183
- items: { type: "string" },
6184
- description: "File paths to check"
6185
- },
6186
- symbols: {
6187
- type: "array",
6188
- items: {
6189
- type: "object",
6190
- properties: {
6191
- file: { type: "string", description: "File path containing the symbols" },
6192
- symbols: {
6193
- type: "array",
6194
- items: { type: "string" },
6195
- description: "Symbol names to check for conflicts"
6196
- }
6197
- },
6198
- required: ["file", "symbols"]
6199
- },
6200
- description: "Symbol-level check. If provided, only checks for conflicts with these specific symbols."
6201
- },
6202
- session_id: {
6203
- type: "string",
6204
- description: "Your session ID (to exclude your own claims from results)"
6205
- }
6206
- },
6207
- required: ["files"]
6208
- }
6209
- },
6210
- {
6211
- name: "collab_release",
6212
- description: "Release a claim when done or abandoning work. By default you can only release your own claims. Use force=true with user confirmation to release stale claims from other sessions.",
6213
- inputSchema: {
6214
- type: "object",
6215
- properties: {
6216
- session_id: {
6217
- type: "string",
6218
- description: "Your session ID (required to verify ownership)"
5396
+ description: "What you plan to do (for create action)"
6219
5397
  },
6220
5398
  claim_id: {
6221
5399
  type: "string",
6222
- description: "Claim ID to release"
5400
+ description: "Claim ID (for release action)"
6223
5401
  },
6224
5402
  status: {
6225
5403
  type: "string",
6226
5404
  enum: ["completed", "abandoned"],
6227
- description: "Whether work was completed or abandoned"
6228
- },
6229
- summary: {
6230
- type: "string",
6231
- description: "Optional summary of what was done (for completed claims)"
6232
- },
6233
- force: {
6234
- type: "boolean",
6235
- description: "Force release even if claim belongs to another session (requires user confirmation)"
6236
- }
6237
- },
6238
- required: ["session_id", "claim_id", "status"]
6239
- }
6240
- },
6241
- {
6242
- name: "collab_claims_list",
6243
- description: "List all WIP claims. Use to see what files are being worked on.",
6244
- inputSchema: {
6245
- type: "object",
6246
- properties: {
6247
- session_id: {
6248
- type: "string",
6249
- description: "Filter by session ID"
6250
- },
6251
- status: {
6252
- type: "string",
6253
- enum: ["active", "completed", "abandoned", "all"],
6254
- description: "Filter by claim status"
6255
- },
6256
- project_root: {
6257
- type: "string",
6258
- description: "Filter by project root"
6259
- }
6260
- }
6261
- }
6262
- },
6263
- {
6264
- name: "collab_claim_update_priority",
6265
- description: "Update the priority of an existing claim. Higher priority claims take precedence in queue ordering.",
6266
- inputSchema: {
6267
- type: "object",
6268
- properties: {
6269
- session_id: {
6270
- type: "string",
6271
- description: "Your session ID"
6272
- },
6273
- claim_id: {
6274
- type: "string",
6275
- description: "Claim ID to update"
6276
- },
6277
- priority: {
6278
- type: "number",
6279
- description: "New priority (0-100). Levels: critical (90-100), high (70-89), normal (40-69), low (0-39)"
6280
- },
6281
- reason: {
6282
- type: "string",
6283
- description: "Optional reason for priority change"
6284
- }
6285
- },
6286
- required: ["session_id", "claim_id", "priority"]
6287
- }
6288
- },
6289
- {
6290
- name: "collab_auto_release",
6291
- description: "Automatically release claims for a file after editing. Call this after Edit/Write operations to release the claim for the edited file. For small scope claims, releases immediately. For medium/large scope claims, requires force=true or auto_release_immediate config.",
6292
- inputSchema: {
6293
- type: "object",
6294
- properties: {
6295
- session_id: {
6296
- type: "string",
6297
- description: "Your session ID"
6298
- },
6299
- file_path: {
6300
- type: "string",
6301
- description: "The file that was just edited"
5405
+ description: "Release status (for release action)"
6302
5406
  },
6303
5407
  force: {
6304
5408
  type: "boolean",
6305
- description: "Force release even for medium/large scope claims"
5409
+ description: "Force release even if not owner (for release action)"
6306
5410
  }
6307
5411
  },
6308
- required: ["session_id", "file_path"]
5412
+ required: ["action", "session_id"]
6309
5413
  }
6310
5414
  }
6311
5415
  ];
6312
5416
  async function handleClaimTool(db, name, args) {
6313
- switch (name) {
6314
- case "collab_claim": {
6315
- const validation = validateInput(claimCreateSchema, args);
6316
- if (!validation.success) {
6317
- return validationError(validation.error);
5417
+ if (name !== "collab_claim") {
5418
+ return createToolResult(`Unknown tool: ${name}`, true);
5419
+ }
5420
+ const action = args.action;
5421
+ const sessionId = args.session_id;
5422
+ if (!action || !sessionId) {
5423
+ return validationError("action and session_id are required");
5424
+ }
5425
+ switch (action) {
5426
+ case "create": {
5427
+ const files = args.files;
5428
+ const intent = args.intent;
5429
+ if (!intent) {
5430
+ return validationError("intent is required for create action");
5431
+ }
5432
+ if (!files || files.length === 0) {
5433
+ return validationError("files array is required for create action");
6318
5434
  }
6319
- const { session_id: sessionId, files, symbols, intent, scope = "medium", priority = 50 } = validation.data;
6320
- const hasSymbols = symbols && symbols.length > 0;
6321
- const priorityInfo = getPriorityLevel(priority);
6322
5435
  const sessionResult = await validateActiveSession(db, sessionId);
6323
5436
  if (!sessionResult.valid) {
6324
5437
  return sessionResult.error;
6325
5438
  }
6326
- const allFiles = new Set(files ?? []);
6327
- if (hasSymbols) {
6328
- for (const sc of symbols) {
6329
- allFiles.add(sc.file);
6330
- }
6331
- }
6332
- const fileList = Array.from(allFiles);
6333
5439
  const { claim } = await createClaim(db, {
6334
5440
  session_id: sessionId,
6335
- files: fileList,
5441
+ files,
6336
5442
  intent,
6337
- scope,
6338
- priority,
6339
- symbols: hasSymbols ? symbols : void 0
5443
+ scope: "medium",
5444
+ priority: 50
6340
5445
  });
6341
5446
  await logAuditEvent(db, {
6342
5447
  session_id: sessionId,
6343
5448
  action: "claim_created",
6344
5449
  entity_type: "claim",
6345
5450
  entity_id: claim.id,
6346
- metadata: { files: fileList, intent, scope, priority }
5451
+ metadata: { files, intent }
6347
5452
  });
6348
5453
  await saveMemory(db, sessionId, {
6349
5454
  category: "state",
6350
5455
  key: `claim_${claim.id}`,
6351
5456
  content: `Working on: ${intent}
6352
- Files: ${fileList.join(", ")}${hasSymbols ? `
6353
- Symbols: ${symbols.map((s) => `${s.file}:[${s.symbols.join(",")}]`).join(", ")}` : ""}`,
5457
+ Files: ${files.join(", ")}`,
6354
5458
  priority: 60,
6355
5459
  related_claim_id: claim.id,
6356
- metadata: {
6357
- claim_id: claim.id,
6358
- files: fileList,
6359
- symbols: hasSymbols ? symbols : void 0,
6360
- scope
6361
- }
5460
+ metadata: { claim_id: claim.id, files }
6362
5461
  });
6363
- const autoRegistered = [];
6364
- for (const file of fileList) {
6365
- const lowerFile = file.toLowerCase();
6366
- const isPlanLike = (lowerFile.endsWith(".md") || lowerFile.endsWith(".txt")) && (lowerFile.includes("plan") || lowerFile.includes("design") || lowerFile.includes("spec") || lowerFile.includes("proposal") || lowerFile.includes("rfc") || lowerFile.includes("adr"));
6367
- if (isPlanLike) {
6368
- try {
6369
- await registerPlan(db, sessionId, {
6370
- file_path: file,
6371
- title: `Plan: ${file.split("/").pop() || file}`,
6372
- content_summary: intent,
6373
- status: "in_progress"
6374
- });
6375
- autoRegistered.push(file);
6376
- } catch {
6377
- }
6378
- }
6379
- }
6380
- const conflicts = await checkConflicts(db, fileList, sessionId, symbols);
5462
+ const conflicts = await checkConflicts(db, files, sessionId);
6381
5463
  if (conflicts.length > 0) {
6382
- for (const conflict of conflicts) {
6383
- await logAuditEvent(db, {
6384
- session_id: sessionId,
6385
- action: "conflict_detected",
6386
- entity_type: "claim",
6387
- entity_id: claim.id,
6388
- metadata: {
6389
- conflicting_session_id: conflict.session_id,
6390
- conflicting_session_name: conflict.session_name ?? void 0,
6391
- files: [conflict.file_path]
6392
- }
6393
- });
6394
- }
6395
- const fileConflicts = conflicts.filter((c) => c.conflict_level === "file");
6396
- const symbolConflicts = conflicts.filter((c) => c.conflict_level === "symbol");
6397
- const conflictDetails = {
6398
- file_level: fileConflicts.map((c) => ({
5464
+ return successResponse({
5465
+ claim_id: claim.id,
5466
+ status: "created_with_conflicts",
5467
+ files,
5468
+ conflicts: conflicts.map((c) => ({
6399
5469
  session_name: c.session_name,
6400
5470
  file: c.file_path,
6401
5471
  intent: c.intent
6402
5472
  })),
6403
- symbol_level: symbolConflicts.map((c) => ({
6404
- session_name: c.session_name,
6405
- file: c.file_path,
6406
- symbol: c.symbol_name,
6407
- symbol_type: c.symbol_type,
6408
- intent: c.intent
6409
- }))
6410
- };
6411
- return createToolResult(
6412
- JSON.stringify(
6413
- {
6414
- claim_id: claim.id,
6415
- status: "created_with_conflicts",
6416
- files: fileList,
6417
- symbols: hasSymbols ? symbols : void 0,
6418
- conflicts: conflictDetails,
6419
- warning: `\u26A0\uFE0F Conflicts detected: ${fileConflicts.length} file-level, ${symbolConflicts.length} symbol-level. Coordinate before proceeding.`,
6420
- auto_registered_plans: autoRegistered.length > 0 ? autoRegistered : void 0
6421
- },
6422
- null,
6423
- 2
6424
- )
6425
- );
5473
+ warning: `\u26A0\uFE0F ${conflicts.length} conflict(s) detected. Coordinate before proceeding.`
5474
+ });
6426
5475
  }
6427
- return createToolResult(
6428
- JSON.stringify({
6429
- claim_id: claim.id,
6430
- status: "created",
6431
- files: fileList,
6432
- symbols: hasSymbols ? symbols : void 0,
6433
- intent,
6434
- scope,
6435
- priority: priorityInfo,
6436
- message: hasSymbols ? "Symbol-level claim created. Other sessions can work on different symbols in the same file." : "Claim created successfully. Other sessions will be warned about these files.",
6437
- auto_registered_plans: autoRegistered.length > 0 ? autoRegistered : void 0,
6438
- auto_registered_note: autoRegistered.length > 0 ? `Auto-protected ${autoRegistered.length} plan file(s). Use collab_plan_update_status to manage lifecycle.` : void 0
6439
- })
6440
- );
5476
+ return successResponse({
5477
+ claim_id: claim.id,
5478
+ status: "created",
5479
+ files,
5480
+ intent,
5481
+ message: "Claim created successfully."
5482
+ });
6441
5483
  }
6442
- case "collab_check": {
6443
- const validation = validateInput(claimCheckSchema, args);
6444
- if (!validation.success) {
6445
- return validationError(validation.error);
6446
- }
6447
- const { files, symbols, session_id: sessionId } = validation.data;
6448
- let hasInProgressTodo = false;
6449
- let todosStatus = null;
6450
- if (sessionId) {
6451
- const session = await getSession(db, sessionId);
6452
- if (session?.todos) {
6453
- try {
6454
- const todos = JSON.parse(session.todos);
6455
- const inProgress = todos.filter((t) => t.status === "in_progress").length;
6456
- const completed = todos.filter((t) => t.status === "completed").length;
6457
- const pending = todos.filter((t) => t.status === "pending").length;
6458
- hasInProgressTodo = inProgress > 0;
6459
- todosStatus = {
6460
- total: todos.length,
6461
- in_progress: inProgress,
6462
- completed,
6463
- pending
6464
- };
6465
- } catch {
6466
- }
6467
- }
6468
- }
6469
- const hasSymbols = symbols && Array.isArray(symbols) && symbols.length > 0;
6470
- const conflicts = await checkConflicts(db, files, sessionId, hasSymbols ? symbols : void 0);
6471
- const blockedFiles = /* @__PURE__ */ new Set();
6472
- const blockedSymbols = /* @__PURE__ */ new Map();
6473
- for (const c of conflicts) {
6474
- if (c.conflict_level === "file") {
6475
- blockedFiles.add(c.file_path);
6476
- } else if (c.conflict_level === "symbol" && c.symbol_name) {
6477
- const existing = blockedSymbols.get(c.file_path) ?? /* @__PURE__ */ new Set();
6478
- existing.add(c.symbol_name);
6479
- blockedSymbols.set(c.file_path, existing);
6480
- }
6481
- }
6482
- let safeSymbols = [];
6483
- let blockedSymbolsList = [];
6484
- if (hasSymbols) {
6485
- for (const sc of symbols) {
6486
- const blocked = blockedSymbols.get(sc.file) ?? /* @__PURE__ */ new Set();
6487
- const safe = sc.symbols.filter((s) => !blocked.has(s));
6488
- const blockedList = sc.symbols.filter((s) => blocked.has(s));
6489
- if (safe.length > 0) {
6490
- safeSymbols.push({ file: sc.file, symbols: safe });
6491
- }
6492
- if (blockedList.length > 0) {
6493
- blockedSymbolsList.push({ file: sc.file, symbols: blockedList });
6494
- }
6495
- }
6496
- }
6497
- const safeFiles = files.filter((f) => !blockedFiles.has(f) && !blockedSymbols.has(f));
6498
- const blockedFilesList = files.filter((f) => blockedFiles.has(f));
6499
- let recommendation;
6500
- let canEdit;
6501
- const hasSafeContent = safeFiles.length > 0 || safeSymbols.length > 0;
6502
- if (conflicts.length === 0) {
6503
- recommendation = "proceed_all";
6504
- canEdit = true;
6505
- } else if (hasSafeContent) {
6506
- recommendation = "proceed_safe_only";
6507
- canEdit = true;
6508
- } else {
6509
- recommendation = "abort";
6510
- canEdit = false;
5484
+ case "check": {
5485
+ const files = args.files;
5486
+ if (!files || files.length === 0) {
5487
+ return validationError("files array is required for check action");
6511
5488
  }
5489
+ const conflicts = await checkConflicts(db, files, sessionId);
6512
5490
  if (conflicts.length === 0) {
6513
- return createToolResult(
6514
- JSON.stringify({
6515
- has_conflicts: false,
6516
- safe: true,
6517
- can_edit: true,
6518
- recommendation: "proceed_all",
6519
- file_status: {
6520
- safe: files,
6521
- blocked: []
6522
- },
6523
- symbol_status: hasSymbols ? { safe: symbols, blocked: [] } : void 0,
6524
- message: hasSymbols ? "All symbols are safe to edit. Proceed." : "All files are safe to edit. Proceed.",
6525
- has_in_progress_todo: hasInProgressTodo,
6526
- todos_status: todosStatus
6527
- })
6528
- );
6529
- }
6530
- const bySession = /* @__PURE__ */ new Map();
6531
- for (const c of conflicts) {
6532
- const key = c.session_id;
6533
- const existing = bySession.get(key) ?? [];
6534
- existing.push(c);
6535
- bySession.set(key, existing);
5491
+ return successResponse({
5492
+ has_conflicts: false,
5493
+ can_edit: true,
5494
+ recommendation: "proceed",
5495
+ safe_files: files,
5496
+ message: "All files are safe to edit. Proceed."
5497
+ });
6536
5498
  }
6537
- const conflictDetails = Array.from(bySession.entries()).map(([sessId, items]) => {
6538
- const fileItems = items.filter((i) => i.conflict_level === "file");
6539
- const symbolItems = items.filter((i) => i.conflict_level === "symbol");
6540
- return {
6541
- session_id: sessId,
6542
- session_name: items[0].session_name,
6543
- intent: items[0].intent,
6544
- scope: items[0].scope,
6545
- files: fileItems.map((i) => i.file_path),
6546
- symbols: symbolItems.map((i) => ({
6547
- file: i.file_path,
6548
- symbol: i.symbol_name,
6549
- type: i.symbol_type
6550
- })),
6551
- started_at: items[0].created_at
6552
- };
5499
+ const blockedFiles = new Set(conflicts.map((c) => c.file_path));
5500
+ const safeFiles = files.filter((f) => !blockedFiles.has(f));
5501
+ return successResponse({
5502
+ has_conflicts: true,
5503
+ can_edit: safeFiles.length > 0,
5504
+ recommendation: safeFiles.length > 0 ? "proceed_safe_only" : "abort",
5505
+ safe_files: safeFiles,
5506
+ blocked_files: Array.from(blockedFiles),
5507
+ conflicts: conflicts.map((c) => ({
5508
+ session_name: c.session_name,
5509
+ file: c.file_path,
5510
+ intent: c.intent
5511
+ })),
5512
+ message: safeFiles.length > 0 ? `Edit ONLY safe files: [${safeFiles.join(", ")}]. Skip blocked files.` : "All files blocked. Coordinate with other session(s)."
6553
5513
  });
6554
- let message;
6555
- if (recommendation === "proceed_safe_only") {
6556
- if (hasSymbols && safeSymbols.length > 0) {
6557
- const safeDesc = safeSymbols.map((s) => `${s.file}:[${s.symbols.join(",")}]`).join(", ");
6558
- const blockedDesc = blockedSymbolsList.map((s) => `${s.file}:[${s.symbols.join(",")}]`).join(", ");
6559
- message = `Edit ONLY these safe symbols: ${safeDesc}. Skip blocked: ${blockedDesc}.`;
6560
- } else {
6561
- message = `Edit ONLY these safe files: [${safeFiles.join(", ")}]. Skip blocked files: [${blockedFilesList.join(", ")}].`;
6562
- }
6563
- } else {
6564
- message = hasSymbols ? `All requested symbols are blocked. Coordinate with other session(s) or wait.` : `All ${files.length} file(s) are blocked. Coordinate with other session(s) or wait.`;
6565
- }
6566
- return createToolResult(
6567
- JSON.stringify(
6568
- {
6569
- has_conflicts: true,
6570
- safe: false,
6571
- can_edit: canEdit,
6572
- recommendation,
6573
- file_status: {
6574
- safe: safeFiles,
6575
- blocked: blockedFilesList
6576
- },
6577
- symbol_status: hasSymbols ? {
6578
- safe: safeSymbols,
6579
- blocked: blockedSymbolsList
6580
- } : void 0,
6581
- conflicts: conflictDetails,
6582
- message,
6583
- has_in_progress_todo: hasInProgressTodo,
6584
- todos_status: todosStatus
6585
- },
6586
- null,
6587
- 2
6588
- )
6589
- );
6590
5514
  }
6591
- case "collab_release": {
6592
- const validation = validateInput(claimReleaseSchema, args);
6593
- if (!validation.success) {
6594
- return validationError(validation.error);
5515
+ case "release": {
5516
+ const claimId = args.claim_id;
5517
+ const status = args.status || "completed";
5518
+ const force = args.force;
5519
+ if (!claimId) {
5520
+ return validationError("claim_id is required for release action");
6595
5521
  }
6596
- const { session_id: sessionId, claim_id: claimId, status, summary, force } = validation.data;
6597
5522
  const claim = await getClaim(db, claimId);
6598
5523
  if (!claim) {
6599
- return errorResponse(
6600
- ERROR_CODES.CLAIM_NOT_FOUND,
6601
- "Claim not found. It may have already been released."
6602
- );
5524
+ return errorResponse(ERROR_CODES.CLAIM_NOT_FOUND, "Claim not found");
6603
5525
  }
6604
- if (claim.session_id !== sessionId) {
5526
+ if (claim.session_id !== sessionId && !force) {
6605
5527
  const callerSession = await getSession(db, sessionId);
6606
5528
  let config = DEFAULT_SESSION_CONFIG;
6607
5529
  if (callerSession?.config) {
@@ -6610,36 +5532,17 @@ Symbols: ${symbols.map((s) => `${s.file}:[${s.symbols.join(",")}]`).join(", ")}`
6610
5532
  } catch {
6611
5533
  }
6612
5534
  }
6613
- const canRelease = force === true || config.allow_release_others;
6614
- if (!canRelease) {
6615
- const claimAge = Date.now() - new Date(claim.created_at).getTime();
6616
- const staleHours = config.stale_threshold_hours;
6617
- const isStale = claimAge > staleHours * 60 * 60 * 1e3;
6618
- return createToolResult(
6619
- JSON.stringify({
6620
- error: "NOT_OWNER",
6621
- message: "You can only release your own claims. This claim belongs to another session.",
6622
- claim_owner: claim.session_name,
6623
- claim_age_hours: Math.round(claimAge / (60 * 60 * 1e3) * 10) / 10,
6624
- is_stale: isStale,
6625
- suggestions: [
6626
- "Use collab_message_send to ask the owner to release it.",
6627
- isStale ? "This claim is stale. Ask user for confirmation, then use force=true to release." : null,
6628
- "Use collab_config to enable allow_release_others for future releases."
6629
- ].filter(Boolean)
6630
- }),
6631
- true
5535
+ if (!config.allow_release_others) {
5536
+ return errorResponse(
5537
+ ERROR_CODES.NOT_OWNER,
5538
+ `Not your claim. Owner: ${claim.session_name}. Use force=true to override.`
6632
5539
  );
6633
5540
  }
6634
5541
  }
6635
5542
  if (claim.status !== "active") {
6636
- return errorResponse(
6637
- ERROR_CODES.CLAIM_ALREADY_RELEASED,
6638
- `Claim was already ${claim.status}.`
6639
- );
5543
+ return errorResponse(ERROR_CODES.CLAIM_ALREADY_RELEASED, `Claim already ${claim.status}`);
6640
5544
  }
6641
- const isOwnClaim = claim.session_id === sessionId;
6642
- await releaseClaim(db, claimId, { status, summary });
5545
+ await releaseClaim(db, claimId, { status });
6643
5546
  await logAuditEvent(db, {
6644
5547
  session_id: sessionId,
6645
5548
  action: "claim_released",
@@ -6648,211 +5551,72 @@ Symbols: ${symbols.map((s) => `${s.file}:[${s.symbols.join(",")}]`).join(", ")}`
6648
5551
  metadata: { status, files: claim.files }
6649
5552
  });
6650
5553
  await clearMemory(db, claim.session_id, { key: `claim_${claimId}` });
6651
- if (status === "completed" && summary) {
6652
- await saveMemory(db, sessionId, {
6653
- category: "decision",
6654
- key: `completed_${claimId}`,
6655
- content: `Completed: ${claim.intent}
6656
- Summary: ${summary}
6657
- Files: ${claim.files.join(", ")}`,
6658
- priority: 50,
6659
- metadata: {
6660
- claim_id: claimId,
6661
- files: claim.files,
6662
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
6663
- }
6664
- });
6665
- }
6666
- const notifiedCount = await notifyQueueOnClaimRelease(db, claimId, sessionId, claim.files);
6667
- return createToolResult(
6668
- JSON.stringify({
6669
- success: true,
6670
- message: isOwnClaim ? `Claim ${status}. Files are now available for other sessions.` : `Claim from ${claim.session_name} forcefully ${status}.`,
6671
- files: claim.files,
6672
- summary: summary ?? null,
6673
- was_forced: !isOwnClaim,
6674
- notified_sessions: notifiedCount
6675
- })
6676
- );
6677
- }
6678
- case "collab_auto_release": {
6679
- const validation = validateInput(autoReleaseSchema, args);
6680
- if (!validation.success) {
6681
- return validationError(validation.error);
6682
- }
6683
- const { session_id: sessionId, file_path: filePath, force } = validation.data;
6684
- const sessionResult = await validateActiveSession(db, sessionId);
6685
- if (!sessionResult.valid) {
6686
- return sessionResult.error;
6687
- }
6688
- const session = await getSession(db, sessionId);
6689
- let config = DEFAULT_SESSION_CONFIG;
6690
- if (session?.config) {
6691
- try {
6692
- config = { ...DEFAULT_SESSION_CONFIG, ...JSON.parse(session.config) };
6693
- } catch {
6694
- }
6695
- }
6696
- const claimInfo = await getClaimInfoByFile(db, sessionId, filePath);
6697
- if (!claimInfo) {
6698
- return successResponse({
6699
- released: false,
6700
- message: "No active claim found for this file",
6701
- file_path: filePath
6702
- });
6703
- }
6704
- const isPartialRelease = claimInfo.file_count > 1;
6705
- const shouldAutoRelease = isPartialRelease || claimInfo.scope === "small" || force === true || config.auto_release_immediate;
6706
- if (!shouldAutoRelease) {
6707
- return successResponse({
6708
- released: false,
6709
- claim_id: claimInfo.claim_id,
6710
- scope: claimInfo.scope,
6711
- file_path: filePath,
6712
- file_count: claimInfo.file_count,
6713
- message: `Claim has ${claimInfo.scope} scope. Use force=true or enable auto_release_immediate config to auto-release.`,
6714
- suggestions: [
6715
- "Use collab_release with claim_id to release the full claim",
6716
- "Use collab_config to enable auto_release_immediate",
6717
- "Use force=true to force auto-release"
6718
- ]
6719
- });
6720
- }
6721
- const result = await releaseClaimByFile(db, sessionId, filePath);
6722
- if (!result.released) {
6723
- return errorResponse(
6724
- ERROR_CODES.RELEASE_FAILED,
6725
- "Failed to release claim. It may have been released by another operation."
6726
- );
6727
- }
6728
- await logAuditEvent(db, {
6729
- session_id: sessionId,
6730
- action: "claim_released",
6731
- entity_type: "claim",
6732
- entity_id: result.claim_id,
6733
- metadata: {
6734
- status: result.partial ? "partial" : "completed",
6735
- files: [filePath],
6736
- auto_release: true,
6737
- partial: result.partial,
6738
- files_remaining: result.files_remaining
6739
- }
6740
- });
6741
- let notifiedCount = 0;
6742
- if (!result.partial) {
6743
- notifiedCount = await notifyQueueOnClaimRelease(db, result.claim_id, sessionId, claimInfo.files);
6744
- }
6745
5554
  return successResponse({
6746
- released: true,
6747
- claim_id: result.claim_id,
6748
- file_path: filePath,
6749
- partial: result.partial,
6750
- files_remaining: result.files_remaining,
6751
- notified_sessions: notifiedCount,
6752
- message: result.partial ? `File released from claim. ${result.files_remaining} file(s) remaining in claim.` : "Claim released successfully."
5555
+ success: true,
5556
+ claim_id: claimId,
5557
+ files: claim.files,
5558
+ message: `Claim ${status}. Files now available.`
6753
5559
  });
6754
5560
  }
6755
- case "collab_claims_list": {
6756
- const validation = validateInput(claimListSchema, args);
6757
- if (!validation.success) {
6758
- return validationError(validation.error);
6759
- }
6760
- const { session_id, status = "active", project_root } = validation.data;
6761
- const claims = await listClaims(db, { session_id, status, project_root });
5561
+ case "list": {
5562
+ const claims = await listClaims(db, { session_id: sessionId, status: "active" });
6762
5563
  return successResponse({
6763
5564
  claims: claims.map((c) => ({
6764
5565
  id: c.id,
6765
- session_id: c.session_id,
6766
5566
  session_name: c.session_name,
6767
5567
  files: c.files,
6768
5568
  intent: c.intent,
6769
- scope: c.scope,
6770
5569
  priority: getPriorityLevel(c.priority),
6771
- status: c.status,
6772
- created_at: c.created_at,
6773
- completed_summary: c.completed_summary
5570
+ created_at: c.created_at
6774
5571
  })),
6775
5572
  total: claims.length
6776
- }, true);
6777
- }
6778
- case "collab_claim_update_priority": {
6779
- const validation = validateInput(claimUpdatePrioritySchema, args);
6780
- if (!validation.success) {
6781
- return validationError(validation.error);
6782
- }
6783
- const { session_id: sessionId, claim_id: claimId, priority, reason } = validation.data;
6784
- const sessionResult = await validateActiveSession(db, sessionId);
6785
- if (!sessionResult.valid) {
6786
- return sessionResult.error;
6787
- }
6788
- const claim = await getClaim(db, claimId);
6789
- if (!claim) {
6790
- return errorResponse(ERROR_CODES.CLAIM_NOT_FOUND, "Claim not found");
6791
- }
6792
- if (claim.session_id !== sessionId) {
6793
- return errorResponse(ERROR_CODES.NOT_OWNER, "You can only update priority of your own claims");
6794
- }
6795
- if (claim.status !== "active") {
6796
- return errorResponse(ERROR_CODES.CLAIM_ALREADY_RELEASED, `Claim is ${claim.status}, cannot update priority`);
6797
- }
6798
- const oldPriority = getPriorityLevel(claim.priority);
6799
- const newPriority = getPriorityLevel(priority);
6800
- const updated = await updateClaimPriority(db, claimId, priority);
6801
- if (!updated) {
6802
- return errorResponse(ERROR_CODES.CLAIM_NOT_FOUND, "Failed to update priority");
6803
- }
6804
- await logAuditEvent(db, {
6805
- session_id: sessionId,
6806
- action: "priority_changed",
6807
- entity_type: "claim",
6808
- entity_id: claimId,
6809
- metadata: {
6810
- old_value: claim.priority,
6811
- new_value: priority,
6812
- reason
6813
- }
6814
- });
6815
- return successResponse({
6816
- success: true,
6817
- claim_id: claimId,
6818
- old_priority: oldPriority,
6819
- new_priority: newPriority,
6820
- reason: reason ?? null,
6821
- message: `Priority updated from ${oldPriority.level} (${oldPriority.value}) to ${newPriority.level} (${newPriority.value})`
6822
5573
  });
6823
5574
  }
6824
5575
  default:
6825
- return createToolResult(`Unknown claim tool: ${name}`, true);
5576
+ return createToolResult(`Unknown action: ${action}`, true);
6826
5577
  }
6827
5578
  }
6828
5579
 
6829
- // src/mcp/tools/message.ts
6830
- var messageTools = [
5580
+ // src/mcp/tools/memory.ts
5581
+ var memoryTools = [
6831
5582
  {
6832
- name: "collab_message_send",
6833
- description: "Send a message to another session or broadcast to all sessions.",
5583
+ name: "collab_memory_save",
5584
+ description: `Save or update context to working memory (upsert). Use to persist findings, decisions, or state that should survive context compaction.`,
6834
5585
  inputSchema: {
6835
5586
  type: "object",
6836
5587
  properties: {
6837
- from_session_id: {
5588
+ session_id: {
6838
5589
  type: "string",
6839
5590
  description: "Your session ID"
6840
5591
  },
6841
- to_session_id: {
5592
+ category: {
5593
+ type: "string",
5594
+ enum: ["finding", "decision", "state", "todo", "important", "context"],
5595
+ description: "Category: finding, decision, state, todo, important, context"
5596
+ },
5597
+ key: {
6842
5598
  type: "string",
6843
- description: "Target session ID. Leave empty to broadcast to all."
5599
+ description: 'Unique identifier (e.g., "auth_bug_root_cause")'
6844
5600
  },
6845
5601
  content: {
6846
5602
  type: "string",
6847
- description: "Message content"
5603
+ description: "The content to remember"
5604
+ },
5605
+ priority: {
5606
+ type: "number",
5607
+ description: "Priority 0-100 (default 50). Higher = more important."
5608
+ },
5609
+ pinned: {
5610
+ type: "boolean",
5611
+ description: "If true, always loaded when recalling active memories."
6848
5612
  }
6849
5613
  },
6850
- required: ["from_session_id", "content"]
5614
+ required: ["session_id", "category", "key", "content"]
6851
5615
  }
6852
5616
  },
6853
5617
  {
6854
- name: "collab_message_list",
6855
- description: "Read messages sent to your session.",
5618
+ name: "collab_memory_recall",
5619
+ description: `Recall memories. Use active=true to get pinned + high priority memories for context restoration.`,
6856
5620
  inputSchema: {
6857
5621
  type: "object",
6858
5622
  properties: {
@@ -6860,2477 +5624,371 @@ var messageTools = [
6860
5624
  type: "string",
6861
5625
  description: "Your session ID"
6862
5626
  },
6863
- unread_only: {
5627
+ active: {
6864
5628
  type: "boolean",
6865
- description: "Only show unread messages"
5629
+ description: "If true, return pinned + high priority memories only (default behavior for context restoration)"
5630
+ },
5631
+ category: {
5632
+ type: "string",
5633
+ enum: ["finding", "decision", "state", "todo", "important", "context"],
5634
+ description: "Filter by category"
5635
+ },
5636
+ key: {
5637
+ type: "string",
5638
+ description: "Get a specific memory by key"
5639
+ }
5640
+ },
5641
+ required: ["session_id"]
5642
+ }
5643
+ },
5644
+ {
5645
+ name: "collab_memory_clear",
5646
+ description: "Clear memories. Specify key, category, or clear_all.",
5647
+ inputSchema: {
5648
+ type: "object",
5649
+ properties: {
5650
+ session_id: {
5651
+ type: "string",
5652
+ description: "Your session ID"
6866
5653
  },
6867
- mark_as_read: {
5654
+ key: {
5655
+ type: "string",
5656
+ description: "Clear specific memory by key"
5657
+ },
5658
+ category: {
5659
+ type: "string",
5660
+ enum: ["finding", "decision", "state", "todo", "important", "context"],
5661
+ description: "Clear all in category"
5662
+ },
5663
+ clear_all: {
6868
5664
  type: "boolean",
6869
- description: "Mark retrieved messages as read"
5665
+ description: "Clear ALL memories (use with caution)"
6870
5666
  }
6871
5667
  },
6872
5668
  required: ["session_id"]
6873
5669
  }
6874
5670
  }
6875
5671
  ];
6876
- async function handleMessageTool(db, name, args) {
5672
+ async function handleMemoryTool(db, name, args) {
5673
+ const sessionId = args.session_id;
5674
+ if (!sessionId) {
5675
+ return validationError("session_id is required");
5676
+ }
5677
+ const sessionCheck = await validateActiveSession(db, sessionId);
5678
+ if (!sessionCheck.valid) {
5679
+ return sessionCheck.error;
5680
+ }
6877
5681
  switch (name) {
6878
- case "collab_message_send": {
6879
- const validation = validateInput(messageSendSchema, args);
6880
- if (!validation.success) {
6881
- return validationError(validation.error);
6882
- }
6883
- const input = validation.data;
6884
- const senderResult = await validateActiveSession(db, input.from_session_id);
6885
- if (!senderResult.valid) {
6886
- return errorResponse(ERROR_CODES.SESSION_INVALID, "Your session is not active.");
6887
- }
6888
- if (input.to_session_id) {
6889
- const targetResult = await validateActiveSession(db, input.to_session_id);
6890
- if (!targetResult.valid) {
6891
- return errorResponse(
6892
- ERROR_CODES.TARGET_SESSION_INVALID,
6893
- "Target session not found or inactive."
6894
- );
6895
- }
6896
- }
6897
- const message = await sendMessage(db, {
6898
- from_session_id: input.from_session_id,
6899
- to_session_id: input.to_session_id,
6900
- content: input.content
5682
+ case "collab_memory_save": {
5683
+ const category = args.category;
5684
+ const key = args.key;
5685
+ const content = args.content;
5686
+ if (!category || !key || !content) {
5687
+ return validationError("category, key, and content are required");
5688
+ }
5689
+ const memory = await saveMemory(db, sessionId, {
5690
+ category,
5691
+ key,
5692
+ content,
5693
+ priority: args.priority ?? 50,
5694
+ pinned: args.pinned ?? false
5695
+ });
5696
+ return successResponse({
5697
+ saved: true,
5698
+ id: memory.id,
5699
+ category: memory.category,
5700
+ key: memory.key,
5701
+ priority: memory.priority,
5702
+ pinned: memory.pinned === 1,
5703
+ message: `Memory saved: ${category}/${key}`
6901
5704
  });
6902
- const senderName = senderResult.session.name ?? "Unknown session";
6903
- const contentPreview = input.content.length > 50 ? input.content.substring(0, 50) + "..." : input.content;
6904
- if (input.to_session_id) {
6905
- await createNotification(db, {
6906
- session_id: input.to_session_id,
6907
- type: "session_message",
6908
- title: `Message from ${senderName}`,
6909
- message: contentPreview,
6910
- reference_type: "message",
6911
- reference_id: message.id,
6912
- metadata: { from_session_id: input.from_session_id, from_session_name: senderName }
5705
+ }
5706
+ case "collab_memory_recall": {
5707
+ const active = args.active;
5708
+ const category = args.category;
5709
+ const key = args.key;
5710
+ if (active) {
5711
+ const memories2 = await getActiveMemories(db, sessionId, {
5712
+ priority_threshold: 70,
5713
+ max_items: 20
6913
5714
  });
6914
- } else {
6915
- const allSessions = await listSessions(db, { include_inactive: false });
6916
- for (const session of allSessions) {
6917
- if (session.id !== input.from_session_id) {
6918
- await createNotification(db, {
6919
- session_id: session.id,
6920
- type: "session_message",
6921
- title: `Broadcast from ${senderName}`,
6922
- message: contentPreview,
6923
- reference_type: "message",
6924
- reference_id: message.id,
6925
- metadata: { from_session_id: input.from_session_id, from_session_name: senderName, is_broadcast: true }
6926
- });
5715
+ const formatted2 = memories2.map((m) => ({
5716
+ category: m.category,
5717
+ key: m.key,
5718
+ content: m.content,
5719
+ priority: m.priority,
5720
+ pinned: m.pinned === 1
5721
+ }));
5722
+ const byCategory = {};
5723
+ for (const mem of formatted2) {
5724
+ if (!byCategory[mem.category]) {
5725
+ byCategory[mem.category] = [];
6927
5726
  }
5727
+ byCategory[mem.category].push({ key: mem.key, content: mem.content });
6928
5728
  }
5729
+ return successResponse({
5730
+ count: formatted2.length,
5731
+ by_category: byCategory,
5732
+ message: `Active memories: ${formatted2.length} items`
5733
+ });
6929
5734
  }
5735
+ const memories = await recallMemory(db, sessionId, { category, key });
5736
+ const formatted = memories.map((m) => ({
5737
+ category: m.category,
5738
+ key: m.key,
5739
+ content: m.content,
5740
+ priority: m.priority,
5741
+ pinned: m.pinned === 1
5742
+ }));
6930
5743
  return successResponse({
6931
- success: true,
6932
- message_id: message.id,
6933
- sent_to: input.to_session_id ?? "all sessions (broadcast)",
6934
- message: "Message sent successfully."
5744
+ count: formatted.length,
5745
+ memories: formatted
6935
5746
  });
6936
5747
  }
6937
- case "collab_message_list": {
6938
- const validation = validateInput(messageListSchema, args);
6939
- if (!validation.success) {
6940
- return validationError(validation.error);
5748
+ case "collab_memory_clear": {
5749
+ const key = args.key;
5750
+ const category = args.category;
5751
+ const clearAll = args.clear_all;
5752
+ if (!key && !category && !clearAll) {
5753
+ return validationError("One of key, category, or clear_all is required");
6941
5754
  }
6942
- const input = validation.data;
6943
- const unreadOnly = input.unread_only ?? true;
6944
- const markAsRead = input.mark_as_read ?? true;
6945
- const messages = await listMessages(db, {
6946
- session_id: input.session_id,
6947
- unread_only: unreadOnly,
6948
- mark_as_read: markAsRead
5755
+ const cleared = await clearMemory(db, sessionId, {
5756
+ key,
5757
+ category,
5758
+ clear_all: clearAll
6949
5759
  });
6950
- if (messages.length === 0) {
6951
- return successResponse({
6952
- messages: [],
6953
- message: unreadOnly ? "No unread messages." : "No messages."
6954
- });
5760
+ let message;
5761
+ if (key) {
5762
+ message = cleared > 0 ? `Memory cleared: ${key}` : `Memory not found: ${key}`;
5763
+ } else if (category) {
5764
+ message = `Cleared ${cleared} memories from category: ${category}`;
5765
+ } else {
5766
+ message = `Cleared all ${cleared} memories`;
6955
5767
  }
6956
- return successResponse({
6957
- messages: messages.map((m) => ({
6958
- id: m.id,
6959
- from_session_id: m.from_session_id,
6960
- content: m.content,
6961
- created_at: m.created_at,
6962
- is_broadcast: m.to_session_id === null
6963
- })),
6964
- total: messages.length,
6965
- marked_as_read: markAsRead
6966
- }, true);
5768
+ return successResponse({ cleared, message });
6967
5769
  }
6968
5770
  default:
6969
- return createToolResult(`Unknown message tool: ${name}`, true);
5771
+ return errorResponse(ERROR_CODES.UNKNOWN_TOOL, `Unknown memory tool: ${name}`);
6970
5772
  }
6971
5773
  }
6972
5774
 
6973
- // src/mcp/tools/decision.ts
6974
- var decisionTools = [
5775
+ // src/mcp/tools/protection.ts
5776
+ var protectionTools = [
6975
5777
  {
6976
- name: "collab_decision_add",
6977
- description: "Record an architectural or design decision for team reference.",
5778
+ name: "collab_protect",
5779
+ description: `Unified tool for file protection. Use action parameter to:
5780
+ - "register": Register a plan or file for protection
5781
+ - "check": Check if a file is protected before deleting
5782
+ - "list": List all protected files`,
6978
5783
  inputSchema: {
6979
5784
  type: "object",
6980
5785
  properties: {
5786
+ action: {
5787
+ type: "string",
5788
+ enum: ["register", "check", "list"],
5789
+ description: "Action to perform"
5790
+ },
6981
5791
  session_id: {
6982
5792
  type: "string",
6983
5793
  description: "Your session ID"
6984
5794
  },
6985
- category: {
5795
+ file_path: {
5796
+ type: "string",
5797
+ description: "File path (for register/check)"
5798
+ },
5799
+ type: {
6986
5800
  type: "string",
6987
- enum: ["architecture", "naming", "api", "database", "ui", "other"],
6988
- description: "Decision category"
5801
+ enum: ["plan", "file"],
5802
+ description: "Type of protection (for register). Default: file"
6989
5803
  },
6990
5804
  title: {
6991
5805
  type: "string",
6992
- description: "Brief title of the decision"
5806
+ description: "Title (for plan registration)"
6993
5807
  },
6994
- description: {
5808
+ content_summary: {
6995
5809
  type: "string",
6996
- description: "Detailed description of the decision and rationale"
5810
+ description: "Summary (for plan registration)"
6997
5811
  }
6998
5812
  },
6999
- required: ["session_id", "title", "description"]
5813
+ required: ["action", "session_id"]
7000
5814
  }
7001
- },
7002
- {
7003
- name: "collab_decision_list",
7004
- description: "List recorded decisions. Use to understand past architectural choices.",
7005
- inputSchema: {
7006
- type: "object",
7007
- properties: {
7008
- category: {
7009
- type: "string",
7010
- enum: ["architecture", "naming", "api", "database", "ui", "other"],
7011
- description: "Filter by category"
7012
- },
7013
- limit: {
7014
- type: "number",
7015
- description: "Maximum number of decisions to return"
5815
+ }
5816
+ ];
5817
+ async function handleProtectionTool(db, name, args) {
5818
+ if (name !== "collab_protect") {
5819
+ return errorResponse(ERROR_CODES.UNKNOWN_TOOL, `Unknown tool: ${name}`);
5820
+ }
5821
+ const action = args.action;
5822
+ const sessionId = args.session_id;
5823
+ if (!action || !sessionId) {
5824
+ return validationError("action and session_id are required");
5825
+ }
5826
+ const sessionCheck = await validateActiveSession(db, sessionId);
5827
+ if (!sessionCheck.valid) {
5828
+ return sessionCheck.error;
5829
+ }
5830
+ switch (action) {
5831
+ case "register": {
5832
+ const filePath = args.file_path;
5833
+ const type = args.type ?? "file";
5834
+ if (!filePath) {
5835
+ return validationError("file_path is required for register");
5836
+ }
5837
+ if (type === "plan") {
5838
+ const title = args.title;
5839
+ const contentSummary = args.content_summary;
5840
+ if (!title || !contentSummary) {
5841
+ return validationError("title and content_summary are required for plan registration");
7016
5842
  }
5843
+ const memory = await registerPlan(db, sessionId, {
5844
+ file_path: filePath,
5845
+ title,
5846
+ content_summary: contentSummary,
5847
+ status: "in_progress"
5848
+ });
5849
+ return successResponse({
5850
+ registered: true,
5851
+ type: "plan",
5852
+ file_path: filePath,
5853
+ title,
5854
+ priority: memory.priority,
5855
+ pinned: memory.pinned === 1,
5856
+ message: `Plan registered and protected: ${title}`
5857
+ });
5858
+ } else {
5859
+ const description = args.description;
5860
+ const memory = await registerCreatedFile(db, sessionId, {
5861
+ file_path: filePath,
5862
+ file_type: "other",
5863
+ description
5864
+ });
5865
+ return successResponse({
5866
+ registered: true,
5867
+ type: "file",
5868
+ file_path: filePath,
5869
+ priority: memory.priority,
5870
+ message: `File registered for protection: ${filePath}`
5871
+ });
7017
5872
  }
7018
5873
  }
7019
- }
7020
- ];
7021
- async function handleDecisionTool(db, name, args) {
7022
- switch (name) {
7023
- case "collab_decision_add": {
7024
- const validation = validateInput(decisionAddSchema, args);
7025
- if (!validation.success) {
7026
- return validationError(validation.error);
5874
+ case "check": {
5875
+ const filePath = args.file_path;
5876
+ if (!filePath) {
5877
+ return validationError("file_path is required for check");
7027
5878
  }
7028
- const input = validation.data;
7029
- const sessionResult = await validateActiveSession(db, input.session_id);
7030
- if (!sessionResult.valid) {
7031
- return sessionResult.error;
5879
+ const result = await isFileProtected(db, sessionId, filePath);
5880
+ if (result.protected) {
5881
+ return successResponse({
5882
+ protected: true,
5883
+ reason: result.reason,
5884
+ warning: `\u26A0\uFE0F Protected (${result.reason}). Confirm before deleting.`
5885
+ });
7032
5886
  }
7033
- const decision = await addDecision(db, {
7034
- session_id: input.session_id,
7035
- category: input.category,
7036
- title: input.title,
7037
- description: input.description
7038
- });
7039
5887
  return successResponse({
7040
- success: true,
7041
- decision_id: decision.id,
7042
- message: "Decision recorded successfully."
5888
+ protected: false,
5889
+ message: "File is not protected."
7043
5890
  });
7044
5891
  }
7045
- case "collab_decision_list": {
7046
- const validation = validateInput(decisionListSchema, args);
7047
- if (!validation.success) {
7048
- return validationError(validation.error);
7049
- }
7050
- const input = validation.data;
7051
- const decisions = await listDecisions(db, {
7052
- category: input.category,
7053
- limit: input.limit ?? 20
7054
- });
5892
+ case "list": {
5893
+ const files = await getProtectedFiles(db, sessionId);
5894
+ const plans = await listPlans(db, sessionId, { include_archived: false });
7055
5895
  return successResponse({
7056
- decisions: decisions.map((d) => ({
7057
- id: d.id,
7058
- category: d.category,
7059
- title: d.title,
7060
- description: d.description,
7061
- created_at: d.created_at
5896
+ protected_files: files.length,
5897
+ plans: plans.length,
5898
+ files,
5899
+ plan_list: plans.map((p) => ({
5900
+ file_path: p.file_path,
5901
+ title: p.title,
5902
+ status: p.status
7062
5903
  })),
7063
- total: decisions.length
7064
- }, true);
5904
+ message: `${files.length} protected file(s), ${plans.length} plan(s)`
5905
+ });
7065
5906
  }
7066
5907
  default:
7067
- return createToolResult(`Unknown decision tool: ${name}`, true);
5908
+ return errorResponse(ERROR_CODES.UNKNOWN_TOOL, `Unknown action: ${action}`);
7068
5909
  }
7069
5910
  }
7070
5911
 
7071
- // src/mcp/tools/lsp.ts
7072
- var LSP_SYMBOL_KIND_MAP = {
7073
- 5: "class",
7074
- // Class
7075
- 6: "method",
7076
- // Method
7077
- 9: "function",
7078
- // Constructor
7079
- 12: "function",
7080
- // Function
7081
- 13: "variable",
7082
- // Variable
7083
- 14: "variable",
7084
- // Constant
7085
- 23: "class"
7086
- // Struct
7087
- // Map others to 'other'
7088
- };
7089
- var lspTools = [
7090
- {
7091
- name: "collab_analyze_symbols",
7092
- description: `Analyze LSP symbols for conflict detection and reference impact.
5912
+ // src/constants.ts
5913
+ import { readFileSync } from "fs";
5914
+ import { join as join2, dirname as dirname2 } from "path";
5915
+ import { fileURLToPath } from "url";
5916
+ function getVersion() {
5917
+ try {
5918
+ const __dirname3 = dirname2(fileURLToPath(import.meta.url));
5919
+ const pkg = JSON.parse(readFileSync(join2(__dirname3, "..", "package.json"), "utf-8"));
5920
+ return pkg.version;
5921
+ } catch {
5922
+ return "0.0.0";
5923
+ }
5924
+ }
5925
+ var VERSION = getVersion();
5926
+ var SERVER_NAME = "session-collab-mcp";
5927
+ var SERVER_INSTRUCTIONS = `
5928
+ # Session Collaboration MCP
7093
5929
 
7094
- WORKFLOW:
7095
- 1. Claude uses LSP.documentSymbol to get symbols from a file
7096
- 2. Claude passes those symbols to this tool
7097
- 3. This tool checks conflicts and returns which symbols are safe/blocked
5930
+ Coordinate sessions and persist context across conversations.
7098
5931
 
7099
- This enables Claude to automatically proceed with safe symbols and skip blocked ones.`,
7100
- inputSchema: {
7101
- type: "object",
7102
- properties: {
7103
- session_id: {
7104
- type: "string",
7105
- description: "Your session ID (to exclude your own claims)"
7106
- },
7107
- files: {
7108
- type: "array",
7109
- items: {
7110
- type: "object",
7111
- properties: {
7112
- file: { type: "string", description: "File path" },
7113
- symbols: {
7114
- type: "array",
7115
- items: {
7116
- type: "object",
7117
- properties: {
7118
- name: { type: "string", description: "Symbol name" },
7119
- kind: { type: "number", description: "LSP SymbolKind" },
7120
- range: {
7121
- type: "object",
7122
- properties: {
7123
- start: {
7124
- type: "object",
7125
- properties: {
7126
- line: { type: "number" },
7127
- character: { type: "number" }
7128
- }
7129
- },
7130
- end: {
7131
- type: "object",
7132
- properties: {
7133
- line: { type: "number" },
7134
- character: { type: "number" }
7135
- }
7136
- }
7137
- }
7138
- },
7139
- children: {
7140
- type: "array",
7141
- description: "Nested symbols (e.g., methods in a class)"
7142
- }
7143
- },
7144
- required: ["name", "kind"]
7145
- },
7146
- description: "LSP DocumentSymbol array from LSP.documentSymbol"
7147
- }
7148
- },
7149
- required: ["file", "symbols"]
7150
- },
7151
- description: "Files with their LSP symbols to analyze"
7152
- },
7153
- references: {
7154
- type: "array",
7155
- items: {
7156
- type: "object",
7157
- properties: {
7158
- symbol: { type: "string", description: "Symbol name" },
7159
- file: { type: "string", description: "File containing the symbol" },
7160
- references: {
7161
- type: "array",
7162
- items: {
7163
- type: "object",
7164
- properties: {
7165
- file: { type: "string" },
7166
- line: { type: "number" },
7167
- context: { type: "string" }
7168
- },
7169
- required: ["file", "line"]
7170
- },
7171
- description: "Locations that reference this symbol (from LSP.findReferences)"
7172
- }
7173
- },
7174
- required: ["symbol", "file", "references"]
7175
- },
7176
- description: "Optional: Reference data from LSP.findReferences for impact analysis"
7177
- },
7178
- check_symbols: {
7179
- type: "array",
7180
- items: { type: "string" },
7181
- description: "Optional: Only analyze these specific symbol names (filter)"
7182
- }
7183
- },
7184
- required: ["session_id", "files"]
7185
- }
7186
- },
7187
- {
7188
- name: "collab_validate_symbols",
7189
- description: `Validate that symbols exist in a file before claiming them.
5932
+ ## 9 Core Tools
7190
5933
 
7191
- Use this to verify symbol names are correct before calling collab_claim.
7192
- Helps prevent claiming non-existent or misspelled symbols.`,
7193
- inputSchema: {
7194
- type: "object",
7195
- properties: {
7196
- file: {
7197
- type: "string",
7198
- description: "File path to validate symbols in"
7199
- },
7200
- symbols: {
7201
- type: "array",
7202
- items: { type: "string" },
7203
- description: "Symbol names to validate"
7204
- },
7205
- lsp_symbols: {
7206
- type: "array",
7207
- items: {
7208
- type: "object",
7209
- properties: {
7210
- name: { type: "string" },
7211
- kind: { type: "number" }
7212
- },
7213
- required: ["name", "kind"]
7214
- },
7215
- description: "LSP symbols from the file (from LSP.documentSymbol)"
7216
- }
7217
- },
7218
- required: ["file", "symbols", "lsp_symbols"]
7219
- }
7220
- },
7221
- {
7222
- name: "collab_store_references",
7223
- description: `Store symbol reference data from LSP.findReferences for impact tracking.
5934
+ ### Session (4 tools)
5935
+ - \`collab_session_start\`: Start session with project_root
5936
+ - \`collab_session_end\`: End session, release claims
5937
+ - \`collab_session_list\`: List active sessions
5938
+ - \`collab_config\`: Configure session behavior
7224
5939
 
7225
- Call this after using LSP.findReferences to persist the reference data.
7226
- This enables automatic impact warnings when other sessions claim related files.`,
7227
- inputSchema: {
7228
- type: "object",
7229
- properties: {
7230
- session_id: {
7231
- type: "string",
7232
- description: "Your session ID"
7233
- },
7234
- references: {
7235
- type: "array",
7236
- items: {
7237
- type: "object",
7238
- properties: {
7239
- source_file: { type: "string", description: "File containing the symbol definition" },
7240
- source_symbol: { type: "string", description: "Symbol name" },
7241
- references: {
7242
- type: "array",
7243
- items: {
7244
- type: "object",
7245
- properties: {
7246
- file: { type: "string" },
7247
- line: { type: "number" },
7248
- context: { type: "string" }
7249
- },
7250
- required: ["file", "line"]
7251
- }
7252
- }
7253
- },
7254
- required: ["source_file", "source_symbol", "references"]
7255
- },
7256
- description: "Reference data from LSP.findReferences"
7257
- },
7258
- clear_existing: {
7259
- type: "boolean",
7260
- description: "Clear existing references from this session before storing (default: false)"
7261
- }
7262
- },
7263
- required: ["session_id", "references"]
7264
- }
7265
- },
7266
- {
7267
- name: "collab_impact_analysis",
7268
- description: `Analyze the impact of modifying a symbol.
5940
+ ### Claims (1 tool)
5941
+ - \`collab_claim\`: Unified claim tool
5942
+ - action: "create" | "check" | "release" | "list"
7269
5943
 
7270
- Returns which files reference this symbol and if any of those files have active claims.
7271
- Use this before making changes to widely-used symbols.`,
7272
- inputSchema: {
7273
- type: "object",
7274
- properties: {
7275
- session_id: {
7276
- type: "string",
7277
- description: "Your session ID (to exclude your own claims)"
7278
- },
7279
- file: {
7280
- type: "string",
7281
- description: "File containing the symbol"
7282
- },
7283
- symbol: {
7284
- type: "string",
7285
- description: "Symbol name to analyze"
7286
- }
7287
- },
7288
- required: ["session_id", "file", "symbol"]
7289
- }
7290
- }
7291
- ];
7292
- function lspKindToSymbolType(kind) {
7293
- return LSP_SYMBOL_KIND_MAP[kind] ?? "other";
7294
- }
7295
- function flattenLspSymbols(symbols, file, parentName) {
7296
- const result = [];
7297
- for (const sym of symbols) {
7298
- const fullName = parentName ? `${parentName}.${sym.name}` : sym.name;
7299
- result.push({
7300
- name: sym.name,
7301
- type: lspKindToSymbolType(sym.kind),
7302
- file,
7303
- fullName
7304
- });
7305
- if (sym.children && sym.children.length > 0) {
7306
- result.push(...flattenLspSymbols(sym.children, file, fullName));
7307
- }
7308
- }
7309
- return result;
7310
- }
7311
- async function handleLspTool(db, name, args) {
7312
- switch (name) {
7313
- case "collab_analyze_symbols": {
7314
- const validation = validateInput(analyzeSymbolsSchema, args);
7315
- if (!validation.success) {
7316
- return createToolResult(
7317
- JSON.stringify({ error: "INVALID_INPUT", message: validation.error }),
7318
- true
7319
- );
7320
- }
7321
- const input = validation.data;
7322
- const files = input.files;
7323
- const references = input.references;
7324
- const checkSymbols = input.check_symbols;
7325
- const referenceMap = /* @__PURE__ */ new Map();
7326
- if (references) {
7327
- for (const ref of references) {
7328
- const key = `${ref.file}:${ref.symbol}`;
7329
- referenceMap.set(key, ref);
7330
- }
7331
- }
7332
- const allSymbols = [];
7333
- for (const fileInput of files) {
7334
- const flattened = flattenLspSymbols(fileInput.symbols, fileInput.file);
7335
- allSymbols.push(...flattened);
7336
- }
7337
- let symbolsToAnalyze = allSymbols;
7338
- if (checkSymbols && checkSymbols.length > 0) {
7339
- const checkSet = new Set(checkSymbols);
7340
- symbolsToAnalyze = allSymbols.filter((s) => checkSet.has(s.name) || checkSet.has(s.fullName));
7341
- }
7342
- const fileList = [...new Set(symbolsToAnalyze.map((s) => s.file))];
7343
- const symbolNames = [...new Set(symbolsToAnalyze.map((s) => s.name))];
7344
- const symbolConflicts = await querySymbolConflicts(db, fileList, symbolNames, input.session_id);
7345
- const fileConflicts = await queryFileConflicts(db, fileList, input.session_id);
7346
- const analyzedSymbols = [];
7347
- const safeSymbols = [];
7348
- const blockedSymbols = [];
7349
- for (const sym of symbolsToAnalyze) {
7350
- const fileConflict = fileConflicts.find((c) => c.file_path === sym.file);
7351
- const symbolConflict = symbolConflicts.find(
7352
- (c) => c.file_path === sym.file && c.symbol_name === sym.name
7353
- );
7354
- const conflict = fileConflict ?? symbolConflict;
7355
- const isBlocked = !!conflict;
7356
- const refKey = `${sym.file}:${sym.name}`;
7357
- const refData = referenceMap.get(refKey);
7358
- let impact;
7359
- if (refData && refData.references.length > 0) {
7360
- const affectedFiles = [...new Set(refData.references.map((r) => r.file))];
7361
- impact = {
7362
- references_count: refData.references.length,
7363
- affected_files: affectedFiles
7364
- };
7365
- }
7366
- const analyzed = {
7367
- name: sym.name,
7368
- type: sym.type,
7369
- file: sym.file,
7370
- conflict_status: isBlocked ? "blocked" : "safe",
7371
- ...conflict && {
7372
- conflict_info: {
7373
- session_name: conflict.session_name,
7374
- intent: conflict.intent,
7375
- claim_id: conflict.claim_id
7376
- }
7377
- },
7378
- ...impact && { impact }
7379
- };
7380
- analyzedSymbols.push(analyzed);
7381
- if (isBlocked) {
7382
- blockedSymbols.push(`${sym.file}:${sym.name}`);
7383
- } else {
7384
- safeSymbols.push(`${sym.file}:${sym.name}`);
7385
- }
7386
- }
7387
- const hasBlocked = blockedSymbols.length > 0;
7388
- const hasSafe = safeSymbols.length > 0;
7389
- let recommendation;
7390
- if (!hasBlocked) {
7391
- recommendation = "proceed_all";
7392
- } else if (hasSafe) {
7393
- recommendation = "proceed_safe_only";
7394
- } else {
7395
- recommendation = "abort";
7396
- }
7397
- let message;
7398
- if (recommendation === "proceed_all") {
7399
- message = `All ${safeSymbols.length} symbols are safe to edit. Proceed.`;
7400
- } else if (recommendation === "proceed_safe_only") {
7401
- message = `Edit ONLY ${safeSymbols.length} safe symbols. ${blockedSymbols.length} symbols are blocked.`;
7402
- } else {
7403
- message = `All ${blockedSymbols.length} symbols are blocked. Coordinate with other sessions.`;
7404
- }
7405
- return createToolResult(
7406
- JSON.stringify(
7407
- {
7408
- can_edit: hasSafe,
7409
- recommendation,
7410
- summary: {
7411
- total: symbolsToAnalyze.length,
7412
- safe: safeSymbols.length,
7413
- blocked: blockedSymbols.length
7414
- },
7415
- symbols: analyzedSymbols,
7416
- safe_symbols: safeSymbols,
7417
- blocked_symbols: blockedSymbols,
7418
- message
7419
- },
7420
- null,
7421
- 2
7422
- )
7423
- );
7424
- }
7425
- case "collab_validate_symbols": {
7426
- const validation = validateInput(validateSymbolsSchema, args);
7427
- if (!validation.success) {
7428
- return createToolResult(
7429
- JSON.stringify({ error: "INVALID_INPUT", message: validation.error }),
7430
- true
7431
- );
7432
- }
7433
- const input = validation.data;
7434
- const availableSymbols = /* @__PURE__ */ new Set();
7435
- for (const lspSym of input.lsp_symbols) {
7436
- availableSymbols.add(lspSym.name);
7437
- }
7438
- const valid = [];
7439
- const invalid = [];
7440
- const suggestions = {};
7441
- for (const sym of input.symbols) {
7442
- if (availableSymbols.has(sym)) {
7443
- valid.push(sym);
7444
- } else {
7445
- invalid.push(sym);
7446
- const similar = Array.from(availableSymbols).filter(
7447
- (avail) => avail.toLowerCase().includes(sym.toLowerCase()) || sym.toLowerCase().includes(avail.toLowerCase())
7448
- );
7449
- if (similar.length > 0) {
7450
- suggestions[sym] = similar.slice(0, 3);
7451
- }
7452
- }
7453
- }
7454
- const allValid = invalid.length === 0;
7455
- return createToolResult(
7456
- JSON.stringify({
7457
- valid: allValid,
7458
- file: input.file,
7459
- valid_symbols: valid,
7460
- invalid_symbols: invalid,
7461
- suggestions: Object.keys(suggestions).length > 0 ? suggestions : void 0,
7462
- available_symbols: Array.from(availableSymbols),
7463
- message: allValid ? `All ${valid.length} symbols are valid.` : `${invalid.length} symbol(s) not found: ${invalid.join(", ")}`
7464
- })
7465
- );
7466
- }
7467
- case "collab_store_references": {
7468
- const validation = validateInput(storeReferencesSchema, args);
7469
- if (!validation.success) {
7470
- return createToolResult(
7471
- JSON.stringify({ error: "INVALID_INPUT", message: validation.error }),
7472
- true
7473
- );
7474
- }
7475
- const input = validation.data;
7476
- let cleared = 0;
7477
- if (input.clear_existing) {
7478
- cleared = await clearSessionReferences(db, input.session_id);
7479
- }
7480
- const result = await storeReferences(db, input.session_id, input.references);
7481
- return createToolResult(
7482
- JSON.stringify({
7483
- success: true,
7484
- stored: result.stored,
7485
- skipped: result.skipped,
7486
- cleared,
7487
- message: `Stored ${result.stored} references${cleared > 0 ? ` (cleared ${cleared} existing)` : ""}.`
7488
- })
7489
- );
7490
- }
7491
- case "collab_impact_analysis": {
7492
- const validation = validateInput(impactAnalysisSchema, args);
7493
- if (!validation.success) {
7494
- return createToolResult(
7495
- JSON.stringify({ error: "INVALID_INPUT", message: validation.error }),
7496
- true
7497
- );
7498
- }
7499
- const input = validation.data;
7500
- const impact = await analyzeClaimImpact(db, input.file, input.symbol, input.session_id);
7501
- let riskLevel;
7502
- if (impact.affected_claims.length > 0) {
7503
- riskLevel = "high";
7504
- } else if (impact.reference_count > 10) {
7505
- riskLevel = "medium";
7506
- } else {
7507
- riskLevel = "low";
7508
- }
7509
- let message;
7510
- if (impact.affected_claims.length > 0) {
7511
- const claimNames = impact.affected_claims.map((c) => c.session_name ?? "unknown").join(", ");
7512
- message = `\u26A0\uFE0F HIGH RISK: ${impact.affected_claims.length} active claim(s) on referencing files (${claimNames}). Coordinate before modifying.`;
7513
- } else if (impact.reference_count > 0) {
7514
- message = `${impact.reference_count} references across ${impact.affected_files.length} file(s). No active claims conflict.`;
7515
- } else {
7516
- message = "No stored references found. Consider running LSP.findReferences and collab_store_references first.";
7517
- }
7518
- return createToolResult(
7519
- JSON.stringify(
7520
- {
7521
- symbol: impact.symbol,
7522
- file: impact.file,
7523
- risk_level: riskLevel,
7524
- reference_count: impact.reference_count,
7525
- affected_files: impact.affected_files,
7526
- affected_claims: impact.affected_claims,
7527
- message
7528
- },
7529
- null,
7530
- 2
7531
- )
7532
- );
7533
- }
7534
- default:
7535
- return createToolResult(`Unknown LSP tool: ${name}`, true);
7536
- }
7537
- }
7538
- async function querySymbolConflicts(db, files, symbolNames, excludeSessionId) {
7539
- if (files.length === 0 || symbolNames.length === 0) {
7540
- return [];
7541
- }
7542
- const filePlaceholders = files.map(() => "?").join(",");
7543
- const symbolPlaceholders = symbolNames.map(() => "?").join(",");
7544
- const query = `
7545
- SELECT
7546
- c.id as claim_id,
7547
- c.session_id,
7548
- s.name as session_name,
7549
- cs.file_path,
7550
- c.intent,
7551
- c.scope,
7552
- c.created_at,
7553
- cs.symbol_name,
7554
- cs.symbol_type
7555
- FROM claim_symbols cs
7556
- JOIN claims c ON cs.claim_id = c.id
7557
- JOIN sessions s ON c.session_id = s.id
7558
- WHERE c.status = 'active'
7559
- AND s.status = 'active'
7560
- AND c.session_id != ?
7561
- AND cs.file_path IN (${filePlaceholders})
7562
- AND cs.symbol_name IN (${symbolPlaceholders})
7563
- `;
7564
- const result = await db.prepare(query).bind(excludeSessionId, ...files, ...symbolNames).all();
7565
- return result.results.map((r) => ({
7566
- ...r,
7567
- conflict_level: "symbol"
7568
- }));
7569
- }
7570
- async function queryFileConflicts(db, files, excludeSessionId) {
7571
- if (files.length === 0) {
7572
- return [];
7573
- }
7574
- const placeholders = files.map(() => "?").join(",");
7575
- const query = `
7576
- SELECT
7577
- c.id as claim_id,
7578
- c.session_id,
7579
- s.name as session_name,
7580
- cf.file_path,
7581
- c.intent,
7582
- c.scope,
7583
- c.created_at
7584
- FROM claim_files cf
7585
- JOIN claims c ON cf.claim_id = c.id
7586
- JOIN sessions s ON c.session_id = s.id
7587
- WHERE c.status = 'active'
7588
- AND s.status = 'active'
7589
- AND c.session_id != ?
7590
- AND cf.file_path IN (${placeholders})
7591
- AND NOT EXISTS (
7592
- SELECT 1 FROM claim_symbols cs WHERE cs.claim_id = c.id AND cs.file_path = cf.file_path
7593
- )
7594
- `;
7595
- const result = await db.prepare(query).bind(excludeSessionId, ...files).all();
7596
- return result.results.map((r) => ({
7597
- ...r,
7598
- conflict_level: "file"
7599
- }));
7600
- }
7601
-
7602
- // src/mcp/tools/history.ts
7603
- var historyTools = [
7604
- {
7605
- name: "collab_history_list",
7606
- description: "List audit history entries. Useful for debugging coordination issues and understanding past actions.",
7607
- inputSchema: {
7608
- type: "object",
7609
- properties: {
7610
- session_id: {
7611
- type: "string",
7612
- description: "Filter by session ID"
7613
- },
7614
- action: {
7615
- type: "string",
7616
- enum: [
7617
- "session_started",
7618
- "session_ended",
7619
- "claim_created",
7620
- "claim_released",
7621
- "conflict_detected",
7622
- "queue_joined",
7623
- "queue_left",
7624
- "priority_changed"
7625
- ],
7626
- description: "Filter by action type"
7627
- },
7628
- entity_type: {
7629
- type: "string",
7630
- enum: ["session", "claim", "queue"],
7631
- description: "Filter by entity type"
7632
- },
7633
- entity_id: {
7634
- type: "string",
7635
- description: "Filter by specific entity ID"
7636
- },
7637
- from_date: {
7638
- type: "string",
7639
- description: "Start date (ISO 8601 format)"
7640
- },
7641
- to_date: {
7642
- type: "string",
7643
- description: "End date (ISO 8601 format)"
7644
- },
7645
- limit: {
7646
- type: "number",
7647
- description: "Maximum entries to return (default: 50, max: 500)"
7648
- }
7649
- }
7650
- }
7651
- }
7652
- ];
7653
- async function handleHistoryTool(db, name, args) {
7654
- switch (name) {
7655
- case "collab_history_list": {
7656
- const validation = validateInput(historyListSchema, args);
7657
- if (!validation.success) {
7658
- return validationError(validation.error);
7659
- }
7660
- const { session_id, action, entity_type, entity_id, from_date, to_date, limit } = validation.data;
7661
- await cleanupOldAuditHistory(db, 7);
7662
- const entries = await listAuditHistory(db, {
7663
- session_id,
7664
- action,
7665
- entity_type,
7666
- entity_id,
7667
- from_date,
7668
- to_date,
7669
- limit
7670
- });
7671
- return successResponse({
7672
- entries: entries.map((e) => ({
7673
- id: e.id,
7674
- session_id: e.session_id,
7675
- session_name: e.session_name,
7676
- action: e.action,
7677
- entity_type: e.entity_type,
7678
- entity_id: e.entity_id,
7679
- metadata: e.metadata ? JSON.parse(e.metadata) : null,
7680
- created_at: e.created_at
7681
- })),
7682
- total: entries.length,
7683
- message: entries.length > 0 ? `Found ${entries.length} audit entries` : "No audit entries found matching the criteria"
7684
- }, true);
7685
- }
7686
- default:
7687
- return successResponse({ error: `Unknown history tool: ${name}` });
7688
- }
7689
- }
7690
-
7691
- // src/mcp/tools/queue.ts
7692
- var queueTools = [
7693
- {
7694
- name: "collab_queue_join",
7695
- description: "Join waiting queue for a blocked claim. Use this when you want to work on files that are currently claimed by another session.",
7696
- inputSchema: {
7697
- type: "object",
7698
- properties: {
7699
- session_id: {
7700
- type: "string",
7701
- description: "Your session ID"
7702
- },
7703
- claim_id: {
7704
- type: "string",
7705
- description: "The claim ID you want to wait for"
7706
- },
7707
- intent: {
7708
- type: "string",
7709
- description: "What you plan to do when the claim becomes available"
7710
- },
7711
- priority: {
7712
- type: "number",
7713
- description: "Priority (0-100). Higher priority entries are served first. Default: 50"
7714
- },
7715
- scope: {
7716
- type: "string",
7717
- enum: ["small", "medium", "large"],
7718
- description: "Estimated scope of your work: small(<30min), medium(30min-2hr), large(>2hr)"
7719
- }
7720
- },
7721
- required: ["session_id", "claim_id", "intent"]
7722
- }
7723
- },
7724
- {
7725
- name: "collab_queue_leave",
7726
- description: "Leave the waiting queue for a claim.",
7727
- inputSchema: {
7728
- type: "object",
7729
- properties: {
7730
- session_id: {
7731
- type: "string",
7732
- description: "Your session ID"
7733
- },
7734
- queue_id: {
7735
- type: "string",
7736
- description: "Queue entry ID to leave"
7737
- }
7738
- },
7739
- required: ["session_id", "queue_id"]
7740
- }
7741
- },
7742
- {
7743
- name: "collab_queue_list",
7744
- description: "List queue entries. View who is waiting for claims.",
7745
- inputSchema: {
7746
- type: "object",
7747
- properties: {
7748
- claim_id: {
7749
- type: "string",
7750
- description: "Filter by claim ID to see who is waiting for a specific claim"
7751
- },
7752
- session_id: {
7753
- type: "string",
7754
- description: "Filter by session ID to see what claims a session is waiting for"
7755
- }
7756
- }
7757
- }
7758
- }
7759
- ];
7760
- async function handleQueueTool(db, name, args) {
7761
- switch (name) {
7762
- case "collab_queue_join": {
7763
- const validation = validateInput(queueJoinSchema, args);
7764
- if (!validation.success) {
7765
- return validationError(validation.error);
7766
- }
7767
- const { session_id: sessionId, claim_id: claimId, intent, priority, scope } = validation.data;
7768
- const sessionResult = await validateActiveSession(db, sessionId);
7769
- if (!sessionResult.valid) {
7770
- return sessionResult.error;
7771
- }
7772
- const claim = await getClaim(db, claimId);
7773
- if (!claim) {
7774
- return errorResponse(ERROR_CODES.CLAIM_NOT_FOUND, "Claim not found");
7775
- }
7776
- if (claim.session_id === sessionId) {
7777
- return errorResponse(
7778
- ERROR_CODES.CANNOT_QUEUE_OWN_CLAIM,
7779
- "You cannot queue for your own claim"
7780
- );
7781
- }
7782
- if (claim.status !== "active") {
7783
- return successResponse({
7784
- message: `Claim is already ${claim.status}. No need to queue - you can claim these files now.`,
7785
- claim_status: claim.status,
7786
- files: claim.files
7787
- });
7788
- }
7789
- const existingQueue = await listQueue(db, { claim_id: claimId, session_id: sessionId });
7790
- if (existingQueue.length > 0) {
7791
- return errorResponse(
7792
- ERROR_CODES.ALREADY_IN_QUEUE,
7793
- "You are already in the queue for this claim"
7794
- );
7795
- }
7796
- const entry = await joinQueue(db, {
7797
- claim_id: claimId,
7798
- session_id: sessionId,
7799
- intent,
7800
- priority,
7801
- scope
7802
- });
7803
- await logAuditEvent(db, {
7804
- session_id: sessionId,
7805
- action: "queue_joined",
7806
- entity_type: "queue",
7807
- entity_id: entry.id,
7808
- metadata: {
7809
- claim_id: claimId,
7810
- position: entry.position,
7811
- priority: entry.priority
7812
- }
7813
- });
7814
- const priorityInfo = getPriorityLevel(entry.priority);
7815
- return successResponse({
7816
- queue_id: entry.id,
7817
- position: entry.position,
7818
- priority: priorityInfo,
7819
- estimated_wait_minutes: entry.estimated_wait_minutes,
7820
- message: `Joined queue at position ${entry.position}. Estimated wait: ${entry.estimated_wait_minutes} minutes.`,
7821
- claim_owner: claim.session_name,
7822
- claim_intent: claim.intent,
7823
- files: claim.files
7824
- });
7825
- }
7826
- case "collab_queue_leave": {
7827
- const validation = validateInput(queueLeaveSchema, args);
7828
- if (!validation.success) {
7829
- return validationError(validation.error);
7830
- }
7831
- const { session_id: sessionId, queue_id: queueId } = validation.data;
7832
- const sessionResult = await validateActiveSession(db, sessionId);
7833
- if (!sessionResult.valid) {
7834
- return sessionResult.error;
7835
- }
7836
- const entry = await getQueueEntry(db, queueId);
7837
- if (!entry) {
7838
- return errorResponse(
7839
- ERROR_CODES.QUEUE_ENTRY_NOT_FOUND,
7840
- "Queue entry not found"
7841
- );
7842
- }
7843
- if (entry.session_id !== sessionId) {
7844
- return errorResponse(
7845
- ERROR_CODES.NOT_OWNER,
7846
- "You can only leave your own queue entries"
7847
- );
7848
- }
7849
- await leaveQueue(db, queueId);
7850
- await logAuditEvent(db, {
7851
- session_id: sessionId,
7852
- action: "queue_left",
7853
- entity_type: "queue",
7854
- entity_id: queueId,
7855
- metadata: { claim_id: entry.claim_id }
7856
- });
7857
- return successResponse({
7858
- success: true,
7859
- message: "Left the queue successfully"
7860
- });
7861
- }
7862
- case "collab_queue_list": {
7863
- const validation = validateInput(queueListSchema, args);
7864
- if (!validation.success) {
7865
- return validationError(validation.error);
7866
- }
7867
- const { claim_id: claimId, session_id: sessionId } = validation.data;
7868
- const entries = await listQueue(db, { claim_id: claimId, session_id: sessionId });
7869
- return successResponse({
7870
- entries: entries.map((e) => ({
7871
- queue_id: e.id,
7872
- claim_id: e.claim_id,
7873
- session_id: e.session_id,
7874
- session_name: e.session_name,
7875
- intent: e.intent,
7876
- position: e.position,
7877
- priority: getPriorityLevel(e.priority),
7878
- scope: e.scope,
7879
- estimated_wait_minutes: e.estimated_wait_minutes,
7880
- claim_owner: e.claim_session_name,
7881
- claim_intent: e.claim_intent,
7882
- files: e.claim_files,
7883
- created_at: e.created_at
7884
- })),
7885
- total: entries.length,
7886
- message: entries.length > 0 ? `Found ${entries.length} queue entries` : "No queue entries found"
7887
- }, true);
7888
- }
7889
- default:
7890
- return successResponse({ error: `Unknown queue tool: ${name}` });
7891
- }
7892
- }
7893
-
7894
- // src/mcp/tools/notification.ts
7895
- var notificationTools = [
7896
- {
7897
- name: "collab_notifications_list",
7898
- description: "List notifications for your session. Check periodically for claim releases, queue updates, and messages.",
7899
- inputSchema: {
7900
- type: "object",
7901
- properties: {
7902
- session_id: {
7903
- type: "string",
7904
- description: "Your session ID"
7905
- },
7906
- unread_only: {
7907
- type: "boolean",
7908
- description: "Only show unread notifications (default: false)"
7909
- },
7910
- type: {
7911
- type: "string",
7912
- enum: ["claim_released", "queue_ready", "conflict_detected", "session_message"],
7913
- description: "Filter by notification type"
7914
- },
7915
- limit: {
7916
- type: "number",
7917
- description: "Maximum notifications to return (default: 50, max: 100)"
7918
- }
7919
- },
7920
- required: ["session_id"]
7921
- }
7922
- },
7923
- {
7924
- name: "collab_notifications_mark_read",
7925
- description: "Mark notifications as read.",
7926
- inputSchema: {
7927
- type: "object",
7928
- properties: {
7929
- session_id: {
7930
- type: "string",
7931
- description: "Your session ID"
7932
- },
7933
- notification_ids: {
7934
- type: "array",
7935
- items: { type: "string" },
7936
- description: "Notification IDs to mark as read"
7937
- }
7938
- },
7939
- required: ["session_id", "notification_ids"]
7940
- }
7941
- }
7942
- ];
7943
- async function handleNotificationTool(db, name, args) {
7944
- switch (name) {
7945
- case "collab_notifications_list": {
7946
- const validation = validateInput(notificationListSchema, args);
7947
- if (!validation.success) {
7948
- return validationError(validation.error);
7949
- }
7950
- const { session_id: sessionId, unread_only, type, limit } = validation.data;
7951
- const sessionResult = await validateActiveSession(db, sessionId);
7952
- if (!sessionResult.valid) {
7953
- return sessionResult.error;
7954
- }
7955
- const notifications = await listNotifications(db, {
7956
- session_id: sessionId,
7957
- unread_only,
7958
- type,
7959
- limit
7960
- });
7961
- const unreadCount = notifications.filter((n) => !n.read_at).length;
7962
- return successResponse({
7963
- notifications: notifications.map((n) => ({
7964
- id: n.id,
7965
- type: n.type,
7966
- title: n.title,
7967
- message: n.message,
7968
- reference_type: n.reference_type,
7969
- reference_id: n.reference_id,
7970
- metadata: n.metadata ? JSON.parse(n.metadata) : null,
7971
- is_read: !!n.read_at,
7972
- created_at: n.created_at
7973
- })),
7974
- total: notifications.length,
7975
- unread_count: unreadCount,
7976
- message: notifications.length > 0 ? `${notifications.length} notification(s), ${unreadCount} unread` : "No notifications"
7977
- }, true);
7978
- }
7979
- case "collab_notifications_mark_read": {
7980
- const validation = validateInput(notificationMarkReadSchema, args);
7981
- if (!validation.success) {
7982
- return validationError(validation.error);
7983
- }
7984
- const { session_id: sessionId, notification_ids: notificationIds } = validation.data;
7985
- const sessionResult = await validateActiveSession(db, sessionId);
7986
- if (!sessionResult.valid) {
7987
- return sessionResult.error;
7988
- }
7989
- for (const notifId of notificationIds) {
7990
- const notif = await getNotification(db, notifId);
7991
- if (!notif) {
7992
- return errorResponse(
7993
- ERROR_CODES.NOTIFICATION_NOT_FOUND,
7994
- `Notification ${notifId} not found`
7995
- );
7996
- }
7997
- if (notif.session_id !== sessionId) {
7998
- return errorResponse(
7999
- ERROR_CODES.NOT_OWNER,
8000
- "You can only mark your own notifications as read"
8001
- );
8002
- }
8003
- }
8004
- const markedCount = await markNotificationsRead(db, notificationIds);
8005
- return successResponse({
8006
- success: true,
8007
- marked_count: markedCount,
8008
- message: `Marked ${markedCount} notification(s) as read`
8009
- });
8010
- }
8011
- default:
8012
- return successResponse({ error: `Unknown notification tool: ${name}` });
8013
- }
8014
- }
8015
-
8016
- // src/mcp/tools/memory.ts
8017
- var memoryTools = [
8018
- {
8019
- name: "collab_memory_save",
8020
- description: "Save important context to working memory. Use this to persist findings, decisions, state, or any critical information that should survive context compaction.",
8021
- inputSchema: {
8022
- type: "object",
8023
- properties: {
8024
- session_id: {
8025
- type: "string",
8026
- description: "Your session ID"
8027
- },
8028
- category: {
8029
- type: "string",
8030
- enum: ["finding", "decision", "state", "todo", "important", "context"],
8031
- description: "Category of memory: finding (discovered facts), decision (choices made), state (current status), todo (pending work), important (critical info), context (general context)"
8032
- },
8033
- key: {
8034
- type: "string",
8035
- description: 'Unique identifier for this memory within the category (e.g., "auth_bug_root_cause")'
8036
- },
8037
- content: {
8038
- type: "string",
8039
- description: "The actual content to remember"
8040
- },
8041
- priority: {
8042
- type: "number",
8043
- description: "Priority 0-100 (default 50). Higher priority memories are more likely to be recalled."
8044
- },
8045
- pinned: {
8046
- type: "boolean",
8047
- description: "If true, this memory will always be loaded when recalling active memories."
8048
- },
8049
- expires_at: {
8050
- type: "string",
8051
- description: "Optional ISO datetime when this memory should expire (e.g., for temporary state)"
8052
- },
8053
- related_claim_id: {
8054
- type: "string",
8055
- description: "Optional claim ID this memory relates to"
8056
- },
8057
- metadata: {
8058
- type: "object",
8059
- description: "Optional additional structured data (file_path, line_number, confidence, etc.)"
8060
- }
8061
- },
8062
- required: ["session_id", "category", "key", "content"]
8063
- }
8064
- },
8065
- {
8066
- name: "collab_memory_recall",
8067
- description: "Recall memories from working memory. Use this to retrieve previously saved context, findings, or state.",
8068
- inputSchema: {
8069
- type: "object",
8070
- properties: {
8071
- session_id: {
8072
- type: "string",
8073
- description: "Your session ID"
8074
- },
8075
- category: {
8076
- type: "string",
8077
- enum: ["finding", "decision", "state", "todo", "important", "context"],
8078
- description: "Filter by category (optional)"
8079
- },
8080
- key: {
8081
- type: "string",
8082
- description: "Get a specific memory by key (optional)"
8083
- },
8084
- pinned_only: {
8085
- type: "boolean",
8086
- description: "Only return pinned memories"
8087
- },
8088
- limit: {
8089
- type: "number",
8090
- description: "Maximum number of memories to return (default: all)"
8091
- }
8092
- },
8093
- required: ["session_id"]
8094
- }
8095
- },
8096
- {
8097
- name: "collab_memory_update",
8098
- description: "Update an existing memory entry. Use this to modify content, priority, or other properties.",
8099
- inputSchema: {
8100
- type: "object",
8101
- properties: {
8102
- session_id: {
8103
- type: "string",
8104
- description: "Your session ID"
8105
- },
8106
- key: {
8107
- type: "string",
8108
- description: "The key of the memory to update"
8109
- },
8110
- content: {
8111
- type: "string",
8112
- description: "New content (optional)"
8113
- },
8114
- priority: {
8115
- type: "number",
8116
- description: "New priority 0-100 (optional)"
8117
- },
8118
- pinned: {
8119
- type: "boolean",
8120
- description: "New pinned status (optional)"
8121
- },
8122
- expires_at: {
8123
- type: "string",
8124
- description: "New expiration time, or null to remove expiration (optional)"
8125
- },
8126
- metadata: {
8127
- type: "object",
8128
- description: "New metadata (replaces existing)"
8129
- }
8130
- },
8131
- required: ["session_id", "key"]
8132
- }
8133
- },
8134
- {
8135
- name: "collab_memory_clear",
8136
- description: "Clear memories from working memory. Can clear by key, category, or all.",
8137
- inputSchema: {
8138
- type: "object",
8139
- properties: {
8140
- session_id: {
8141
- type: "string",
8142
- description: "Your session ID"
8143
- },
8144
- key: {
8145
- type: "string",
8146
- description: "Clear a specific memory by key"
8147
- },
8148
- category: {
8149
- type: "string",
8150
- enum: ["finding", "decision", "state", "todo", "important", "context"],
8151
- description: "Clear all memories in a category"
8152
- },
8153
- clear_all: {
8154
- type: "boolean",
8155
- description: "Clear ALL memories for this session (use with caution)"
8156
- }
8157
- },
8158
- required: ["session_id"]
8159
- }
8160
- },
8161
- {
8162
- name: "collab_memory_pin",
8163
- description: "Pin or unpin a memory. Pinned memories are always included when loading active memories.",
8164
- inputSchema: {
8165
- type: "object",
8166
- properties: {
8167
- session_id: {
8168
- type: "string",
8169
- description: "Your session ID"
8170
- },
8171
- key: {
8172
- type: "string",
8173
- description: "The key of the memory to pin/unpin"
8174
- },
8175
- pinned: {
8176
- type: "boolean",
8177
- description: "True to pin, false to unpin"
8178
- }
8179
- },
8180
- required: ["session_id", "key", "pinned"]
8181
- }
8182
- },
8183
- {
8184
- name: "collab_memory_stats",
8185
- description: "Get statistics about working memory usage for this session.",
8186
- inputSchema: {
8187
- type: "object",
8188
- properties: {
8189
- session_id: {
8190
- type: "string",
8191
- description: "Your session ID"
8192
- }
8193
- },
8194
- required: ["session_id"]
8195
- }
8196
- },
8197
- {
8198
- name: "collab_memory_active",
8199
- description: "Get all active (pinned + high priority) memories. This is the main function for loading context that should be preserved.",
8200
- inputSchema: {
8201
- type: "object",
8202
- properties: {
8203
- session_id: {
8204
- type: "string",
8205
- description: "Your session ID"
8206
- },
8207
- priority_threshold: {
8208
- type: "number",
8209
- description: "Minimum priority to include (default: 70)"
8210
- },
8211
- max_items: {
8212
- type: "number",
8213
- description: "Maximum items to return (default: 20)"
8214
- }
8215
- },
8216
- required: ["session_id"]
8217
- }
8218
- }
8219
- ];
8220
- async function handleMemoryTool(db, name, args) {
8221
- switch (name) {
8222
- case "collab_memory_save":
8223
- return handleMemorySave(db, args);
8224
- case "collab_memory_recall":
8225
- return handleMemoryRecall(db, args);
8226
- case "collab_memory_update":
8227
- return handleMemoryUpdate(db, args);
8228
- case "collab_memory_clear":
8229
- return handleMemoryClear(db, args);
8230
- case "collab_memory_pin":
8231
- return handleMemoryPin(db, args);
8232
- case "collab_memory_stats":
8233
- return handleMemoryStats(db, args);
8234
- case "collab_memory_active":
8235
- return handleMemoryActive(db, args);
8236
- default:
8237
- return errorResponse(ERROR_CODES.UNKNOWN_TOOL, `Unknown memory tool: ${name}`);
8238
- }
8239
- }
8240
- async function handleMemorySave(db, args) {
8241
- const sessionId = args.session_id;
8242
- const category = args.category;
8243
- const key = args.key;
8244
- const content = args.content;
8245
- if (!sessionId || !category || !key || !content) {
8246
- return validationError("session_id, category, key, and content are required");
8247
- }
8248
- const sessionCheck = await validateActiveSession(db, sessionId);
8249
- if (!sessionCheck.valid) {
8250
- return sessionCheck.error;
8251
- }
8252
- const memory = await saveMemory(db, sessionId, {
8253
- category,
8254
- key,
8255
- content,
8256
- priority: args.priority ?? 50,
8257
- pinned: args.pinned ?? false,
8258
- expires_at: args.expires_at,
8259
- related_claim_id: args.related_claim_id,
8260
- related_decision_id: args.related_decision_id,
8261
- metadata: args.metadata
8262
- });
8263
- return successResponse({
8264
- saved: true,
8265
- memory: {
8266
- id: memory.id,
8267
- category: memory.category,
8268
- key: memory.key,
8269
- priority: memory.priority,
8270
- pinned: memory.pinned === 1,
8271
- created_at: memory.created_at,
8272
- updated_at: memory.updated_at
8273
- },
8274
- message: `Memory saved: ${category}/${key}`
8275
- });
8276
- }
8277
- async function handleMemoryRecall(db, args) {
8278
- const sessionId = args.session_id;
8279
- if (!sessionId) {
8280
- return validationError("session_id is required");
8281
- }
8282
- const sessionCheck = await validateActiveSession(db, sessionId);
8283
- if (!sessionCheck.valid) {
8284
- return sessionCheck.error;
8285
- }
8286
- const memories = await recallMemory(db, sessionId, {
8287
- category: args.category,
8288
- key: args.key,
8289
- pinned_only: args.pinned_only,
8290
- limit: args.limit
8291
- });
8292
- const formattedMemories = memories.map((m) => ({
8293
- category: m.category,
8294
- key: m.key,
8295
- content: m.content,
8296
- priority: m.priority,
8297
- pinned: m.pinned === 1,
8298
- created_at: m.created_at,
8299
- updated_at: m.updated_at,
8300
- expires_at: m.expires_at,
8301
- metadata: m.metadata ? JSON.parse(m.metadata) : null
8302
- }));
8303
- return successResponse({
8304
- count: formattedMemories.length,
8305
- memories: formattedMemories
8306
- });
8307
- }
8308
- async function handleMemoryUpdate(db, args) {
8309
- const sessionId = args.session_id;
8310
- const key = args.key;
8311
- if (!sessionId || !key) {
8312
- return validationError("session_id and key are required");
8313
- }
8314
- const sessionCheck = await validateActiveSession(db, sessionId);
8315
- if (!sessionCheck.valid) {
8316
- return sessionCheck.error;
8317
- }
8318
- const updates = {};
8319
- if (args.content !== void 0) updates.content = args.content;
8320
- if (args.priority !== void 0) updates.priority = args.priority;
8321
- if (args.pinned !== void 0) updates.pinned = args.pinned;
8322
- if (args.expires_at !== void 0) updates.expires_at = args.expires_at;
8323
- if (args.metadata !== void 0) updates.metadata = args.metadata;
8324
- if (Object.keys(updates).length === 0) {
8325
- return validationError("At least one field to update is required");
8326
- }
8327
- const updated = await updateMemory(db, sessionId, key, updates);
8328
- if (!updated) {
8329
- return errorResponse(ERROR_CODES.MEMORY_NOT_FOUND, `Memory not found: ${key}`);
8330
- }
8331
- return successResponse({
8332
- updated: true,
8333
- key,
8334
- message: `Memory updated: ${key}`
8335
- });
8336
- }
8337
- async function handleMemoryClear(db, args) {
8338
- const sessionId = args.session_id;
8339
- if (!sessionId) {
8340
- return validationError("session_id is required");
8341
- }
8342
- const sessionCheck = await validateActiveSession(db, sessionId);
8343
- if (!sessionCheck.valid) {
8344
- return sessionCheck.error;
8345
- }
8346
- const key = args.key;
8347
- const category = args.category;
8348
- const clearAll = args.clear_all;
8349
- if (!key && !category && !clearAll) {
8350
- return validationError("One of key, category, or clear_all is required");
8351
- }
8352
- const cleared = await clearMemory(db, sessionId, {
8353
- key,
8354
- category,
8355
- clear_all: clearAll
8356
- });
8357
- let message;
8358
- if (key) {
8359
- message = cleared > 0 ? `Memory cleared: ${key}` : `Memory not found: ${key}`;
8360
- } else if (category) {
8361
- message = `Cleared ${cleared} memories from category: ${category}`;
8362
- } else {
8363
- message = `Cleared all ${cleared} memories`;
8364
- }
8365
- return successResponse({
8366
- cleared,
8367
- message
8368
- });
8369
- }
8370
- async function handleMemoryPin(db, args) {
8371
- const sessionId = args.session_id;
8372
- const key = args.key;
8373
- const pinned = args.pinned;
8374
- if (!sessionId || !key || pinned === void 0) {
8375
- return validationError("session_id, key, and pinned are required");
8376
- }
8377
- const sessionCheck = await validateActiveSession(db, sessionId);
8378
- if (!sessionCheck.valid) {
8379
- return sessionCheck.error;
8380
- }
8381
- const updated = await pinMemory(db, sessionId, key, pinned);
8382
- if (!updated) {
8383
- return errorResponse(ERROR_CODES.MEMORY_NOT_FOUND, `Memory not found: ${key}`);
8384
- }
8385
- return successResponse({
8386
- updated: true,
8387
- key,
8388
- pinned,
8389
- message: pinned ? `Memory pinned: ${key}` : `Memory unpinned: ${key}`
8390
- });
8391
- }
8392
- async function handleMemoryStats(db, args) {
8393
- const sessionId = args.session_id;
8394
- if (!sessionId) {
8395
- return validationError("session_id is required");
8396
- }
8397
- const sessionCheck = await validateActiveSession(db, sessionId);
8398
- if (!sessionCheck.valid) {
8399
- return sessionCheck.error;
8400
- }
8401
- const stats = await getMemoryStats(db, sessionId);
8402
- return successResponse({
8403
- ...stats,
8404
- message: `Working memory: ${stats.total} total, ${stats.pinned_count} pinned`
8405
- });
8406
- }
8407
- async function handleMemoryActive(db, args) {
8408
- const sessionId = args.session_id;
8409
- if (!sessionId) {
8410
- return validationError("session_id is required");
8411
- }
8412
- const sessionCheck = await validateActiveSession(db, sessionId);
8413
- if (!sessionCheck.valid) {
8414
- return sessionCheck.error;
8415
- }
8416
- const memories = await getActiveMemories(db, sessionId, {
8417
- priority_threshold: args.priority_threshold,
8418
- max_items: args.max_items
8419
- });
8420
- const formattedMemories = memories.map((m) => ({
8421
- category: m.category,
8422
- key: m.key,
8423
- content: m.content,
8424
- priority: m.priority,
8425
- pinned: m.pinned === 1
8426
- }));
8427
- const byCategory = {};
8428
- for (const mem of formattedMemories) {
8429
- if (!byCategory[mem.category]) {
8430
- byCategory[mem.category] = [];
8431
- }
8432
- byCategory[mem.category].push({
8433
- key: mem.key,
8434
- content: mem.content,
8435
- priority: mem.priority,
8436
- pinned: mem.pinned
8437
- });
8438
- }
8439
- return successResponse({
8440
- count: formattedMemories.length,
8441
- by_category: byCategory,
8442
- memories: formattedMemories,
8443
- message: `Active memories: ${formattedMemories.length} items (pinned + priority >= threshold)`
8444
- });
8445
- }
8446
-
8447
- // src/mcp/tools/protection.ts
8448
- var protectionTools = [
8449
- {
8450
- name: "collab_plan_register",
8451
- description: "Register a plan document for protection. Plans are automatically pinned with high priority to prevent context loss.",
8452
- inputSchema: {
8453
- type: "object",
8454
- properties: {
8455
- session_id: {
8456
- type: "string",
8457
- description: "Your session ID"
8458
- },
8459
- file_path: {
8460
- type: "string",
8461
- description: "Path to the plan file"
8462
- },
8463
- title: {
8464
- type: "string",
8465
- description: "Title of the plan"
8466
- },
8467
- content_summary: {
8468
- type: "string",
8469
- description: "Summary of the plan content (key points, steps)"
8470
- },
8471
- status: {
8472
- type: "string",
8473
- enum: ["draft", "approved", "in_progress"],
8474
- description: "Initial status of the plan (default: draft)"
8475
- }
8476
- },
8477
- required: ["session_id", "file_path", "title", "content_summary"]
8478
- }
8479
- },
8480
- {
8481
- name: "collab_plan_update_status",
8482
- description: "Update plan status. Use this to progress plan through lifecycle: draft \u2192 approved \u2192 in_progress \u2192 completed \u2192 archived",
8483
- inputSchema: {
8484
- type: "object",
8485
- properties: {
8486
- session_id: {
8487
- type: "string",
8488
- description: "Your session ID"
8489
- },
8490
- file_path: {
8491
- type: "string",
8492
- description: "Path to the plan file"
8493
- },
8494
- status: {
8495
- type: "string",
8496
- enum: ["draft", "approved", "in_progress", "completed", "archived"],
8497
- description: "New status for the plan"
8498
- },
8499
- summary: {
8500
- type: "string",
8501
- description: "Optional updated summary (for completed plans, describe what was achieved)"
8502
- }
8503
- },
8504
- required: ["session_id", "file_path", "status"]
8505
- }
8506
- },
8507
- {
8508
- name: "collab_plan_get",
8509
- description: "Get plan information by file path.",
8510
- inputSchema: {
8511
- type: "object",
8512
- properties: {
8513
- session_id: {
8514
- type: "string",
8515
- description: "Your session ID"
8516
- },
8517
- file_path: {
8518
- type: "string",
8519
- description: "Path to the plan file"
8520
- }
8521
- },
8522
- required: ["session_id", "file_path"]
8523
- }
8524
- },
8525
- {
8526
- name: "collab_plan_list",
8527
- description: "List all plans for this session.",
8528
- inputSchema: {
8529
- type: "object",
8530
- properties: {
8531
- session_id: {
8532
- type: "string",
8533
- description: "Your session ID"
8534
- },
8535
- status: {
8536
- type: "string",
8537
- enum: ["draft", "approved", "in_progress", "completed", "archived"],
8538
- description: "Filter by status"
8539
- },
8540
- include_archived: {
8541
- type: "boolean",
8542
- description: "Include archived plans (default: false)"
8543
- }
8544
- },
8545
- required: ["session_id"]
8546
- }
8547
- },
8548
- {
8549
- name: "collab_file_register",
8550
- description: "Register a file created in this session for protection. Protected files will trigger warnings before deletion.",
8551
- inputSchema: {
8552
- type: "object",
8553
- properties: {
8554
- session_id: {
8555
- type: "string",
8556
- description: "Your session ID"
8557
- },
8558
- file_path: {
8559
- type: "string",
8560
- description: "Path to the created file"
8561
- },
8562
- file_type: {
8563
- type: "string",
8564
- enum: ["plan", "code", "config", "doc", "other"],
8565
- description: "Type of file (default: other)"
8566
- },
8567
- description: {
8568
- type: "string",
8569
- description: "Brief description of the file purpose"
8570
- }
8571
- },
8572
- required: ["session_id", "file_path"]
8573
- }
8574
- },
8575
- {
8576
- name: "collab_file_list_created",
8577
- description: "List all files created in this session.",
8578
- inputSchema: {
8579
- type: "object",
8580
- properties: {
8581
- session_id: {
8582
- type: "string",
8583
- description: "Your session ID"
8584
- }
8585
- },
8586
- required: ["session_id"]
8587
- }
8588
- },
8589
- {
8590
- name: "collab_file_check_protected",
8591
- description: "Check if a file is protected. Use this before deleting or overwriting files to prevent accidental loss.",
8592
- inputSchema: {
8593
- type: "object",
8594
- properties: {
8595
- session_id: {
8596
- type: "string",
8597
- description: "Your session ID"
8598
- },
8599
- file_path: {
8600
- type: "string",
8601
- description: "Path to check"
8602
- }
8603
- },
8604
- required: ["session_id", "file_path"]
8605
- }
8606
- },
8607
- {
8608
- name: "collab_file_list_protected",
8609
- description: "List all protected files in this session (plans + created files).",
8610
- inputSchema: {
8611
- type: "object",
8612
- properties: {
8613
- session_id: {
8614
- type: "string",
8615
- description: "Your session ID"
8616
- }
8617
- },
8618
- required: ["session_id"]
8619
- }
8620
- }
8621
- ];
8622
- async function handleProtectionTool(db, name, args) {
8623
- switch (name) {
8624
- case "collab_plan_register":
8625
- return handlePlanRegister(db, args);
8626
- case "collab_plan_update_status":
8627
- return handlePlanUpdateStatus(db, args);
8628
- case "collab_plan_get":
8629
- return handlePlanGet(db, args);
8630
- case "collab_plan_list":
8631
- return handlePlanList(db, args);
8632
- case "collab_file_register":
8633
- return handleFileRegister(db, args);
8634
- case "collab_file_list_created":
8635
- return handleFileListCreated(db, args);
8636
- case "collab_file_check_protected":
8637
- return handleFileCheckProtected(db, args);
8638
- case "collab_file_list_protected":
8639
- return handleFileListProtected(db, args);
8640
- default:
8641
- return errorResponse(ERROR_CODES.UNKNOWN_TOOL, `Unknown protection tool: ${name}`);
8642
- }
8643
- }
8644
- async function handlePlanRegister(db, args) {
8645
- const sessionId = args.session_id;
8646
- const filePath = args.file_path;
8647
- const title = args.title;
8648
- const contentSummary = args.content_summary;
8649
- const status = args.status ?? "draft";
8650
- if (!sessionId || !filePath || !title || !contentSummary) {
8651
- return validationError("session_id, file_path, title, and content_summary are required");
8652
- }
8653
- const sessionCheck = await validateActiveSession(db, sessionId);
8654
- if (!sessionCheck.valid) {
8655
- return sessionCheck.error;
8656
- }
8657
- const memory = await registerPlan(db, sessionId, {
8658
- file_path: filePath,
8659
- title,
8660
- content_summary: contentSummary,
8661
- status
8662
- });
8663
- return successResponse({
8664
- registered: true,
8665
- plan: {
8666
- file_path: filePath,
8667
- title,
8668
- status,
8669
- priority: memory.priority,
8670
- pinned: memory.pinned === 1
8671
- },
8672
- message: `Plan registered and protected: ${title}`
8673
- });
8674
- }
8675
- async function handlePlanUpdateStatus(db, args) {
8676
- const sessionId = args.session_id;
8677
- const filePath = args.file_path;
8678
- const status = args.status;
8679
- const summary = args.summary;
8680
- if (!sessionId || !filePath || !status) {
8681
- return validationError("session_id, file_path, and status are required");
8682
- }
8683
- const sessionCheck = await validateActiveSession(db, sessionId);
8684
- if (!sessionCheck.valid) {
8685
- return sessionCheck.error;
8686
- }
8687
- const updated = await updatePlanStatus(db, sessionId, filePath, status, summary);
8688
- if (!updated) {
8689
- return errorResponse(ERROR_CODES.MEMORY_NOT_FOUND, `Plan not found: ${filePath}`);
8690
- }
8691
- let message = `Plan status updated to: ${status}`;
8692
- if (status === "completed") {
8693
- message += ". Plan protection reduced (unpinned, lower priority).";
8694
- } else if (status === "archived") {
8695
- message += ". Plan archived and will be excluded from active memory.";
8696
- }
8697
- return successResponse({
8698
- updated: true,
8699
- file_path: filePath,
8700
- new_status: status,
8701
- message
8702
- });
8703
- }
8704
- async function handlePlanGet(db, args) {
8705
- const sessionId = args.session_id;
8706
- const filePath = args.file_path;
8707
- if (!sessionId || !filePath) {
8708
- return validationError("session_id and file_path are required");
8709
- }
8710
- const sessionCheck = await validateActiveSession(db, sessionId);
8711
- if (!sessionCheck.valid) {
8712
- return sessionCheck.error;
8713
- }
8714
- const plan = await getPlan(db, sessionId, filePath);
8715
- if (!plan) {
8716
- return successResponse({
8717
- found: false,
8718
- message: `No plan found for: ${filePath}`
8719
- });
8720
- }
8721
- return successResponse({
8722
- found: true,
8723
- plan
8724
- });
8725
- }
8726
- async function handlePlanList(db, args) {
8727
- const sessionId = args.session_id;
8728
- const status = args.status;
8729
- const includeArchived = args.include_archived;
8730
- if (!sessionId) {
8731
- return validationError("session_id is required");
8732
- }
8733
- const sessionCheck = await validateActiveSession(db, sessionId);
8734
- if (!sessionCheck.valid) {
8735
- return sessionCheck.error;
8736
- }
8737
- const plans = await listPlans(db, sessionId, { status, include_archived: includeArchived });
8738
- return successResponse({
8739
- count: plans.length,
8740
- plans
8741
- });
8742
- }
8743
- async function handleFileRegister(db, args) {
8744
- const sessionId = args.session_id;
8745
- const filePath = args.file_path;
8746
- const fileType = args.file_type;
8747
- const description = args.description;
8748
- if (!sessionId || !filePath) {
8749
- return validationError("session_id and file_path are required");
8750
- }
8751
- const sessionCheck = await validateActiveSession(db, sessionId);
8752
- if (!sessionCheck.valid) {
8753
- return sessionCheck.error;
8754
- }
8755
- const memory = await registerCreatedFile(db, sessionId, {
8756
- file_path: filePath,
8757
- file_type: fileType,
8758
- description
8759
- });
8760
- return successResponse({
8761
- registered: true,
8762
- file: {
8763
- file_path: filePath,
8764
- file_type: fileType ?? "other",
8765
- priority: memory.priority,
8766
- pinned: memory.pinned === 1
8767
- },
8768
- message: `File registered for protection: ${filePath}`
8769
- });
8770
- }
8771
- async function handleFileListCreated(db, args) {
8772
- const sessionId = args.session_id;
8773
- if (!sessionId) {
8774
- return validationError("session_id is required");
8775
- }
8776
- const sessionCheck = await validateActiveSession(db, sessionId);
8777
- if (!sessionCheck.valid) {
8778
- return sessionCheck.error;
8779
- }
8780
- const files = await getCreatedFiles(db, sessionId);
8781
- return successResponse({
8782
- count: files.length,
8783
- files
8784
- });
8785
- }
8786
- async function handleFileCheckProtected(db, args) {
8787
- const sessionId = args.session_id;
8788
- const filePath = args.file_path;
8789
- if (!sessionId || !filePath) {
8790
- return validationError("session_id and file_path are required");
8791
- }
8792
- const sessionCheck = await validateActiveSession(db, sessionId);
8793
- if (!sessionCheck.valid) {
8794
- return sessionCheck.error;
8795
- }
8796
- const result = await isFileProtected(db, sessionId, filePath);
8797
- if (result.protected) {
8798
- return successResponse({
8799
- protected: true,
8800
- reason: result.reason,
8801
- details: result.details,
8802
- warning: `\u26A0\uFE0F This file is protected (${result.reason}). Confirm before deleting or overwriting.`
8803
- });
8804
- }
8805
- return successResponse({
8806
- protected: false,
8807
- message: "File is not protected."
8808
- });
8809
- }
8810
- async function handleFileListProtected(db, args) {
8811
- const sessionId = args.session_id;
8812
- if (!sessionId) {
8813
- return validationError("session_id is required");
8814
- }
8815
- const sessionCheck = await validateActiveSession(db, sessionId);
8816
- if (!sessionCheck.valid) {
8817
- return sessionCheck.error;
8818
- }
8819
- const files = await getProtectedFiles(db, sessionId);
8820
- return successResponse({
8821
- count: files.length,
8822
- files,
8823
- message: files.length > 0 ? `${files.length} protected file(s). Check before deleting.` : "No protected files."
8824
- });
8825
- }
8826
-
8827
- // src/constants.ts
8828
- import { readFileSync } from "fs";
8829
- import { join as join2, dirname as dirname2 } from "path";
8830
- import { fileURLToPath } from "url";
8831
- function getVersion() {
8832
- try {
8833
- const __dirname3 = dirname2(fileURLToPath(import.meta.url));
8834
- const pkg = JSON.parse(readFileSync(join2(__dirname3, "..", "package.json"), "utf-8"));
8835
- return pkg.version;
8836
- } catch {
8837
- return "0.0.0";
8838
- }
8839
- }
8840
- var VERSION = getVersion();
8841
- var SERVER_NAME = "session-collab-mcp";
8842
- var SERVER_INSTRUCTIONS = `
8843
- # Session Collaboration
8844
-
8845
- This MCP server coordinates multiple Claude Code sessions working on the same codebase.
8846
-
8847
- ## Required Behaviors
8848
-
8849
- 1. **On conversation start**: Immediately call \`collab_session_start\` with:
8850
- - project_root: Current working directory
8851
- - name: Descriptive session name (e.g., "feature-auth", "bugfix-api")
8852
-
8853
- 2. **Before editing any file**: Call \`collab_check\` with the file path to verify no conflicts
8854
-
8855
- 3. **Follow the \`recommendation\` from collab_check automatically**:
8856
- - \`proceed_all\`: All files/symbols safe. Edit them without asking user.
8857
- - \`proceed_safe_only\`: Some content blocked. Edit ONLY safe files/symbols, skip blocked. No need to ask user.
8858
- - \`abort\`: All content blocked. Inform user and suggest coordination.
8859
-
8860
- 4. **For significant changes**: Call \`collab_claim\` before starting work
8861
-
8862
- 5. **When done**: Call \`collab_release\` with YOUR session_id to free them
8863
- - Or use \`collab_auto_release\` after Edit/Write to release individual files
8864
-
8865
- 6. **On conversation end**: Call \`collab_session_end\` to clean up
8866
-
8867
- ## Auto-Release (NEW in 0.7.1)
8868
-
8869
- Automatically release claims after editing:
8870
-
8871
- ### Immediate Release
8872
- After Edit/Write, call \`collab_auto_release\` with:
8873
- - \`session_id\`: Your session ID
8874
- - \`file_path\`: The file you just edited
8875
- - \`force\`: (optional) Force release for medium/large scope claims
8876
-
8877
- **Behavior by scope:**
8878
- - \`small\`: Auto-releases immediately
8879
- - \`medium/large\`: Requires \`force=true\` or \`auto_release_immediate\` config
8880
-
8881
- ### Stale Claim Cleanup
8882
- Claims are automatically cleaned up when:
8883
- - Session has \`auto_release_stale: true\` in config
8884
- - Claim exceeds \`stale_threshold_hours\` (default: 2 hours)
8885
-
8886
- ### Configuration
8887
- Use \`collab_config\` to enable auto-release:
8888
- \`\`\`json
8889
- {
8890
- "session_id": "...",
8891
- "auto_release_immediate": true,
8892
- "auto_release_stale": true,
8893
- "stale_threshold_hours": 2,
8894
- "auto_release_delay_minutes": 5
8895
- }
8896
- \`\`\`
8897
-
8898
- ## Symbol-Level Claims (Fine-Grained)
8899
-
8900
- Use symbol-level claims when modifying specific functions/classes, allowing other sessions to work on different parts of the same file.
8901
-
8902
- **Claim specific symbols:**
8903
- \`\`\`json
8904
- {
8905
- "symbols": [
8906
- { "file": "src/auth.ts", "symbols": ["validateToken", "refreshToken"] }
8907
- ],
8908
- "intent": "Refactoring token validation"
8909
- }
8910
- \`\`\`
8911
-
8912
- **Check specific symbols:**
8913
- \`\`\`json
8914
- {
8915
- "files": ["src/auth.ts"],
8916
- "symbols": [
8917
- { "file": "src/auth.ts", "symbols": ["validateToken"] }
8918
- ]
8919
- }
8920
- \`\`\`
8921
-
8922
- **Conflict levels:**
8923
- - \`file\`: Whole file is claimed (no symbols specified)
8924
- - \`symbol\`: Only specific functions/classes are claimed
8925
-
8926
- **Example scenario:**
8927
- - Session A claims \`validateToken\` in auth.ts
8928
- - Session B wants to modify \`refreshToken\` in auth.ts
8929
- - \u2192 No conflict! Session B can proceed.
8930
-
8931
- ## Auto-Decision Rules
8932
-
8933
- When \`collab_check\` returns:
8934
- - \`can_edit: true\` \u2192 Proceed with safe content automatically
8935
- - \`can_edit: false\` \u2192 Stop and inform user about blocked content
8936
-
8937
- For symbol-level checks, use \`symbol_status.safe\` and \`symbol_status.blocked\`.
8938
-
8939
- ## Permission Rules
8940
-
8941
- - You can ONLY release claims that belong to YOUR session
8942
- - To release another session's claim, you must ask the user and they must explicitly confirm
8943
- - Use \`force=true\` in \`collab_release\` only after user explicitly confirms
8944
-
8945
- ## Conflict Handling Modes
8946
-
8947
- Configure your session behavior with \`collab_config\`:
8948
-
8949
- - **"strict"**: Always ask user, never bypass or auto-release
8950
- - **"smart"** (default): Auto-proceed with safe content, ask for blocked
8951
- - **"bypass"**: Proceed despite conflicts (just warn, don't block)
8952
-
8953
- ## LSP Integration (Advanced)
8954
-
8955
- For precise symbol validation and impact analysis, use LSP tools:
8956
-
8957
- ### Workflow with LSP
8958
-
8959
- 1. **Get symbols from LSP**: Use \`LSP.documentSymbol\` to get actual symbols in a file
8960
- 2. **Validate before claiming**: Use \`collab_validate_symbols\` to verify symbol names
8961
- 3. **Analyze conflicts with context**: Use \`collab_analyze_symbols\` for enhanced conflict detection
8962
-
8963
- ### collab_validate_symbols
8964
-
8965
- Validate symbol names exist before claiming:
8966
-
8967
- \`\`\`
8968
- 1. Claude: LSP.documentSymbol("src/auth.ts")
8969
- 2. Claude: collab_validate_symbols({
8970
- file: "src/auth.ts",
8971
- symbols: ["validateToken", "refreshTokne"], // typo!
8972
- lsp_symbols: [/* LSP output */]
8973
- })
8974
- 3. Response: { invalid_symbols: ["refreshTokne"], suggestions: { "refreshTokne": ["refreshToken"] } }
8975
- \`\`\`
8976
-
8977
- ### collab_analyze_symbols
8978
-
8979
- Enhanced conflict detection with LSP data:
8980
-
8981
- \`\`\`
8982
- 1. Claude: LSP.documentSymbol("src/auth.ts")
8983
- 2. Claude: LSP.findReferences("validateToken")
8984
- 3. Claude: collab_analyze_symbols({
8985
- session_id: "...",
8986
- files: [{ file: "src/auth.ts", symbols: [/* LSP symbols */] }],
8987
- references: [{ symbol: "validateToken", file: "src/auth.ts", references: [...] }]
8988
- })
8989
- 4. Response: {
8990
- can_edit: true,
8991
- recommendation: "proceed_safe_only",
8992
- symbols: [
8993
- { name: "validateToken", conflict_status: "blocked", impact: { references_count: 5, affected_files: [...] } },
8994
- { name: "refreshToken", conflict_status: "safe" }
8995
- ]
8996
- }
8997
- \`\`\`
8998
-
8999
- ### Benefits of LSP Integration
9000
-
9001
- - **Accurate symbol names**: No typos in claims
9002
- - **Impact awareness**: Know which files will be affected by changes
9003
- - **Smart prioritization**: Focus on low-impact changes first
9004
-
9005
- ## Reference Tracking & Impact Analysis
9006
-
9007
- Store and query symbol references for smart conflict detection:
9008
-
9009
- ### collab_store_references
9010
-
9011
- Persist LSP reference data for future impact queries:
9012
-
9013
- \`\`\`
9014
- 1. Claude: LSP.findReferences("validateToken")
9015
- 2. Claude: collab_store_references({
9016
- session_id: "...",
9017
- references: [{
9018
- source_file: "src/auth.ts",
9019
- source_symbol: "validateToken",
9020
- references: [
9021
- { file: "src/api/users.ts", line: 15 },
9022
- { file: "src/api/orders.ts", line: 23 }
9023
- ]
9024
- }]
9025
- })
9026
- \`\`\`
9027
-
9028
- ### collab_impact_analysis
9029
-
9030
- Check if modifying a symbol would affect files claimed by others:
9031
-
9032
- \`\`\`
9033
- Claude: collab_impact_analysis({
9034
- session_id: "...",
9035
- file: "src/auth.ts",
9036
- symbol: "validateToken"
9037
- })
9038
-
9039
- Response: {
9040
- risk_level: "high",
9041
- reference_count: 3,
9042
- affected_files: ["src/api/users.ts", "src/api/orders.ts"],
9043
- affected_claims: [{ session_name: "other-session", intent: "..." }],
9044
- message: "HIGH RISK: 1 active claim on referencing files"
9045
- }
9046
- \`\`\`
9047
-
9048
- ### Risk Levels
9049
-
9050
- - **high**: Other sessions have claims on files that reference this symbol
9051
- - **medium**: Many references (>10) but no active claims conflict
9052
- - **low**: Few references, no conflicts
9053
-
9054
- ## Priority System
9055
-
9056
- Claims have priority levels (0-100):
9057
- - **Critical (90-100)**: Urgent production fixes
9058
- - **High (70-89)**: Important features
9059
- - **Normal (40-69)**: Regular work (default: 50)
9060
- - **Low (0-39)**: Nice-to-have changes
9061
-
9062
- Set priority when claiming:
9063
- \`\`\`json
9064
- {
9065
- "session_id": "...",
9066
- "files": ["src/critical-bug.ts"],
9067
- "intent": "Fix production crash",
9068
- "priority": 95
9069
- }
9070
- \`\`\`
9071
-
9072
- Use \`collab_claim_update_priority\` to escalate urgent work.
9073
-
9074
- ## Claim Queue
9075
-
9076
- When blocked by another claim, join the waiting queue:
9077
-
9078
- 1. **Join queue**: \`collab_queue_join\` with the blocked claim's ID
9079
- 2. **Check notifications**: \`collab_notifications_list\` for \`queue_ready\` notification
9080
- 3. **When notified**: Proceed with your work
9081
-
9082
- Higher priority claims are served first within the queue.
9083
-
9084
- Queue tools:
9085
- - \`collab_queue_join\`: Join waiting queue for a blocked claim
9086
- - \`collab_queue_leave\`: Leave the queue if no longer needed
9087
- - \`collab_queue_list\`: View queue status
9088
-
9089
- ## Notifications
9090
-
9091
- Check \`collab_notifications_list\` periodically for:
9092
- - **claim_released**: Claim you waited for is now available
9093
- - **queue_ready**: You're next in queue, proceed with your work
9094
- - **conflict_detected**: Someone claimed files you're interested in
9095
- - **session_message**: Direct message from another session
9096
-
9097
- Mark notifications as read with \`collab_notifications_mark_read\`.
9098
-
9099
- ## Audit History
9100
-
9101
- Use \`collab_history_list\` to debug coordination issues:
9102
- - Track session starts/ends
9103
- - View claim creation/release history
9104
- - Identify conflict patterns
9105
- - Monitor queue activity
9106
-
9107
- Filter by session, action type, or date range. Entries auto-deleted after 7 days.
9108
-
9109
- ## Working Memory (Context Persistence)
9110
-
9111
- Persist important context to survive Claude's automatic compaction/summarization.
9112
-
9113
- ### When to Use
9114
-
9115
- - **Findings**: Important discoveries during investigation
9116
- - **Decisions**: Choices made and their rationale
9117
- - **State**: Current status, file paths, variable values
9118
- - **Context**: General context that should be remembered
9119
-
9120
- ### Save Important Context
9121
-
9122
- \`\`\`
9123
- collab_memory_save({
9124
- session_id: "...",
9125
- category: "finding",
9126
- key: "auth_bug_root_cause",
9127
- content: "JWT validation skipped in middleware due to order issue",
9128
- priority: 80,
9129
- pinned: true
9130
- })
9131
- \`\`\`
9132
-
9133
- ### Recall Context
9134
-
9135
- \`\`\`
9136
- collab_memory_recall({
9137
- session_id: "...",
9138
- category: "finding" // optional filter
9139
- })
9140
- \`\`\`
9141
-
9142
- ### Get Active Memories
9143
-
9144
- Get all pinned + high-priority memories for context restoration:
9145
-
9146
- \`\`\`
9147
- collab_memory_active({
9148
- session_id: "...",
9149
- priority_threshold: 70
9150
- })
9151
- \`\`\`
9152
-
9153
- ### Memory Categories
9154
-
9155
- - **finding**: Discovered facts, root causes, patterns
9156
- - **decision**: Architectural choices, implementation decisions
9157
- - **state**: Current file, line number, tracking variables
9158
- - **todo**: Pending work items, follow-ups
9159
- - **important**: Critical information that must not be lost
9160
- - **context**: General context for understanding
9161
-
9162
- ### Priority Levels
9163
-
9164
- - **90-100**: Critical - always recall
9165
- - **70-89**: High - recall by default
9166
- - **50-69**: Normal - recall on demand
9167
- - **0-49**: Low - background context
9168
-
9169
- ### Pinned Memories
9170
-
9171
- Pin critical memories to ensure they are always loaded:
9172
-
9173
- \`\`\`
9174
- collab_memory_pin({
9175
- session_id: "...",
9176
- key: "root_cause",
9177
- pinned: true
9178
- })
9179
- \`\`\`
9180
-
9181
- ### Memory Tools
9182
-
9183
- | Tool | Purpose |
9184
- |------|---------|
9185
- | \`collab_memory_save\` | Save important context |
9186
- | \`collab_memory_recall\` | Retrieve saved memories |
9187
- | \`collab_memory_update\` | Update existing memory |
9188
- | \`collab_memory_clear\` | Clear memories |
9189
- | \`collab_memory_pin\` | Pin/unpin a memory |
9190
- | \`collab_memory_stats\` | Get memory statistics |
9191
- | \`collab_memory_active\` | Get all active memories |
9192
-
9193
- ## Plan & File Protection
9194
-
9195
- Protect important files (plans, session-created files) from accidental deletion or overwriting.
9196
-
9197
- ### Register Plans
9198
-
9199
- After creating a plan document, register it for protection:
9200
-
9201
- \`\`\`
9202
- collab_plan_register({
9203
- session_id: "...",
9204
- file_path: "docs/implementation-plan.md",
9205
- title: "Auth System Refactor Plan",
9206
- content_summary: "1. Migrate to JWT\\n2. Add refresh tokens\\n3. Update middleware",
9207
- status: "approved"
9208
- })
9209
- \`\`\`
9210
-
9211
- Plans are automatically:
9212
- - **Pinned** (always appear in active memory)
9213
- - **High priority** (95/100)
9214
- - **Protected** from deletion warnings
9215
-
9216
- ### Plan Status Lifecycle
9217
-
9218
- \`\`\`
9219
- draft \u2192 approved \u2192 in_progress \u2192 completed \u2192 archived
9220
- \u2193
9221
- (Protection reduced)
9222
- (Priority: 50 \u2192 30)
9223
- (Unpinned)
9224
- \`\`\`
9225
-
9226
- Update plan status as work progresses:
9227
-
9228
- \`\`\`
9229
- collab_plan_update_status({
9230
- session_id: "...",
9231
- file_path: "docs/implementation-plan.md",
9232
- status: "completed",
9233
- summary: "All tasks completed. JWT auth implemented."
9234
- })
9235
- \`\`\`
9236
-
9237
- ### Register Created Files
9238
-
9239
- Track important files created during the session:
9240
-
9241
- \`\`\`
9242
- collab_file_register({
9243
- session_id: "...",
9244
- file_path: "src/auth/jwt-validator.ts",
9245
- file_type: "code",
9246
- description: "New JWT validation utility"
9247
- })
9248
- \`\`\`
9249
-
9250
- ### Check Before Deleting
9251
-
9252
- Before deleting any file, check if it's protected:
9253
-
9254
- \`\`\`
9255
- collab_file_check_protected({
9256
- session_id: "...",
9257
- file_path: "docs/implementation-plan.md"
9258
- })
9259
-
9260
- // Response:
9261
- {
9262
- protected: true,
9263
- reason: "plan",
9264
- warning: "\u26A0\uFE0F This file is protected (plan). Confirm before deleting."
9265
- }
9266
- \`\`\`
5944
+ ### Memory (3 tools)
5945
+ - \`collab_memory_save\`: Save context (upsert)
5946
+ - \`collab_memory_recall\`: Recall context (use active=true for restoration)
5947
+ - \`collab_memory_clear\`: Clear memories
9267
5948
 
9268
- ### Protection Tools
5949
+ ### Protection (1 tool)
5950
+ - \`collab_protect\`: Unified protection tool
5951
+ - action: "register" | "check" | "list"
9269
5952
 
9270
- | Tool | Purpose |
9271
- |------|---------|
9272
- | \`collab_plan_register\` | Register a plan for protection |
9273
- | \`collab_plan_update_status\` | Update plan status |
9274
- | \`collab_plan_get\` | Get plan info |
9275
- | \`collab_plan_list\` | List all plans |
9276
- | \`collab_file_register\` | Register created file |
9277
- | \`collab_file_list_created\` | List created files |
9278
- | \`collab_file_check_protected\` | Check if file is protected |
9279
- | \`collab_file_list_protected\` | List all protected files |
5953
+ ## Workflow
9280
5954
 
9281
- ### Why This Matters
5955
+ 1. **On start**: \`collab_session_start\` with project_root
5956
+ 2. **Before editing**: \`collab_claim\` action="check"
5957
+ 3. **For changes**: \`collab_claim\` action="create"
5958
+ 4. **Save context**: \`collab_memory_save\` for important findings
5959
+ 5. **When done**: \`collab_claim\` action="release"
5960
+ 6. **On end**: \`collab_session_end\`
9282
5961
 
9283
- When Claude's context is compacted:
9284
- - Important plan details may be lost
9285
- - Session-created files might be forgotten
9286
- - Claude might accidentally delete recently created files
5962
+ ## Memory Categories
9287
5963
 
9288
- With protection:
9289
- - Plans are pinned in working memory
9290
- - Created files are tracked
9291
- - Deletion triggers warnings
9292
- - Context survives summarization
5964
+ - **finding**: Discovered facts, root causes
5965
+ - **decision**: Architectural choices
5966
+ - **state**: Current tracking info
5967
+ - **important**: Critical context
5968
+ - **context**: General context
9293
5969
 
9294
5970
  ## Best Practices
9295
5971
 
9296
- - **Prefer symbol-level claims** for focused changes (single function/class)
9297
- - **Use file-level claims** for large refactors affecting many symbols
9298
- - **Use LSP validation** when unsure about symbol names
9299
- - **Check references** before modifying widely-used symbols
9300
- - **Set appropriate priority** for urgent work (95+ for production fixes)
9301
- - **Check notifications** when waiting for blocked claims
9302
- - **Save important findings** to working memory as you discover them
9303
- - **Pin critical context** that must survive compaction
9304
- - Claim early, release when done
9305
- - Use descriptive intents (e.g., "Refactoring validateToken for JWT support")
5972
+ - Save important findings to memory as you discover them
5973
+ - Use active=true in recall to restore context
5974
+ - Register plans with collab_protect for protection
5975
+ - Check files before editing to avoid conflicts
9306
5976
  `.trim();
9307
5977
 
9308
5978
  // src/mcp/server.ts
9309
- var ALL_TOOLS = [...sessionTools, ...claimTools, ...messageTools, ...decisionTools, ...lspTools, ...historyTools, ...queueTools, ...notificationTools, ...memoryTools, ...protectionTools];
5979
+ var ALL_TOOLS = [...sessionTools, ...claimTools, ...memoryTools, ...protectionTools];
9310
5980
  function getMcpTools() {
9311
5981
  return ALL_TOOLS;
9312
5982
  }
9313
5983
  async function handleMcpRequest(db, name, args) {
9314
5984
  try {
9315
- if (name.startsWith("collab_session_") || name === "collab_status_update" || name === "collab_config") {
5985
+ if (name.startsWith("collab_session_") || name === "collab_config" || name === "collab_status") {
9316
5986
  return await handleSessionTool(db, name, args);
9317
- } else if (name.startsWith("collab_claim") || name === "collab_check" || name === "collab_release" || name === "collab_auto_release") {
5987
+ } else if (name === "collab_claim") {
9318
5988
  return await handleClaimTool(db, name, args);
9319
- } else if (name.startsWith("collab_message_")) {
9320
- return await handleMessageTool(db, name, args);
9321
- } else if (name.startsWith("collab_decision_")) {
9322
- return await handleDecisionTool(db, name, args);
9323
- } else if (name === "collab_analyze_symbols" || name === "collab_validate_symbols" || name === "collab_store_references" || name === "collab_impact_analysis") {
9324
- return await handleLspTool(db, name, args);
9325
- } else if (name === "collab_history_list") {
9326
- return await handleHistoryTool(db, name, args);
9327
- } else if (name.startsWith("collab_queue_")) {
9328
- return await handleQueueTool(db, name, args);
9329
- } else if (name.startsWith("collab_notifications_")) {
9330
- return await handleNotificationTool(db, name, args);
9331
5989
  } else if (name.startsWith("collab_memory_")) {
9332
5990
  return await handleMemoryTool(db, name, args);
9333
- } else if (name.startsWith("collab_plan_") || name.startsWith("collab_file_")) {
5991
+ } else if (name === "collab_protect") {
9334
5992
  return await handleProtectionTool(db, name, args);
9335
5993
  } else {
9336
5994
  return createToolResult(`Unknown tool: ${name}`, true);