@virsanghavi/axis-server 1.9.1 → 1.10.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.
@@ -21,7 +21,7 @@ The core value proposition: **agents that coordinate like a team, not individual
21
21
  2. **File Locking**: Atomic, per-file locks with 30-minute TTL. Agents call `propose_file_access` before editing. Prevents merge conflicts.
22
22
  3. **Live Notepad**: Real-time shared memory. Agents log progress so others know what's happening. Cleared on `finalize_session`.
23
23
  4. **Context Mirroring**: `get_project_soul` returns this file + conventions to ground agents in project reality.
24
- 5. **RAG Search**: `search_codebase` and `search_docs` for semantic search over indexed files and documentation.
24
+ 5. **RAG Search**: `search_codebase` and `search_docs` for semantic search over indexed files and documentation. **Agents MUST use `search_codebase` before writing new code** — search for similar patterns (e.g. "authentication flow", "API route handler") before creating files. Call `index_file` after creating or significantly changing files.
25
25
  6. **Session Management**: `finalize_session` archives the notepad, clears locks, resets for new work.
26
26
  7. **Billing**: Stripe-based Pro tier ($25/mo) with API key management, usage tracking, and retention flow.
27
27
 
@@ -1119,8 +1119,10 @@ ${notepad}`;
1119
1119
  }
1120
1120
  async getProjectSoul() {
1121
1121
  let soul = "## Project Soul\n";
1122
+ let context = "";
1123
+ let couldNotRead = false;
1122
1124
  try {
1123
- const context = await this.contextManager.readFile("context.md");
1125
+ context = await this.contextManager.readFile("context.md");
1124
1126
  soul += `
1125
1127
  ### Context
1126
1128
  ${context}`;
@@ -1129,8 +1131,23 @@ ${context}`;
1129
1131
  ### Conventions
1130
1132
  ${conventions}`;
1131
1133
  } catch (_e) {
1134
+ couldNotRead = true;
1132
1135
  soul += "\n(Could not read local context files)";
1133
1136
  }
1137
+ const unfilled = couldNotRead || /Describe your project|<!-- Describe|This project uses Axis/i.test(context) || context.trim().length < 450 && /# Project Context/i.test(context);
1138
+ if (unfilled) {
1139
+ soul += `
1140
+
1141
+ ### MANDATORY: Project soul is not yet filled
1142
+ You MUST fill the project soul before doing any other work. Do not proceed with other tasks until it is filled.
1143
+
1144
+ **How to fill the project soul:**
1145
+ 1. Use \`search_codebase\` to explore the repo and infer what this project is about.
1146
+ 2. Call \`update_project_soul\` with \`context\` (project overview, architecture, core features, deployment) and \`conventions\` (language standards, styling, code patterns, agent norms).
1147
+ 3. If the codebase is empty or has nothing to search: ask the user what the project is about, then call \`update_project_soul\` with their answer.
1148
+
1149
+ Do NOT skip this. Do NOT proceed with other work until the soul is populated. Working without a filled soul means every decision you make lacks context.`;
1150
+ }
1134
1151
  return soul;
1135
1152
  }
1136
1153
  // --- Billing & Usage ---
@@ -1270,6 +1287,7 @@ import fs4 from "fs";
1270
1287
  import fs3 from "fs/promises";
1271
1288
  import fsSync2 from "fs";
1272
1289
  import path3 from "path";
1290
+ import { spawnSync } from "child_process";
1273
1291
  var SKIP_DIRS = /* @__PURE__ */ new Set([
1274
1292
  "node_modules",
1275
1293
  ".git",
@@ -1430,14 +1448,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1430
1448
  "also",
1431
1449
  "very",
1432
1450
  "really",
1433
- "quite",
1434
- "show",
1435
- "look",
1436
- "locate",
1437
- "using",
1438
- "used",
1439
- "need",
1440
- "want"
1451
+ "quite"
1441
1452
  ]);
1442
1453
  var MAX_FILE_SIZE = 256 * 1024;
1443
1454
  var MAX_RESULTS = 20;
