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.
- package/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- 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
|
+
};
|