phewsh 0.10.0 → 0.11.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/README.md +35 -22
- package/bin/phewsh.js +4 -0
- package/commands/mcp.js +368 -0
- package/commands/serve.js +277 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -12,6 +12,23 @@ npm install -g phewsh
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
phewsh # enter the interactive shell
|
|
15
|
+
phewsh serve # start live execution bridge for phewsh.com/intent
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Live Execution
|
|
19
|
+
|
|
20
|
+
Connect the web app to your local machine for real-time task execution:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
phewsh serve
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This starts a bridge on `localhost:7483`. Open [phewsh.com/intent](https://phewsh.com/intent) and go to the Work tab — you'll see a green "Live" indicator. Agent tasks get a "Run Live" button that executes via Claude Code on your machine.
|
|
27
|
+
|
|
28
|
+
## Interactive Shell
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
phewsh
|
|
15
32
|
```
|
|
16
33
|
|
|
17
34
|
Inside the shell:
|
|
@@ -22,7 +39,6 @@ Inside the shell:
|
|
|
22
39
|
/push Sync local to cloud
|
|
23
40
|
/pull Sync cloud to local
|
|
24
41
|
/context View loaded artifacts
|
|
25
|
-
/sync Check sync status
|
|
26
42
|
/help All commands
|
|
27
43
|
```
|
|
28
44
|
|
|
@@ -36,9 +52,25 @@ Creates three structured artifacts in `.intent/`:
|
|
|
36
52
|
|
|
37
53
|
These artifacts become persistent context for AI conversations, both in the shell and across tools.
|
|
38
54
|
|
|
55
|
+
## All Commands
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
phewsh # Interactive AI session with .intent/ context
|
|
59
|
+
phewsh serve # Live execution bridge for web app
|
|
60
|
+
phewsh clarify # AI-assisted artifact generation
|
|
61
|
+
phewsh intent --init # Create .intent/ without entering the shell
|
|
62
|
+
phewsh intent --status # Check artifact state
|
|
63
|
+
phewsh ai run "prompt" # One-shot AI with .intent/ context
|
|
64
|
+
phewsh login # Authenticate + set API key
|
|
65
|
+
phewsh push # Sync local .intent/ to cloud
|
|
66
|
+
phewsh pull # Sync cloud to local
|
|
67
|
+
phewsh mcp setup # Configure MCP server for agent connectivity
|
|
68
|
+
phewsh style # Build your style identity
|
|
69
|
+
```
|
|
70
|
+
|
|
39
71
|
## Sync
|
|
40
72
|
|
|
41
|
-
CLI and web (phewsh.com/intent) share the same cloud via Supabase.
|
|
73
|
+
CLI and web ([phewsh.com/intent](https://phewsh.com/intent)) share the same cloud via Supabase.
|
|
42
74
|
|
|
43
75
|
```bash
|
|
44
76
|
phewsh login # authenticate
|
|
@@ -46,26 +78,7 @@ phewsh push # upload .intent/ to cloud
|
|
|
46
78
|
phewsh pull # download from cloud
|
|
47
79
|
```
|
|
48
80
|
|
|
49
|
-
Sync is manual. The CLI shows status on startup
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
↓ Cloud is newer (2h ago) — run /pull
|
|
53
|
-
↑ Local changes not pushed (15m ago) — run /push
|
|
54
|
-
↕ synced
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The `/intent` Claude Code skill writes local files only — run `phewsh push` after using it.
|
|
58
|
-
|
|
59
|
-
## Non-interactive mode
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
phewsh intent --init # Create .intent/ without entering the shell
|
|
63
|
-
phewsh intent --status # Check artifact state
|
|
64
|
-
phewsh clarify # AI-assisted artifact generation
|
|
65
|
-
phewsh ai run "prompt" # One-shot AI with .intent/ context
|
|
66
|
-
phewsh push # Sync to cloud
|
|
67
|
-
phewsh pull # Sync from cloud
|
|
68
|
-
```
|
|
81
|
+
Sync is manual. The CLI shows status on startup.
|
|
69
82
|
|
|
70
83
|
## Web app
|
|
71
84
|
|
package/bin/phewsh.js
CHANGED
|
@@ -48,6 +48,8 @@ const COMMANDS = {
|
|
|
48
48
|
style: () => require('../commands/style'),
|
|
49
49
|
mbhd: () => require('../commands/mbhd'),
|
|
50
50
|
sap: () => require('../commands/sap'),
|
|
51
|
+
mcp: () => require('../commands/mcp')(),
|
|
52
|
+
serve: () => require('../commands/serve')(),
|
|
51
53
|
help: showHelp,
|
|
52
54
|
version: showVersion,
|
|
53
55
|
};
|
|
@@ -72,6 +74,8 @@ function showHelp() {
|
|
|
72
74
|
console.log(` ${w('intent')} Manage .intent/ artifacts — status, open, evolve`);
|
|
73
75
|
console.log(` ${w('ai')} One-shot AI prompt (reads .intent/)`);
|
|
74
76
|
console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
|
|
77
|
+
console.log(` ${w('serve')} Start live execution bridge for the web app`);
|
|
78
|
+
console.log(` ${w('mcp')} Connect AI agents — setup, sync, status`);
|
|
75
79
|
console.log(` ${w('sap')} Sustainable AI Protocol — usage and accountability`);
|
|
76
80
|
console.log(` ${w('style')} Build your style identity — ingest, profile, sync`);
|
|
77
81
|
console.log(` ${w('mbhd')} MBHD music engine`);
|
package/commands/mcp.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// phewsh mcp — Set up and sync the PHEWSH MCP server for agent connectivity
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// phewsh mcp setup — Install MCP server deps + configure Claude Code
|
|
5
|
+
// phewsh mcp sync — Sync local .intent/ + cloud projects → ~/.phewsh/projects.json
|
|
6
|
+
// phewsh mcp status — Check what agents can see right now
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
|
|
14
|
+
const PROJECTS_FILE = path.join(PHEWSH_DIR, 'projects.json');
|
|
15
|
+
const RESULTS_DIR = path.join(PHEWSH_DIR, 'results');
|
|
16
|
+
const SESSIONS_DIR = path.join(PHEWSH_DIR, 'sessions');
|
|
17
|
+
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
18
|
+
|
|
19
|
+
// ANSI helpers
|
|
20
|
+
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
21
|
+
const d = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
22
|
+
const g = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
23
|
+
const w = (s) => `\x1b[97m${s}\x1b[0m`;
|
|
24
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
25
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
26
|
+
|
|
27
|
+
function ensureDirs() {
|
|
28
|
+
[PHEWSH_DIR, RESULTS_DIR, SESSIONS_DIR].forEach(dir => {
|
|
29
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadLocalProject() {
|
|
34
|
+
if (!fs.existsSync(INTENT_DIR)) return null;
|
|
35
|
+
|
|
36
|
+
const project = {
|
|
37
|
+
id: 'local',
|
|
38
|
+
name: path.basename(process.cwd()),
|
|
39
|
+
source: 'local',
|
|
40
|
+
artifacts: {},
|
|
41
|
+
actions: [],
|
|
42
|
+
decisionGate: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const files = ['vision.md', 'plan.md', 'next.md', 'status.md', 'narrative.md'];
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const filePath = path.join(INTENT_DIR, file);
|
|
48
|
+
if (fs.existsSync(filePath)) {
|
|
49
|
+
const kind = file.replace('.md', '');
|
|
50
|
+
project.artifacts[kind] = {
|
|
51
|
+
kind,
|
|
52
|
+
content: fs.readFileSync(filePath, 'utf-8'),
|
|
53
|
+
};
|
|
54
|
+
if (kind === 'vision') {
|
|
55
|
+
const firstLine = project.artifacts[kind].content.split('\n').find(l => l.trim().length > 5);
|
|
56
|
+
if (firstLine) project.name = firstLine.replace(/^#+\s*/, '').trim().slice(0, 60);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read project.json for gate, actions, constraints
|
|
62
|
+
const metaPath = path.join(INTENT_DIR, 'project.json');
|
|
63
|
+
if (fs.existsSync(metaPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
66
|
+
Object.assign(project, meta);
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Object.keys(project.artifacts).length > 0 ? project : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function loadCloudProjects() {
|
|
74
|
+
const configPath = path.join(PHEWSH_DIR, 'config.json');
|
|
75
|
+
if (!fs.existsSync(configPath)) return [];
|
|
76
|
+
|
|
77
|
+
let config;
|
|
78
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { return []; }
|
|
79
|
+
if (!config?.supabaseAccessToken || !config?.supabaseUserId) return [];
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const { select, refreshSession } = require('../lib/supabase');
|
|
83
|
+
|
|
84
|
+
// Refresh token if needed
|
|
85
|
+
if (config.supabaseRefreshToken) {
|
|
86
|
+
const session = await refreshSession(config.supabaseRefreshToken);
|
|
87
|
+
if (session?.access_token) {
|
|
88
|
+
config.supabaseAccessToken = session.access_token;
|
|
89
|
+
config.supabaseRefreshToken = session.refresh_token;
|
|
90
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const projects = await select(
|
|
95
|
+
'projects',
|
|
96
|
+
`user_id=eq.${config.supabaseUserId}&select=id,name,archetype,freeform_text,pps_json`,
|
|
97
|
+
config.supabaseAccessToken
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Also fetch artifacts for each project
|
|
101
|
+
const enriched = [];
|
|
102
|
+
for (const p of projects) {
|
|
103
|
+
const artifacts = await select(
|
|
104
|
+
'artifacts',
|
|
105
|
+
`project_id=eq.${p.id}&user_id=eq.${config.supabaseUserId}&select=kind,content`,
|
|
106
|
+
config.supabaseAccessToken
|
|
107
|
+
).catch(() => []);
|
|
108
|
+
|
|
109
|
+
const artifactsMap = {};
|
|
110
|
+
for (const a of artifacts) {
|
|
111
|
+
artifactsMap[a.kind] = { kind: a.kind, content: a.content };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
enriched.push({
|
|
115
|
+
id: p.id,
|
|
116
|
+
name: p.name,
|
|
117
|
+
source: 'cloud',
|
|
118
|
+
archetype: p.archetype,
|
|
119
|
+
tldr: p.freeform_text?.slice(0, 200),
|
|
120
|
+
artifacts: artifactsMap,
|
|
121
|
+
actions: p.pps_json?.actions || [],
|
|
122
|
+
decisionGate: p.pps_json?.decisionGate || null,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return enriched;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.log(g(` Could not fetch cloud projects: ${err.message}`));
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findMcpServerPath() {
|
|
133
|
+
// Check common locations
|
|
134
|
+
const candidates = [
|
|
135
|
+
path.join(__dirname, '..', '..', 'mcp', 'src', 'index.js'), // monorepo sibling
|
|
136
|
+
path.join(process.cwd(), 'mcp', 'src', 'index.js'), // cwd
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// Also check if @phewsh/mcp-server is globally installed
|
|
140
|
+
try {
|
|
141
|
+
const globalPath = execSync('npm root -g', { encoding: 'utf-8' }).trim();
|
|
142
|
+
candidates.push(path.join(globalPath, '@phewsh', 'mcp-server', 'src', 'index.js'));
|
|
143
|
+
} catch { /* not installed globally */ }
|
|
144
|
+
|
|
145
|
+
for (const p of candidates) {
|
|
146
|
+
if (fs.existsSync(p)) return path.resolve(p);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function setup() {
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(` ${b(w('PHEWSH MCP Setup'))}`);
|
|
154
|
+
console.log(` ${g('Connect your AI agents to your project intelligence')}`);
|
|
155
|
+
console.log('');
|
|
156
|
+
|
|
157
|
+
ensureDirs();
|
|
158
|
+
|
|
159
|
+
// 1. Find or install MCP server
|
|
160
|
+
let serverPath = findMcpServerPath();
|
|
161
|
+
|
|
162
|
+
if (!serverPath) {
|
|
163
|
+
console.log(` ${yellow('MCP server not found locally.')}`);
|
|
164
|
+
console.log(` ${g('Install it:')}`);
|
|
165
|
+
console.log(` npm install -g @phewsh/mcp-server`);
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(` ${g('Or if you have the phewsh repo:')}`);
|
|
168
|
+
console.log(` cd mcp && npm install`);
|
|
169
|
+
console.log('');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(` ${green('Found MCP server:')} ${g(serverPath)}`);
|
|
174
|
+
|
|
175
|
+
// 2. Check if deps are installed
|
|
176
|
+
const mcpDir = path.dirname(path.dirname(serverPath));
|
|
177
|
+
const nodeModules = path.join(mcpDir, 'node_modules');
|
|
178
|
+
if (!fs.existsSync(nodeModules)) {
|
|
179
|
+
console.log(` ${yellow('Installing dependencies...')}`);
|
|
180
|
+
try {
|
|
181
|
+
execSync('npm install', { cwd: mcpDir, stdio: 'pipe' });
|
|
182
|
+
console.log(` ${green('Dependencies installed.')}`);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(` ${yellow('Failed to install deps.')} Run manually: cd ${mcpDir} && npm install`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 3. Generate settings.json snippet
|
|
189
|
+
const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
190
|
+
const snippet = {
|
|
191
|
+
mcpServers: {
|
|
192
|
+
phewsh: {
|
|
193
|
+
command: 'node',
|
|
194
|
+
args: [serverPath],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(` ${b('Add this to')} ${w(claudeSettingsPath)}${b(':')}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(g(' ' + JSON.stringify(snippet, null, 2).split('\n').join('\n ')));
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
// Check if already configured
|
|
206
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
207
|
+
try {
|
|
208
|
+
const existing = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
|
|
209
|
+
if (existing.mcpServers?.phewsh) {
|
|
210
|
+
console.log(` ${green('Already configured in Claude Code settings.')}`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(` ${yellow('Not yet configured.')} Add the snippet above to your settings.`);
|
|
213
|
+
}
|
|
214
|
+
} catch { /* ignore */ }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 4. Sync projects
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(` ${g('Running initial sync...')}`);
|
|
220
|
+
await sync();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function sync() {
|
|
224
|
+
ensureDirs();
|
|
225
|
+
|
|
226
|
+
const projects = [];
|
|
227
|
+
let localCount = 0;
|
|
228
|
+
let cloudCount = 0;
|
|
229
|
+
|
|
230
|
+
// Load local project
|
|
231
|
+
const local = loadLocalProject();
|
|
232
|
+
if (local) {
|
|
233
|
+
projects.push(local);
|
|
234
|
+
localCount = 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Load cloud projects
|
|
238
|
+
const cloud = await loadCloudProjects();
|
|
239
|
+
// Deduplicate — if local project matches a cloud one by name, prefer local
|
|
240
|
+
for (const cp of cloud) {
|
|
241
|
+
if (!projects.find(p => p.name === cp.name)) {
|
|
242
|
+
projects.push(cp);
|
|
243
|
+
cloudCount++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Write projects.json
|
|
248
|
+
fs.writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2));
|
|
249
|
+
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log(` ${green('Synced')} ${b(projects.length + '')} project${projects.length !== 1 ? 's' : ''} → ${g(PROJECTS_FILE)}`);
|
|
252
|
+
if (localCount > 0) console.log(` ${localCount} from local .intent/`);
|
|
253
|
+
if (cloudCount > 0) console.log(` ${cloudCount} from cloud`);
|
|
254
|
+
projects.forEach(p => {
|
|
255
|
+
const actions = p.actions || [];
|
|
256
|
+
const pending = actions.filter(a => a.state === 'intended').length;
|
|
257
|
+
const progress = actions.length > 0
|
|
258
|
+
? ` (${actions.filter(a => a.state === 'reconciled').length}/${actions.length} tasks)`
|
|
259
|
+
: '';
|
|
260
|
+
console.log(` ${g('•')} ${p.name}${progress}${pending > 0 ? ` — ${pending} pending` : ''}`);
|
|
261
|
+
});
|
|
262
|
+
console.log('');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function status() {
|
|
266
|
+
ensureDirs();
|
|
267
|
+
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log(` ${b(w('PHEWSH MCP Status'))}`);
|
|
270
|
+
console.log('');
|
|
271
|
+
|
|
272
|
+
// Check projects.json
|
|
273
|
+
if (fs.existsSync(PROJECTS_FILE)) {
|
|
274
|
+
try {
|
|
275
|
+
const projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, 'utf-8'));
|
|
276
|
+
console.log(` ${green('projects.json:')} ${projects.length} project${projects.length !== 1 ? 's' : ''}`);
|
|
277
|
+
projects.forEach(p => {
|
|
278
|
+
const actions = p.actions || [];
|
|
279
|
+
const pending = actions.filter(a => a.state === 'intended').length;
|
|
280
|
+
console.log(` ${g('•')} ${p.name} [${p.source || 'unknown'}]${pending > 0 ? ` — ${yellow(pending + ' pending')}` : ''}`);
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
console.log(` ${yellow('projects.json:')} exists but unreadable`);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
console.log(` ${yellow('projects.json:')} not found. Run ${w('phewsh mcp sync')} first.`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check results
|
|
290
|
+
if (fs.existsSync(RESULTS_DIR)) {
|
|
291
|
+
const resultFiles = fs.readdirSync(RESULTS_DIR).filter(f => f.endsWith('.json'));
|
|
292
|
+
const blockers = resultFiles.filter(f => f.startsWith('blocker_'));
|
|
293
|
+
const completions = resultFiles.filter(f => !f.startsWith('blocker_'));
|
|
294
|
+
if (resultFiles.length > 0) {
|
|
295
|
+
console.log(` ${green('results:')} ${completions.length} completion${completions.length !== 1 ? 's' : ''}, ${blockers.length} blocker${blockers.length !== 1 ? 's' : ''}`);
|
|
296
|
+
} else {
|
|
297
|
+
console.log(` ${g('results:')} none yet`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check sessions
|
|
302
|
+
if (fs.existsSync(SESSIONS_DIR)) {
|
|
303
|
+
const sessionFiles = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
|
|
304
|
+
if (sessionFiles.length > 0) {
|
|
305
|
+
let totalEvents = 0;
|
|
306
|
+
for (const f of sessionFiles) {
|
|
307
|
+
try {
|
|
308
|
+
const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8'));
|
|
309
|
+
totalEvents += data.length;
|
|
310
|
+
} catch { /* skip */ }
|
|
311
|
+
}
|
|
312
|
+
console.log(` ${green('sessions:')} ${totalEvents} events across ${sessionFiles.length} project${sessionFiles.length !== 1 ? 's' : ''}`);
|
|
313
|
+
} else {
|
|
314
|
+
console.log(` ${g('sessions:')} none yet`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check MCP server
|
|
319
|
+
const serverPath = findMcpServerPath();
|
|
320
|
+
if (serverPath) {
|
|
321
|
+
console.log(` ${green('server:')} ${g(serverPath)}`);
|
|
322
|
+
} else {
|
|
323
|
+
console.log(` ${yellow('server:')} not found. Run ${w('phewsh mcp setup')}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check Claude Code config
|
|
327
|
+
const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
328
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
329
|
+
try {
|
|
330
|
+
const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
|
|
331
|
+
if (settings.mcpServers?.phewsh) {
|
|
332
|
+
console.log(` ${green('claude code:')} configured`);
|
|
333
|
+
} else {
|
|
334
|
+
console.log(` ${yellow('claude code:')} settings exist but phewsh not configured`);
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
console.log(` ${yellow('claude code:')} settings unreadable`);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
console.log(` ${g('claude code:')} no settings.json found`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log('');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function main() {
|
|
347
|
+
const subcommand = process.argv[3] || 'status';
|
|
348
|
+
|
|
349
|
+
switch (subcommand) {
|
|
350
|
+
case 'setup':
|
|
351
|
+
await setup();
|
|
352
|
+
break;
|
|
353
|
+
case 'sync':
|
|
354
|
+
await sync();
|
|
355
|
+
break;
|
|
356
|
+
case 'status':
|
|
357
|
+
await status();
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
console.log(`\n ${b('phewsh mcp')} — Connect AI agents to your project intelligence\n`);
|
|
361
|
+
console.log(` ${w('setup')} Install and configure the MCP server`);
|
|
362
|
+
console.log(` ${w('sync')} Sync projects → ~/.phewsh/projects.json`);
|
|
363
|
+
console.log(` ${w('status')} Check what agents can see right now`);
|
|
364
|
+
console.log('');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = main;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// phewsh serve — HTTP bridge server for live execution from the web UI
|
|
2
|
+
//
|
|
3
|
+
// Starts a local server that the PHEWSH web app connects to for live task execution.
|
|
4
|
+
// When running, the web UI shows a green "Live" indicator and "Run Live" buttons.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// phewsh serve Start on default port (7483)
|
|
8
|
+
// phewsh serve --port 8080 Start on custom port
|
|
9
|
+
|
|
10
|
+
const http = require('http');
|
|
11
|
+
const { execSync, spawn } = require('child_process');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
18
|
+
const g = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
19
|
+
const w = (s) => `\x1b[97m${s}\x1b[0m`;
|
|
20
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
21
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
22
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
23
|
+
|
|
24
|
+
// ─── Configuration ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function getPort() {
|
|
27
|
+
const idx = process.argv.indexOf('--port');
|
|
28
|
+
if (idx !== -1 && process.argv[idx + 1]) return parseInt(process.argv[idx + 1], 10);
|
|
29
|
+
return 7483;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Runtime Detection ─────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function detectRuntimes() {
|
|
35
|
+
const runtimes = [];
|
|
36
|
+
|
|
37
|
+
// Check for Claude Code CLI
|
|
38
|
+
try {
|
|
39
|
+
execSync('which claude', { stdio: 'pipe' });
|
|
40
|
+
runtimes.push({ id: 'claude-code', label: 'Claude Code', connected: true });
|
|
41
|
+
} catch {
|
|
42
|
+
runtimes.push({ id: 'claude-code', label: 'Claude Code', connected: false });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Always report human as available
|
|
46
|
+
runtimes.push({ id: 'human', label: 'You', connected: true });
|
|
47
|
+
runtimes.push({ id: 'generic', label: 'AI Draft', connected: false });
|
|
48
|
+
|
|
49
|
+
return runtimes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Job Queue ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const jobs = new Map();
|
|
55
|
+
|
|
56
|
+
function createJob(actionId, runtimeId, packet) {
|
|
57
|
+
const jobId = crypto.randomUUID();
|
|
58
|
+
jobs.set(jobId, {
|
|
59
|
+
jobId,
|
|
60
|
+
actionId,
|
|
61
|
+
runtimeId,
|
|
62
|
+
packet,
|
|
63
|
+
status: 'queued',
|
|
64
|
+
statusText: 'Queued — waiting to execute',
|
|
65
|
+
result: null,
|
|
66
|
+
error: null,
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
});
|
|
69
|
+
return jobId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function executeJob(jobId) {
|
|
73
|
+
const job = jobs.get(jobId);
|
|
74
|
+
if (!job) return;
|
|
75
|
+
|
|
76
|
+
job.status = 'executing';
|
|
77
|
+
job.statusText = 'Starting execution...';
|
|
78
|
+
|
|
79
|
+
const { runtimeId, packet } = job;
|
|
80
|
+
|
|
81
|
+
if (runtimeId === 'claude-code') {
|
|
82
|
+
await executeViaClaude(job, packet);
|
|
83
|
+
} else {
|
|
84
|
+
job.status = 'error';
|
|
85
|
+
job.error = `Runtime ${runtimeId} not supported for live execution yet`;
|
|
86
|
+
job.statusText = 'Unsupported runtime';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function executeViaClaude(job, packet) {
|
|
91
|
+
// Build a prompt from the dispatch packet
|
|
92
|
+
const prompt = [
|
|
93
|
+
`# Task: ${packet.objective?.task || 'Execute task'}`,
|
|
94
|
+
'',
|
|
95
|
+
packet.objective?.task || '',
|
|
96
|
+
'',
|
|
97
|
+
packet.context?.plan ? `## Context\n${packet.context.plan}` : '',
|
|
98
|
+
'',
|
|
99
|
+
packet.verification?.criteria?.length
|
|
100
|
+
? `## Verify\n${packet.verification.criteria.map(c => `- ${c}`).join('\n')}`
|
|
101
|
+
: '',
|
|
102
|
+
'',
|
|
103
|
+
'## Instructions',
|
|
104
|
+
'Execute this task and report the result. Be specific about what you did and what the outcome was.',
|
|
105
|
+
'If the task involves code, show the relevant code. If it involves commands, show what you ran.',
|
|
106
|
+
].filter(Boolean).join('\n');
|
|
107
|
+
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
job.statusText = 'Launching Claude Code...';
|
|
110
|
+
|
|
111
|
+
// Use claude CLI with --print flag for non-interactive output
|
|
112
|
+
const child = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
|
|
113
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
114
|
+
env: { ...process.env },
|
|
115
|
+
cwd: process.cwd(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
let stdout = '';
|
|
119
|
+
let stderr = '';
|
|
120
|
+
|
|
121
|
+
child.stdout.on('data', (data) => {
|
|
122
|
+
stdout += data.toString();
|
|
123
|
+
// Update status with last line of output as progress indicator
|
|
124
|
+
const lines = stdout.trim().split('\n').filter(l => l.trim());
|
|
125
|
+
if (lines.length > 0) {
|
|
126
|
+
const lastLine = lines[lines.length - 1].trim().slice(0, 80);
|
|
127
|
+
job.statusText = lastLine || 'Working...';
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
child.stderr.on('data', (data) => {
|
|
132
|
+
stderr += data.toString();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on('close', (code) => {
|
|
136
|
+
if (code === 0 && stdout.trim()) {
|
|
137
|
+
job.status = 'done';
|
|
138
|
+
job.result = stdout.trim();
|
|
139
|
+
job.statusText = 'Complete';
|
|
140
|
+
console.log(` ${green('✓')} Job ${job.jobId.slice(0, 8)} completed`);
|
|
141
|
+
} else {
|
|
142
|
+
job.status = 'error';
|
|
143
|
+
job.error = stderr.trim() || `Claude exited with code ${code}`;
|
|
144
|
+
job.statusText = 'Failed';
|
|
145
|
+
console.log(` ${yellow('✗')} Job ${job.jobId.slice(0, 8)} failed: ${job.error.slice(0, 100)}`);
|
|
146
|
+
}
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
child.on('error', (err) => {
|
|
151
|
+
job.status = 'error';
|
|
152
|
+
job.error = err.message;
|
|
153
|
+
job.statusText = 'Failed to start';
|
|
154
|
+
console.log(` ${yellow('✗')} Job ${job.jobId.slice(0, 8)} error: ${err.message}`);
|
|
155
|
+
resolve();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── HTTP Server ───────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
function parseBody(req) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
let body = '';
|
|
165
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
166
|
+
req.on('end', () => {
|
|
167
|
+
try { resolve(JSON.parse(body)); }
|
|
168
|
+
catch { reject(new Error('Invalid JSON')); }
|
|
169
|
+
});
|
|
170
|
+
req.on('error', reject);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function cors(res) {
|
|
175
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
176
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
177
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function json(res, data, status = 200) {
|
|
181
|
+
cors(res);
|
|
182
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
183
|
+
res.end(JSON.stringify(data));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function main() {
|
|
187
|
+
const port = getPort();
|
|
188
|
+
const runtimes = detectRuntimes();
|
|
189
|
+
const hasClaudeCode = runtimes.find(r => r.id === 'claude-code')?.connected;
|
|
190
|
+
|
|
191
|
+
const server = http.createServer(async (req, res) => {
|
|
192
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
193
|
+
|
|
194
|
+
// CORS preflight
|
|
195
|
+
if (req.method === 'OPTIONS') {
|
|
196
|
+
cors(res);
|
|
197
|
+
res.writeHead(204);
|
|
198
|
+
res.end();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Health check
|
|
203
|
+
if (url.pathname === '/health' && req.method === 'GET') {
|
|
204
|
+
return json(res, {
|
|
205
|
+
status: 'ok',
|
|
206
|
+
runtimes: detectRuntimes(),
|
|
207
|
+
version: require('../package.json').version,
|
|
208
|
+
uptime: process.uptime(),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Dispatch a task
|
|
213
|
+
if (url.pathname === '/dispatch' && req.method === 'POST') {
|
|
214
|
+
try {
|
|
215
|
+
const body = await parseBody(req);
|
|
216
|
+
const { actionId, runtimeId, packet } = body;
|
|
217
|
+
|
|
218
|
+
if (!actionId || !runtimeId || !packet) {
|
|
219
|
+
return json(res, { error: 'Missing actionId, runtimeId, or packet' }, 400);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const jobId = createJob(actionId, runtimeId, packet);
|
|
223
|
+
console.log(` ${cyan('→')} Dispatched job ${jobId.slice(0, 8)} for ${runtimeId}: ${packet.objective?.task?.slice(0, 60) || 'task'}`);
|
|
224
|
+
|
|
225
|
+
// Start execution in background
|
|
226
|
+
executeJob(jobId);
|
|
227
|
+
|
|
228
|
+
return json(res, { jobId, status: 'queued' });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return json(res, { error: err.message }, 400);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check job status
|
|
235
|
+
const statusMatch = url.pathname.match(/^\/status\/(.+)$/);
|
|
236
|
+
if (statusMatch && req.method === 'GET') {
|
|
237
|
+
const jobId = statusMatch[1];
|
|
238
|
+
const job = jobs.get(jobId);
|
|
239
|
+
if (!job) return json(res, { error: 'Job not found' }, 404);
|
|
240
|
+
return json(res, {
|
|
241
|
+
jobId: job.jobId,
|
|
242
|
+
status: job.status,
|
|
243
|
+
statusText: job.statusText,
|
|
244
|
+
result: job.result,
|
|
245
|
+
error: job.error,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 404
|
|
250
|
+
json(res, { error: 'Not found' }, 404);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
server.listen(port, () => {
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(` ${b(w('PHEWSH Serve'))} ${g('v' + require('../package.json').version)}`);
|
|
256
|
+
console.log(` ${g('Live execution bridge for phewsh.com/intent')}`);
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(` ${green('●')} Running on ${w(`http://localhost:${port}`)}`);
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log(` ${b('Connected runtimes:')}`);
|
|
261
|
+
runtimes.forEach(r => {
|
|
262
|
+
const status = r.connected ? green('● connected') : g('○ not found');
|
|
263
|
+
console.log(` ${r.label}: ${status}`);
|
|
264
|
+
});
|
|
265
|
+
if (!hasClaudeCode) {
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(` ${yellow('Tip:')} Install Claude Code CLI for live task execution`);
|
|
268
|
+
console.log(` ${g('https://docs.anthropic.com/en/docs/claude-code')}`);
|
|
269
|
+
}
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log(` ${g('Open phewsh.com/intent → Work tab to see the Live indicator')}`);
|
|
272
|
+
console.log(` ${g('Press Ctrl+C to stop')}`);
|
|
273
|
+
console.log('');
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = main;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phewsh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Turn intent into action. Structure your thinking, execute your next step.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"phewsh": "bin/phewsh.js"
|
|
@@ -31,5 +31,7 @@
|
|
|
31
31
|
"homepage": "https://phewsh.com",
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
34
36
|
}
|
|
35
37
|
}
|