phewsh 0.8.0 → 0.10.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,7 +46,7 @@ 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
51
  help: showHelp,
52
52
  version: showVersion,
@@ -74,7 +74,7 @@ function showHelp() {
74
74
  console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
75
75
  console.log(` ${w('sap')} Sustainable AI Protocol — usage and accountability`);
76
76
  console.log(` ${w('style')} Build your style identity — ingest, profile, sync`);
77
- console.log(` ${w('music')} MBHD music engine`);
77
+ console.log(` ${w('mbhd')} MBHD music engine`);
78
78
  console.log('');
79
79
  console.log(` ${b('Quick start')}`);
80
80
  console.log(` ${g('phewsh login')} Set up identity + API key`);
@@ -117,23 +117,9 @@ function exitAfterUpdate(code = 0) {
117
117
  }
118
118
 
119
119
  if (!command) {
120
- // Bare `phewsh` — drop into persistent session
121
- // If no API key, fall back to help
122
- const fs = require('fs');
123
- const path = require('path');
124
- const os = require('os');
125
- const configPath = path.join(os.homedir(), '.phewsh', 'config.json');
126
- let hasKey = false;
127
- try {
128
- const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
129
- hasKey = !!cfg.apiKey;
130
- } catch {}
131
- if (hasKey) {
132
- COMMANDS.session();
133
- } else {
134
- showHelp();
135
- exitAfterUpdate(0);
136
- }
120
+ // Bare `phewsh` — always drop into persistent session
121
+ // Session handles missing API key gracefully with /login and /key commands
122
+ COMMANDS.session();
137
123
  } else if (command === 'help' || command === '--help' || command === '-h') {
138
124
  showHelp();
139
125
  exitAfterUpdate(0);
@@ -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
 
@@ -8,21 +8,113 @@ const os = require('os');
8
8
  const readline = require('readline');
9
9
  const { trackSap } = require('../lib/supabase');
10
10
 
11
- const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
11
+ const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
12
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
12
13
  const INTENT_DIR = path.join(process.cwd(), '.intent');
13
- const HISTORY_PATH = path.join(os.homedir(), '.phewsh', 'session_history.json');
14
+
15
+ const { select, refreshSession: refreshSess } = require('../lib/supabase');
16
+ const { readPPS } = require('../lib/pps');
17
+ const { push, pull, ensureValidToken } = require('./sync');
14
18
 
15
19
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
16
20
  const d = (s) => `\x1b[2m${s}\x1b[0m`;
21
+ const w = (s) => `\x1b[97m${s}\x1b[0m`;
17
22
  const g = (s) => `\x1b[90m${s}\x1b[0m`;
18
23
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
19
24
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
25
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
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
+
100
+ const MODELS = {
101
+ 'claude-sonnet': { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic' },
102
+ 'claude-opus': { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'anthropic' },
103
+ 'claude-haiku': { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'anthropic' },
104
+ };
105
+
106
+ const DEFAULT_MODEL = 'claude-sonnet';
20
107
 
21
108
  function loadConfig() {
22
109
  if (!fs.existsSync(CONFIG_PATH)) return null;
23
110
  try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
24
111
  }
25
112
 
113
+ function saveConfig(config) {
114
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
115
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
116
+ }
117
+
26
118
  function loadIntentContext() {
27
119
  const files = ['vision.md', 'plan.md', 'next.md'];
28
120
  const loaded = [];
@@ -49,9 +141,8 @@ function buildSystemPrompt(intentFiles) {
49
141
  return `${base}\n\nThe user has structured intent artifacts for this project. Use them as primary context — stay aligned with their vision, plan, and next actions.\n\n${sections}`;
50
142
  }
51
143
 
52
- async function streamChat(apiKey, messages, systemPrompt, config) {
53
- const model = 'claude-sonnet-4-6';
54
- const body = { model, max_tokens: 2048, messages, stream: true };
144
+ async function streamChat(apiKey, messages, systemPrompt, modelId) {
145
+ const body = { model: modelId, max_tokens: 2048, messages, stream: true };
55
146
  if (systemPrompt) body.system = systemPrompt;
56
147
 
57
148
  const response = await fetch('https://api.anthropic.com/v1/messages', {
@@ -97,42 +188,65 @@ async function streamChat(apiKey, messages, systemPrompt, config) {
97
188
 
98
189
  process.stdout.write('\n');
99
190
 
100
- // SAP tracking (fire-and-forget)
101
- trackSap({
102
- userId: config.supabaseUserId,
103
- source: 'cli',
104
- model,
105
- promptTokens,
106
- completionTokens,
107
- accessToken: config.supabaseAccessToken,
108
- });
109
-
110
- return { content: fullResponse, promptTokens, completionTokens };
191
+ return { content: fullResponse, promptTokens, completionTokens, model: modelId };
111
192
  }
112
193
 
113
194
  async function main() {
114
- const config = loadConfig();
115
-
116
- if (!config?.apiKey) {
117
- console.log('\n No API key found. Run `phewsh login --set-key` first.');
118
- console.log(' Or start at: `phewsh login`\n');
119
- process.exit(1);
120
- }
121
-
122
- const intentFiles = loadIntentContext();
123
- const systemPrompt = buildSystemPrompt(intentFiles);
195
+ let config = loadConfig();
196
+ let intentFiles = loadIntentContext();
197
+ let systemPrompt = buildSystemPrompt(intentFiles);
124
198
  const messages = []; // conversation history
125
199
  const projectName = path.basename(process.cwd());
200
+ let currentModel = DEFAULT_MODEL;
201
+ let totalPromptTokens = 0;
202
+ let totalCompletionTokens = 0;
126
203
 
127
- // Session banner
204
+ // Session banner with PHEWSH branding
128
205
  console.log('');
129
206
  console.log(` ${d('😮‍💨')} ${d('🤫')}`);
130
207
  console.log('');
208
+ console.log(` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
209
+ console.log(` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
210
+ console.log('');
211
+
212
+ if (!config?.apiKey) {
213
+ console.log(` ${yellow('⚠')} No API key configured.`);
214
+ console.log(` ${g('Run')} /login ${g('to set up identity + API key')}`);
215
+ console.log(` ${g('Or')} /key ${g('to add an API key directly')}`);
216
+ console.log('');
217
+ }
218
+
131
219
  if (intentFiles.length > 0) {
132
- console.log(` ${green('●')} Session started ${g('·')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
220
+ console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
133
221
  } else {
134
- console.log(` ${green('●')} Session started ${g('·')} ${cyan(projectName)} ${g('·')} no .intent/ context`);
222
+ console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} no .intent/ context`);
223
+ console.log(` ${g(' run /init to create .intent/ artifacts')}`);
135
224
  }
225
+ console.log(` ${g(' model:')} ${MODELS[currentModel].name}`);
226
+ if (config?.email) {
227
+ console.log(` ${g(' user:')} ${config.email}`);
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
+
249
+ console.log('');
136
250
  console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
137
251
  console.log('');
138
252
 
@@ -155,23 +269,46 @@ async function main() {
155
269
 
156
270
  // Slash commands
157
271
  if (input.startsWith('/')) {
158
- const cmd = input.slice(1).split(' ')[0].toLowerCase();
272
+ const parts = input.slice(1).split(/\s+/);
273
+ const cmd = parts[0].toLowerCase();
274
+ const cmdArg = parts.slice(1).join(' ');
159
275
 
160
276
  if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
161
- console.log(`\n ${g('Session ended · ' + messages.length / 2 + ' exchanges')}\n`);
277
+ const turns = messages.length / 2;
278
+ console.log(`\n ${g('Session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}\n`);
162
279
  process.exit(0);
163
280
  }
164
281
 
165
- if (cmd === 'help') {
282
+ if (cmd === 'help' || cmd === 'h') {
166
283
  console.log(`
167
284
  ${b('Session commands')}
168
285
 
169
- ${g('/quit')} End session
286
+ ${w('conversation')}
170
287
  ${g('/clear')} Clear conversation history
288
+ ${g('/run')} ${d('<prompt>')} One-shot prompt (doesn't add to conversation)
289
+ ${g('/quit')} End session
290
+
291
+ ${w('project')}
292
+ ${g('/init')} Create .intent/ artifacts in this directory
293
+ ${g('/clarify')} AI-assisted artifact generation
171
294
  ${g('/context')} Show loaded .intent/ files
295
+ ${g('/reload')} Reload .intent/ context from disk
172
296
  ${g('/status')} Show session stats
173
- ${g('/reload')} Reload .intent/ context
174
- ${g('/system')} Show system prompt (debug)
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
+
303
+ ${w('configuration')}
304
+ ${g('/login')} Set up identity + cloud sync
305
+ ${g('/key')} Set or update your API key
306
+ ${g('/model')} ${d('<name>')} Switch model (sonnet, opus, haiku)
307
+ ${g('/models')} List available models
308
+ ${g('/provider')} Show current provider info
309
+
310
+ ${w('debug')}
311
+ ${g('/system')} Show current system prompt
175
312
  `);
176
313
  rl.prompt();
177
314
  return;
@@ -190,7 +327,7 @@ async function main() {
190
327
  intentFiles.forEach(f => console.log(` ${green('●')} ${f.file} ${g('(' + f.content.length + ' chars)')}`));
191
328
  } else {
192
329
  console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
193
- console.log(` ${g('Run')} phewsh clarify ${g('to create one')}`);
330
+ console.log(` ${g('Run')} /init ${g('to create one')}`);
194
331
  }
195
332
  console.log('');
196
333
  rl.prompt();
@@ -199,21 +336,25 @@ async function main() {
199
336
 
200
337
  if (cmd === 'status') {
201
338
  const turns = messages.length / 2;
339
+ config = loadConfig(); // refresh
202
340
  console.log(`\n ${b('Session')}`);
203
341
  console.log(` Turns ${turns}`);
342
+ console.log(` Tokens ${totalPromptTokens}→${totalCompletionTokens} (in→out)`);
204
343
  console.log(` Project ${projectName}`);
205
344
  console.log(` Context ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none'}`);
206
- console.log(` Provider anthropic (claude-sonnet-4-6)`);
345
+ console.log(` Model ${MODELS[currentModel].name}`);
346
+ console.log(` Provider ${MODELS[currentModel].provider}`);
347
+ if (config?.email) console.log(` User ${config.email}`);
348
+ console.log(` API key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
207
349
  console.log('');
208
350
  rl.prompt();
209
351
  return;
210
352
  }
211
353
 
212
354
  if (cmd === 'reload') {
213
- const reloaded = loadIntentContext();
214
- intentFiles.length = 0;
215
- intentFiles.push(...reloaded);
216
- console.log(` ${green('●')} Reloaded ${reloaded.length} artifact${reloaded.length !== 1 ? 's' : ''}`);
355
+ intentFiles = loadIntentContext();
356
+ systemPrompt = buildSystemPrompt(intentFiles);
357
+ console.log(` ${green('●')} Reloaded ${intentFiles.length} artifact${intentFiles.length !== 1 ? 's' : ''}`);
217
358
  rl.prompt();
218
359
  return;
219
360
  }
@@ -224,22 +365,270 @@ async function main() {
224
365
  return;
225
366
  }
226
367
 
227
- // Unknown slash command — treat as normal input
368
+ if (cmd === 'init') {
369
+ if (fs.existsSync(path.join(INTENT_DIR, 'vision.md'))) {
370
+ console.log(`\n ${g('.intent/ already exists in')} ${process.cwd()}`);
371
+ console.log(` ${g('Use /reload to refresh context')}\n`);
372
+ } else {
373
+ try {
374
+ // Delegate to the intent --init command
375
+ const { execSync } = require('child_process');
376
+ execSync('node ' + path.join(__dirname, 'intent.js') + ' --init', { stdio: 'inherit' });
377
+ // Reload context after init
378
+ intentFiles = loadIntentContext();
379
+ systemPrompt = buildSystemPrompt(intentFiles);
380
+ if (intentFiles.length > 0) {
381
+ console.log(` ${green('●')} Context loaded: ${intentFiles.map(f => f.file).join(', ')}`);
382
+ }
383
+ } catch (err) {
384
+ console.error(` ${g('Init failed:')} ${err.message}`);
385
+ }
386
+ }
387
+ console.log('');
388
+ rl.prompt();
389
+ return;
390
+ }
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
+
480
+ if (cmd === 'login') {
481
+ try {
482
+ const { execSync } = require('child_process');
483
+ execSync('node ' + path.join(__dirname, 'login.js'), { stdio: 'inherit' });
484
+ config = loadConfig(); // refresh after login
485
+ } catch (err) {
486
+ console.error(` ${g('Login failed:')} ${err.message}`);
487
+ }
488
+ rl.prompt();
489
+ return;
490
+ }
491
+
492
+ if (cmd === 'key') {
493
+ const keyRl = readline.createInterface({ input: process.stdin, output: process.stdout });
494
+ keyRl.question(`\n Anthropic API key\n > `, (apiKey) => {
495
+ keyRl.close();
496
+ apiKey = apiKey.trim();
497
+ if (apiKey) {
498
+ config = loadConfig() || {};
499
+ config.apiKey = apiKey;
500
+ saveConfig(config);
501
+ console.log(` ${green('●')} API key saved\n`);
502
+ } else {
503
+ console.log(` ${g('Cancelled')}\n`);
504
+ }
505
+ rl.prompt();
506
+ });
507
+ return;
508
+ }
509
+
510
+ if (cmd === 'models') {
511
+ console.log(`\n ${b('Available models')}\n`);
512
+ for (const [key, model] of Object.entries(MODELS)) {
513
+ const active = key === currentModel ? ` ${green('●')}` : '';
514
+ console.log(` ${w(key.padEnd(16))} ${g(model.name)}${active}`);
515
+ }
516
+ console.log(`\n ${g('Switch with:')} /model <name>\n`);
517
+ rl.prompt();
518
+ return;
519
+ }
520
+
521
+ if (cmd === 'model') {
522
+ if (!cmdArg) {
523
+ console.log(` ${g('Current:')} ${MODELS[currentModel].name}`);
524
+ console.log(` ${g('Usage:')} /model <sonnet|opus|haiku>`);
525
+ rl.prompt();
526
+ return;
527
+ }
528
+ // Fuzzy match model name
529
+ const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '');
530
+ const match = Object.keys(MODELS).find(k =>
531
+ k.includes(query) || MODELS[k].name.toLowerCase().includes(query)
532
+ );
533
+ if (match) {
534
+ currentModel = match;
535
+ console.log(` ${green('●')} Switched to ${MODELS[match].name}`);
536
+ } else {
537
+ console.log(` ${g('Unknown model. Available:')} ${Object.keys(MODELS).join(', ')}`);
538
+ }
539
+ rl.prompt();
540
+ return;
541
+ }
542
+
543
+ if (cmd === 'provider') {
544
+ const model = MODELS[currentModel];
545
+ console.log(`\n ${b('Provider')}`);
546
+ console.log(` API Anthropic (direct)`);
547
+ console.log(` Model ${model.name}`);
548
+ console.log(` Endpoint api.anthropic.com/v1/messages`);
549
+ console.log(` Key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
550
+ console.log('');
551
+ rl.prompt();
552
+ return;
553
+ }
554
+
555
+ if (cmd === 'run') {
556
+ if (!cmdArg) {
557
+ console.log(` ${g('Usage:')} /run <prompt>`);
558
+ rl.prompt();
559
+ return;
560
+ }
561
+ if (!config?.apiKey) {
562
+ console.log(` ${yellow('⚠')} No API key. Run /key to set one.`);
563
+ rl.prompt();
564
+ return;
565
+ }
566
+ // One-shot: don't add to conversation history
567
+ console.log('');
568
+ try {
569
+ const result = await streamChat(
570
+ config.apiKey,
571
+ [{ role: 'user', content: cmdArg }],
572
+ systemPrompt,
573
+ MODELS[currentModel].id
574
+ );
575
+ if (result.promptTokens || result.completionTokens) {
576
+ console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
577
+ }
578
+ trackSap({
579
+ userId: config.supabaseUserId,
580
+ source: 'cli',
581
+ model: MODELS[currentModel].id,
582
+ promptTokens: result.promptTokens,
583
+ completionTokens: result.completionTokens,
584
+ accessToken: config.supabaseAccessToken,
585
+ });
586
+ } catch (err) {
587
+ console.error(`\n ${err.message}\n`);
588
+ }
589
+ console.log('');
590
+ rl.prompt();
591
+ return;
592
+ }
593
+
594
+ // Unknown slash command
595
+ console.log(` ${g('Unknown command:')} /${cmd} ${g('— type /help for available commands')}`);
596
+ rl.prompt();
597
+ return;
228
598
  }
229
599
 
230
600
  // Regular input → send to AI
231
- messages.push({ role: 'user', content: input });
601
+ if (!config?.apiKey) {
602
+ console.log(`\n ${yellow('⚠')} No API key configured. Run /key to set one.\n`);
603
+ rl.prompt();
604
+ return;
605
+ }
232
606
 
607
+ messages.push({ role: 'user', content: input });
233
608
  console.log('');
234
609
 
235
610
  try {
236
- const result = await streamChat(config.apiKey, messages, systemPrompt, config);
611
+ const result = await streamChat(config.apiKey, messages, systemPrompt, MODELS[currentModel].id);
237
612
  messages.push({ role: 'assistant', content: result.content });
238
613
 
614
+ // Track totals
615
+ if (result.promptTokens) totalPromptTokens += result.promptTokens;
616
+ if (result.completionTokens) totalCompletionTokens += result.completionTokens;
617
+
239
618
  // Token count footer
240
619
  if (result.promptTokens || result.completionTokens) {
241
- console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
620
+ console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
242
621
  }
622
+
623
+ // SAP tracking (fire-and-forget)
624
+ trackSap({
625
+ userId: config.supabaseUserId,
626
+ source: 'cli',
627
+ model: MODELS[currentModel].id,
628
+ promptTokens: result.promptTokens,
629
+ completionTokens: result.completionTokens,
630
+ accessToken: config.supabaseAccessToken,
631
+ });
243
632
  } catch (err) {
244
633
  console.error(`\n ${err.message}\n`);
245
634
  // Remove the failed user message
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.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"