codeep 2.0.0 → 2.0.1

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.
@@ -82,6 +82,15 @@ const COMMAND_DESCRIPTIONS = {
82
82
  'profile': 'Save/load settings profiles',
83
83
  'tasks': 'Show pending tasks from codeep.dev dashboard',
84
84
  'sync': 'Sync learning preferences and profiles to codeep.dev',
85
+ // 2.0 — surfaced for `/` autocomplete; documented in /help too.
86
+ 'compact': 'Summarize older messages to free up context',
87
+ 'commands': 'List custom slash commands in .codeep/commands/*.md',
88
+ 'checkpoint': 'Snapshot the session (conversation + provider/model + git HEAD)',
89
+ 'checkpoints': 'List saved checkpoints for this workspace',
90
+ 'rewind': 'Restore conversation from a saved checkpoint',
91
+ 'hooks': 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)',
92
+ 'mcp': 'Manage MCP servers (browse, install, add, remove, resources, prompts)',
93
+ 'openrouter': 'Tune OpenRouter routing (preferred / ignore providers, fallbacks, privacy)',
85
94
  };
86
95
  import { helpCategories, keyboardShortcuts } from './components/Help.js';
87
96
  import { handleSettingsKey, SETTINGS } from './components/Settings.js';
@@ -217,6 +226,10 @@ export class App {
217
226
  'provider', 'model', 'protocol', 'lang', 'grant', 'login', 'logout',
218
227
  'context-save', 'context-load', 'context-clear', 'learn',
219
228
  'cost', 'tasks', 'account', 'sync',
229
+ // 2.0 — extensions, checkpoints, MCP, custom commands, OpenRouter prefs.
230
+ // Keep in lockstep with COMMAND_DESCRIPTIONS below and helpCategories.
231
+ 'compact', 'commands', 'checkpoint', 'checkpoints', 'rewind',
232
+ 'hooks', 'mcp', 'openrouter',
220
233
  'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
221
234
  ];
222
235
  constructor(options) {
@@ -1685,6 +1685,296 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
1685
1685
  ctx.app.addMessage({ role: 'system', content: formatCommandList(loadCustomCommands(ctx.projectPath)) });
1686
1686
  break;
1687
1687
  }
