phewsh 0.9.0 → 0.11.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # phewsh
2
2
 
3
- Turn intent into action.
3
+ Turn intent into action. Structure your thinking, execute your next step.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,24 +8,64 @@ Turn intent into action.
8
8
  npm install -g phewsh
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Quick Start
12
12
 
13
13
  ```bash
14
- phewsh intent --init # Create .intent/ in any project
15
- phewsh intent --status # Check artifact state and next actions
16
- phewsh intent --open # Open the web compass
17
- phewsh intent --evolve # Update artifacts as the project grows
14
+ phewsh # enter the interactive shell
15
+ ```
16
+
17
+ Inside the shell:
18
+
19
+ ```
20
+ /init Create .intent/ artifacts
21
+ /clarify AI-assisted artifact generation
22
+ /push Sync local to cloud
23
+ /pull Sync cloud to local
24
+ /context View loaded artifacts
25
+ /sync Check sync status
26
+ /help All commands
18
27
  ```
19
28
 
20
29
  ## What it does
21
30
 
22
- `phewsh intent --init` asks what you're building and what success looks like, then creates three structured artifacts in `.intent/`:
31
+ Creates three structured artifacts in `.intent/`:
23
32
 
24
33
  - **vision.md** — The north star. Why this exists and where it's going.
25
34
  - **plan.md** — The strategy. Phases, systems, sequence, constraints.
26
- - **next.md** — Right now. Executable checklist with copy-paste commands.
35
+ - **next.md** — Right now. Executable checklist with next actions.
36
+
37
+ These artifacts become persistent context for AI conversations, both in the shell and across tools.
38
+
39
+ ## Sync
40
+
41
+ CLI and web (phewsh.com/intent) share the same cloud via Supabase.
42
+
43
+ ```bash
44
+ phewsh login # authenticate
45
+ phewsh push # upload .intent/ to cloud
46
+ phewsh pull # download from cloud
47
+ ```
48
+
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
+ ```
27
56
 
28
- Drop `.intent/` in any project and your AI tools gain full context instantly.
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
+ ```
29
69
 
30
70
  ## Web app
31
71
 