@@ -1446,7 +1457,7 @@ var MAX_MATCHES_PER_FILE = 6;
1446
1457
  function extractKeywords(query) {
1447
1458
  const words = query.toLowerCase().replace(/[^\w\s\-_.]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
1448
1459
  const filtered = words.filter((w) => !STOP_WORDS.has(w));
1449
- const result = filtered.length > 0 ? filtered : words.filter((w) => w.length >= 3);
1460
+ const result = filtered.length > 0 ? filtered : words;
1450
1461
  return [...new Set(result)];
1451
1462
  }
1452
1463
  var PROJECT_ROOT_MARKERS = [
@@ -1464,7 +1475,7 @@ var PROJECT_ROOT_MARKERS = [
1464
1475
  "AGENTS.md"
1465
1476
  ];
1466
1477
  function detectProjectRoot(startDir) {
1467
- let current = startDir;
1478
+ let current = path3.resolve(startDir);
1468
1479
  const root = path3.parse(current).root;
1469
1480
  while (current !== root) {
1470
1481
  for (const marker of PROJECT_ROOT_MARKERS) {
@@ -1527,8 +1538,7 @@ async function searchFile(filePath, rootDir, keywords) {
1527
1538
  const matchedKeywords = keywords.filter((kw) => contentLower.includes(kw));
1528
1539
  if (matchedKeywords.length === 0) return null;
1529
1540
  const coverage = matchedKeywords.length / keywords.length;
1530
- if (keywords.length >= 3 && coverage < 0.4) return null;
1531
- if (keywords.length === 2 && matchedKeywords.length < 1) return null;
1541
+ if (coverage < 0.2) return null;
1532
1542
  const lines = content.split("\n");
1533
1543
  let score = coverage * coverage * matchedKeywords.length;
1534
1544
  const relLower = relativePath.toLowerCase();
@@ -1580,58 +1590,200 @@ async function searchFile(filePath, rootDir, keywords) {
1580
1590
  }
1581
1591
  return { filePath, relativePath, score, matchedKeywords, regions };
1582
1592
  }
1593
+ function runRipgrep(pattern, cwd) {
1594
+ const p = (pattern || "").trim();
1595
+ if (!p || p.length > 200) return [];
1596
+ const result = spawnSync("rg", [
1597
+ "--line-number",
1598
+ "--no-heading",
1599
+ "--color",
1600
+ "never",
1601
+ "--max-count",
1602
+ "3",
1603
+ // Max 3 matches per file per pattern
1604
+ "-C",
1605
+ "1",
1606
+ // 1 line context
1607
+ "--ignore-case",
1608
+ "--max-filesize",
1609
+ "256K",
1610
+ "-F",
1611
+ p,
1612
+ // Fixed string (literal) — no regex escaping needed
1613
+ "."
1614
+ ], {
1615
+ cwd,
1616
+ encoding: "utf-8",
1617
+ timeout: 8e3,
1618
+ maxBuffer: 4 * 1024 * 1024
1619
+ });
1620
+ if (result.error || result.status !== 0) {
1621
+ return [];
1622
+ }
1623
+ const hits = [];
1624
+ const lines = (result.stdout || "").trim().split("\n").filter(Boolean);
1625
+ for (const line of lines) {
1626
+ const match = line.match(/^(.+?):(\d+):(.+)$/);
1627
+ if (match) {
1628
+ const [, file, lineNum, content] = match;
1629
+ const relPath = path3.relative(cwd, file);
1630
+ hits.push({
1631
+ file: relPath,
1632
+ line: parseInt(lineNum, 10),
1633
+ content: content.trim(),
1634
+ pattern: p
1635
+ });
1636
+ }
1637
+ }
1638
+ return hits;
1639
+ }
1640
+ function ripgrepAvailable() {
1641
+ const r = spawnSync("rg", ["--version"], { encoding: "utf-8" });
1642
+ return !r.error && r.status === 0;
1643
+ }
1644
+ async function warpgrepSearch(query, cwd) {
1645
+ const keywords = extractKeywords(query);
1646
+ if (keywords.length === 0) {
1647
+ const tokens = query.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
1648
+ if (tokens.length === 0) return "";
1649
+ keywords.push(tokens[0]);
1650
+ }
1651
+ const allHits = [];
1652
+ const seen = /* @__PURE__ */ new Set();
1653
+ for (const kw of keywords.slice(0, 5)) {
1654
+ const hits = runRipgrep(kw, cwd);
1655
+ for (const h of hits) {
1656
+ const key = `${h.file}:${h.line}`;
1657
+ if (!seen.has(key)) {
1658
+ seen.add(key);
1659
+ allHits.push(h);
1660
+ }
1661
+ }
1662
+ }
1663
+ if (allHits.length === 0) return "";
1664
+ const byFile = /* @__PURE__ */ new Map();
1665
+ for (const h of allHits) {
1666
+ const list = byFile.get(h.file) || [];
1667
+ if (list.length < MAX_MATCHES_PER_FILE) list.push(h);
1668
+ byFile.set(h.file, list);
1669
+ }
1670
+ const lines = [];
1671
+ lines.push(`Found ${allHits.length} match(es) via ripgrep (keywords: ${keywords.join(", ")})
1672
+ `);
1673
+ lines.push("\u2550".repeat(60) + "\n");
1674
+ const sortedFiles = [...byFile.keys()].sort();
1675
+ for (const relPath of sortedFiles.slice(0, MAX_RESULTS)) {
1676
+ const hits = byFile.get(relPath);
1677
+ lines.push(`${relPath}
1678
+ `);
1679
+ for (const h of hits) {
1680
+ lines.push(` ${h.line.toString().padStart(4)}| ${h.content}`);
1681
+ }
1682
+ lines.push("");
1683
+ }
1684
+ return lines.join("\n");
1685
+ }
1583
1686
  async function localSearch(query, rootDir) {
1687
+ const q = typeof query === "string" ? query.trim() : "";
1584
1688
  const rawCwd = rootDir || process.cwd();
1585
1689
  const cwd = detectProjectRoot(rawCwd);
1586
- const keywords = extractKeywords(query);
1690
+ const keywords = extractKeywords(q);
1587
1691
  if (cwd !== rawCwd) {
1588
1692
  logger.info(`[localSearch] Detected project root: ${cwd} (CWD was: ${rawCwd})`);
1589
1693
  }
1590
- if (keywords.length === 0) {
1694
+ const hasTerms = keywords.length > 0 || q.replace(/[^\w\s]/g, " ").split(/\s+/).some((w) => w.length >= 2);
1695
+ if (!hasTerms) {
1591
1696
  return "Could not extract meaningful search terms from the query. Try being more specific (e.g. 'authentication middleware' instead of 'how does it work').";
1592
1697
  }
1593
- logger.info(`[localSearch] Query: "${query}" \u2192 Keywords: [${keywords.join(", ")}] in ${cwd}`);
1594
- const files = await walkDir(cwd);
1595
- logger.info(`[localSearch] Scanning ${files.length} files`);
1596
- const BATCH_SIZE = 50;
1597
- const allMatches = [];
1598
- for (let i = 0; i < files.length; i += BATCH_SIZE) {
1599
- const batch = files.slice(i, i + BATCH_SIZE);
1600
- const results = await Promise.all(
1601
- batch.map((f) => searchFile(f, cwd, keywords))
1602
- );
1603
- for (const r of results) {
1604
- if (r) allMatches.push(r);
1605
- }
1606
- }
1607
- allMatches.sort((a, b) => b.score - a.score);
1608
- const topMatches = allMatches.slice(0, MAX_RESULTS);
1609
- if (topMatches.length === 0) {
1610
- return `No matches found for: "${query}" (searched ${files.length} files for keywords: ${keywords.join(", ")}).
1611
- Try different terms or check if the code exists in this project.`;
1612
- }
1613
- let output = `Found ${allMatches.length} matching file${allMatches.length === 1 ? "" : "s"} (showing top ${topMatches.length}, searched ${files.length} files)
1698
+ logger.info(`[localSearch] Query: "${q}" \u2192 Keywords: [${keywords.join(", ")}] in ${cwd}`);
1699
+ const useRipgrep = ripgrepAvailable();
1700
+ const [rgResults, keyResults] = await Promise.all([
1701
+ useRipgrep ? warpgrepSearch(q, cwd) : Promise.resolve(""),
1702
+ (async () => {
1703
+ const kws2 = keywords.length > 0 ? keywords : q.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2).slice(0, 5);
1704
+ if (kws2.length === 0) return "";
1705
+ const files = await walkDir(cwd);
1706
+ logger.info(`[localSearch] Scanning ${files.length} files (keyword search)`);
1707
+ const BATCH_SIZE = 50;
1708
+ const allMatches = [];
1709
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
1710
+ const batch = files.slice(i, i + BATCH_SIZE);
1711
+ const results = await Promise.all(batch.map((f) => searchFile(f, cwd, kws2)));
1712
+ for (const r of results) {
1713
+ if (r) allMatches.push(r);
1714
+ }
1715
+ }
1716
+ allMatches.sort((a, b) => b.score - a.score);
1717
+ const topMatches = allMatches.slice(0, MAX_RESULTS);
1718
+ if (topMatches.length === 0) return "";
1719
+ let out = `Found ${allMatches.length} matching file${allMatches.length === 1 ? "" : "s"} (showing top ${topMatches.length}, searched ${files.length} files)
1614
1720
  `;
1615
- output += `Keywords: ${keywords.join(", ")}
1721
+ out += `Keywords: ${kws2.join(", ")}
1616
1722
  `;
1617
- output += "\u2550".repeat(60) + "\n\n";
1618
- for (const match of topMatches) {
1619
- output += `\u{1F4C4} ${match.relativePath}
1723
+ out += "\u2550".repeat(60) + "\n\n";
1724
+ for (const match of topMatches) {
1725
+ out += `${match.relativePath}
1620
1726
  `;
1621
- output += ` Keywords matched: ${match.matchedKeywords.join(", ")} | Score: ${match.score.toFixed(1)}
1727
+ out += ` Keywords matched: ${match.matchedKeywords.join(", ")} | Score: ${match.score.toFixed(1)}
1622
1728
  `;
1623
- if (match.regions.length > 0) {
1624
- output += " \u2500\u2500\u2500\u2500\u2500\n";
1625
- for (const region of match.regions) {
1626
- output += region.lines.split("\n").map((l) => ` ${l}`).join("\n") + "\n";
1627
- if (region !== match.regions[match.regions.length - 1]) {
1628
- output += " ...\n";
1729
+ if (match.regions.length > 0) {
1730
+ out += " \u2500\u2500\u2500\u2500\u2500\n";
1731
+ for (const region of match.regions) {
1732
+ out += region.lines.split("\n").map((l) => ` ${l}`).join("\n") + "\n";
1733
+ if (region !== match.regions[match.regions.length - 1]) out += " ...\n";
1734
+ }
1629
1735
  }
1736
+ out += "\n";
1737
+ }
1738
+ return out;
1739
+ })()
1740
+ ]);
1741
+ const rgHasResults = rgResults && !rgResults.startsWith("Found 0");
1742
+ const keyHasResults = keyResults && keyResults.length > 50;
1743
+ if (rgHasResults && keyHasResults) {
1744
+ return rgResults + "\n\n--- Also from keyword search ---\n\n" + keyResults;
1745
+ }
1746
+ if (rgHasResults) return rgResults;
1747
+ if (keyHasResults) return keyResults;
1748
+ const kws = keywords.length > 0 ? keywords : q.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2).slice(0, 5);
1749
+ if (kws.length >= 3) {
1750
+ const fallbackKws = kws.slice(0, 2);
1751
+ const files = await walkDir(cwd);
1752
+ const fallbackMatches = [];
1753
+ for (let i = 0; i < files.length; i += 50) {
1754
+ const batch = files.slice(i, i + 50);
1755
+ const results = await Promise.all(batch.map((f) => searchFile(f, cwd, fallbackKws)));
1756
+ for (const r of results) {
1757
+ if (r) fallbackMatches.push(r);
1758
+ }
1759
+ }
1760
+ if (fallbackMatches.length > 0) {
1761
+ fallbackMatches.sort((a, b) => b.score - a.score);
1762
+ const top = fallbackMatches.slice(0, MAX_RESULTS);
1763
+ let out = `Found ${fallbackMatches.length} matching file${fallbackMatches.length === 1 ? "" : "s"} (fallback: fewer keywords, showing top ${top.length})
1764
+ `;
1765
+ out += `Keywords: ${fallbackKws.join(", ")} (original: ${kws.join(", ")})
1766
+ `;
1767
+ out += "\u2550".repeat(60) + "\n\n";
1768
+ for (const match of top) {
1769
+ out += `\u{1F4C4} ${match.relativePath}
1770
+ `;
1771
+ out += ` Keywords matched: ${match.matchedKeywords.join(", ")} | Score: ${match.score.toFixed(1)}
1772
+ `;
1773
+ if (match.regions.length > 0) {
1774
+ out += " \u2500\u2500\u2500\u2500\u2500\n";
1775
+ for (const region of match.regions) {
1776
+ out += region.lines.split("\n").map((l) => ` ${l}`).join("\n") + "\n";
1777
+ if (region !== match.regions[match.regions.length - 1]) out += " ...\n";
1778
+ }
1779
+ }
1780
+ out += "\n";
1630
1781
  }
1782
+ return out;
1631
1783
  }
1632
- output += "\n";
1633
1784
  }
1634
- return output;
1785
+ return `No matches found for: "${q}" (searched for: ${kws.join(", ") || "query terms"}).
1786
+ Try different terms or check if the code exists in this project.`;
1635
1787
  }
1636
1788
 
1637
1789
  // ../../src/local/mcp-server.ts
@@ -1847,30 +1999,21 @@ async function ensureFileSystem() {
1847
1999
  ["context.md", `# Project Context
1848
2000
 
1849
2001
  ## Overview
1850
- This project uses Axis \u2014 an open-source coordination layer for AI agents.
1851
- Axis provides shared context, atomic file locks, a job board, and real-time
1852
- activity feeds so that multiple agents (Cursor, Claude, Windsurf, Codex, etc.)
1853
- can work on the same codebase without conflicts.
2002
+ <!-- THIS FILE IS PART OF THE PROJECT SOUL. If it still contains placeholders, agents MUST fill it before doing any other work.
2003
+ How to fill: use search_codebase to explore the repo, then call update_project_soul(context: "...your content...") to replace this placeholder.
2004
+ If the repo is empty: ask the user what the project is about, then call update_project_soul with their answer.
2005
+ Do not proceed with other tasks until this file has real content. -->
1854
2006
 
1855
2007
  ## Architecture
1856
- - **MCP Server**: Exposes tools (locks, jobs, context, search) via the Model Context Protocol.
1857
- - **Supabase Backend**: Postgres for state (locks, jobs, profiles); Realtime for live feeds.
1858
- - **Frontend**: Next.js App Router + Tailwind CSS dashboard at useaxis.dev.
1859
- - **npm Packages**: @virsanghavi/axis-server (runtime), @virsanghavi/axis-init (scaffolding).
2008
+ <!-- Stack, high-level design, and key systems -->
1860
2009
 
1861
2010
  ## Core Features
1862
- 1. File Locking \u2014 atomic, cross-IDE locks with 30-min TTL.
1863
- 2. Job Board \u2014 post / claim / complete tasks with priorities and dependencies.
1864
- 3. Shared Context \u2014 live notepad visible to every agent in real time.
1865
- 4. RAG Search \u2014 vector search over the indexed codebase.
1866
- 5. Soul Files \u2014 context.md, conventions.md, activity.md define project identity.
2011
+ <!-- List main capabilities of this project -->
1867
2012
  `],
1868
2013
  ["conventions.md", `# Coding Conventions
1869
2014
 
1870
2015
  ## Language & Style
1871
- - TypeScript everywhere (strict mode).
1872
- - Tailwind CSS for styling; no raw CSS unless unavoidable.
1873
- - Functional React components; prefer server components in Next.js App Router.
2016
+ <!-- Your language, framework, and styling guidelines -->
1874
2017
 
1875
2018
  ## Agent Behavioral Norms (MANDATORY)
1876
2019
 
@@ -1994,11 +2137,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1994
2137
  },
1995
2138
  {
1996
2139
  name: UPDATE_CONTEXT_TOOL,
1997
- description: "**APPEND OR OVERWRITE** shared context files.\n- Use this to update the project's long-term memory (e.g., adding a new convention, updating the architectural goal).\n- For short-term updates (like 'I just fixed bug X'), use `update_shared_context` (Notepad) instead.\n- Supports `append: true` (default: false) to add to the end of a file.",
2140
+ description: "**APPEND OR OVERWRITE** any shared context file.\n- To update the project soul (context.md / conventions.md), prefer `update_project_soul` instead \u2014 it handles both files in one call.\n- Use this tool for other context files (e.g., `activity.md`) or when you need to append to a file.\n- For short-term updates (like 'I just fixed bug X'), use `update_shared_context` (Notepad) instead.\n- Supports `append: true` (default: false) to add to the end of a file.",
1998
2141
  inputSchema: {
1999
2142
  type: "object",
2000
2143
  properties: {
2001
- filename: { type: "string", description: "File to update (e.g. 'activity.md')" },
2144
+ filename: { type: "string", description: "File to update (e.g. 'activity.md'). For soul files, prefer update_project_soul instead." },
2002
2145
  content: { type: "string", description: "The new content to write or append." },
2003
2146
  append: { type: "boolean", description: "Whether to append to the end of the file (true) or overwrite it (false). Default: false." }
2004
2147
  },
@@ -2083,9 +2226,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2083
2226
  },
2084
2227
  {
2085
2228
  name: "get_project_soul",
2086
- description: "**MANDATORY FIRST CALL**: Returns the project's goals, architecture, conventions, and active state.\n- Combines `context.md`, `conventions.md`, and other core directives into a single prompt.\n- You MUST call this as your FIRST action in every new session or task \u2014 before reading files, before responding to the user, before anything else.\n- Skipping this call means you are working without context and will make wrong decisions.",
2229
+ description: "**MANDATORY FIRST CALL**: Returns the project's goals, architecture, conventions, and active state.\n- Combines `context.md` (project goals/architecture) and `conventions.md` (coding standards/norms) into a single prompt.\n- You MUST call this as your FIRST action in every new session or task \u2014 before reading files, before responding to the user, before anything else.\n- If the project soul is not yet filled (you'll see a 'MANDATORY: Project soul is not yet filled' message), you MUST fill it before any other work:\n 1. Use `search_codebase` to explore the repo and infer project details.\n 2. Call `update_project_soul` with `context` and/or `conventions` params to populate the soul in one call.\n 3. If there is nothing to search, ask the user what the project is about, then call `update_project_soul`.\n- Skipping this call means you are working without context and will make wrong decisions.",
2087
2230
  inputSchema: { type: "object", properties: {}, required: [] }
2088
2231
  },
2232
+ {
2233
+ name: "update_project_soul",
2234
+ description: "**UPDATE THE PROJECT SOUL** \u2014 write project context and/or conventions in a single call.\n- The project soul consists of `context.md` (goals, architecture, core features) and `conventions.md` (coding standards, agent norms).\n- Provide `context` to update `context.md`, `conventions` to update `conventions.md`, or both.\n- Use this when `get_project_soul` says the soul is unfilled, or whenever you need to update long-term project knowledge.\n- This replaces the file contents entirely (not append). For appending, use `update_context` instead.",
2235
+ inputSchema: {
2236
+ type: "object",
2237
+ properties: {
2238
+ context: { type: "string", description: "Full content for context.md (project overview, architecture, core features, deployment). Omit to leave unchanged." },
2239
+ conventions: { type: "string", description: "Full content for conventions.md (language standards, styling, code patterns, agent norms). Omit to leave unchanged." }
2240
+ },
2241
+ required: []
2242
+ }
2243
+ },
2089
2244
  // --- Job Board (Task Orchestration) ---
2090
2245
  {
2091
2246
  name: "post_job",
@@ -2169,15 +2324,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2169
2324
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
2170
2325
  const { name, arguments: args } = request.params;
2171
2326
  logger.info("Tool call", { name });
2172
- if (isSubscriptionStale()) {
2173
- await verifySubscription();
2174
- }
2175
- if (!subscription.valid) {
2176
- logger.warn(`[subscription] Blocking tool call "${name}" \u2014 subscription invalid`);
2177
- return {
2178
- content: [{ type: "text", text: getSubscriptionBlockMessage() }],
2179
- isError: true
2180
- };
2327
+ if (process.env.AXIS_SKIP_SUBSCRIPTION_CHECK === "1") {
2328
+ } else {
2329
+ if (isSubscriptionStale()) {
2330
+ await verifySubscription();
2331
+ }
2332
+ if (!subscription.valid) {
2333
+ logger.warn(`[subscription] Blocking tool call "${name}" \u2014 subscription invalid`);
2334
+ return {
2335
+ content: [{ type: "text", text: getSubscriptionBlockMessage() }],
2336
+ isError: true
2337
+ };
2338
+ }
2181
2339
  }
2182
2340
  if (name === READ_CONTEXT_TOOL) {
2183
2341
  const filename = String(args?.filename);
@@ -2330,6 +2488,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2330
2488
  const result = await nerveCenter.getProjectSoul();
2331
2489
  return { content: [{ type: "text", text: result }] };
2332
2490
  }
2491
+ if (name === "update_project_soul") {
2492
+ const { context, conventions } = args;
2493
+ const updated = [];
2494
+ if (context) {
2495
+ await manager.updateFile("context.md", context, false);
2496
+ updated.push("context.md");
2497
+ }
2498
+ if (conventions) {
2499
+ await manager.updateFile("conventions.md", conventions, false);
2500
+ updated.push("conventions.md");
2501
+ }
2502
+ if (updated.length === 0) {
2503
+ return { content: [{ type: "text", text: "No changes \u2014 provide `context` and/or `conventions` parameters." }] };
2504
+ }
2505
+ return { content: [{ type: "text", text: `Project soul updated: ${updated.join(", ")}` }] };
2506
+ }
2333
2507
  if (name === "post_job") {
2334
2508
  const { title, description, priority, dependencies } = args;
2335
2509
  const result = await nerveCenter.postJob(title, description, priority, dependencies);
package/package.json CHANGED
@@ -1,33 +1,37 @@
1
1
  {
2
- "name": "@virsanghavi/axis-server",
3
- "version": "1.9.1",
4
- "description": "Axis MCP Server CLI",
5
- "main": "dist/index.js",
6
- "bin": {
7
- "axis-server": "dist/cli.js"
8
- },
9
- "scripts": {
10
- "start": "node dist/cli.js",
11
- "build": "tsup bin/cli.ts --format cjs --out-dir dist --clean --shims && tsup ../../src/local/mcp-server.ts --format esm --out-dir dist",
12
- "prepublishOnly": "npm run build"
13
- },
14
- "dependencies": {
15
- "commander": "^11.0.0",
16
- "chalk": "^5.3.0",
17
- "@modelcontextprotocol/sdk": "^0.6.0",
18
- "@supabase/supabase-js": "^2.39.0",
19
- "async-mutex": "^0.5.0",
20
- "dotenv": "^16.3.1",
21
- "fs-extra": "^11.2.0",
22
- "openai": "^4.24.0",
23
- "zod": "^3.22.4",
24
- "execa": "^8.0.0"
25
- },
26
- "devDependencies": {
27
- "@types/node": "^20.0.0",
28
- "@types/fs-extra": "^11.0.4",
29
- "tsup": "^8.0.1",
30
- "tsx": "^4.7.0",
31
- "typescript": "^5.0.0"
32
- }
2
+ "name": "@virsanghavi/axis-server",
3
+ "version": "1.10.0",
4
+ "description": "Axis MCP Server CLI",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "axis-server": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node dist/cli.js",
11
+ "build": "tsup bin/cli.ts --format cjs --out-dir dist --clean --shims && tsup ../../src/local/mcp-server.ts --format esm --out-dir dist",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^11.0.0",
16
+ "chalk": "^5.3.0",
17
+ "@modelcontextprotocol/sdk": "^0.6.0",
18
+ "@supabase/supabase-js": "^2.39.0",
19
+ "async-mutex": "^0.5.0",
20
+ "dotenv": "^16.3.1",
21
+ "fs-extra": "^11.2.0",
22
+ "openai": "^4.24.0",
23
+ "zod": "^3.22.4",
24
+ "execa": "^8.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "@types/fs-extra": "^11.0.4",
29
+ "tsup": "^8.0.1",
30
+ "tsx": "^4.7.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ }
33
37
  }