1688
+ case 'mcp': {
1689
+ // Mirrors the ACP `/mcp` handler in src/acp/commands.ts. In TUI the
1690
+ // session id is the constant `codeep-tui` (the same one main.ts uses
1691
+ // when it spawns project MCP servers in the background) and the
1692
+ // workspace root is ctx.projectPath. Without a project we can still
1693
+ // browse the marketplace, but anything that mutates project config
1694
+ // refuses with a clear message.
1695
+ const sub = args[0]?.toLowerCase();
1696
+ const TUI_SESSION = 'codeep-tui';
1697
+ const projectPath = ctx.projectPath;
1698
+ const requireProject = () => {
1699
+ if (!projectPath) {
1700
+ ctx.app.notify('Open a project (cd into it before running codeep) to add or modify MCP servers.');
1701
+ return false;
1702
+ }
1703
+ return true;
1704
+ };
1705
+ const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfig } = await import('../utils/mcpConfig.js');
1706
+ const { registerSessionServers } = await import('../utils/mcpRegistry.js');
1707
+ if (sub === 'add') {
1708
+ if (!requireProject())
1709
+ break;
1710
+ const name = args[1];
1711
+ const command = args[2];
1712
+ if (!name || !command) {
1713
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp add <name> <command> [args...]` — e.g. `/mcp add fs npx @modelcontextprotocol/server-filesystem /path`' });
1714
+ break;
1715
+ }
1716
+ const extraArgs = args.slice(3);
1717
+ addProjectMcpServer(projectPath, { name, command, args: extraArgs });
1718
+ ctx.app.notify(`Saved MCP server ${name} to .codeep/mcp_servers.json. Spawning…`);
1719
+ const merged = loadMcpServerConfig(projectPath);
1720
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1721
+ const ok = registered.filter(t => t.serverName === name);
1722
+ const failed = errors.find(e => e.server === name);
1723
+ ctx.app.addMessage({
1724
+ role: 'system',
1725
+ content: failed
1726
+ ? `Saved \`${name}\` but spawn failed: \`${failed.error}\``
1727
+ : `Added \`${name}\` (${ok.length} tool${ok.length === 1 ? '' : 's'} available).`,
1728
+ });
1729
+ break;
1730
+ }
1731
+ if (sub === 'remove') {
1732
+ if (!requireProject())
1733
+ break;
1734
+ const name = args[1];
1735
+ if (!name) {
1736
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp remove <name>`' });
1737
+ break;
1738
+ }
1739
+ const removed = removeProjectMcpServer(projectPath, name);
1740
+ if (!removed) {
1741
+ ctx.app.addMessage({ role: 'system', content: `No project-scoped MCP server named \`${name}\`.` });
1742
+ break;
1743
+ }
1744
+ const merged = loadMcpServerConfig(projectPath);
1745
+ await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1746
+ ctx.app.addMessage({ role: 'system', content: `Removed \`${name}\` from project config and stopped its process.` });
1747
+ break;
1748
+ }
1749
+ if (sub === 'browse') {
1750
+ const { formatMarketplaceList, findMarketplaceEntry, formatMarketplaceEntry, MCP_MARKETPLACE } = await import('../utils/mcpMarketplace.js');
1751
+ const detail = args[1];
1752
+ if (detail) {
1753
+ const entry = findMarketplaceEntry(detail);
1754
+ if (!entry) {
1755
+ ctx.app.addMessage({ role: 'system', content: `Marketplace id not found: \`${detail}\`. Run \`/mcp browse\` for the list.` });
1756
+ }
1757
+ else {
1758
+ const argHints = entry.argHints?.map(h => `<${h.placeholder ?? 'arg'}>`).join(' ') ?? '';
1759
+ ctx.app.addMessage({ role: 'system', content: formatMarketplaceEntry(entry) + `\n\nInstall with \`/mcp install ${entry.id} ${argHints}\`` });
1760
+ }
1761
+ break;
1762
+ }
1763
+ ctx.app.addMessage({ role: 'system', content: formatMarketplaceList() + `\n\nRun \`/mcp browse <id>\` for details or \`/mcp install <id> [args]\` to install. Total: ${MCP_MARKETPLACE.length}.` });
1764
+ break;
1765
+ }
1766
+ if (sub === 'install') {
1767
+ if (!requireProject())
1768
+ break;
1769
+ const id = args[1];
1770
+ if (!id) {
1771
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp install <id> [extra args...]` — run `/mcp browse` to see ids.' });
1772
+ break;
1773
+ }
1774
+ const { findMarketplaceEntry } = await import('../utils/mcpMarketplace.js');
1775
+ const entry = findMarketplaceEntry(id);
1776
+ if (!entry) {
1777
+ ctx.app.addMessage({ role: 'system', content: `Marketplace id not found: \`${id}\`. Run \`/mcp browse\` for the list.` });
1778
+ break;
1779
+ }
1780
+ const extraArgs = args.slice(2);
1781
+ const fullArgs = [...(entry.server.args ?? []), ...extraArgs];
1782
+ addProjectMcpServer(projectPath, {
1783
+ name: entry.id,
1784
+ command: entry.server.command,
1785
+ args: fullArgs,
1786
+ env: entry.server.env,
1787
+ url: entry.server.url,
1788
+ headers: entry.server.headers,
1789
+ });
1790
+ ctx.app.notify(`Saved ${entry.id} to project config. Spawning…`);
1791
+ const merged = loadMcpServerConfig(projectPath);
1792
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1793
+ const failed = errors.find(e => e.server === entry.id);
1794
+ const lines = [];
1795
+ if (failed) {
1796
+ lines.push(`Saved \`${entry.id}\` but spawn failed: \`${failed.error}\``);
1797
+ }
1798
+ else {
1799
+ const ok = registered.filter(t => t.serverName === entry.id);
1800
+ lines.push(`Installed **${entry.name}** (\`${entry.id}\`) — ${ok.length} tool${ok.length === 1 ? '' : 's'} available.`);
1801
+ }
1802
+ if (entry.envNotes?.length) {
1803
+ lines.push('', '**Environment variables you may need:**');
1804
+ for (const e of entry.envNotes) {
1805
+ const req = e.required ? ' (required)' : '';
1806
+ lines.push(`- \`${e.name}\`${req} — ${e.description}`);
1807
+ }
1808
+ }
1809
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
1810
+ break;
1811
+ }
1812
+ if (sub === 'reload') {
1813
+ if (!requireProject())
1814
+ break;
1815
+ ctx.app.notify('Reloading MCP server config…');
1816
+ const merged = loadMcpServerConfig(projectPath);
1817
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1818
+ const lines = [`## MCP reloaded`, '', `**${registered.length}** tool${registered.length === 1 ? '' : 's'} from **${merged.length}** server${merged.length === 1 ? '' : 's'}.`];
1819
+ if (errors.length > 0) {
1820
+ lines.push('', '### Failed servers');
1821
+ for (const e of errors)
1822
+ lines.push(`- **${e.server}** — \`${e.error}\``);
1823
+ }
1824
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
1825
+ break;
1826
+ }
1827
+ if (sub === 'resources') {
1828
+ const { getSessionResources, awaitSessionReady } = await import('../utils/mcpRegistry.js');
1829
+ await awaitSessionReady(TUI_SESSION);
1830
+ const groups = await getSessionResources(TUI_SESSION);
1831
+ if (groups.length === 0) {
1832
+ ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes resources._' });
1833
+ break;
1834
+ }
1835
+ const lines = ['## MCP resources', ''];
1836
+ for (const g of groups) {
1837
+ lines.push(`**${g.serverName}** — ${g.resources.length} resource${g.resources.length === 1 ? '' : 's'}`);
1838
+ for (const r of g.resources) {
1839
+ const label = r.name ? `${r.name} — ` : '';
1840
+ const mime = r.mimeType ? ` (${r.mimeType})` : '';
1841
+ lines.push(`- ${label}\`${r.uri}\`${mime}${r.description ? ` — ${r.description}` : ''}`);
1842
+ }
1843
+ lines.push('');
1844
+ }
1845
+ lines.push('Read one with `/mcp read <uri>`.');
1846
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1847
+ break;
1848
+ }
1849
+ if (sub === 'read') {
1850
+ const uri = args[1];
1851
+ if (!uri) {
1852
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp read <uri>` — run `/mcp resources` to see available URIs.' });
1853
+ break;
1854
+ }
1855
+ const { readSessionResource } = await import('../utils/mcpRegistry.js');
1856
+ try {
1857
+ const contents = await readSessionResource(TUI_SESSION, uri);
1858
+ if (contents.length === 0) {
1859
+ ctx.app.addMessage({ role: 'system', content: `_No content returned for \`${uri}\`._` });
1860
+ break;
1861
+ }
1862
+ const lines = [`## Resource: \`${uri}\``, ''];
1863
+ for (const c of contents) {
1864
+ if (c.text !== undefined) {
1865
+ const fence = c.mimeType?.includes('json') ? 'json' : c.mimeType?.includes('markdown') ? 'markdown' : '';
1866
+ lines.push('```' + fence);
1867
+ lines.push(c.text);
1868
+ lines.push('```');
1869
+ }
1870
+ else if (c.blob) {
1871
+ lines.push(`_(${c.mimeType ?? 'binary'} blob, ${c.blob.length} base64 chars — not rendered)_`);
1872
+ }
1873
+ }
1874
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
1875
+ }
1876
+ catch (err) {
1877
+ ctx.app.addMessage({ role: 'system', content: `Failed to read \`${uri}\`: ${err.message}` });
1878
+ }
1879
+ break;
1880
+ }
1881
+ if (sub === 'prompts') {
1882
+ const { getSessionPrompts, awaitSessionReady } = await import('../utils/mcpRegistry.js');
1883
+ await awaitSessionReady(TUI_SESSION);
1884
+ const groups = await getSessionPrompts(TUI_SESSION);
1885
+ if (groups.length === 0) {
1886
+ ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes prompt templates._' });
1887
+ break;
1888
+ }
1889
+ const lines = ['## MCP prompt templates', ''];
1890
+ for (const g of groups) {
1891
+ lines.push(`**${g.serverName}** — ${g.prompts.length} prompt${g.prompts.length === 1 ? '' : 's'}`);
1892
+ for (const p of g.prompts) {
1893
+ const argList = p.arguments?.length
1894
+ ? ` (${p.arguments.map(a => a.required ? a.name : `[${a.name}]`).join(', ')})`
1895
+ : '';
1896
+ lines.push(`- \`${p.name}\`${argList}${p.description ? ` — ${p.description}` : ''}`);
1897
+ }
1898
+ lines.push('');
1899
+ }
1900
+ lines.push('Materialise one with `/mcp prompt <server> <name> [key=value...]`.');
1901
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1902
+ break;
1903
+ }
1904
+ if (sub === 'prompt') {
1905
+ const serverName = args[1];
1906
+ const name = args[2];
1907
+ if (!serverName || !name) {
1908
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp prompt <server> <name> [key=value ...]`' });
1909
+ break;
1910
+ }
1911
+ const promptArgs = {};
1912
+ for (const tok of args.slice(3)) {
1913
+ const eq = tok.indexOf('=');
1914
+ if (eq > 0)
1915
+ promptArgs[tok.slice(0, eq)] = tok.slice(eq + 1);
1916
+ }
1917
+ const { getSessionPrompt } = await import('../utils/mcpRegistry.js');
1918
+ try {
1919
+ const { description, messages } = await getSessionPrompt(TUI_SESSION, serverName, name, promptArgs);
1920
+ const lines = [`## Prompt \`${serverName}/${name}\``];
1921
+ if (description)
1922
+ lines.push(`_${description}_`);
1923
+ lines.push('');
1924
+ for (const m of messages) {
1925
+ const text = typeof m.content?.text === 'string' ? m.content.text : JSON.stringify(m.content);
1926
+ lines.push(`**${m.role}:** ${text}`);
1927
+ lines.push('');
1928
+ }
1929
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1930
+ }
1931
+ catch (err) {
1932
+ ctx.app.addMessage({ role: 'system', content: `Failed to materialise prompt: ${err.message}` });
1933
+ }
1934
+ break;
1935
+ }
1936
+ // Default: list servers + tools for the current session.
1937
+ const { getSessionTools, getSessionRegistrationErrors, awaitSessionReady } = await import('../utils/mcpRegistry.js');
1938
+ await awaitSessionReady(TUI_SESSION);
1939
+ const tools = await getSessionTools(TUI_SESSION);
1940
+ const mcpErrors = getSessionRegistrationErrors(TUI_SESSION);
1941
+ if (tools.length === 0 && mcpErrors.length === 0) {
1942
+ ctx.app.addMessage({
1943
+ role: 'system',
1944
+ content: [
1945
+ '_No MCP servers connected to this session._',
1946
+ '',
1947
+ 'Add one with `/mcp add <name> <command> [args...]` — it persists to `.codeep/mcp_servers.json`.',
1948
+ 'Or browse the marketplace with `/mcp browse` and install with `/mcp install <id>`.',
1949
+ ].join('\n'),
1950
+ });
1951
+ break;
1952
+ }
1953
+ const lines = ['## MCP servers', ''];
1954
+ if (tools.length > 0) {
1955
+ const byServer = new Map();
1956
+ for (const t of tools) {
1957
+ if (!byServer.has(t.serverName))
1958
+ byServer.set(t.serverName, []);
1959
+ byServer.get(t.serverName).push(t);
1960
+ }
1961
+ for (const [serverName, serverTools] of byServer) {
1962
+ lines.push(`**${serverName}** — ${serverTools.length} tool${serverTools.length === 1 ? '' : 's'}`);
1963
+ for (const t of serverTools) {
1964
+ const desc = t.description ? ` — ${t.description}` : '';
1965
+ lines.push(`- \`${t.agentName}\`${desc}`);
1966
+ }
1967
+ lines.push('');
1968
+ }
1969
+ }
1970
+ if (mcpErrors.length > 0) {
1971
+ lines.push('### Failed servers');
1972
+ for (const e of mcpErrors)
1973
+ lines.push(`- **${e.server}** — \`${e.error}\``);
1974
+ }
1975
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1976
+ break;
1977
+ }
1688
1978
  default: {
1689
1979
  // 1. Try custom user command (project + global Markdown templates).
1690
1980
  const { findCustomCommand, expandCommand } = await import('../utils/customCommands.js');
@@ -29,6 +29,16 @@ export const helpCategories = [
29
29
  { key: '/rename <name>', description: 'Rename current session' },
30
30
  { key: '/search <term>', description: 'Search chat history' },
31
31
  { key: '/export [md|json|txt]', description: 'Export chat' },
32
+ { key: '/compact [keepN]', description: 'AI-summarize older messages to free up context (keeps last N)' },
33
+ ],
34
+ },
35
+ {
36
+ title: 'Checkpoints (2.0)',
37
+ items: [
38
+ { key: '/checkpoint [name]', description: 'Snapshot conversation + provider/model + git HEAD' },
39
+ { key: '/checkpoints', description: 'List saved checkpoints in this workspace' },
40
+ { key: '/rewind <id>', description: 'Restore conversation from a checkpoint' },
41
+ { key: '/checkpoint delete <id>', description: 'Delete a saved checkpoint' },
32
42
  ],
33
43
  },