package/bin/phewsh.js CHANGED
@@ -46,8 +46,10 @@ const COMMANDS = {
46
46
  login: () => require('../commands/login'),
47
47
  ai: () => require('../commands/ai'),
48
48
  style: () => require('../commands/style'),
49
- music: () => require('../commands/music'),
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,9 +74,11 @@ 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
- console.log(` ${w('music')} MBHD music engine`);
81
+ console.log(` ${w('mbhd')} MBHD music engine`);
78
82
  console.log('');
79
83
  console.log(` ${b('Quick start')}`);
80
84
  console.log(` ${g('phewsh login')} Set up identity + API key`);
@@ -1,6 +1,6 @@
1
1
  const { execSync } = require('child_process');
2
2
 
3
- const WEB_URL = 'https://phewsh.com/music';
3
+ const WEB_URL = 'https://phewsh.com/mbhd';
4
4
 
5
5
  console.log(`\n 🎵 Opening MBHD Music Engine...\n`);
6
6
 
@@ -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;
@@ -12,6 +12,10 @@ const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
12
12
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
13
13
  const INTENT_DIR = path.join(process.cwd(), '.intent');
14
14
 
15
+ const { select, refreshSession: refreshSess } = require('../lib/supabase');
16
+ const { readPPS } = require('../lib/pps');
17
+ const { push, pull, ensureValidToken } = require('./sync');
18
+
15
19
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
16
20
  const d = (s) => `\x1b[2m${s}\x1b[0m`;
17
21
  const w = (s) => `\x1b[97m${s}\x1b[0m`;
@@ -20,6 +24,79 @@ const green = (s) => `\x1b[32m${s}\x1b[0m`;
20
24
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
21
25
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
22
26
 
27
+ // Sync awareness: compare local .intent/ timestamps with cloud updated_at
28
+ async function checkSyncStatus(config) {
29
+ if (!config?.supabaseUserId || !config?.supabaseAccessToken) return null;
30
+ if (!fs.existsSync(INTENT_DIR)) return null;
31
+
32
+ try {
33
+ const token = await ensureValidToken(config);
34
+ if (!token) return null;
35
+
36
+ const pps = readPPS(INTENT_DIR);
37
+ const cloudId = pps?.adapters?.phewsh?.cloud_id;
38
+ const projectName = path.basename(process.cwd());
39
+
40
+ // Find cloud project
41
+ const query = cloudId
42
+ ? `id=eq.${cloudId}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`
43
+ : `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`;
44
+
45
+ const projects = await select('projects', query, token);
46
+ if (projects.length === 0) return { status: 'local-only' };
47
+
48
+ const project = projects[0];
49
+
50
+ // Get latest cloud artifact updated_at
51
+ const artifacts = await select(
52
+ 'artifacts',
53
+ `project_id=eq.${project.id}&user_id=eq.${config.supabaseUserId}&select=kind,updated_at&order=updated_at.desc&limit=1`,
54
+ token
55
+ );
56
+
57
+ const cloudTime = artifacts.length > 0
58
+ ? new Date(artifacts[0].updated_at).getTime()
59
+ : new Date(project.updated_at).getTime();
60
+
61
+ // Get latest local file mtime
62
+ const localFiles = ['vision.md', 'plan.md', 'next.md'];
63
+ let latestLocal = 0;
64
+ for (const file of localFiles) {
65
+ const filePath = path.join(INTENT_DIR, file);
66
+ if (fs.existsSync(filePath)) {
67
+ const mtime = fs.statSync(filePath).mtimeMs;
68
+ if (mtime > latestLocal) latestLocal = mtime;
69
+ }
70
+ }
71
+
72
+ if (latestLocal === 0) return { status: 'local-only' };
73
+
74
+ const drift = Math.abs(cloudTime - latestLocal);
75
+ // Within 60 seconds = synced
76
+ if (drift < 60000) return { status: 'synced' };
77
+
78
+ if (cloudTime > latestLocal) {
79
+ const ago = formatAgo(Date.now() - cloudTime);
80
+ return { status: 'cloud-newer', ago };
81
+ } else {
82
+ const ago = formatAgo(Date.now() - latestLocal);
83
+ return { status: 'local-newer', ago };
84
+ }
85
+ } catch {
86
+ return null; // Network error — silently skip
87
+ }
88
+ }
89
+
90
+ function formatAgo(ms) {
91
+ const mins = Math.floor(ms / 60000);
92
+ if (mins < 1) return 'just now';
93
+ if (mins < 60) return `${mins}m ago`;
94
+ const hrs = Math.floor(mins / 60);
95
+ if (hrs < 24) return `${hrs}h ago`;
96
+ const days = Math.floor(hrs / 24);
97
+ return `${days}d ago`;
98
+ }
99
+
23
100
  const MODELS = {
24
101
  'claude-sonnet': { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic' },
25
102
  'claude-opus': { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'anthropic' },
@@ -149,6 +226,26 @@ async function main() {
149
226
  if (config?.email) {
150
227
  console.log(` ${g(' user:')} ${config.email}`);
151
228
  }
229
+
230
+ // Sync status check (non-blocking, 3s timeout)
231
+ if (config?.supabaseUserId && intentFiles.length > 0) {
232
+ const syncResult = await Promise.race([
233
+ checkSyncStatus(config),
234
+ new Promise(resolve => setTimeout(() => resolve(null), 3000)),
235
+ ]);
236
+ if (syncResult) {
237
+ if (syncResult.status === 'cloud-newer') {
238
+ console.log(` ${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull`);
239
+ } else if (syncResult.status === 'local-newer') {
240
+ console.log(` ${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push`);
241
+ } else if (syncResult.status === 'synced') {
242
+ console.log(` ${green('↕')} ${g('synced')}`);
243
+ } else if (syncResult.status === 'local-only') {
244
+ console.log(` ${g('↕ not linked to cloud — run /push to sync')}`);
245
+ }
246
+ }
247
+ }
248
+
152
249
  console.log('');
153
250
  console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
154
251
  console.log('');
@@ -193,10 +290,16 @@ async function main() {
193
290
 
194
291
  ${w('project')}
195
292
  ${g('/init')} Create .intent/ artifacts in this directory
293
+ ${g('/clarify')} AI-assisted artifact generation
196
294
  ${g('/context')} Show loaded .intent/ files
197
295
  ${g('/reload')} Reload .intent/ context from disk
198
296
  ${g('/status')} Show session stats
199
297
 
298
+ ${w('sync')}
299
+ ${g('/push')} Push .intent/ to cloud
300
+ ${g('/pull')} Pull .intent/ from cloud (reloads context)
301
+ ${g('/sync')} Check sync status
302
+
200
303
  ${w('configuration')}
201
304
  ${g('/login')} Set up identity + cloud sync
202
305
  ${g('/key')} Set or update your API key
@@ -286,6 +389,94 @@ async function main() {
286
389
  return;
287
390
  }
288
391
 
392
+ if (cmd === 'clarify') {
393
+ if (!config?.apiKey) {
394
+ console.log(`\n ${yellow('⚠')} No API key. Run /key to set one.\n`);
395
+ rl.prompt();
396
+ return;
397
+ }
398
+ try {
399
+ const { execSync } = require('child_process');
400
+ const args = cmdArg ? `--text "${cmdArg.replace(/"/g, '\\"')}"` : '';
401
+ execSync(`node ${path.join(__dirname, 'clarify.js')} ${args}`, { stdio: 'inherit' });
402
+ // Reload context after clarify
403
+ intentFiles = loadIntentContext();
404
+ systemPrompt = buildSystemPrompt(intentFiles);
405
+ if (intentFiles.length > 0) {
406
+ console.log(` ${green('●')} Context loaded: ${intentFiles.map(f => f.file).join(', ')}`);
407
+ }
408
+ } catch (err) {
409
+ console.error(` ${g('Clarify failed:')} ${err.message}`);
410
+ }
411
+ console.log('');
412
+ rl.prompt();
413
+ return;
414
+ }
415
+
416
+ if (cmd === 'push') {
417
+ if (!config?.supabaseUserId) {
418
+ console.log(`\n ${yellow('⚠')} Not logged in. Run /login first.\n`);
419
+ rl.prompt();
420
+ return;
421
+ }
422
+ try {
423
+ const token = await ensureValidToken(config);
424
+ if (!token) { console.log(`\n ${yellow('⚠')} Session expired. Run /login.\n`); rl.prompt(); return; }
425
+ await push(config, token);
426
+ } catch (err) {
427
+ console.error(` ${yellow('⚠')} Push failed: ${err.message}\n`);
428
+ }
429
+ rl.prompt();
430
+ return;
431
+ }
432
+
433
+ if (cmd === 'pull') {
434
+ if (!config?.supabaseUserId) {
435
+ console.log(`\n ${yellow('⚠')} Not logged in. Run /login first.\n`);
436
+ rl.prompt();
437
+ return;
438
+ }
439
+ try {
440
+ const token = await ensureValidToken(config);
441
+ if (!token) { console.log(`\n ${yellow('⚠')} Session expired. Run /login.\n`); rl.prompt(); return; }
442
+ await pull(config, token);
443
+ // Reload context after pull
444
+ intentFiles = loadIntentContext();
445
+ systemPrompt = buildSystemPrompt(intentFiles);
446
+ if (intentFiles.length > 0) {
447
+ console.log(` ${green('●')} Context reloaded: ${intentFiles.map(f => f.file).join(', ')}`);
448
+ }
449
+ } catch (err) {
450
+ console.error(` ${yellow('⚠')} Pull failed: ${err.message}\n`);
451
+ }
452
+ console.log('');
453
+ rl.prompt();
454
+ return;
455
+ }
456
+
457
+ if (cmd === 'sync') {
458
+ // Show sync status
459
+ if (!config?.supabaseUserId) {
460
+ console.log(`\n ${yellow('⚠')} Not logged in. Run /login first.\n`);
461
+ rl.prompt();
462
+ return;
463
+ }
464
+ const syncResult = await checkSyncStatus(config);
465
+ if (!syncResult) {
466
+ console.log(`\n ${g('Could not check sync status')}\n`);
467
+ } else if (syncResult.status === 'cloud-newer') {
468
+ console.log(`\n ${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull\n`);
469
+ } else if (syncResult.status === 'local-newer') {
470
+ console.log(`\n ${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push\n`);
471
+ } else if (syncResult.status === 'synced') {
472
+ console.log(`\n ${green('↕')} In sync\n`);
473
+ } else if (syncResult.status === 'local-only') {
474
+ console.log(`\n ${g('↕ Not linked to cloud — run /push to sync')}\n`);
475
+ }
476
+ rl.prompt();
477
+ return;
478
+ }
479
+
289
480
  if (cmd === 'login') {
290
481
  try {
291
482
  const { execSync } = require('child_process');
package/commands/sync.js CHANGED
@@ -113,6 +113,7 @@ async function push(config, token) {
113
113
  if (!localPPS.adapters.phewsh) localPPS.adapters.phewsh = {};
114
114
  localPPS.adapters.phewsh.cloud_id = project.id;
115
115
  localPPS.adapters.phewsh.last_synced = new Date().toISOString();
116
+ localPPS.adapters.phewsh.last_updated_by = 'cli';
116
117
  writePPS(INTENT_DIR, localPPS);
117
118
  }
118
119
 
@@ -164,7 +165,7 @@ async function pull(config, token, cloudId = null) {
164
165
  // Keep any local adapter links
165
166
  if (localPPS?.adapters) merged.adapters = { ...project.pps_json.adapters, ...localPPS.adapters };
166
167
  merged.adapters = merged.adapters || {};
167
- merged.adapters.phewsh = { cloud_id: project.id, last_synced: new Date().toISOString() };
168
+ merged.adapters.phewsh = { cloud_id: project.id, last_synced: new Date().toISOString(), last_updated_by: 'pull' };
168
169
  writePPS(INTENT_DIR, merged);
169
170
  pulled.push('pps.json');
170
171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"