phewsh 0.11.10 → 0.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/phewsh.js CHANGED
@@ -8,6 +8,8 @@ const b = (s) => `\x1b[1m${s}\x1b[0m`; // bold
8
8
  const d = (s) => `\x1b[2m${s}\x1b[0m`; // dim
9
9
  const w = (s) => `\x1b[97m${s}\x1b[0m`; // bright white
10
10
  const g = (s) => `\x1b[90m${s}\x1b[0m`; // dark gray
11
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
12
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
11
13
 
12
14
  function showBrand() {
13
15
  const fs = require('fs');
@@ -15,24 +17,32 @@ function showBrand() {
15
17
  const os = require('os');
16
18
  const hasIntent = fs.existsSync(path.join(process.cwd(), '.intent', 'vision.md'));
17
19
  const configPath = path.join(os.homedir(), '.phewsh', 'config.json');
18
- let hint = g(' run "phewsh intent --init" to start');
19
- if (hasIntent) {
20
- hint = g(' .intent/ loaded · run "phewsh ai run \\"...\\"" to execute');
21
- } else {
22
- try {
23
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
24
- if (config?.email) hint = g(` logged in as ${config.email} · run "phewsh intent --init" to start`);
25
- } catch { /* no config */ }
26
- }
20
+ let hasKey = false;
21
+ let email = null;
22
+ try {
23
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
24
+ hasKey = !!config?.apiKey;
25
+ email = config?.email;
26
+ } catch { /* no config */ }
27
27
 
28
28
  console.log('');
29
29
  console.log(` ${d('😮\u200d💨')} ${d('🤫')}`);
30
30
  console.log('');
31
31
  console.log(` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
32
32
  console.log(` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
33
- console.log(` ${g('Build with clarity. Execute without drift.')}`);
33
+ console.log(` ${g('Your project identity for every AI tool.')}`);
34
34
  console.log('');
35
- console.log(hint);
35
+
36
+ // Context-aware hint
37
+ if (!hasKey) {
38
+ console.log(` ${g('Get started:')} ${w('phewsh')} ${g('(guided setup, takes 60 seconds)')}`);
39
+ } else if (hasIntent) {
40
+ console.log(` ${green('●')} .intent/ loaded ${g('·')} ${w('phewsh')} ${g('to chat ·')} ${w('phewsh watch')} ${g('to sync')}`);
41
+ } else if (email) {
42
+ console.log(` ${g('logged in as')} ${email} ${g('·')} ${w('phewsh')} ${g('to start')}`);
43
+ } else {
44
+ console.log(` ${g('Ready.')} ${w('phewsh')} ${g('to start a session.')}`);
45
+ }
36
46
  console.log('');
37
47
  }
38
48
 
@@ -64,33 +74,38 @@ function showVersion() {
64
74
  function showHelp() {
65
75
  const pkg = require('../package.json');
66
76
  showBrand();
67
- console.log(` ${g('v' + pkg.version)} · ${g('phewsh.com')}\n`);
68
- console.log(` ${b('Just type')} ${w('phewsh')} ${b('to start a session.')}`);
69
- console.log(` ${g('Opens a persistent AI shell with your .intent/ context injected.')}`);
77
+ console.log(` ${g('v' + pkg.version)} · ${g('phewsh.com/cli')}\n`);
78
+ console.log(` ${g(''.repeat(48))}`);
79
+ console.log(` ${b('Just type')} ${w('phewsh')} ${b('to start.')} ${g('Everything else is optional.')}`);
80
+ console.log(` ${g('─'.repeat(48))}`);
81
+ console.log('');
82
+ console.log(` ${b(w('start here'))}`);
83
+ console.log(` ${cyan('phewsh')} Open AI session — type naturally, get guided`);
84
+ console.log(` ${cyan('phewsh clarify')} Turn a messy idea into a structured spec`);
85
+ console.log(` ${cyan('phewsh login')} Set up identity + API key`);
86
+ console.log('');
87
+ console.log(` ${b(w('project'))}`);
88
+ console.log(` ${cyan('intent')} ${g('Manage .intent/ artifacts — status, open, evolve')}`);
89
+ console.log(` ${cyan('gate')} ${g('Declare constraints (budget, time, skill, urgency)')}`);
90
+ console.log(` ${cyan('context')} ${g('Export portable context for any AI tool')}`);
91
+ console.log(` ${cyan('ai')} ${g('One-shot AI prompt with .intent/ context')}`);
92
+ console.log('');
93
+ console.log(` ${b(w('sync'))}`);
94
+ console.log(` ${cyan('push')} ${g('Push .intent/ to cloud')}`);
95
+ console.log(` ${cyan('pull')} ${g('Pull from cloud to .intent/')}`);
96
+ console.log(` ${cyan('watch')} ${g('Live sync — CLAUDE.md + web dashboard auto-update')}`);
97
+ console.log(` ${cyan('serve')} ${g('Execution bridge for the web app')}`);
98
+ console.log('');
99
+ console.log(` ${b(w('connect'))}`);
100
+ console.log(` ${cyan('mcp')} ${g('Connect AI agents via MCP protocol')}`);
101
+ console.log(` ${cyan('link')} ${g('Link local .intent/ to cloud project')}`);
70
102
  console.log('');
71
- console.log(` ${b('Commands')}`);
72
- console.log(` ${w('(bare)')} Open persistent AI sessionjust type naturally`);
73
- console.log(` ${w('clarify')} Turn messy intent into a structured project spec`);
74
- console.log(` ${w('push')} Push local .intent/ to cloud`);
75
- console.log(` ${w('pull')} Pull project from cloud to .intent/`);
76
- console.log(` ${w('link')} Link local .intent/ to a cloud project`);
77
- console.log(` ${w('intent')} Manage .intent/ artifacts — status, open, evolve`);
78
- console.log(` ${w('ai')} One-shot AI prompt (reads .intent/)`);
79
- console.log(` ${w('gate')} Declare operational constraints (budget, time, skill)`);
80
- console.log(` ${w('context')} Export portable context for any AI tool`);
81
- console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
82
- console.log(` ${w('watch')} Live sync — .intent/ changes push to cloud + CLAUDE.md`);
83
- console.log(` ${w('serve')} Start live execution bridge for the web app`);
84
- console.log(` ${w('mcp')} Connect AI agents — setup, sync, status`);
85
- console.log(` ${w('sap')} Sustainable AI Protocol — usage and accountability`);
86
- console.log(` ${w('style')} Build your style identity — ingest, profile, sync`);
87
- console.log(` ${w('mbhd')} MBHD music engine`);
103
+ console.log(` ${b(w('more'))}`);
104
+ console.log(` ${cyan('sap')} ${g('Sustainable AI Protocolusage tracking')}`);
105
+ console.log(` ${cyan('style')} ${g('Style identity ingest, profile, sync')}`);
106
+ console.log(` ${cyan('mbhd')} ${g('MBHD music engine')}`);
88
107
  console.log('');
89
- console.log(` ${b('Quick start')}`);
90
- console.log(` ${g('phewsh login')} Set up identity + API key`);
91
- console.log(` ${g('phewsh')} Open AI session (with .intent/ context)`);
92
- console.log(` ${g('phewsh clarify')} Compile messy intent → structured spec`);
93
- console.log(` ${g('phewsh ai run "what\'s next?"')} One-shot prompt`);
108
+ console.log(` ${g('Works standalone · Inside Claude Code · Inside Cursor · With any MCP agent')}`);
94
109
  console.log('');
95
110
  }
96
111
 
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const os = require('os');
8
8
  const readline = require('readline');
9
9
  const { trackSap } = require('../lib/supabase');
10
+ const ui = require('../lib/ui');
10
11
 
11
12
  const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
12
13
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
@@ -16,13 +17,7 @@ const { select, refreshSession: refreshSess } = require('../lib/supabase');
16
17
  const { readPPS } = require('../lib/pps');
17
18
  const { push, pull, ensureValidToken } = require('./sync');
18
19
 
19
- const b = (s) => `\x1b[1m${s}\x1b[0m`;
20
- const d = (s) => `\x1b[2m${s}\x1b[0m`;
21
- const w = (s) => `\x1b[97m${s}\x1b[0m`;
22
- const g = (s) => `\x1b[90m${s}\x1b[0m`;
23
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
24
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
25
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
20
+ const { b, d, w, g, green, cyan, yellow } = ui;
26
21
 
27
22
  // Sync awareness: compare local .intent/ timestamps with cloud updated_at
28
23
  async function checkSyncStatus(config) {
@@ -145,6 +140,9 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
145
140
  const body = { model: modelId, max_tokens: 2048, messages, stream: true };
146
141
  if (systemPrompt) body.system = systemPrompt;
147
142
 
143
+ // Start spinner while waiting for first token
144
+ const spin = ui.spinner('thinking');
145
+
148
146
  const response = await fetch('https://api.anthropic.com/v1/messages', {
149
147
  method: 'POST',
150
148
  headers: {
@@ -156,6 +154,7 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
156
154
  });
157
155
 
158
156
  if (!response.ok) {
157
+ spin.stop();
159
158
  const err = await response.json().catch(() => ({}));
160
159
  const msg = err.error?.message || `API error ${response.status}`;
161
160
  if (response.status === 401 || msg.includes('invalid')) {
@@ -167,6 +166,7 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
167
166
  let fullResponse = '';
168
167
  let promptTokens = null;
169
168
  let completionTokens = null;
169
+ let firstToken = true;
170
170
 
171
171
  for await (const chunk of response.body) {
172
172
  const text = Buffer.from(chunk).toString('utf-8');
@@ -177,6 +177,10 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
177
177
  try {
178
178
  const parsed = JSON.parse(data);
179
179
  if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
180
+ if (firstToken) {
181
+ spin.stop(); // Clear spinner before first output
182
+ firstToken = false;
183
+ }
180
184
  process.stdout.write(parsed.delta.text);
181
185
  fullResponse += parsed.delta.text;
182
186
  }
@@ -190,6 +194,7 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
190
194
  }
191
195
  }
192
196
 
197
+ if (firstToken) spin.stop(); // In case we got no tokens
193
198
  process.stdout.write('\n');
194
199
 
195
200
  return { content: fullResponse, promptTokens, completionTokens, model: modelId };
@@ -205,14 +210,10 @@ async function main() {
205
210
  let totalPromptTokens = 0;
206
211
  let totalCompletionTokens = 0;
207
212
 
208
- // Session banner with PHEWSH branding
209
- console.log('');
210
- console.log(` ${d('😮‍💨')} ${d('🤫')}`);
211
- console.log('');
212
- console.log(` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
213
- console.log(` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
214
- console.log('');
213
+ // ── Animated brand reveal ──────────────────────────────
214
+ await ui.brandReveal();
215
215
 
216
+ // ── First-run welcome or status panel ──────────────────
216
217
  if (!config?.apiKey) {
217
218
  console.log(` ${b(w('Welcome to PHEWSH.'))}`);
218
219
  console.log(` ${g('Your AI knows your project. No more re-explaining.')}`);
@@ -230,6 +231,7 @@ async function main() {
230
231
  console.log('');
231
232
  console.log(` ${g('Got a key? Type')} /key ${g('to paste it in.')}`);
232
233
  console.log(` ${g('Want cloud sync too?')} /login ${g('connects your identity.')}`);
234
+ console.log(` ${g('Curious?')} /tour ${g('to see what PHEWSH can do (no key needed).')}`);
233
235
  console.log('');
234
236
  } else if (!config.apiKey.startsWith('sk-')) {
235
237
  console.log(` ${yellow('!')} Stored API key looks invalid.`);
@@ -238,6 +240,7 @@ async function main() {
238
240
  config.apiKey = null; // Don't try to use a bad key
239
241
  }
240
242
 
243
+ // ── Project status ─────────────────────────────────────
241
244
  if (intentFiles.length > 0) {
242
245
  console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
243
246
  } else {
@@ -249,6 +252,9 @@ async function main() {
249
252
  console.log(` ${g(' user:')} ${config.email}`);
250
253
  }
251
254
 
255
+ // ── Interop line ───────────────────────────────────────
256
+ ui.interopLine(config, intentFiles);
257
+
252
258
  // Sync status check (non-blocking, 3s timeout)
253
259
  if (config?.supabaseUserId && intentFiles.length > 0) {
254
260
  const syncResult = await Promise.race([
@@ -269,17 +275,20 @@ async function main() {
269
275
  }
270
276
 
271
277
  console.log('');
278
+ ui.divider('─');
272
279
  if (!config?.apiKey) {
273
- console.log(` ${g('type')} /key ${g('to get started · /help for all commands · /quit to exit')}`);
280
+ console.log(` ${g('type')} /key ${g('to get started ·')} /tour ${g('to explore ·')} /help ${g('for all commands')}`);
274
281
  } else {
282
+ console.log(` ${ui.randomTip()}`);
275
283
  console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
276
284
  }
285
+ ui.divider('─');
277
286
  console.log('');
278
287
 
279
288
  const rl = readline.createInterface({
280
289
  input: process.stdin,
281
290
  output: process.stdout,
282
- prompt: ` ${green('>')} `,
291
+ prompt: ` ${cyan('phewsh')} ${green('>')} `,
283
292
  historySize: 100,
284
293
  });
285
294
 
@@ -301,48 +310,77 @@ async function main() {
301
310
 
302
311
  if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
303
312
  const turns = messages.length / 2;
304
- console.log(`\n ${g('Session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}\n`);
313
+ console.log('');
314
+ ui.divider('─');
315
+ console.log(` ${g('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
316
+ ui.divider('─');
317
+ console.log('');
305
318
  process.exit(0);
306
319
  }
307
320
 
308
321
  if (cmd === 'help' || cmd === 'h') {
309
322
  console.log(`
310
- ${b('Session commands')}
323
+ ${b(w('Session commands'))}
311
324
 
312
325
  ${w('conversation')}
313
- ${g('/clear')} Clear conversation history
314
- ${g('/run')} ${d('<prompt>')} One-shot prompt (doesn't add to conversation)
315
- ${g('/quit')} End session
326
+ ${cyan('/clear')} Clear conversation history
327
+ ${cyan('/run')} ${d('<prompt>')} One-shot prompt (doesn't add to conversation)
328
+ ${cyan('/quit')} End session
316
329
 
317
330
  ${w('project')}
318
- ${g('/init')} Create .intent/ artifacts in this directory
319
- ${g('/clarify')} AI-assisted artifact generation
320
- ${g('/gate')} Declare operational constraints (budget, time, skill)
321
- ${g('/export')} Export portable context for other AI tools
322
- ${g('/context')} Show loaded .intent/ files
323
- ${g('/reload')} Reload .intent/ context from disk
324
- ${g('/status')} Show session stats
331
+ ${cyan('/init')} Create .intent/ artifacts in this directory
332
+ ${cyan('/clarify')} AI-assisted artifact generation
333
+ ${cyan('/gate')} Declare operational constraints (budget, time, skill)
334
+ ${cyan('/export')} Export portable context for other AI tools
335
+ ${cyan('/context')} Show loaded .intent/ files
336
+ ${cyan('/reload')} Reload .intent/ context from disk
337
+ ${cyan('/status')} Show session stats
325
338
 
326
339
  ${w('sync')}
327
- ${g('/push')} Push .intent/ to cloud
328
- ${g('/pull')} Pull .intent/ from cloud (reloads context)
329
- ${g('/sync')} Check sync status
340
+ ${cyan('/push')} Push .intent/ to cloud
341
+ ${cyan('/pull')} Pull .intent/ from cloud (reloads context)
342
+ ${cyan('/sync')} Check sync status
330
343
 
331
344
  ${w('configuration')}
332
- ${g('/login')} Set up identity + cloud sync
333
- ${g('/key')} Set or update your API key
334
- ${g('/model')} ${d('<name>')} Switch model (sonnet, opus, haiku)
335
- ${g('/models')} List available models
336
- ${g('/provider')} Show current provider info
337
- ${g('/update')} Update phewsh to the latest version
338
-
339
- ${w('debug')}
340
- ${g('/system')} Show current system prompt
345
+ ${cyan('/login')} Set up identity + cloud sync
346
+ ${cyan('/key')} Set or update your API key
347
+ ${cyan('/model')} ${d('<name>')} Switch model (sonnet, opus, haiku)
348
+ ${cyan('/models')} List available models
349
+ ${cyan('/provider')} Show current provider info
350
+ ${cyan('/update')} Update phewsh to the latest version
351
+
352
+ ${w('explore')}
353
+ ${cyan('/tour')} Guided walkthrough of everything PHEWSH can do
354
+ ${cyan('/system')} Show current system prompt
341
355
  `);
342
356
  rl.prompt();
343
357
  return;
344
358
  }
345
359
 
360
+ // ── /tour — guided walkthrough ─────────────────────
361
+ if (cmd === 'tour') {
362
+ const pages = ui.TOUR_PAGES;
363
+ let pageIdx = cmdArg ? parseInt(cmdArg) - 1 : 0;
364
+ if (isNaN(pageIdx) || pageIdx < 0) pageIdx = 0;
365
+ if (pageIdx >= pages.length) pageIdx = pages.length - 1;
366
+
367
+ const page = pages[pageIdx];
368
+ console.log('');
369
+ ui.divider('─');
370
+ console.log(` ${b(w(page.title))} ${g(`(${pageIdx + 1}/${pages.length})`)}`);
371
+ ui.divider('─');
372
+ page.body.forEach(line => console.log(line));
373
+ console.log('');
374
+ if (pageIdx < pages.length - 1) {
375
+ console.log(` ${g('next:')} /tour ${pageIdx + 2} ${g('·')} ${g('or /tour 1-' + pages.length + ' to jump')}`);
376
+ } else {
377
+ console.log(` ${green('●')} ${g('End of tour. You\'re ready to go.')}`);
378
+ }
379
+ console.log('');
380
+ rl.prompt();
381
+ return;
382
+ }
383
+
346
384
  if (cmd === 'clear') {
347
385
  messages.length = 0;
348
386
  console.log(` ${g('conversation cleared')}`);
@@ -352,8 +390,11 @@ async function main() {
352
390
 
353
391
  if (cmd === 'context') {
354
392
  if (intentFiles.length > 0) {
355
- console.log(`\n Loaded from ${cyan('.intent/')}:`);
356
- intentFiles.forEach(f => console.log(` ${green('')} ${f.file} ${g('(' + f.content.length + ' chars)')}`));
393
+ console.log('');
394
+ console.log(` ${b('Loaded from')} ${cyan('.intent/')}`);
395
+ ui.divider('─');
396
+ intentFiles.forEach(f => console.log(` ${green('●')} ${w(f.file)} ${g('(' + f.content.length + ' chars)')}`));
397
+ ui.divider('─');
357
398
  } else {
358
399
  console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
359
400
  console.log(` ${g('Run')} /init ${g('to create one')}`);
@@ -366,16 +407,16 @@ async function main() {
366
407
  if (cmd === 'status') {
367
408
  const turns = messages.length / 2;
368
409
  config = loadConfig(); // refresh
369
- console.log(`\n ${b('Session')}`);
370
- console.log(` Turns ${turns}`);
371
- console.log(` Tokens ${totalPromptTokens}→${totalCompletionTokens} (in→out)`);
372
- console.log(` Project ${projectName}`);
373
- console.log(` Context ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none'}`);
374
- console.log(` Model ${MODELS[currentModel].name}`);
375
- console.log(` Provider ${MODELS[currentModel].provider}`);
376
- if (config?.email) console.log(` User ${config.email}`);
377
- console.log(` API key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
378
- console.log('');
410
+ ui.statusPanel('Session', [
411
+ ['Turns', String(turns)],
412
+ ['Tokens', `${totalPromptTokens} in → ${totalCompletionTokens} out`],
413
+ ['Project', projectName, 'cyan'],
414
+ ['Context', intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none', intentFiles.length > 0 ? 'green' : 'yellow'],
415
+ ['Model', MODELS[currentModel].name],
416
+ ['Provider', MODELS[currentModel].provider],
417
+ ['User', config?.email || g('not logged in')],
418
+ ['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '...' : 'not set', config?.apiKey ? 'green' : 'yellow'],
419
+ ]);
379
420
  rl.prompt();
380
421
  return;
381
422
  }
@@ -420,7 +461,7 @@ async function main() {
420
461
 
421
462
  if (cmd === 'clarify') {
422
463
  if (!config?.apiKey) {
423
- console.log(`\n ${yellow('')} No API key. Run /key to set one.\n`);
464
+ console.log(`\n ${yellow('!')} No API key. Run /key to set one.\n`);
424
465
  rl.prompt();
425
466
  return;
426
467
  }
@@ -464,7 +505,7 @@ async function main() {
464
505
  if (content) {
465
506
  const outPath = path.join(process.cwd(), '.phewsh.context');
466
507
  fs.writeFileSync(outPath, content);
467
- console.log(`\n ${green('')} Written to ${outPath}\n`);
508
+ console.log(`\n ${green('')} Written to ${outPath}\n`);
468
509
  } else {
469
510
  console.log(`\n ${g('No artifacts to export')}\n`);
470
511
  }
@@ -477,16 +518,16 @@ async function main() {
477
518
 
478
519
  if (cmd === 'push') {
479
520
  if (!config?.supabaseUserId) {
480
- console.log(`\n ${yellow('')} Not logged in. Run /login first.\n`);
521
+ console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
481
522
  rl.prompt();
482
523
  return;
483
524
  }
484
525
  try {
485
526
  const token = await ensureValidToken(config);
486
- if (!token) { console.log(`\n ${yellow('')} Session expired. Run /login.\n`); rl.prompt(); return; }
527
+ if (!token) { console.log(`\n ${yellow('!')} Session expired. Run /login.\n`); rl.prompt(); return; }
487
528
  await push(config, token);
488
529
  } catch (err) {
489
- console.error(` ${yellow('')} Push failed: ${err.message}\n`);
530
+ console.error(` ${yellow('!')} Push failed: ${err.message}\n`);
490
531
  }
491
532
  rl.prompt();
492
533
  return;
@@ -494,13 +535,13 @@ async function main() {
494
535
 
495
536
  if (cmd === 'pull') {
496
537
  if (!config?.supabaseUserId) {
497
- console.log(`\n ${yellow('')} Not logged in. Run /login first.\n`);
538
+ console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
498
539
  rl.prompt();
499
540
  return;
500
541
  }
501
542
  try {
502
543
  const token = await ensureValidToken(config);
503
- if (!token) { console.log(`\n ${yellow('')} Session expired. Run /login.\n`); rl.prompt(); return; }
544
+ if (!token) { console.log(`\n ${yellow('!')} Session expired. Run /login.\n`); rl.prompt(); return; }
504
545
  await pull(config, token);
505
546
  // Reload context after pull
506
547
  intentFiles = loadIntentContext();
@@ -509,7 +550,7 @@ async function main() {
509
550
  console.log(` ${green('●')} Context reloaded: ${intentFiles.map(f => f.file).join(', ')}`);
510
551
  }
511
552
  } catch (err) {
512
- console.error(` ${yellow('')} Pull failed: ${err.message}\n`);
553
+ console.error(` ${yellow('!')} Pull failed: ${err.message}\n`);
513
554
  }
514
555
  console.log('');
515
556
  rl.prompt();
@@ -519,22 +560,24 @@ async function main() {
519
560
  if (cmd === 'sync') {
520
561
  // Show sync status
521
562
  if (!config?.supabaseUserId) {
522
- console.log(`\n ${yellow('')} Not logged in. Run /login first.\n`);
563
+ console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
523
564
  rl.prompt();
524
565
  return;
525
566
  }
567
+ const syncSpin = ui.spinner('checking sync');
526
568
  const syncResult = await checkSyncStatus(config);
527
569
  if (!syncResult) {
528
- console.log(`\n ${g('Could not check sync status')}\n`);
570
+ syncSpin.stop(`${g('Could not check sync status')}`);
529
571
  } else if (syncResult.status === 'cloud-newer') {
530
- console.log(`\n ${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull\n`);
572
+ syncSpin.stop(`${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull`);
531
573
  } else if (syncResult.status === 'local-newer') {
532
- console.log(`\n ${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push\n`);
574
+ syncSpin.stop(`${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push`);
533
575
  } else if (syncResult.status === 'synced') {
534
- console.log(`\n ${green('↕')} In sync\n`);
576
+ syncSpin.stop(`${green('↕')} In sync`);
535
577
  } else if (syncResult.status === 'local-only') {
536
- console.log(`\n ${g('↕ Not linked to cloud — run /push to sync')}\n`);
578
+ syncSpin.stop(`${g('↕ Not linked to cloud — run /push to sync')}`);
537
579
  }
580
+ console.log('');
538
581
  rl.prompt();
539
582
  return;
540
583
  }
@@ -575,7 +618,9 @@ async function main() {
575
618
  return;
576
619
  }
577
620
  console.log('');
578
- console.log(` ${b('Where to get an API key:')}`);
621
+ ui.divider('');
622
+ console.log(` ${b(w('Where to get an API key'))}`);
623
+ ui.divider('─');
579
624
  console.log('');
580
625
  console.log(` ${cyan('Anthropic')} ${g('(recommended)')}`);
581
626
  console.log(` ${g('1.')} Go to ${w('console.anthropic.com/settings/keys')}`);
@@ -585,6 +630,9 @@ async function main() {
585
630
  console.log(` ${g('1.')} Go to ${w('openrouter.ai/keys')}`);
586
631
  console.log(` ${g('2.')} Create key → copy it (starts with sk-or-)`)
587
632
  console.log('');
633
+ console.log(` ${g('Note: API keys are separate from ChatGPT Plus / Claude Pro subscriptions.')}`);
634
+ console.log(` ${g('Both providers offer free credits to get started.')}`);
635
+ console.log('');
588
636
  const keyRl = readline.createInterface({ input: process.stdin, output: process.stdout });
589
637
  keyRl.question(` Paste your API key\n > `, (apiKey) => {
590
638
  keyRl.close();
@@ -605,7 +653,10 @@ async function main() {
605
653
  }
606
654
 
607
655
  if (cmd === 'models') {
608
- console.log(`\n ${b('Available models')}\n`);
656
+ console.log('');
657
+ ui.divider('─');
658
+ console.log(` ${b(w('Available models'))}`);
659
+ ui.divider('─');
609
660
  for (const [key, model] of Object.entries(MODELS)) {
610
661
  const active = key === currentModel ? ` ${green('●')}` : '';
611
662
  console.log(` ${w(key.padEnd(16))} ${g(model.name)}${active}`);
@@ -639,35 +690,36 @@ async function main() {
639
690
 
640
691
  if (cmd === 'provider') {
641
692
  const model = MODELS[currentModel];
642
- console.log(`\n ${b('Provider')}`);
643
- console.log(` API Anthropic (direct)`);
644
- console.log(` Model ${model.name}`);
645
- console.log(` Endpoint api.anthropic.com/v1/messages`);
646
- console.log(` Key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
647
- console.log('');
693
+ ui.statusPanel('Provider', [
694
+ ['API', 'Anthropic (direct)'],
695
+ ['Model', model.name, 'cyan'],
696
+ ['Endpoint', 'api.anthropic.com/v1/messages'],
697
+ ['Key', config?.apiKey ? config.apiKey.slice(0, 8) + '...' : 'not set', config?.apiKey ? 'green' : 'yellow'],
698
+ ]);
648
699
  rl.prompt();
649
700
  return;
650
701
  }
651
702
 
652
703
  if (cmd === 'update' || cmd === 'upgrade') {
653
- console.log(`\n ${g('Checking for updates...')}`);
704
+ const updateSpin = ui.spinner('checking for updates');
654
705
  try {
655
706
  const pkg = require('../../package.json');
656
707
  const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(5000) });
657
708
  const data = await res.json();
658
709
  if (!data.version || data.version === pkg.version) {
659
- console.log(` ${green('●')} Already on the latest version (${pkg.version})\n`);
710
+ updateSpin.stop(`${green('●')} Already on the latest version (${pkg.version})`);
711
+ console.log('');
660
712
  rl.prompt();
661
713
  return;
662
714
  }
663
- console.log(` ${cyan(pkg.version)} → ${cyan(data.version)}`);
715
+ updateSpin.stop(`${cyan(pkg.version)} → ${cyan(data.version)}`);
664
716
  console.log(` ${g('Installing...')}\n`);
665
717
  const { execSync } = require('child_process');
666
718
  execSync(`npm install -g ${pkg.name}@latest`, { stdio: 'inherit' });
667
719
  console.log(`\n ${green('●')} Updated to ${data.version}`);
668
720
  console.log(` ${g('Restart phewsh to use the new version.')}\n`);
669
721
  } catch (err) {
670
- console.log(` ${yellow('!')} Update failed: ${err.message}`);
722
+ updateSpin.stop(`${yellow('!')} Update failed: ${err.message}`);
671
723
  console.log(` ${g('You can update manually:')} npm install -g phewsh\n`);
672
724
  }
673
725
  rl.prompt();
@@ -681,7 +733,7 @@ async function main() {
681
733
  return;
682
734
  }
683
735
  if (!config?.apiKey) {
684
- console.log(` ${yellow('')} No API key. Run /key to set one.`);
736
+ console.log(` ${yellow('!')} No API key. Run /key to set one.`);
685
737
  rl.prompt();
686
738
  return;
687
739
  }
@@ -727,6 +779,7 @@ async function main() {
727
779
  console.log('');
728
780
  console.log(` ${g('Get a key:')} ${cyan('console.anthropic.com/settings/keys')}`);
729
781
  console.log(` ${g('Or try:')} ${cyan('openrouter.ai/keys')}`);
782
+ console.log(` ${g('Explore:')} ${cyan('/tour')} ${g('to see what PHEWSH does (no key needed)')}`);
730
783
  console.log('');
731
784
  rl.prompt();
732
785
  return;
package/lib/ui.js ADDED
@@ -0,0 +1,252 @@
1
+ // phewsh ui — zero-dependency terminal animations and visual helpers
2
+ // Pure ANSI escape sequences. No chalk, no ora, no bloat.
3
+
4
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
5
+ const d = (s) => `\x1b[2m${s}\x1b[0m`;
6
+ const w = (s) => `\x1b[97m${s}\x1b[0m`;
7
+ const g = (s) => `\x1b[90m${s}\x1b[0m`;
8
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
9
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
10
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
11
+ const magenta = (s) => `\x1b[35m${s}\x1b[0m`;
12
+ const blue = (s) => `\x1b[34m${s}\x1b[0m`;
13
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
14
+
15
+ // ANSI cursor control
16
+ const hide = '\x1b[?25l';
17
+ const show = '\x1b[?25h';
18
+ const up = (n = 1) => `\x1b[${n}A`;
19
+ const clearLine = '\x1b[2K\r';
20
+
21
+ // ── Spinner ──────────────────────────────────────────────
22
+ // Returns { stop } — call stop() when work is done.
23
+ const SPINNER_FRAMES = [' ·', ' · ·', ' · · ·', ' · · · ·', '· · · · ·', ' · · · ·', ' · · ·', ' · ·'];
24
+ const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
25
+ const BREATH_FRAMES = ['░', '▒', '▓', '█', '▓', '▒', '░', ' '];
26
+ const PULSE_FRAMES = ['·', '•', '●', '•'];
27
+
28
+ function spinner(text = 'thinking', style = 'braille') {
29
+ const frames = style === 'dots' ? SPINNER_FRAMES
30
+ : style === 'breath' ? BREATH_FRAMES
31
+ : style === 'pulse' ? PULSE_FRAMES
32
+ : BRAILLE_FRAMES;
33
+ let i = 0;
34
+ let stopped = false;
35
+ process.stdout.write(hide);
36
+ const interval = setInterval(() => {
37
+ if (stopped) return;
38
+ const frame = frames[i % frames.length];
39
+ process.stdout.write(`${clearLine} ${cyan(frame)} ${g(text)}`);
40
+ i++;
41
+ }, style === 'breath' ? 120 : 80);
42
+
43
+ return {
44
+ update(newText) { text = newText; },
45
+ stop(finalText) {
46
+ stopped = true;
47
+ clearInterval(interval);
48
+ process.stdout.write(`${clearLine}${show}`);
49
+ if (finalText) process.stdout.write(` ${finalText}\n`);
50
+ }
51
+ };
52
+ }
53
+
54
+ // ── Animated brand reveal ────────────────────────────────
55
+ // Draws the PHEWSH logo line by line with a cascading effect.
56
+ function brandReveal(fast = false) {
57
+ return new Promise((resolve) => {
58
+ const lines = [
59
+ '',
60
+ ` ${d('😮\u200d💨')} ${d('🤫')}`,
61
+ '',
62
+ ` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`,
63
+ ` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`,
64
+ '',
65
+ ];
66
+
67
+ if (fast) {
68
+ lines.forEach(l => console.log(l));
69
+ resolve();
70
+ return;
71
+ }
72
+
73
+ let idx = 0;
74
+ const interval = setInterval(() => {
75
+ if (idx >= lines.length) {
76
+ clearInterval(interval);
77
+ resolve();
78
+ return;
79
+ }
80
+ console.log(lines[idx]);
81
+ idx++;
82
+ }, 60);
83
+ });
84
+ }
85
+
86
+ // ── Status panel ─────────────────────────────────────────
87
+ // Draws a bordered status box with labeled rows.
88
+ function statusPanel(title, rows) {
89
+ const maxLabel = Math.max(...rows.map(r => r[0].length));
90
+ console.log('');
91
+ console.log(` ${b(w(title))}`);
92
+ console.log(` ${g('─'.repeat(48))}`);
93
+ for (const [label, value, color] of rows) {
94
+ const colorFn = color === 'green' ? green
95
+ : color === 'yellow' ? yellow
96
+ : color === 'cyan' ? cyan
97
+ : color === 'red' ? red
98
+ : (s) => s;
99
+ console.log(` ${g(label.padEnd(maxLabel + 2))} ${colorFn(value)}`);
100
+ }
101
+ console.log(` ${g('─'.repeat(48))}`);
102
+ console.log('');
103
+ }
104
+
105
+ // ── Interop badge line ───────────────────────────────────
106
+ // Shows where phewsh is connected / can connect.
107
+ function interopLine(config, intentFiles) {
108
+ const parts = [];
109
+ if (intentFiles.length > 0) parts.push(green('●') + ' .intent/');
110
+ if (config?.apiKey) parts.push(green('●') + ' AI');
111
+ if (config?.supabaseUserId) parts.push(green('●') + ' cloud');
112
+
113
+ // Always show what's available
114
+ const available = [];
115
+ available.push(g('Claude Code'));
116
+ available.push(g('Cursor'));
117
+ available.push(g('ChatGPT'));
118
+ available.push(g('MCP'));
119
+
120
+ if (parts.length > 0) {
121
+ console.log(` ${g('connected')} ${parts.join(g(' · '))}`);
122
+ }
123
+ console.log(` ${g('works with')} ${available.join(g(' · '))}`);
124
+ }
125
+
126
+ // ── Divider ──────────────────────────────────────────────
127
+ function divider(char = '·', width = 48) {
128
+ console.log(` ${g(char.repeat(width))}`);
129
+ }
130
+
131
+ // ── Typewriter ───────────────────────────────────────────
132
+ // Writes text character by character. Returns a promise.
133
+ function typewrite(text, speed = 20) {
134
+ return new Promise((resolve) => {
135
+ let i = 0;
136
+ const interval = setInterval(() => {
137
+ if (i >= text.length) {
138
+ clearInterval(interval);
139
+ process.stdout.write('\n');
140
+ resolve();
141
+ return;
142
+ }
143
+ process.stdout.write(text[i]);
144
+ i++;
145
+ }, speed);
146
+ });
147
+ }
148
+
149
+ // ── Welcome tips (rotates each session) ──────────────────
150
+ const TIPS = [
151
+ `${g('tip:')} Type ${w('/clarify')} to turn a messy idea into a structured spec`,
152
+ `${g('tip:')} ${w('/gate')} lets you set budget, time, and skill constraints`,
153
+ `${g('tip:')} Run ${w('phewsh watch')} in another tab for live sync to CLAUDE.md`,
154
+ `${g('tip:')} ${w('/export')} creates a portable context file for any AI tool`,
155
+ `${g('tip:')} ${w('/model opus')} switches to Claude Opus for complex reasoning`,
156
+ `${g('tip:')} Your ${w('.intent/')} files are plain markdown — edit them anytime`,
157
+ `${g('tip:')} ${w('phewsh context --copy')} puts your project context on the clipboard`,
158
+ `${g('tip:')} ${w('/tour')} walks you through everything PHEWSH can do`,
159
+ ];
160
+
161
+ function randomTip() {
162
+ return TIPS[Math.floor(Math.random() * TIPS.length)];
163
+ }
164
+
165
+ // ── Tour content ─────────────────────────────────────────
166
+ const TOUR_PAGES = [
167
+ {
168
+ title: 'What is PHEWSH?',
169
+ body: [
170
+ ` PHEWSH gives your project a ${b('portable identity')}.`,
171
+ ` Define what you're building once — every AI tool reads it.`,
172
+ '',
173
+ ` It works ${w('standalone')} as its own AI shell,`,
174
+ ` and ${w('inside')} Claude Code, Cursor, ChatGPT, and any MCP agent.`,
175
+ '',
176
+ ` ${g('Your project context lives in')} ${cyan('.intent/')} ${g('— plain markdown files.')}`,
177
+ ` ${g('You own them. They travel with your code.')}`,
178
+ ]
179
+ },
180
+ {
181
+ title: 'The .intent/ directory',
182
+ body: [
183
+ ` ${cyan('.intent/')}`,
184
+ ` ${green('vision.md')} ${g('What this project is and why it exists')}`,
185
+ ` ${green('plan.md')} ${g('Strategy, phases, milestones')}`,
186
+ ` ${green('next.md')} ${g('Current tasks and what to do right now')}`,
187
+ ` ${yellow('gate.json')} ${g('Your constraints (budget, time, skill)')}`,
188
+ '',
189
+ ` ${g('Create these with')} /init ${g('or')} /clarify`,
190
+ ]
191
+ },
192
+ {
193
+ title: 'Standalone mode',
194
+ body: [
195
+ ` When you run ${w('phewsh')} on its own, you get an AI shell`,
196
+ ` that knows your project inside and out.`,
197
+ '',
198
+ ` Just type naturally:`,
199
+ ` ${cyan('>')} what should I focus on today?`,
200
+ ` ${cyan('>')} is my plan realistic given my budget?`,
201
+ ` ${cyan('>')} break this feature into tasks`,
202
+ '',
203
+ ` ${g('Every message includes your vision, plan, and constraints.')}`,
204
+ ]
205
+ },
206
+ {
207
+ title: 'Inside other tools',
208
+ body: [
209
+ ` ${b('Claude Code')} ${g('phewsh watch → auto-updates CLAUDE.md')}`,
210
+ ` ${b('Cursor')} ${g('phewsh context --file → .phewsh.context')}`,
211
+ ` ${b('ChatGPT')} ${g('phewsh context --copy → paste into Custom Instructions')}`,
212
+ ` ${b('MCP agents')} ${g('phewsh mcp setup → agents pull tasks automatically')}`,
213
+ '',
214
+ ` ${g('Same project identity, every tool. No re-explaining.')}`,
215
+ ]
216
+ },
217
+ {
218
+ title: 'The Decision Gate',
219
+ body: [
220
+ ` Before you build, decide ${b('whether')} to build.`,
221
+ ` The gate captures what you can actually spend:`,
222
+ '',
223
+ ` ${g('Budget')} ${w('$50')} ${g('Skill')} ${w('expert')}`,
224
+ ` ${g('Time')} ${w('15 hrs/week')} ${g('Urgency')} ${w('high')}`,
225
+ '',
226
+ ` ${g('These constraints shape every AI response.')}`,
227
+ ` ${g('Run')} /gate activate ${g('to set yours.')}`,
228
+ ]
229
+ },
230
+ {
231
+ title: 'You\'re ready',
232
+ body: [
233
+ ` ${green('●')} Type naturally to chat with your project context`,
234
+ ` ${green('●')} ${w('/init')} or ${w('/clarify')} to create .intent/ artifacts`,
235
+ ` ${green('●')} ${w('/gate')} to set your constraints`,
236
+ ` ${green('●')} ${w('/help')} for all commands`,
237
+ '',
238
+ ` ${g('PHEWSH is your project\'s home base.')}`,
239
+ ` ${g('The AI that knows you doesn\'t need to be re-taught.')}`,
240
+ ]
241
+ },
242
+ ];
243
+
244
+ module.exports = {
245
+ // Colors
246
+ b, d, w, g, green, cyan, yellow, magenta, blue, red,
247
+ // Components
248
+ spinner, brandReveal, statusPanel, interopLine, divider, typewrite,
249
+ randomTip, TOUR_PAGES,
250
+ // ANSI helpers
251
+ hide, show, up, clearLine,
252
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.11.10",
3
+ "version": "0.11.11",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"