34
44
  {
@@ -110,6 +120,24 @@ export const helpCategories = [
110
120
  { key: '/logout', description: 'Logout from provider' },
111
121
  { key: '/profile save <name>', description: 'Save current provider+model as profile' },
112
122
  { key: '/profile list', description: 'List saved profiles' },
123
+ { key: '/openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)' },
124
+ ],
125
+ },
126
+ {
127
+ title: 'Extensions & MCP (2.0)',
128
+ items: [
129
+ { key: '/mcp', description: 'List connected MCP servers + their tools' },
130
+ { key: '/mcp browse [id]', description: 'Browse marketplace (12 servers) or show one' },
131
+ { key: '/mcp install <id> [args]', description: 'Install a marketplace server into this project' },
132
+ { key: '/mcp add <name> <cmd>', description: 'Add a custom MCP server (npx, binary, etc.)' },
133
+ { key: '/mcp remove <name>', description: 'Remove a project-scoped MCP server' },
134
+ { key: '/mcp reload', description: 'Re-read .codeep/mcp_servers.json (after manual edit)' },
135
+ { key: '/mcp resources', description: 'List resources exposed by connected servers' },
136
+ { key: '/mcp read <uri>', description: 'Read one MCP resource' },
137
+ { key: '/mcp prompts', description: 'List prompt templates exposed by servers' },
138
+ { key: '/mcp prompt <server> <name>', description: 'Materialize a prompt with arguments (key=value)' },
139
+ { key: '/hooks', description: 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)' },
140
+ { key: '/commands', description: 'List custom slash commands (.codeep/commands/*.md)' },
113
141
  ],
114
142
  },
