create-walle 0.1.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 (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. package/template/wall-e/tools/slack-mcp.js +290 -0
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: slack-backfill
3
+ description: >
4
+ Full Slack history backfill from 2022 to present. Searches month by month,
5
+ paginates through all results, and stores messages in WALL-E's brain.
6
+ Use for initial setup or catching up on missed months.
7
+ version: 1.0.0
8
+ author: juncao
9
+ execution: script
10
+ entry: ../../../scripts/slack-backfill.js
11
+ args: []
12
+ trigger:
13
+ type: manual
14
+ config:
15
+ mode:
16
+ type: string
17
+ enum: [full, incremental]
18
+ default: full
19
+ description: "full = month-by-month from 2022, incremental = only new messages"
20
+ month:
21
+ type: string
22
+ default: ""
23
+ description: "Single month to backfill (e.g. 2024-09). Empty = all months."
24
+ tags: [slack, messaging, backfill, ingestion, history]
25
+ permissions:
26
+ - slack:read
27
+ - brain:write
28
+ ---
29
+ # Slack History Backfill
30
+
31
+ ## What This Skill Does
32
+
33
+ Performs a comprehensive backfill of Slack message history from June 2022
34
+ to the present month. Uses Slack search API with pagination (up to 20 pages
35
+ per query) to capture all messages.
36
+
37
+ ## How It Works
38
+
39
+ 1. Generate month ranges from 2022-06 to current month
40
+ 2. For each month, search for messages from the owner (via SLACK_OWNER_HANDLE env var)
41
+ 3. Paginate through all results (up to 20 pages x 20 results)
42
+ 4. Parse detailed result format (channel, sender, timestamp, message_ts)
43
+ 5. Deduplicate using source_id = `slack-{message_ts}`
44
+ 6. Emit CHECKPOINT after each month for task runner resume support
45
+
46
+ ## Modes
47
+
48
+ - `full` (default) -- Scans every month from 2022-06 to now. Skips already-ingested messages.
49
+ - `incremental` -- Only searches for messages after the latest timestamp in the brain.
50
+ - Single month -- Pass a month string like `2024-09` to backfill just that month.
51
+
52
+ ## Checkpoint Support
53
+
54
+ The script reads `WALL_E_CHECKPOINT` env var to resume from a specific month.
55
+ Each completed month emits `CHECKPOINT:YYYY-MM` for the task runner to save.
56
+
57
+ ## Output
58
+
59
+ Prints progress per month and final summary:
60
+ `Total: {before} -> {after} (+{delta})`
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: slack-sync
3
+ description: >
4
+ Pull latest Slack messages from active conversations into WALL-E's brain.
5
+ Discovers recently active channels, fetches new messages incrementally.
6
+ Use for keeping Slack context fresh.
7
+ version: 1.0.0
8
+ author: juncao
9
+ execution: script
10
+ entry: ../../../scripts/slack-channel-history.js
11
+ args: ["--sync"]
12
+ trigger:
13
+ type: interval
14
+ schedule: "every 15m"
15
+ config:
16
+ mode:
17
+ type: string
18
+ enum: [incremental, full]
19
+ default: incremental
20
+ tags: [slack, messaging, sync, ingestion]
21
+ permissions:
22
+ - slack:read
23
+ - brain:write
24
+ ---
25
+ # Slack Conversation Sync
26
+
27
+ ## What This Skill Does
28
+
29
+ Incrementally syncs Slack messages from your most active conversations
30
+ into WALL-E's brain. Only checks recently active channels (last 7 days)
31
+ to minimize API calls and run time.
32
+
33
+ ## How It Works
34
+
35
+ 1. Search Slack for YOUR recent messages to discover active channels
36
+ 2. Query brain DB for channels with recent activity (last 3 days)
37
+ 3. For each active channel, fetch messages since last sync using pagination
38
+ 4. Deduplicate and store new messages as memories with source_id
39
+ 5. Emit CHECKPOINT after each channel for resume support
40
+
41
+ ## Configuration
42
+
43
+ - `mode: incremental` (default) -- Only pulls channels with activity in the last 7 days (~35 channels, ~1 min)
44
+ - `mode: full` -- Pulls ALL known channels (~800 channels, ~15 min)
45
+
46
+ ## Output
47
+
48
+ Prints progress per channel and emits CHECKPOINT lines for task runner resume support.
49
+ Returns summary: `{ new_messages, skipped, channels_done, total_after }`
50
+
51
+ ## Error Handling
52
+
53
+ - Slack token expiry triggers exit code 1 with clear error message
54
+ - Channel not found errors are logged and skipped
55
+ - Other errors are logged per-channel and processing continues
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const CLAUDE_DIR = path.join(process.env.HOME, '.claude');
6
+ const SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
7
+ const PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins');
8
+ const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
9
+ const SETTINGS_LOCAL_PATH = path.join(CLAUDE_DIR, 'settings.local.json');
10
+
11
+ /**
12
+ * Read Claude Code's MCP server configurations.
13
+ */
14
+ function readMcpConfig() {
15
+ const configs = [];
16
+ for (const settingsPath of [SETTINGS_PATH, SETTINGS_LOCAL_PATH]) {
17
+ try {
18
+ if (!fs.existsSync(settingsPath)) continue;
19
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
20
+ const mcpServers = settings.mcpServers || {};
21
+ for (const [name, config] of Object.entries(mcpServers)) {
22
+ configs.push({
23
+ name,
24
+ command: config.command,
25
+ args: config.args || [],
26
+ env: Object.keys(config.env || {}), // keys only, not values (security)
27
+ source: settingsPath,
28
+ });
29
+ }
30
+ } catch {}
31
+ }
32
+ return configs;
33
+ }
34
+
35
+ /**
36
+ * Read Claude Code's installed skills.
37
+ * Returns skill metadata (name, description, triggers) but not full content.
38
+ */
39
+ function readClaudeSkills() {
40
+ const skills = [];
41
+
42
+ // Read from ~/.claude/skills/
43
+ if (fs.existsSync(SKILLS_DIR)) {
44
+ for (const file of fs.readdirSync(SKILLS_DIR)) {
45
+ if (!file.endsWith('.md')) continue;
46
+ try {
47
+ const content = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf8');
48
+ const name = file.replace('.md', '');
49
+ const description = extractDescription(content);
50
+ skills.push({ name, description, source: 'user-skills', path: path.join(SKILLS_DIR, file) });
51
+ } catch {}
52
+ }
53
+ }
54
+
55
+ // Read from ~/.claude/plugins/ (installed plugins with skills)
56
+ if (fs.existsSync(PLUGINS_DIR)) {
57
+ try {
58
+ scanPluginSkills(PLUGINS_DIR, skills);
59
+ } catch {}
60
+ }
61
+
62
+ return skills;
63
+ }
64
+
65
+ function scanPluginSkills(dir, skills, depth) {
66
+ if ((depth || 0) > 4) return;
67
+ try {
68
+ for (const entry of fs.readdirSync(dir)) {
69
+ const fullPath = path.join(dir, entry);
70
+ const stat = fs.statSync(fullPath);
71
+ if (stat.isDirectory()) {
72
+ // Look for skill definition files
73
+ const skillFiles = ['skill.md', 'README.md', 'index.md'];
74
+ for (const sf of skillFiles) {
75
+ const sfPath = path.join(fullPath, sf);
76
+ if (fs.existsSync(sfPath)) {
77
+ try {
78
+ const content = fs.readFileSync(sfPath, 'utf8');
79
+ const description = extractDescription(content);
80
+ skills.push({ name: entry, description, source: 'plugin', path: sfPath });
81
+ } catch {}
82
+ }
83
+ }
84
+ scanPluginSkills(fullPath, skills, (depth || 0) + 1);
85
+ }
86
+ }
87
+ } catch {}
88
+ }
89
+
90
+ function extractDescription(content) {
91
+ // Extract first paragraph or heading as description
92
+ const lines = content.split('\n').filter(l => l.trim());
93
+ for (const line of lines) {
94
+ const trimmed = line.trim();
95
+ if (trimmed.startsWith('#')) {
96
+ return trimmed.replace(/^#+\s*/, '');
97
+ }
98
+ if (trimmed.length > 20 && !trimmed.startsWith('```') && !trimmed.startsWith('-')) {
99
+ return trimmed.slice(0, 200);
100
+ }
101
+ }
102
+ return '';
103
+ }
104
+
105
+ /**
106
+ * Generate WALL-E skill suggestions based on Claude Code's capabilities.
107
+ */
108
+ function suggestSkillsFromClaudeCode() {
109
+ const mcpServers = readMcpConfig();
110
+ const claudeSkills = readClaudeSkills();
111
+ const suggestions = [];
112
+
113
+ // Suggest skills based on MCP servers
114
+ for (const mcp of mcpServers) {
115
+ if (mcp.name.includes('slack') || mcp.name.includes('Slack')) {
116
+ suggestions.push({
117
+ name: 'fetch-slack-messages',
118
+ description: `Fetch recent Slack messages using ${mcp.name} MCP server`,
119
+ source: 'mcp',
120
+ mcp_server: mcp.name,
121
+ });
122
+ }
123
+ if (mcp.name.includes('glean') || mcp.name.includes('Glean')) {
124
+ suggestions.push({
125
+ name: 'search-company-docs',
126
+ description: `Search company documents and knowledge via ${mcp.name}`,
127
+ source: 'mcp',
128
+ mcp_server: mcp.name,
129
+ });
130
+ }
131
+ if (mcp.name.includes('github') || mcp.name.includes('GitHub')) {
132
+ suggestions.push({
133
+ name: 'fetch-github-activity',
134
+ description: `Fetch GitHub PRs, commits, and reviews via ${mcp.name}`,
135
+ source: 'mcp',
136
+ mcp_server: mcp.name,
137
+ });
138
+ }
139
+ }
140
+
141
+ return { mcpServers, claudeSkills, suggestions };
142
+ }
143
+
144
+ module.exports = { readMcpConfig, readClaudeSkills, suggestSkillsFromClaudeCode };
@@ -0,0 +1,407 @@
1
+ 'use strict';
2
+ const { spawn } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { EventEmitter } = require('events');
6
+
7
+ const CLAUDE_DIR = path.join(process.env.HOME, '.claude');
8
+
9
+ // Cache of active MCP connections
10
+ const connections = new Map();
11
+
12
+ /**
13
+ * Read all MCP server configs from Claude Code.
14
+ */
15
+ function loadMcpConfigs() {
16
+ const configs = {};
17
+
18
+ // From settings.json (HTTP servers)
19
+ try {
20
+ const settings = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, 'settings.json'), 'utf8'));
21
+ for (const [name, cfg] of Object.entries(settings.mcpServers || {})) {
22
+ configs[name] = { ...cfg, name };
23
+ }
24
+ } catch {}
25
+
26
+ // From mcp.json (stdio servers)
27
+ try {
28
+ const mcp = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, 'mcp.json'), 'utf8'));
29
+ for (const [name, cfg] of Object.entries(mcp.mcpServers || mcp)) {
30
+ if (name === 'mcpServers') continue;
31
+ configs[name] = { ...cfg, name };
32
+ }
33
+ } catch {}
34
+
35
+ // From .credentials.json — OAuth-authenticated MCP servers (Glean, Slack, Mezmo, etc.)
36
+ // Re-read every time since Claude Code may refresh tokens
37
+ try {
38
+ const creds = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, '.credentials.json'), 'utf8'));
39
+ const mcpOAuth = creds.mcpOAuth || {};
40
+ for (const [key, entry] of Object.entries(mcpOAuth)) {
41
+ const serverName = entry.serverName || key.split('|')[0];
42
+ // Skip if already configured from settings.json (but allow credential override)
43
+ if (configs[serverName] && !configs[serverName]._fromCredentials) {
44
+ // Update token on existing config if we have a fresher one
45
+ if (entry.accessToken && entry.serverUrl) {
46
+ configs[serverName].oauth = {
47
+ accessToken: entry.accessToken,
48
+ refreshToken: entry.refreshToken,
49
+ expiresAt: entry.expiresAt,
50
+ };
51
+ }
52
+ continue;
53
+ }
54
+ // Only add servers that have a URL and a valid (non-expired) access token
55
+ const isExpired = entry.expiresAt && entry.expiresAt < Date.now();
56
+ if (entry.serverUrl && entry.accessToken && !isExpired) {
57
+ configs[serverName] = {
58
+ name: serverName,
59
+ type: 'http',
60
+ url: entry.serverUrl,
61
+ oauth: {
62
+ accessToken: entry.accessToken,
63
+ refreshToken: entry.refreshToken,
64
+ expiresAt: entry.expiresAt,
65
+ },
66
+ _fromCredentials: true,
67
+ };
68
+ }
69
+ }
70
+ } catch {}
71
+
72
+ return configs;
73
+ }
74
+
75
+ /**
76
+ * MCP stdio transport — spawns a process and communicates via JSON-RPC over stdin/stdout
77
+ */
78
+ class StdioTransport extends EventEmitter {
79
+ constructor(config) {
80
+ super();
81
+ this.config = config;
82
+ this.process = null;
83
+ this.buffer = '';
84
+ this.nextId = 1;
85
+ this.pending = new Map();
86
+ this.initialized = false;
87
+ }
88
+
89
+ async connect() {
90
+ const env = { ...process.env, ...(this.config.env || {}) };
91
+ this.process = spawn(this.config.command, this.config.args || [], {
92
+ env,
93
+ stdio: ['pipe', 'pipe', 'pipe'],
94
+ cwd: process.env.HOME,
95
+ });
96
+
97
+ this.process.stdout.on('data', (data) => {
98
+ this.buffer += data.toString();
99
+ this._processBuffer();
100
+ });
101
+
102
+ this.process.stderr.on('data', (data) => {
103
+ // MCP servers may log to stderr — ignore
104
+ });
105
+
106
+ this.process.on('error', (err) => {
107
+ console.error(`[mcp-client] Process error for ${this.config.name}:`, err.message);
108
+ });
109
+
110
+ this.process.on('exit', (code) => {
111
+ this.initialized = false;
112
+ // Reject all pending requests
113
+ for (const [id, { reject }] of this.pending) {
114
+ reject(new Error(`MCP server ${this.config.name} exited with code ${code}`));
115
+ }
116
+ this.pending.clear();
117
+ });
118
+
119
+ // Initialize
120
+ await this._send('initialize', {
121
+ protocolVersion: '2024-11-05',
122
+ capabilities: {},
123
+ clientInfo: { name: 'wall-e', version: '0.1.0' },
124
+ });
125
+
126
+ // Send initialized notification (no id, no response expected)
127
+ this._write({ jsonrpc: '2.0', method: 'notifications/initialized' });
128
+ this.initialized = true;
129
+ }
130
+
131
+ async _send(method, params) {
132
+ return new Promise((resolve, reject) => {
133
+ const id = this.nextId++;
134
+ const timeout = setTimeout(() => {
135
+ this.pending.delete(id);
136
+ reject(new Error(`MCP request ${method} timed out after 30s`));
137
+ }, 30000);
138
+
139
+ this.pending.set(id, {
140
+ resolve: (result) => { clearTimeout(timeout); resolve(result); },
141
+ reject: (err) => { clearTimeout(timeout); reject(err); },
142
+ });
143
+
144
+ this._write({ jsonrpc: '2.0', id, method, params });
145
+ });
146
+ }
147
+
148
+ _write(msg) {
149
+ if (!this.process || !this.process.stdin.writable) {
150
+ throw new Error(`MCP server ${this.config.name} not connected`);
151
+ }
152
+ this.process.stdin.write(JSON.stringify(msg) + '\n');
153
+ }
154
+
155
+ _processBuffer() {
156
+ const lines = this.buffer.split('\n');
157
+ this.buffer = lines.pop() || '';
158
+ for (const line of lines) {
159
+ if (!line.trim()) continue;
160
+ try {
161
+ const msg = JSON.parse(line);
162
+ if (msg.id && this.pending.has(msg.id)) {
163
+ const { resolve, reject } = this.pending.get(msg.id);
164
+ this.pending.delete(msg.id);
165
+ if (msg.error) {
166
+ reject(new Error(msg.error.message || JSON.stringify(msg.error)));
167
+ } else {
168
+ resolve(msg.result);
169
+ }
170
+ }
171
+ } catch {}
172
+ }
173
+ }
174
+
175
+ async listTools() {
176
+ const result = await this._send('tools/list', {});
177
+ return result.tools || [];
178
+ }
179
+
180
+ async callTool(name, args) {
181
+ const result = await this._send('tools/call', { name, arguments: args });
182
+ return result;
183
+ }
184
+
185
+ disconnect() {
186
+ if (this.process) {
187
+ this.process.kill();
188
+ this.process = null;
189
+ }
190
+ this.initialized = false;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * MCP HTTP transport — sends JSON-RPC over HTTP POST
196
+ */
197
+ class HttpTransport {
198
+ constructor(config) {
199
+ this.config = config;
200
+ this.url = config.url;
201
+ this.nextId = 1;
202
+ this.initialized = false;
203
+ this.sessionUrl = null;
204
+ // OAuth tokens would be loaded from stored auth
205
+ this.authHeaders = {};
206
+ }
207
+
208
+ async connect() {
209
+ // Try to load OAuth token if configured
210
+ if (this.config.oauth) {
211
+ this._loadOAuthToken();
212
+ }
213
+
214
+ // Initialize
215
+ const result = await this._send('initialize', {
216
+ protocolVersion: '2024-11-05',
217
+ capabilities: {},
218
+ clientInfo: { name: 'wall-e', version: '0.1.0' },
219
+ });
220
+
221
+ // Send initialized notification
222
+ await this._sendNotification('notifications/initialized');
223
+ this.initialized = true;
224
+ return result;
225
+ }
226
+
227
+ _loadOAuthToken() {
228
+ // First check if the config already has an OAuth token (from .credentials.json)
229
+ if (this.config.oauth && this.config.oauth.accessToken) {
230
+ this.authHeaders['Authorization'] = `Bearer ${this.config.oauth.accessToken}`;
231
+ return;
232
+ }
233
+ // Fallback: try file-based token storage
234
+ const tokenPaths = [
235
+ path.join(CLAUDE_DIR, '.mcp-auth', this.config.name, 'tokens.json'),
236
+ path.join(CLAUDE_DIR, 'mcp-tokens', this.config.name + '.json'),
237
+ ];
238
+ for (const p of tokenPaths) {
239
+ try {
240
+ if (fs.existsSync(p)) {
241
+ const tokens = JSON.parse(fs.readFileSync(p, 'utf8'));
242
+ if (tokens.access_token) {
243
+ this.authHeaders['Authorization'] = `Bearer ${tokens.access_token}`;
244
+ return;
245
+ }
246
+ }
247
+ } catch {}
248
+ }
249
+ }
250
+
251
+ async _send(method, params) {
252
+ const id = this.nextId++;
253
+ const body = { jsonrpc: '2.0', id, method };
254
+ if (params) body.params = params;
255
+
256
+ const res = await fetch(this.sessionUrl || this.url, {
257
+ method: 'POST',
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ 'Accept': 'application/json, text/event-stream',
261
+ ...this.authHeaders,
262
+ },
263
+ body: JSON.stringify(body),
264
+ signal: AbortSignal.timeout(30000),
265
+ });
266
+
267
+ // Check for session URL in Mcp-Session header
268
+ const sessionHeader = res.headers.get('mcp-session');
269
+ if (sessionHeader) this.sessionUrl = this.url;
270
+
271
+ if (!res.ok) {
272
+ const text = await res.text();
273
+ throw new Error(`MCP HTTP error ${res.status}: ${text.slice(0, 200)}`);
274
+ }
275
+
276
+ const contentType = res.headers.get('content-type') || '';
277
+
278
+ // Handle SSE responses
279
+ if (contentType.includes('text/event-stream')) {
280
+ const text = await res.text();
281
+ // Parse SSE for the result
282
+ for (const line of text.split('\n')) {
283
+ if (line.startsWith('data: ')) {
284
+ try {
285
+ const data = JSON.parse(line.slice(6));
286
+ if (data.id === id) {
287
+ if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
288
+ return data.result;
289
+ }
290
+ } catch (e) {
291
+ if (e.message.includes('MCP')) throw e;
292
+ }
293
+ }
294
+ }
295
+ return null;
296
+ }
297
+
298
+ // Handle JSON response
299
+ const data = await res.json();
300
+ if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
301
+ return data.result;
302
+ }
303
+
304
+ async _sendNotification(method) {
305
+ try {
306
+ await fetch(this.sessionUrl || this.url, {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json', ...this.authHeaders },
309
+ body: JSON.stringify({ jsonrpc: '2.0', method }),
310
+ signal: AbortSignal.timeout(5000),
311
+ });
312
+ } catch {}
313
+ }
314
+
315
+ async listTools() {
316
+ const result = await this._send('tools/list', {});
317
+ return result?.tools || [];
318
+ }
319
+
320
+ async callTool(name, args) {
321
+ return this._send('tools/call', { name, arguments: args });
322
+ }
323
+
324
+ disconnect() {
325
+ this.initialized = false;
326
+ this.sessionUrl = null;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Get or create a connection to an MCP server.
332
+ */
333
+ async function getConnection(serverName) {
334
+ if (connections.has(serverName)) {
335
+ const conn = connections.get(serverName);
336
+ if (conn.initialized) return conn;
337
+ // Reconnect if disconnected
338
+ connections.delete(serverName);
339
+ }
340
+
341
+ const configs = loadMcpConfigs();
342
+ const config = configs[serverName];
343
+ if (!config) throw new Error(`MCP server "${serverName}" not found in config`);
344
+
345
+ let transport;
346
+ if (config.type === 'http' && config.url) {
347
+ transport = new HttpTransport(config);
348
+ } else if (config.command) {
349
+ transport = new StdioTransport(config);
350
+ } else {
351
+ throw new Error(`Unsupported MCP server config for "${serverName}"`);
352
+ }
353
+
354
+ await transport.connect();
355
+ connections.set(serverName, transport);
356
+ return transport;
357
+ }
358
+
359
+ /**
360
+ * List all available MCP tools across all configured servers.
361
+ */
362
+ async function listAllTools() {
363
+ const configs = loadMcpConfigs();
364
+ const allTools = [];
365
+
366
+ for (const [name, config] of Object.entries(configs)) {
367
+ try {
368
+ const conn = await getConnection(name);
369
+ const tools = await conn.listTools();
370
+ for (const tool of tools) {
371
+ allTools.push({ ...tool, server: name });
372
+ }
373
+ } catch (err) {
374
+ console.error(`[mcp-client] Failed to list tools from ${name}:`, err.message);
375
+ }
376
+ }
377
+
378
+ return allTools;
379
+ }
380
+
381
+ /**
382
+ * Call an MCP tool on a specific server.
383
+ */
384
+ async function callMcpTool(serverName, toolName, args) {
385
+ const conn = await getConnection(serverName);
386
+ return conn.callTool(toolName, args);
387
+ }
388
+
389
+ /**
390
+ * Disconnect all MCP servers.
391
+ */
392
+ function disconnectAll() {
393
+ for (const [name, conn] of connections) {
394
+ try { conn.disconnect(); } catch {}
395
+ }
396
+ connections.clear();
397
+ }
398
+
399
+ module.exports = {
400
+ loadMcpConfigs,
401
+ getConnection,
402
+ listAllTools,
403
+ callMcpTool,
404
+ disconnectAll,
405
+ StdioTransport,
406
+ HttpTransport,
407
+ };