meshwire 0.1.0 → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meshwire",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI and MCP tools for the MeshWire multi-agent messaging service",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -17,8 +17,14 @@
17
17
  "dev": "node bin/meshwire.js"
18
18
  },
19
19
  "keywords": [
20
- "meshwire", "multi-agent", "agent-mesh", "mcp", "cli",
21
- "agent-communication", "long-polling", "ai-agents"
20
+ "meshwire",
21
+ "multi-agent",
22
+ "agent-mesh",
23
+ "mcp",
24
+ "cli",
25
+ "agent-communication",
26
+ "long-polling",
27
+ "ai-agents"
22
28
  ],
23
29
  "author": "htekdev",
24
30
  "license": "MIT",
package/src/auth.js ADDED
@@ -0,0 +1,75 @@
1
+ // MeshWire credential resolution — reads from multiple sources in priority order
2
+ //
3
+ // Priority: credentials.json > MESHWIRE_TOKEN env > .mesh.json > config.json > null
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
5
+ import { homedir } from 'os';
6
+ import { join } from 'path';
7
+ import { readConfig } from './config.js';
8
+ import { readMeshJson } from './mesh-schema.js';
9
+
10
+ const CREDS_DIR = join(homedir(), '.meshwire');
11
+ const CREDS_FILE = join(CREDS_DIR, 'credentials.json');
12
+
13
+ export function readCredentials() {
14
+ if (!existsSync(CREDS_FILE)) return null;
15
+ try {
16
+ return JSON.parse(readFileSync(CREDS_FILE, 'utf8'));
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function writeCredentials(data) {
23
+ mkdirSync(CREDS_DIR, { recursive: true });
24
+ writeFileSync(CREDS_FILE, JSON.stringify(data, null, 2) + '\n', 'utf8');
25
+ }
26
+
27
+ // Resolve token from all sources in priority order
28
+ export function resolveToken() {
29
+ // 1. credentials.json
30
+ const creds = readCredentials();
31
+ if (creds?.token) return creds.token;
32
+
33
+ // 2. Environment variable
34
+ if (process.env.MESHWIRE_TOKEN) return process.env.MESHWIRE_TOKEN;
35
+
36
+ // 3. Legacy config.json
37
+ const config = readConfig();
38
+ if (config.token) return config.token;
39
+
40
+ return null;
41
+ }
42
+
43
+ // Resolve mesh ID — workspace .mesh.json takes precedence (it's repo-specific)
44
+ export function resolveMeshId() {
45
+ // 1. Workspace .mesh.json
46
+ const meshJson = readMeshJson();
47
+ if (meshJson?.mesh_id) return meshJson.mesh_id;
48
+
49
+ // 2. credentials.json default mesh
50
+ const creds = readCredentials();
51
+ if (creds?.defaultMeshId) return creds.defaultMeshId;
52
+
53
+ // 3. Environment variable
54
+ if (process.env.MESHWIRE_MESH_ID) return process.env.MESHWIRE_MESH_ID;
55
+
56
+ // 4. Legacy config.json
57
+ const config = readConfig();
58
+ if (config.meshId) return config.meshId;
59
+
60
+ return null;
61
+ }
62
+
63
+ export function resolveAgentName() {
64
+ const meshJson = readMeshJson();
65
+ if (meshJson?.agent_name) return meshJson.agent_name;
66
+
67
+ const config = readConfig();
68
+ return config.agentName || 'meshwire-agent';
69
+ }
70
+
71
+ export function resolveUrl() {
72
+ if (process.env.MESHWIRE_URL) return process.env.MESHWIRE_URL;
73
+ const config = readConfig();
74
+ return config.url || 'https://meshwire.io';
75
+ }
package/src/cli.js CHANGED
@@ -9,6 +9,7 @@ import { cmdListen } from './commands/listen.js';
9
9
  import { cmdAgents } from './commands/agents.js';
10
10
  import { cmdMesh } from './commands/mesh.js';
11
11
  import { cmdIntegrate } from './commands/integrate.js';
12
+ import { cmdLogin } from './commands/login.js';
12
13
  import { cmdMcp } from './mcp/server.js';
13
14
 
14
15
  export async function run(version) {
@@ -23,6 +24,15 @@ export async function run(version) {
23
24
  )
24
25
  .version(version, '-v, --version');
25
26
 
27
+ // ─── meshwire login ──────────────────────────────────────────────
28
+ program
29
+ .command('login')
30
+ .description('Sign in with GitHub — saves credentials to ~/.meshwire/credentials.json')
31
+ .option('--url <url>', 'MeshWire URL', 'https://meshwire.io')
32
+ .option('--force', 'Re-authenticate even if already signed in')
33
+ .option('--skip-mesh', 'Skip mesh setup prompt after login')
34
+ .action(cmdLogin);
35
+
26
36
  // ─── meshwire init ───────────────────────────────────────────────
27
37
  program
28
38
  .command('init')
@@ -30,6 +40,12 @@ export async function run(version) {
30
40
  .option('--token <token>', 'API token (mw_...)')
31
41
  .option('--url <url>', 'MeshWire API URL', 'https://meshwire.io')
32
42
  .option('--mesh <meshId>', 'Mesh ID to connect to')
43
+ .option(
44
+ '--harness <name>',
45
+ 'Set up for a specific harness: copilot | claude | hermes | cursor',
46
+ )
47
+ .option('--agent <name>', 'Agent name for harness setup')
48
+ .option('--workspace <name>', 'Workspace name for .mesh.json')
33
49
  .action(cmdInit);
34
50
 
35
51
  // ─── meshwire status ─────────────────────────────────────────────
@@ -1,8 +1,9 @@
1
- // meshwire init — configure token, URL, and mesh
1
+ // meshwire init — configure token, URL, and mesh. Supports --harness for harness-specific setup.
2
2
  import chalk from 'chalk';
3
3
  import { createInterface } from 'readline/promises';
4
4
  import { writeConfig, readConfig } from '../config.js';
5
5
  import { MeshWireClient } from '../api.js';
6
+ import { resolveToken, resolveMeshId, readCredentials, writeCredentials } from '../auth.js';
6
7
 
7
8
  const MESHWIRE_URL = 'https://meshwire.io';
8
9
 
@@ -12,8 +13,54 @@ function prompt(rl, question, defaultVal) {
12
13
  }
13
14
 
14
15
  export async function cmdInit(opts) {
16
+ // ── Harness-specific setup ────────────────────────────────────────────────
17
+ if (opts.harness) {
18
+ const harness = opts.harness.toLowerCase();
19
+ const config = readConfig();
20
+ const meshId = opts.mesh || resolveMeshId();
21
+ const token = opts.token || resolveToken();
22
+ const url = opts.url || config.url || MESHWIRE_URL;
23
+
24
+ switch (harness) {
25
+ case 'copilot': {
26
+ const { setupCopilot } = await import('../harness/copilot.js');
27
+ await setupCopilot({ meshId, agentName: opts.agent, meshwireUrl: url, workspaceName: opts.workspace });
28
+ return;
29
+ }
30
+ case 'claude':
31
+ case 'cursor': {
32
+ const { setupClaude } = await import('../harness/claude.js');
33
+ await setupClaude({ meshId, agentName: opts.agent, meshwireUrl: url, workspaceName: opts.workspace });
34
+ return;
35
+ }
36
+ case 'hermes': {
37
+ const { setupHermes } = await import('../harness/hermes.js');
38
+ await setupHermes({ meshId, agentName: opts.agent, meshwireUrl: url, workspaceName: opts.workspace });
39
+ return;
40
+ }
41
+ default:
42
+ console.error(chalk.red(` Unknown harness: ${harness}`));
43
+ console.error(chalk.dim(' Supported: copilot, claude, cursor, hermes\n'));
44
+ process.exit(1);
45
+ }
46
+ }
47
+
15
48
  console.log('\n' + chalk.bold('🕸 MeshWire Setup') + '\n');
16
49
 
50
+ // Auto-detect existing token from credentials or env
51
+ const detectedToken = resolveToken();
52
+ const detectedMesh = resolveMeshId();
53
+
54
+ if (detectedToken && !opts.token) {
55
+ const creds = readCredentials();
56
+ console.log(chalk.dim(` Found credentials for ${creds?.login || 'your account'}`));
57
+ console.log(chalk.dim(` Token: mw_••••••••${detectedToken.slice(-6)}`));
58
+ if (detectedMesh) {
59
+ console.log(chalk.dim(` Mesh: ${detectedMesh}`));
60
+ }
61
+ console.log('');
62
+ }
63
+
17
64
  // If token + mesh passed via flags, use them directly
18
65
  if (opts.token && opts.mesh) {
19
66
  const config = writeConfig({
@@ -21,6 +68,7 @@ export async function cmdInit(opts) {
21
68
  url: opts.url || MESHWIRE_URL,
22
69
  meshId: opts.mesh,
23
70
  });
71
+ writeCredentials({ token: opts.token, defaultMeshId: opts.mesh, savedAt: new Date().toISOString() });
24
72
  printSuccess(config);
25
73
  return;
26
74
  }
@@ -29,12 +77,16 @@ export async function cmdInit(opts) {
29
77
  const rl = createInterface({ input: process.stdin, output: process.stdout });
30
78
 
31
79
  try {
32
- console.log(chalk.dim(` Get your token at: ${MESHWIRE_URL}/dashboard\n`));
80
+ // Suggest login if no credentials found
81
+ if (!detectedToken) {
82
+ console.log(chalk.dim(` No credentials found. Run ${chalk.cyan('meshwire login')} for browser auth,`));
83
+ console.log(chalk.dim(` or get your token at: ${MESHWIRE_URL}/dashboard\n`));
84
+ }
33
85
 
34
- const token = opts.token || await prompt(rl, 'API token (mw_...)', existing.token);
86
+ const token = opts.token || detectedToken || await prompt(rl, 'API token (mw_...)', existing.token);
35
87
  if (!token || !token.startsWith('mw_')) {
36
88
  console.error(chalk.red('\n Token must start with mw_'));
37
- console.error(chalk.dim(` Sign in at ${MESHWIRE_URL} to get yours.\n`));
89
+ console.error(chalk.dim(` Run 'meshwire login' or sign in at ${MESHWIRE_URL}.\n`));
38
90
  process.exit(1);
39
91
  }
40
92
 
@@ -88,8 +140,11 @@ function printSuccess(config) {
88
140
  console.log(` ${chalk.bold('Agent:')} ${config.agentId || '(not registered)'}`);
89
141
  console.log(` ${chalk.bold('URL:')} ${config.url}`);
90
142
  console.log('\n' + chalk.dim(' Next steps:'));
91
- console.log(chalk.cyan(' meshwire status') + chalk.dim(' — check connection'));
92
- console.log(chalk.cyan(' meshwire listen') + chalk.dim(' — watch for messages'));
93
- console.log(chalk.cyan(' meshwire send "hello"') + chalk.dim(' — send your first message'));
94
- console.log(chalk.cyan(' meshwire integrate') + chalk.dim(' get full integration guide\n'));
143
+ console.log(chalk.cyan(' meshwire status') + chalk.dim(' — check connection'));
144
+ console.log(chalk.cyan(' meshwire listen') + chalk.dim(' — watch for messages'));
145
+ console.log(chalk.cyan(' meshwire send "hello"') + chalk.dim(' — send your first message'));
146
+ console.log(chalk.cyan(' meshwire init --harness copilot') + chalk.dim(' set up Copilot CLI extension'));
147
+ console.log(chalk.cyan(' meshwire init --harness claude') + chalk.dim(' — set up Claude Desktop MCP'));
148
+ console.log(chalk.cyan(' meshwire init --harness hermes') + chalk.dim(' — set up Hermes integration'));
149
+ console.log(chalk.cyan(' meshwire integrate') + chalk.dim(' — get full API guide\n'));
95
150
  }
@@ -0,0 +1,179 @@
1
+ // meshwire login — browser-based OAuth, saves credentials to ~/.meshwire/credentials.json
2
+ //
3
+ // Flow:
4
+ // 1. Find a free local port
5
+ // 2. Start a tiny HTTP server to receive the OAuth callback
6
+ // 3. Open browser to meshwire.io/auth/cli?port=PORT
7
+ // 4. User signs in with GitHub (standard OAuth flow)
8
+ // 5. Server redirects to http://localhost:PORT?token=mw_xxx&login=username
9
+ // 6. CLI captures the token, saves to ~/.meshwire/credentials.json
10
+ // 7. Optionally register as an agent in the last-used mesh
11
+
12
+ import { createServer } from 'http';
13
+ import { URL } from 'url';
14
+ import chalk from 'chalk';
15
+ import { writeCredentials, readCredentials } from '../auth.js';
16
+ import { readConfig, writeConfig } from '../config.js';
17
+ import { MeshWireClient } from '../api.js';
18
+
19
+ const DEFAULT_URL = 'https://meshwire.io';
20
+ const CLI_PORT_RANGE = [57700, 57799]; // range of ports to try
21
+
22
+ function findFreePort() {
23
+ return new Promise((resolve, reject) => {
24
+ const server = createServer();
25
+ server.listen(0, '127.0.0.1', () => {
26
+ const { port } = server.address();
27
+ server.close(() => resolve(port));
28
+ });
29
+ server.on('error', reject);
30
+ });
31
+ }
32
+
33
+ function waitForCallback(port, timeoutMs = 120_000) {
34
+ return new Promise((resolve, reject) => {
35
+ const timer = setTimeout(() => {
36
+ server.close();
37
+ reject(new Error('Login timed out after 2 minutes. Try again.'));
38
+ }, timeoutMs);
39
+
40
+ const server = createServer((req, res) => {
41
+ const url = new URL(req.url, `http://localhost:${port}`);
42
+ const token = url.searchParams.get('token');
43
+ const login = url.searchParams.get('login');
44
+ const error = url.searchParams.get('error');
45
+
46
+ // Send a nice success/error page back to the browser
47
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
48
+ if (token) {
49
+ res.end(`<!DOCTYPE html><html><head><title>MeshWire</title>
50
+ <style>body{font-family:system-ui;background:#080808;color:#f5f5f5;display:grid;place-items:center;height:100vh;margin:0}
51
+ .box{text-align:center;padding:40px;border:1px solid rgba(124,58,237,.4);border-radius:20px;background:rgba(124,58,237,.08)}
52
+ h2{color:#a78bfa;margin-bottom:12px}p{color:rgba(255,255,255,.6)}</style></head>
53
+ <body><div class="box"><h2>✅ Signed in as ${login || 'you'}!</h2>
54
+ <p>You can close this tab and return to your terminal.</p></div></body></html>`);
55
+ } else {
56
+ res.end(`<!DOCTYPE html><html><head><title>MeshWire</title>
57
+ <style>body{font-family:system-ui;background:#080808;color:#f5f5f5;display:grid;place-items:center;height:100vh;margin:0}
58
+ .box{text-align:center;padding:40px;border:1px solid rgba(251,113,133,.4);border-radius:20px}
59
+ h2{color:#fb7185;margin-bottom:12px}p{color:rgba(255,255,255,.6)}</style></head>
60
+ <body><div class="box"><h2>❌ Login failed</h2>
61
+ <p>${error || 'Unknown error'}. Please close this tab and try again.</p></div></body></html>`);
62
+ }
63
+
64
+ clearTimeout(timer);
65
+ server.close();
66
+
67
+ if (token) {
68
+ resolve({ token, login });
69
+ } else {
70
+ reject(new Error(`Login failed: ${error || 'unknown error'}`));
71
+ }
72
+ });
73
+
74
+ server.listen(port, '127.0.0.1', () => {
75
+ // Server is ready
76
+ });
77
+
78
+ server.on('error', (err) => {
79
+ clearTimeout(timer);
80
+ reject(err);
81
+ });
82
+ });
83
+ }
84
+
85
+ async function openBrowser(url) {
86
+ const { platform } = process;
87
+ const { execSync } = await import('child_process');
88
+ try {
89
+ if (platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' });
90
+ else if (platform === 'win32') execSync(`start "" "${url}"`, { shell: true, stdio: 'ignore' });
91
+ else execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ export async function cmdLogin(opts) {
99
+ const meshwireUrl = opts.url || readConfig().url || DEFAULT_URL;
100
+
101
+ console.log('\n' + chalk.bold('🕸 MeshWire Login') + '\n');
102
+
103
+ // Check if already logged in
104
+ const existing = readCredentials();
105
+ if (existing?.token && !opts.force) {
106
+ console.log(chalk.dim(' Already signed in as ') + chalk.bold(existing.login || 'unknown'));
107
+ console.log(chalk.dim(' Use --force to re-authenticate.\n'));
108
+
109
+ const { createInterface } = await import('readline/promises');
110
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
111
+ const answer = await rl.question(chalk.dim(' Re-authenticate? (y/N) '));
112
+ rl.close();
113
+ if (!answer.toLowerCase().startsWith('y')) {
114
+ console.log(chalk.dim('\n Keeping existing credentials.\n'));
115
+ return;
116
+ }
117
+ }
118
+
119
+ // Find a free port
120
+ const port = await findFreePort();
121
+ const authUrl = `${meshwireUrl}/auth/cli?port=${port}`;
122
+
123
+ console.log(chalk.dim(' Opening browser for GitHub OAuth...\n'));
124
+ console.log(chalk.cyan(` ${authUrl}\n`));
125
+ console.log(chalk.dim(' If the browser did not open, paste the URL above.\n'));
126
+
127
+ const opened = await openBrowser(authUrl);
128
+ if (!opened) {
129
+ console.log(chalk.yellow(' Could not open browser automatically.'));
130
+ console.log(chalk.dim(' Please open the URL above in your browser.\n'));
131
+ }
132
+
133
+ // Wait for callback
134
+ let result;
135
+ try {
136
+ result = await waitForCallback(port);
137
+ } catch (err) {
138
+ console.error('\n' + chalk.red(` ✗ ${err.message}\n`));
139
+ process.exit(1);
140
+ }
141
+
142
+ const { token, login } = result;
143
+
144
+ // Save to credentials.json
145
+ writeCredentials({ token, login, savedAt: new Date().toISOString() });
146
+
147
+ // Also update legacy config.json for backward compat
148
+ writeConfig({ token });
149
+
150
+ console.log(chalk.bold.green(`\n ✅ Signed in as ${login}!\n`));
151
+ console.log(chalk.dim(' Credentials saved to ~/.meshwire/credentials.json\n'));
152
+
153
+ // Optionally create a mesh if none exists
154
+ const config = readConfig();
155
+ if (!config.meshId && !opts.skipMesh) {
156
+ console.log(chalk.dim(' No mesh configured yet.'));
157
+ console.log(chalk.dim(` Run ${chalk.cyan('meshwire mesh create')} to create one, or`));
158
+ console.log(chalk.dim(` run ${chalk.cyan('meshwire init')} for full interactive setup.\n`));
159
+ } else if (config.meshId) {
160
+ // Update credentials with default mesh
161
+ writeCredentials({ token, login, defaultMeshId: config.meshId, savedAt: new Date().toISOString() });
162
+ console.log(chalk.dim(` Default mesh: ${config.meshId}`));
163
+ console.log('');
164
+ }
165
+
166
+ // Verify connection
167
+ try {
168
+ const client = new MeshWireClient({ url: meshwireUrl, token });
169
+ await client.health();
170
+ console.log(chalk.green(' ✓ Connected to MeshWire\n'));
171
+ } catch {
172
+ console.log(chalk.yellow(' ⚠ Could not verify connection — check your URL\n'));
173
+ }
174
+
175
+ console.log(chalk.dim(' Next steps:'));
176
+ console.log(chalk.cyan(' meshwire init') + chalk.dim(' — full workspace setup'));
177
+ console.log(chalk.cyan(' meshwire init --harness copilot') + chalk.dim(' — set up Copilot CLI extension'));
178
+ console.log(chalk.cyan(' meshwire status') + chalk.dim(' — verify everything is wired\n'));
179
+ }
@@ -0,0 +1,88 @@
1
+ // Claude Desktop / Cursor harness setup
2
+ // Generates MCP server config at ~/Library/Application Support/Claude/claude_desktop_config.json
3
+ // (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows)
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
6
+ import { homedir, platform } from 'os';
7
+ import { join } from 'path';
8
+ import chalk from 'chalk';
9
+ import { writeMeshJson, readMeshJson } from '../mesh-schema.js';
10
+ import { readCredentials } from '../auth.js';
11
+ import { MeshWireClient } from '../api.js';
12
+
13
+ function getClaudeConfigPath() {
14
+ if (platform() === 'win32') {
15
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
16
+ }
17
+ if (platform() === 'darwin') {
18
+ return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
19
+ }
20
+ return join(homedir(), '.config', 'claude', 'claude_desktop_config.json');
21
+ }
22
+
23
+ export async function setupClaude({ meshId, agentName, meshwireUrl, workspaceName }) {
24
+ console.log('\n' + chalk.bold('⚙️ Setting up Claude Desktop / Cursor harness') + '\n');
25
+
26
+ const creds = readCredentials();
27
+ if (!creds?.token) {
28
+ console.error(chalk.red(' ✗ Not authenticated. Run `meshwire login` first.\n'));
29
+ process.exit(1);
30
+ }
31
+
32
+ // 1. Write .mesh.json
33
+ const existing = readMeshJson() || {};
34
+ const meshJsonData = {
35
+ mesh_id: meshId || existing.mesh_id,
36
+ workspace_name: workspaceName || existing.workspace_name || process.cwd().split(/[/\\]/).pop(),
37
+ agent_name: agentName || existing.agent_name || 'claude-agent',
38
+ harness: 'claude',
39
+ };
40
+
41
+ if (!meshJsonData.mesh_id) {
42
+ console.error(chalk.red(' ✗ No mesh ID. Run `meshwire mesh create` first.\n'));
43
+ process.exit(1);
44
+ }
45
+
46
+ writeMeshJson(meshJsonData);
47
+ console.log(chalk.green(' ✓ .mesh.json written'));
48
+
49
+ // 2. Update Claude Desktop config
50
+ const configPath = getClaudeConfigPath();
51
+ let config = {};
52
+ if (existsSync(configPath)) {
53
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { config = {}; }
54
+ }
55
+
56
+ config.mcpServers = config.mcpServers || {};
57
+ config.mcpServers.meshwire = {
58
+ command: 'npx',
59
+ args: ['meshwire', 'mcp', '--mesh', meshJsonData.mesh_id, '--agent', meshJsonData.agent_name],
60
+ env: {
61
+ MESHWIRE_TOKEN: creds.token,
62
+ MESHWIRE_URL: meshwireUrl || 'https://meshwire.io',
63
+ },
64
+ };
65
+
66
+ mkdirSync(join(configPath, '..'), { recursive: true });
67
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
68
+ console.log(chalk.green(`\n ✓ Claude Desktop config updated`));
69
+ console.log(chalk.dim(` ${configPath}`));
70
+
71
+ // 3. Register agent
72
+ try {
73
+ const client = new MeshWireClient({ url: meshwireUrl, token: creds.token, meshId: meshJsonData.mesh_id });
74
+ const agent = await client.registerAgent(meshJsonData.mesh_id, {
75
+ name: meshJsonData.agent_name,
76
+ description: 'Claude Desktop / Cursor agent',
77
+ workspace: meshJsonData.workspace_name,
78
+ metadata: { platform: 'claude', harness: 'meshwire' },
79
+ });
80
+ console.log(chalk.green(` ✓ Registered as ${agent.name} (${agent.agent_id})`));
81
+ } catch (err) {
82
+ console.log(chalk.yellow(` ⚠ ${err.message}`));
83
+ }
84
+
85
+ console.log('\n' + chalk.bold.green(' ✅ Claude harness ready!\n'));
86
+ console.log(chalk.dim(' Restart Claude Desktop to load the MeshWire MCP server.'));
87
+ console.log(chalk.dim(' MCP tools: meshwire_send_message, meshwire_get_messages, meshwire_list_agents\n'));
88
+ }
@@ -0,0 +1,266 @@
1
+ // Copilot CLI harness setup
2
+ // Generates a user-level Copilot extension at ~/.copilot/extensions/meshwire.mjs
3
+ // The extension reads credentials from ~/.meshwire/credentials.json and
4
+ // workspace config from .mesh.json in the current directory.
5
+
6
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { join } from 'path';
9
+ import chalk from 'chalk';
10
+ import { writeMeshJson, readMeshJson } from '../mesh-schema.js';
11
+ import { readCredentials } from '../auth.js';
12
+ import { MeshWireClient } from '../api.js';
13
+
14
+ const EXTENSION_DIR = join(homedir(), '.copilot', 'extensions');
15
+ const EXTENSION_FILE = join(EXTENSION_DIR, 'meshwire.mjs');
16
+
17
+ // The generated extension template
18
+ function buildExtensionSource({ meshwireUrl }) {
19
+ return `// MeshWire — Copilot CLI Extension
20
+ // Auto-generated by: meshwire init --harness copilot
21
+ // Reads credentials from ~/.meshwire/credentials.json
22
+ // Reads workspace mesh from .mesh.json in the current directory
23
+
24
+ import { readFileSync, existsSync } from 'fs';
25
+ import { homedir } from 'os';
26
+ import { join } from 'path';
27
+
28
+ const CREDS_FILE = join(homedir(), '.meshwire', 'credentials.json');
29
+ const DEFAULT_URL = '${meshwireUrl}';
30
+
31
+ function loadCreds() {
32
+ if (!existsSync(CREDS_FILE)) return null;
33
+ try { return JSON.parse(readFileSync(CREDS_FILE, 'utf8')); } catch { return null; }
34
+ }
35
+
36
+ function loadMeshJson() {
37
+ const file = join(process.cwd(), '.mesh.json');
38
+ if (!existsSync(file)) return null;
39
+ try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
40
+ }
41
+
42
+ async function api(path, method = 'GET', body = null) {
43
+ const creds = loadCreds();
44
+ if (!creds?.token) throw new Error('Not authenticated. Run: meshwire login');
45
+ const meshJson = loadMeshJson();
46
+ const url = DEFAULT_URL + path.replace('{meshId}', meshJson?.mesh_id || '');
47
+ const opts = {
48
+ method,
49
+ headers: {
50
+ 'Authorization': 'Bearer ' + creds.token,
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ };
54
+ if (body) opts.body = JSON.stringify(body);
55
+ const res = await fetch(url, opts);
56
+ if (!res.ok) {
57
+ const txt = await res.text().catch(() => '');
58
+ let msg; try { msg = JSON.parse(txt).error; } catch { msg = txt || res.statusText; }
59
+ throw new Error(msg);
60
+ }
61
+ return res.json();
62
+ }
63
+
64
+ // ─── Tool Definitions ────────────────────────────────────────────────────────
65
+
66
+ export const tools = [
67
+ {
68
+ name: 'mesh_send_message',
69
+ description: 'Send a message to agents in the mesh. Use recipient_id "*" to broadcast.',
70
+ parameters: {
71
+ type: 'object',
72
+ properties: {
73
+ content: { type: 'string', description: 'Message content' },
74
+ recipient_id: { type: 'string', default: '*', description: 'Target agent ID or "*" for all' },
75
+ priority: { type: 'string', enum: ['urgent', 'high', 'normal', 'low'], default: 'normal' },
76
+ },
77
+ required: ['content'],
78
+ },
79
+ async execute({ content, recipient_id = '*', priority = 'normal' }) {
80
+ const creds = loadCreds();
81
+ const meshJson = loadMeshJson();
82
+ if (!meshJson?.mesh_id) throw new Error('No mesh configured. Add .mesh.json to this workspace.');
83
+ const agent_id = creds?.agentId || 'copilot-cli';
84
+ const msg = await api(
85
+ '/mesh/{meshId}/messages', 'POST',
86
+ { sender_id: agent_id, recipient_id, content, priority }
87
+ );
88
+ return { sent: true, message_id: msg.message_id, to: recipient_id };
89
+ },
90
+ },
91
+
92
+ {
93
+ name: 'mesh_get_messages',
94
+ description: 'Fetch new messages from the mesh. Uses long-polling (up to 10s).',
95
+ parameters: {
96
+ type: 'object',
97
+ properties: {
98
+ offset: { type: 'integer', default: 0, description: 'Return messages with ID > offset' },
99
+ timeout: { type: 'integer', default: 10, description: 'Poll timeout in seconds (max 30)' },
100
+ },
101
+ },
102
+ async execute({ offset = 0, timeout = 10 }) {
103
+ const creds = loadCreds();
104
+ const meshJson = loadMeshJson();
105
+ if (!meshJson?.mesh_id) throw new Error('No mesh configured. Add .mesh.json to this workspace.');
106
+ const agentId = creds?.agentId || undefined;
107
+ const params = new URLSearchParams({ offset: String(offset), timeout: String(Math.min(timeout, 30)) });
108
+ if (agentId) params.set('recipient', agentId);
109
+ const result = await api('/mesh/{meshId}/messages?' + params.toString());
110
+ if (!result.messages?.length) return { messages: [], count: 0 };
111
+ return {
112
+ count: result.count,
113
+ messages: result.messages.map(m => ({
114
+ id: m.message_id,
115
+ from: m.sender_id,
116
+ to: m.recipient_id,
117
+ content: m.content,
118
+ priority: m.priority,
119
+ time: m.created_at,
120
+ })),
121
+ };
122
+ },
123
+ },
124
+
125
+ {
126
+ name: 'mesh_list_agents',
127
+ description: 'List all agents currently registered in this workspace mesh.',
128
+ parameters: { type: 'object', properties: {} },
129
+ async execute() {
130
+ const meshJson = loadMeshJson();
131
+ if (!meshJson?.mesh_id) throw new Error('No mesh configured. Add .mesh.json to this workspace.');
132
+ const result = await api('/mesh/{meshId}/agents');
133
+ return {
134
+ count: result.count,
135
+ agents: result.agents.map(a => ({
136
+ id: a.agent_id,
137
+ name: a.name,
138
+ status: a.status,
139
+ workspace: a.workspace,
140
+ last_seen: a.last_seen,
141
+ })),
142
+ };
143
+ },
144
+ },
145
+
146
+ {
147
+ name: 'mesh_status',
148
+ description: 'Check MeshWire connection status and current mesh/agent info.',
149
+ parameters: { type: 'object', properties: {} },
150
+ async execute() {
151
+ const creds = loadCreds();
152
+ const meshJson = loadMeshJson();
153
+ const health = await fetch(DEFAULT_URL + '/health').then(r => r.json()).catch(() => null);
154
+ return {
155
+ authenticated: Boolean(creds?.token),
156
+ login: creds?.login || null,
157
+ mesh_id: meshJson?.mesh_id || null,
158
+ workspace: meshJson?.workspace_name || null,
159
+ agent_name: meshJson?.agent_name || null,
160
+ service: health?.status || 'unreachable',
161
+ };
162
+ },
163
+ },
164
+ ];
165
+
166
+ // ─── Hooks ───────────────────────────────────────────────────────────────────
167
+
168
+ export const hooks = {
169
+ // Called on session start — register this agent in the mesh
170
+ async onSessionStart({ agentName } = {}) {
171
+ const creds = loadCreds();
172
+ const meshJson = loadMeshJson();
173
+ if (!creds?.token || !meshJson?.mesh_id) return;
174
+
175
+ const name = agentName || meshJson?.agent_name || 'copilot-cli';
176
+ try {
177
+ const agent = await api('/mesh/{meshId}/agents', 'POST', {
178
+ name,
179
+ description: 'GitHub Copilot CLI agent',
180
+ workspace: process.cwd().split('/').pop() || 'workspace',
181
+ metadata: { platform: 'copilot-cli', harness: 'meshwire' },
182
+ });
183
+ // Store agent ID in a runtime variable for this session
184
+ if (typeof globalThis !== 'undefined') {
185
+ globalThis.__meshwire_agent_id__ = agent.agent_id;
186
+ }
187
+ console.log('[meshwire] Registered as ' + name + ' (' + agent.agent_id + ')');
188
+ } catch (err) {
189
+ console.warn('[meshwire] Could not register agent:', err.message);
190
+ }
191
+ },
192
+
193
+ // Called periodically — keep heartbeat alive
194
+ async onHeartbeat() {
195
+ const creds = loadCreds();
196
+ const meshJson = loadMeshJson();
197
+ const agentId = globalThis.__meshwire_agent_id__;
198
+ if (!creds?.token || !meshJson?.mesh_id || !agentId) return;
199
+ await api('/mesh/{meshId}/agents/' + agentId + '/heartbeat', 'POST').catch(() => {});
200
+ },
201
+ };
202
+ `;
203
+ }
204
+
205
+ export async function setupCopilot({ meshId, agentName, meshwireUrl, workspaceName }) {
206
+ console.log('\n' + chalk.bold('⚙️ Setting up Copilot CLI harness') + '\n');
207
+
208
+ const creds = readCredentials();
209
+ if (!creds?.token) {
210
+ console.error(chalk.red(' ✗ Not authenticated. Run `meshwire login` first.\n'));
211
+ process.exit(1);
212
+ }
213
+
214
+ // 1. Create/update .mesh.json in the current workspace
215
+ const existing = readMeshJson() || {};
216
+ const meshJsonData = {
217
+ mesh_id: meshId || existing.mesh_id,
218
+ workspace_name: workspaceName || existing.workspace_name || process.cwd().split(/[/\\]/).pop(),
219
+ agent_name: agentName || existing.agent_name || 'copilot-agent',
220
+ harness: 'copilot',
221
+ };
222
+
223
+ if (!meshJsonData.mesh_id) {
224
+ console.error(chalk.red(' ✗ No mesh ID. Create a mesh first: meshwire mesh create\n'));
225
+ process.exit(1);
226
+ }
227
+
228
+ writeMeshJson(meshJsonData);
229
+ console.log(chalk.green(' ✓ .mesh.json written'));
230
+ console.log(chalk.dim(` mesh_id: ${meshJsonData.mesh_id}`));
231
+ console.log(chalk.dim(` agent_name: ${meshJsonData.agent_name}`));
232
+ console.log(chalk.dim(` harness: copilot`));
233
+
234
+ // 2. Generate and write the Copilot extension
235
+ mkdirSync(EXTENSION_DIR, { recursive: true });
236
+ const source = buildExtensionSource({ meshwireUrl: meshwireUrl || 'https://meshwire.io' });
237
+ writeFileSync(EXTENSION_FILE, source, 'utf8');
238
+ console.log(chalk.green(`\n ✓ Extension installed at ${EXTENSION_FILE}`));
239
+
240
+ // 3. Register the agent in the mesh now
241
+ console.log(chalk.dim('\n Registering agent in mesh...'));
242
+ try {
243
+ const client = new MeshWireClient({ url: meshwireUrl, token: creds.token, meshId: meshJsonData.mesh_id });
244
+ const agent = await client.registerAgent(meshJsonData.mesh_id, {
245
+ name: meshJsonData.agent_name,
246
+ description: 'GitHub Copilot CLI agent',
247
+ workspace: meshJsonData.workspace_name,
248
+ metadata: { platform: 'copilot-cli', harness: 'meshwire' },
249
+ });
250
+ console.log(chalk.green(` ✓ Registered as ${agent.name} (${agent.agent_id})`));
251
+ } catch (err) {
252
+ console.log(chalk.yellow(` ⚠ Could not register now: ${err.message}`));
253
+ console.log(chalk.dim(' The extension will auto-register on next Copilot CLI session.'));
254
+ }
255
+
256
+ // 4. Print summary
257
+ console.log('\n' + chalk.bold.green(' ✅ Copilot harness ready!\n'));
258
+ console.log(chalk.dim(' The extension provides these tools in every Copilot session:'));
259
+ console.log(chalk.cyan(' mesh_send_message') + chalk.dim(' — broadcast or target a specific agent'));
260
+ console.log(chalk.cyan(' mesh_get_messages') + chalk.dim(' — poll for new messages'));
261
+ console.log(chalk.cyan(' mesh_list_agents') + chalk.dim(' — discover agents in the mesh'));
262
+ console.log(chalk.cyan(' mesh_status') + chalk.dim(' — check connection health'));
263
+ console.log('');
264
+ console.log(chalk.dim(' The .mesh.json file in this repo defines which mesh Copilot connects to.'));
265
+ console.log(chalk.dim(' Commit .mesh.json so your teammates auto-join the same mesh.\n'));
266
+ }
@@ -0,0 +1,101 @@
1
+ // Hermes / generic harness setup
2
+ // Writes a skill document + env file for Hermes (Pi) and other harnesses
3
+
4
+ import { writeFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ import chalk from 'chalk';
7
+ import { writeMeshJson, readMeshJson } from '../mesh-schema.js';
8
+ import { readCredentials } from '../auth.js';
9
+ import { MeshWireClient } from '../api.js';
10
+
11
+ export async function setupHermes({ meshId, agentName, meshwireUrl, workspaceName }) {
12
+ console.log('\n' + chalk.bold('⚙️ Setting up Hermes / generic harness') + '\n');
13
+
14
+ const creds = readCredentials();
15
+ if (!creds?.token) {
16
+ console.error(chalk.red(' ✗ Not authenticated. Run `meshwire login` first.\n'));
17
+ process.exit(1);
18
+ }
19
+
20
+ const existing = readMeshJson() || {};
21
+ const meshJsonData = {
22
+ mesh_id: meshId || existing.mesh_id,
23
+ workspace_name: workspaceName || existing.workspace_name || 'hermes',
24
+ agent_name: agentName || existing.agent_name || 'hermes-agent',
25
+ harness: 'hermes',
26
+ };
27
+
28
+ if (!meshJsonData.mesh_id) {
29
+ console.error(chalk.red(' ✗ No mesh ID. Run `meshwire mesh create` first.\n'));
30
+ process.exit(1);
31
+ }
32
+
33
+ writeMeshJson(meshJsonData);
34
+ console.log(chalk.green(' ✓ .mesh.json written'));
35
+
36
+ // Write a .env.meshwire file for Hermes to source
37
+ const envContent = [
38
+ `MESHWIRE_TOKEN=${creds.token}`,
39
+ `MESHWIRE_URL=${meshwireUrl || 'https://meshwire.io'}`,
40
+ `MESHWIRE_MESH_ID=${meshJsonData.mesh_id}`,
41
+ `MESHWIRE_AGENT_NAME=${meshJsonData.agent_name}`,
42
+ '',
43
+ ].join('\n');
44
+
45
+ writeFileSync(join(process.cwd(), '.env.meshwire'), envContent, 'utf8');
46
+ console.log(chalk.green(' ✓ .env.meshwire written'));
47
+
48
+ // Write skill document
49
+ const skillContent = `# MeshWire Skill — ${meshJsonData.workspace_name}
50
+
51
+ ## Mesh
52
+ \`\`\`
53
+ MESH_ID=${meshJsonData.mesh_id}
54
+ AGENT_NAME=${meshJsonData.agent_name}
55
+ BASE_URL=${meshwireUrl || 'https://meshwire.io'}
56
+ \`\`\`
57
+
58
+ ## Quick commands
59
+
60
+ ### Send message
61
+ \`\`\`bash
62
+ curl -sX POST $MESHWIRE_URL/mesh/${meshJsonData.mesh_id}/messages \\
63
+ -H "Authorization: Bearer $MESHWIRE_TOKEN" \\
64
+ -H "Content-Type: application/json" \\
65
+ -d '{"sender_id":"YOUR_AGENT_ID","content":"hello","recipient_id":"*"}'
66
+ \`\`\`
67
+
68
+ ### Poll messages
69
+ \`\`\`bash
70
+ curl -s "$MESHWIRE_URL/mesh/${meshJsonData.mesh_id}/messages?timeout=30&offset=0" \\
71
+ -H "Authorization: Bearer $MESHWIRE_TOKEN"
72
+ \`\`\`
73
+
74
+ ### List agents
75
+ \`\`\`bash
76
+ curl -s "$MESHWIRE_URL/mesh/${meshJsonData.mesh_id}/agents" \\
77
+ -H "Authorization: Bearer $MESHWIRE_TOKEN"
78
+ \`\`\`
79
+ `;
80
+
81
+ writeFileSync(join(process.cwd(), 'MESHWIRE_SKILL.md'), skillContent, 'utf8');
82
+ console.log(chalk.green(' ✓ MESHWIRE_SKILL.md written'));
83
+
84
+ // Register agent
85
+ try {
86
+ const client = new MeshWireClient({ url: meshwireUrl, token: creds.token, meshId: meshJsonData.mesh_id });
87
+ const agent = await client.registerAgent(meshJsonData.mesh_id, {
88
+ name: meshJsonData.agent_name,
89
+ description: 'Hermes agent',
90
+ workspace: meshJsonData.workspace_name,
91
+ metadata: { platform: 'hermes', harness: 'meshwire' },
92
+ });
93
+ console.log(chalk.green(` ✓ Registered as ${agent.name} (${agent.agent_id})`));
94
+ } catch (err) {
95
+ console.log(chalk.yellow(` ⚠ ${err.message}`));
96
+ }
97
+
98
+ console.log('\n' + chalk.bold.green(' ✅ Hermes harness ready!\n'));
99
+ console.log(chalk.dim(' Source .env.meshwire in your Hermes startup script.'));
100
+ console.log(chalk.dim(' Drop MESHWIRE_SKILL.md into your agent context.\n'));
101
+ }
@@ -0,0 +1,41 @@
1
+ // .mesh.json — workspace-level mesh config
2
+ // Lives in the repo root. Defines which mesh this workspace participates in.
3
+ //
4
+ // Schema:
5
+ // {
6
+ // "mesh_id": "kR9xQpLmW3aZ", — which mesh this workspace joins
7
+ // "workspace_name": "rocha-family", — human label for this workspace
8
+ // "agent_name": "copilot-assistant", — how this agent appears in the mesh
9
+ // "harness": "copilot" — which harness drives this workspace
10
+ // }
11
+
12
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
13
+ import { join } from 'path';
14
+
15
+ const MESH_JSON_FILE = '.mesh.json';
16
+
17
+ export function readMeshJson(cwd = process.cwd()) {
18
+ const file = join(cwd, MESH_JSON_FILE);
19
+ if (!existsSync(file)) return null;
20
+ try {
21
+ return JSON.parse(readFileSync(file, 'utf8'));
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ export function writeMeshJson(data, cwd = process.cwd()) {
28
+ const file = join(cwd, MESH_JSON_FILE);
29
+ writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
30
+ }
31
+
32
+ export function meshJsonExists(cwd = process.cwd()) {
33
+ return existsSync(join(cwd, MESH_JSON_FILE));
34
+ }
35
+
36
+ export const MESH_JSON_SCHEMA = {
37
+ mesh_id: 'string — which mesh this workspace connects to',
38
+ workspace_name: 'string — human-readable label for this workspace',
39
+ agent_name: 'string — how this agent appears in the mesh',
40
+ harness: 'string — copilot | claude | hermes | cursor | raw',
41
+ };