115
143
  {
@@ -24,10 +24,13 @@ export interface RemoteSkill {
24
24
  updated_at: string;
25
25
  }
26
26
  /**
27
- * Publish a project-scoped skill bundle to codeep.dev.
28
- * The bundle MUST exist under `<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`
29
- * — global skills aren't publishable from this helper (intentional: users
30
- * publish work from a project they're owning, not from their global hoard).
27
+ * Publish a skill bundle to codeep.dev. The bundle may be project-scoped
28
+ * (`<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`) or global
29
+ * (`~/.codeep/skills/<slug>/SKILL.md`) either is publishable. The
30
+ * `--public` flag (translated into `opts.isPublic`) is the user's
31
+ * explicit consent gate; we don't gate further on bundle scope. If both
32
+ * exist with the same slug, the project copy wins (mirrors the rest of
33
+ * the bundle-loading flow).
31
34
  */
32
35
  export declare function publishBundle(workspaceRoot: string, slug: string, opts?: {
33
36
  isPublic?: boolean;
@@ -17,18 +17,24 @@ import { getSyncToken } from '../config/index.js';
17
17
  import { findSkillBundle } from './skillBundles.js';
18
18
  const API_BASE = 'https://codeep.dev';
19
19
  /**
20
- * Publish a project-scoped skill bundle to codeep.dev.
21
- * The bundle MUST exist under `<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`
22
- * — global skills aren't publishable from this helper (intentional: users
23
- * publish work from a project they're owning, not from their global hoard).
20
+ * Publish a skill bundle to codeep.dev. The bundle may be project-scoped
21
+ * (`<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`) or global
22
+ * (`~/.codeep/skills/<slug>/SKILL.md`) either is publishable. The
23
+ * `--public` flag (translated into `opts.isPublic`) is the user's
24
+ * explicit consent gate; we don't gate further on bundle scope. If both
25
+ * exist with the same slug, the project copy wins (mirrors the rest of
26
+ * the bundle-loading flow).
24
27
  */
25
28
  export async function publishBundle(workspaceRoot, slug, opts = {}) {
26
29
  const token = getSyncToken();
27
30
  if (!token)
28
31
  return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
29
32
  const bundle = findSkillBundle(slug, workspaceRoot);
30
- if (!bundle || bundle.scope !== 'project') {
31
- return { ok: false, error: `Skill bundle "${slug}" not found in this project. Run \`/skills create-bundle ${slug}\` first.` };
33
+ if (!bundle) {
34
+ return {
35
+ ok: false,
36
+ error: `Skill bundle "${slug}" not found in either \`.codeep/skills/${slug}/\` (project) or \`~/.codeep/skills/${slug}/\` (global). Run \`/skills create-bundle ${slug}\` to scaffold one, or check \`/skills bundles\` to see what's available.`,
37
+ };
32
38
  }
33
39
  // Build the SKILL.md text we'll publish. We re-serialise from the loaded
34
40
  // bundle rather than reading the file again — that way frontmatter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",