phewsh 0.11.11 → 0.11.12

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.
Files changed (3) hide show
  1. package/commands/session.js +168 -183
  2. package/lib/ui.js +202 -112
  3. package/package.json +1 -1
@@ -17,7 +17,9 @@ const { select, refreshSession: refreshSess } = require('../lib/supabase');
17
17
  const { readPPS } = require('../lib/pps');
18
18
  const { push, pull, ensureValidToken } = require('./sync');
19
19
 
20
- const { b, d, w, g, green, cyan, yellow } = ui;
20
+ // Brand palette shortcuts
21
+ const { b, d, w, g, green, cyan, yellow,
22
+ teal, peach, sage, slate, cream, ember } = ui;
21
23
 
22
24
  // Sync awareness: compare local .intent/ timestamps with cloud updated_at
23
25
  async function checkSyncStatus(config) {
@@ -32,7 +34,6 @@ async function checkSyncStatus(config) {
32
34
  const cloudId = pps?.adapters?.phewsh?.cloud_id;
33
35
  const projectName = path.basename(process.cwd());
34
36
 
35
- // Find cloud project
36
37
  const query = cloudId
37
38
  ? `id=eq.${cloudId}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`
38
39
  : `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`;
@@ -42,7 +43,6 @@ async function checkSyncStatus(config) {
42
43
 
43
44
  const project = projects[0];
44
45
 
45
- // Get latest cloud artifact updated_at
46
46
  const artifacts = await select(
47
47
  'artifacts',
48
48
  `project_id=eq.${project.id}&user_id=eq.${config.supabaseUserId}&select=kind,updated_at&order=updated_at.desc&limit=1`,
@@ -53,7 +53,6 @@ async function checkSyncStatus(config) {
53
53
  ? new Date(artifacts[0].updated_at).getTime()
54
54
  : new Date(project.updated_at).getTime();
55
55
 
56
- // Get latest local file mtime
57
56
  const localFiles = ['vision.md', 'plan.md', 'next.md'];
58
57
  let latestLocal = 0;
59
58
  for (const file of localFiles) {
@@ -67,7 +66,6 @@ async function checkSyncStatus(config) {
67
66
  if (latestLocal === 0) return { status: 'local-only' };
68
67
 
69
68
  const drift = Math.abs(cloudTime - latestLocal);
70
- // Within 60 seconds = synced
71
69
  if (drift < 60000) return { status: 'synced' };
72
70
 
73
71
  if (cloudTime > latestLocal) {
@@ -78,7 +76,7 @@ async function checkSyncStatus(config) {
78
76
  return { status: 'local-newer', ago };
79
77
  }
80
78
  } catch {
81
- return null; // Network error — silently skip
79
+ return null;
82
80
  }
83
81
  }
84
82
 
@@ -140,7 +138,6 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
140
138
  const body = { model: modelId, max_tokens: 2048, messages, stream: true };
141
139
  if (systemPrompt) body.system = systemPrompt;
142
140
 
143
- // Start spinner while waiting for first token
144
141
  const spin = ui.spinner('thinking');
145
142
 
146
143
  const response = await fetch('https://api.anthropic.com/v1/messages', {
@@ -178,7 +175,7 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
178
175
  const parsed = JSON.parse(data);
179
176
  if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
180
177
  if (firstToken) {
181
- spin.stop(); // Clear spinner before first output
178
+ spin.stop();
182
179
  firstToken = false;
183
180
  }
184
181
  process.stdout.write(parsed.delta.text);
@@ -194,7 +191,7 @@ async function streamChat(apiKey, messages, systemPrompt, modelId) {
194
191
  }
195
192
  }
196
193
 
197
- if (firstToken) spin.stop(); // In case we got no tokens
194
+ if (firstToken) spin.stop();
198
195
  process.stdout.write('\n');
199
196
 
200
197
  return { content: fullResponse, promptTokens, completionTokens, model: modelId };
@@ -204,58 +201,58 @@ async function main() {
204
201
  let config = loadConfig();
205
202
  let intentFiles = loadIntentContext();
206
203
  let systemPrompt = buildSystemPrompt(intentFiles);
207
- const messages = []; // conversation history
204
+ const messages = [];
208
205
  const projectName = path.basename(process.cwd());
209
206
  let currentModel = DEFAULT_MODEL;
210
207
  let totalPromptTokens = 0;
211
208
  let totalCompletionTokens = 0;
212
209
 
213
- // ── Animated brand reveal ──────────────────────────────
210
+ // ── The Exhale: animated brand reveal ──────────────────
214
211
  await ui.brandReveal();
215
212
 
216
- // ── First-run welcome or status panel ──────────────────
213
+ // ── First-run welcome ──────────────────────────────────
217
214
  if (!config?.apiKey) {
218
- console.log(` ${b(w('Welcome to PHEWSH.'))}`);
219
- console.log(` ${g('Your AI knows your project. No more re-explaining.')}`);
215
+ console.log(` ${b(cream('Welcome.'))}`);
216
+ console.log(` ${sage('Your AI already knows your project. No more re-explaining.')}`);
220
217
  console.log('');
221
- console.log(` ${w('To chat, you need an API key.')} ${g('(not a subscription)')}`);
222
- console.log(` ${g('ChatGPT Plus / Claude Pro are separate API keys are pay-as-you-go.')}`);
218
+ console.log(` ${cream('To chat, you need an API key.')} ${slate('(not a subscription)')}`);
219
+ console.log(` ${slate('ChatGPT Plus / Claude Pro don\'t include API access.')}`);
220
+ console.log(` ${slate('API keys are pay-as-you-go — both providers offer free credits.')}`);
223
221
  console.log('');
224
- console.log(` ${cyan('1')} ${b('Anthropic')} ${g('(recommended)')}`);
225
- console.log(` ${g('console.anthropic.com/settings/keys')}`);
226
- console.log(` ${g('Direct access to Claude. Best quality. ~$0.01/message.')}`);
222
+ console.log(` ${teal('1')} ${b(cream('Anthropic'))} ${slate('(recommended)')}`);
223
+ console.log(` ${sage('console.anthropic.com/settings/keys')}`);
224
+ console.log(` ${slate('Direct Claude access. Best quality. ~$0.01/message.')}`);
227
225
  console.log('');
228
- console.log(` ${cyan('2')} ${b('OpenRouter')}`);
229
- console.log(` ${g('openrouter.ai/keys')}`);
230
- console.log(` ${g('One key, many models (Claude, GPT, Gemini, etc.)')}`);
226
+ console.log(` ${teal('2')} ${b(cream('OpenRouter'))}`);
227
+ console.log(` ${sage('openrouter.ai/keys')}`);
228
+ console.log(` ${slate('One key Claude, GPT, Gemini, and more.')}`);
231
229
  console.log('');
232
- console.log(` ${g('Got a key? Type')} /key ${g('to paste it in.')}`);
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).')}`);
230
+ console.log(` ${sage('Got a key?')} ${cream('/key')} ${sage('to paste it in.')}`);
231
+ console.log(` ${sage('Curious?')} ${cream('/tour')} ${sage('to explore (no key needed).')}`);
235
232
  console.log('');
236
233
  } else if (!config.apiKey.startsWith('sk-')) {
237
- console.log(` ${yellow('!')} Stored API key looks invalid.`);
238
- console.log(` ${g('Run')} /key ${g('to set a new one')}`);
234
+ console.log(` ${ember('!')} ${sage('Stored API key looks invalid.')}`);
235
+ console.log(` ${sage('Run')} ${cream('/key')} ${sage('to set a new one')}`);
239
236
  console.log('');
240
- config.apiKey = null; // Don't try to use a bad key
237
+ config.apiKey = null;
241
238
  }
242
239
 
243
240
  // ── Project status ─────────────────────────────────────
244
241
  if (intentFiles.length > 0) {
245
- console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
242
+ console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(intentFiles.map(f => f.file).join(', '))}`);
246
243
  } else {
247
- console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} no .intent/ context`);
248
- console.log(` ${g(' run /init to create .intent/ artifacts')}`);
244
+ console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage('no .intent/ context')}`);
245
+ console.log(` ${slate(' run /init to create .intent/ artifacts')}`);
249
246
  }
250
- console.log(` ${g(' model:')} ${MODELS[currentModel].name}`);
247
+ console.log(` ${slate(' model:')} ${sage(MODELS[currentModel].name)}`);
251
248
  if (config?.email) {
252
- console.log(` ${g(' user:')} ${config.email}`);
249
+ console.log(` ${slate(' user:')} ${sage(config.email)}`);
253
250
  }
254
251
 
255
- // ── Interop line ───────────────────────────────────────
252
+ // ── Interop ────────────────────────────────────────────
256
253
  ui.interopLine(config, intentFiles);
257
254
 
258
- // Sync status check (non-blocking, 3s timeout)
255
+ // Sync status (non-blocking)
259
256
  if (config?.supabaseUserId && intentFiles.length > 0) {
260
257
  const syncResult = await Promise.race([
261
258
  checkSyncStatus(config),
@@ -263,32 +260,32 @@ async function main() {
263
260
  ]);
264
261
  if (syncResult) {
265
262
  if (syncResult.status === 'cloud-newer') {
266
- console.log(` ${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull`);
263
+ console.log(` ${ember('↓')} ${sage('Cloud is newer (' + syncResult.ago + ') — run /pull')}`);
267
264
  } else if (syncResult.status === 'local-newer') {
268
- console.log(` ${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push`);
265
+ console.log(` ${ember('↑')} ${sage('Local changes not pushed (' + syncResult.ago + ') — run /push')}`);
269
266
  } else if (syncResult.status === 'synced') {
270
- console.log(` ${green('↕')} ${g('synced')}`);
267
+ console.log(` ${teal('↕')} ${slate('synced')}`);
271
268
  } else if (syncResult.status === 'local-only') {
272
- console.log(` ${g('↕ not linked to cloud — run /push to sync')}`);
269
+ console.log(` ${slate('↕ not linked to cloud — run /push to sync')}`);
273
270
  }
274
271
  }
275
272
  }
276
273
 
277
274
  console.log('');
278
- ui.divider('');
275
+ ui.divider('line');
279
276
  if (!config?.apiKey) {
280
- console.log(` ${g('type')} /key ${g('to get started ·')} /tour ${g('to explore ·')} /help ${g('for all commands')}`);
277
+ console.log(` ${sage('type')} ${cream('/key')} ${sage('to get started ·')} ${cream('/tour')} ${sage('to explore ·')} ${cream('/help')} ${sage('for commands')}`);
281
278
  } else {
282
279
  console.log(` ${ui.randomTip()}`);
283
- console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
280
+ console.log(` ${sage('type naturally ·')} ${cream('/help')} ${sage('for commands ·')} ${cream('/quit')} ${sage('to exit')}`);
284
281
  }
285
- ui.divider('');
282
+ ui.divider('line');
286
283
  console.log('');
287
284
 
288
285
  const rl = readline.createInterface({
289
286
  input: process.stdin,
290
287
  output: process.stdout,
291
- prompt: ` ${cyan('phewsh')} ${green('>')} `,
288
+ prompt: ` ${teal('phewsh')} ${sage('>')} `,
292
289
  historySize: 100,
293
290
  });
294
291
 
@@ -311,53 +308,55 @@ async function main() {
311
308
  if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
312
309
  const turns = messages.length / 2;
313
310
  console.log('');
314
- ui.divider('');
315
- console.log(` ${g('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
316
- ui.divider('');
311
+ ui.divider('line');
312
+ console.log(` ${sage('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
313
+ ui.divider('line');
317
314
  console.log('');
318
315
  process.exit(0);
319
316
  }
320
317
 
321
318
  if (cmd === 'help' || cmd === 'h') {
322
- console.log(`
323
- ${b(w('Session commands'))}
324
-
325
- ${w('conversation')}
326
- ${cyan('/clear')} Clear conversation history
327
- ${cyan('/run')} ${d('<prompt>')} One-shot prompt (doesn't add to conversation)
328
- ${cyan('/quit')} End session
329
-
330
- ${w('project')}
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
338
-
339
- ${w('sync')}
340
- ${cyan('/push')} Push .intent/ to cloud
341
- ${cyan('/pull')} Pull .intent/ from cloud (reloads context)
342
- ${cyan('/sync')} Check sync status
343
-
344
- ${w('configuration')}
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
355
- `);
319
+ console.log('');
320
+ ui.divider('line');
321
+ console.log(` ${b(cream('Session commands'))}`);
322
+ ui.divider('line');
323
+ console.log('');
324
+ console.log(` ${cream('conversation')}`);
325
+ console.log(` ${teal('/clear')} ${sage('Clear conversation history')}`);
326
+ console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
327
+ console.log(` ${teal('/quit')} ${sage('End session')}`);
328
+ console.log('');
329
+ console.log(` ${cream('project')}`);
330
+ console.log(` ${teal('/init')} ${sage('Create .intent/ artifacts in this directory')}`);
331
+ console.log(` ${teal('/clarify')} ${sage('AI-assisted artifact generation')}`);
332
+ console.log(` ${teal('/gate')} ${sage('Declare constraints (budget, time, skill)')}`);
333
+ console.log(` ${teal('/export')} ${sage('Export portable context for other AI tools')}`);
334
+ console.log(` ${teal('/context')} ${sage('Show loaded .intent/ files')}`);
335
+ console.log(` ${teal('/reload')} ${sage('Reload .intent/ context from disk')}`);
336
+ console.log(` ${teal('/status')} ${sage('Show session stats')}`);
337
+ console.log('');
338
+ console.log(` ${cream('sync')}`);
339
+ console.log(` ${teal('/push')} ${sage('Push .intent/ to cloud')}`);
340
+ console.log(` ${teal('/pull')} ${sage('Pull .intent/ from cloud (reloads context)')}`);
341
+ console.log(` ${teal('/sync')} ${sage('Check sync status')}`);
342
+ console.log('');
343
+ console.log(` ${cream('configuration')}`);
344
+ console.log(` ${teal('/login')} ${sage('Set up identity + cloud sync')}`);
345
+ console.log(` ${teal('/key')} ${sage('Set or update your API key')}`);
346
+ console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model (sonnet, opus, haiku)')}`);
347
+ console.log(` ${teal('/models')} ${sage('List available models')}`);
348
+ console.log(` ${teal('/provider')} ${sage('Show current provider info')}`);
349
+ console.log(` ${teal('/update')} ${sage('Update phewsh to the latest version')}`);
350
+ console.log('');
351
+ console.log(` ${cream('explore')}`);
352
+ console.log(` ${teal('/tour')} ${sage('Guided walkthrough of PHEWSH')}`);
353
+ console.log(` ${teal('/system')} ${sage('Show current system prompt')}`);
354
+ console.log('');
356
355
  rl.prompt();
357
356
  return;
358
357
  }
359
358
 
360
- // ── /tour — guided walkthrough ─────────────────────
359
+ // ── /tour ──────────────────────────────────────────
361
360
  if (cmd === 'tour') {
362
361
  const pages = ui.TOUR_PAGES;
363
362
  let pageIdx = cmdArg ? parseInt(cmdArg) - 1 : 0;
@@ -366,15 +365,15 @@ async function main() {
366
365
 
367
366
  const page = pages[pageIdx];
368
367
  console.log('');
369
- ui.divider('');
370
- console.log(` ${b(w(page.title))} ${g(`(${pageIdx + 1}/${pages.length})`)}`);
371
- ui.divider('');
368
+ ui.divider('line');
369
+ console.log(` ${b(cream(page.title))} ${slate(`(${pageIdx + 1}/${pages.length})`)}`);
370
+ ui.divider('line');
372
371
  page.body.forEach(line => console.log(line));
373
372
  console.log('');
374
373
  if (pageIdx < pages.length - 1) {
375
- console.log(` ${g('next:')} /tour ${pageIdx + 2} ${g('·')} ${g('or /tour 1-' + pages.length + ' to jump')}`);
374
+ console.log(` ${sage('next:')} ${cream('/tour ' + (pageIdx + 2))} ${slate('·')} ${sage('/tour 1-' + pages.length + ' to jump')}`);
376
375
  } else {
377
- console.log(` ${green('●')} ${g('End of tour. You\'re ready to go.')}`);
376
+ console.log(` ${teal('●')} ${sage('End of tour. You\'re ready.')}`);
378
377
  }
379
378
  console.log('');
380
379
  rl.prompt();
@@ -383,7 +382,7 @@ async function main() {
383
382
 
384
383
  if (cmd === 'clear') {
385
384
  messages.length = 0;
386
- console.log(` ${g('conversation cleared')}`);
385
+ console.log(` ${sage('conversation cleared')}`);
387
386
  rl.prompt();
388
387
  return;
389
388
  }
@@ -391,13 +390,13 @@ async function main() {
391
390
  if (cmd === 'context') {
392
391
  if (intentFiles.length > 0) {
393
392
  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('');
393
+ console.log(` ${b(cream('Loaded from'))} ${teal('.intent/')}`);
394
+ ui.divider('line');
395
+ intentFiles.forEach(f => console.log(` ${teal('●')} ${cream(f.file)} ${slate('(' + f.content.length + ' chars)')}`));
396
+ ui.divider('line');
398
397
  } else {
399
- console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
400
- console.log(` ${g('Run')} /init ${g('to create one')}`);
398
+ console.log(`\n ${sage('No .intent/ context found in')} ${slate(process.cwd())}`);
399
+ console.log(` ${sage('Run')} ${cream('/init')} ${sage('to create one')}`);
401
400
  }
402
401
  console.log('');
403
402
  rl.prompt();
@@ -406,7 +405,7 @@ async function main() {
406
405
 
407
406
  if (cmd === 'status') {
408
407
  const turns = messages.length / 2;
409
- config = loadConfig(); // refresh
408
+ config = loadConfig();
410
409
  ui.statusPanel('Session', [
411
410
  ['Turns', String(turns)],
412
411
  ['Tokens', `${totalPromptTokens} in → ${totalCompletionTokens} out`],
@@ -414,7 +413,7 @@ async function main() {
414
413
  ['Context', intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none', intentFiles.length > 0 ? 'green' : 'yellow'],
415
414
  ['Model', MODELS[currentModel].name],
416
415
  ['Provider', MODELS[currentModel].provider],
417
- ['User', config?.email || g('not logged in')],
416
+ ['User', config?.email || slate('not logged in')],
418
417
  ['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '...' : 'not set', config?.apiKey ? 'green' : 'yellow'],
419
418
  ]);
420
419
  rl.prompt();
@@ -424,34 +423,32 @@ async function main() {
424
423
  if (cmd === 'reload') {
425
424
  intentFiles = loadIntentContext();
426
425
  systemPrompt = buildSystemPrompt(intentFiles);
427
- console.log(` ${green('●')} Reloaded ${intentFiles.length} artifact${intentFiles.length !== 1 ? 's' : ''}`);
426
+ console.log(` ${teal('●')} ${sage('Reloaded ' + intentFiles.length + ' artifact' + (intentFiles.length !== 1 ? 's' : ''))}`);
428
427
  rl.prompt();
429
428
  return;
430
429
  }
431
430
 
432
431
  if (cmd === 'system') {
433
- console.log(`\n${g(systemPrompt)}\n`);
432
+ console.log(`\n${slate(systemPrompt)}\n`);
434
433
  rl.prompt();
435
434
  return;
436
435
  }
437
436
 
438
437
  if (cmd === 'init') {
439
438
  if (fs.existsSync(path.join(INTENT_DIR, 'vision.md'))) {
440
- console.log(`\n ${g('.intent/ already exists in')} ${process.cwd()}`);
441
- console.log(` ${g('Use /reload to refresh context')}\n`);
439
+ console.log(`\n ${sage('.intent/ already exists in')} ${slate(process.cwd())}`);
440
+ console.log(` ${sage('Use /reload to refresh context')}\n`);
442
441
  } else {
443
442
  try {
444
- // Delegate to the intent --init command
445
443
  const { execSync } = require('child_process');
446
444
  execSync('node ' + path.join(__dirname, 'intent.js') + ' --init', { stdio: 'inherit' });
447
- // Reload context after init
448
445
  intentFiles = loadIntentContext();
449
446
  systemPrompt = buildSystemPrompt(intentFiles);
450
447
  if (intentFiles.length > 0) {
451
- console.log(` ${green('●')} Context loaded: ${intentFiles.map(f => f.file).join(', ')}`);
448
+ console.log(` ${teal('●')} ${sage('Context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
452
449
  }
453
450
  } catch (err) {
454
- console.error(` ${g('Init failed:')} ${err.message}`);
451
+ console.error(` ${sage('Init failed:')} ${err.message}`);
455
452
  }
456
453
  }
457
454
  console.log('');
@@ -461,7 +458,7 @@ async function main() {
461
458
 
462
459
  if (cmd === 'clarify') {
463
460
  if (!config?.apiKey) {
464
- console.log(`\n ${yellow('!')} No API key. Run /key to set one.\n`);
461
+ console.log(`\n ${ember('!')} ${sage('No API key. Run /key to set one.')}\n`);
465
462
  rl.prompt();
466
463
  return;
467
464
  }
@@ -469,14 +466,13 @@ async function main() {
469
466
  const { execSync } = require('child_process');
470
467
  const args = cmdArg ? `--text "${cmdArg.replace(/"/g, '\\"')}"` : '';
471
468
  execSync(`node ${path.join(__dirname, 'clarify.js')} ${args}`, { stdio: 'inherit' });
472
- // Reload context after clarify
473
469
  intentFiles = loadIntentContext();
474
470
  systemPrompt = buildSystemPrompt(intentFiles);
475
471
  if (intentFiles.length > 0) {
476
- console.log(` ${green('●')} Context loaded: ${intentFiles.map(f => f.file).join(', ')}`);
472
+ console.log(` ${teal('●')} ${sage('Context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
477
473
  }
478
474
  } catch (err) {
479
- console.error(` ${g('Clarify failed:')} ${err.message}`);
475
+ console.error(` ${sage('Clarify failed:')} ${err.message}`);
480
476
  }
481
477
  console.log('');
482
478
  rl.prompt();
@@ -488,11 +484,10 @@ async function main() {
488
484
  const { execSync } = require('child_process');
489
485
  const gateArg = cmdArg || 'status';
490
486
  execSync(`node ${path.join(__dirname, 'gate.js')} ${gateArg}`, { stdio: 'inherit' });
491
- // Reload context after gate changes (gate.json may have updated)
492
487
  intentFiles = loadIntentContext();
493
488
  systemPrompt = buildSystemPrompt(intentFiles);
494
489
  } catch (err) {
495
- console.error(` ${g('Gate failed:')} ${err.message}`);
490
+ console.error(` ${sage('Gate failed:')} ${err.message}`);
496
491
  }
497
492
  rl.prompt();
498
493
  return;
@@ -505,12 +500,12 @@ async function main() {
505
500
  if (content) {
506
501
  const outPath = path.join(process.cwd(), '.phewsh.context');
507
502
  fs.writeFileSync(outPath, content);
508
- console.log(`\n ${green('●')} Written to ${outPath}\n`);
503
+ console.log(`\n ${teal('●')} ${sage('Written to')} ${cream(outPath)}\n`);
509
504
  } else {
510
- console.log(`\n ${g('No artifacts to export')}\n`);
505
+ console.log(`\n ${sage('No artifacts to export')}\n`);
511
506
  }
512
507
  } catch (err) {
513
- console.error(` ${g('Export failed:')} ${err.message}`);
508
+ console.error(` ${sage('Export failed:')} ${err.message}`);
514
509
  }
515
510
  rl.prompt();
516
511
  return;
@@ -518,16 +513,16 @@ async function main() {
518
513
 
519
514
  if (cmd === 'push') {
520
515
  if (!config?.supabaseUserId) {
521
- console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
516
+ console.log(`\n ${ember('!')} ${sage('Not logged in. Run /login first.')}\n`);
522
517
  rl.prompt();
523
518
  return;
524
519
  }
525
520
  try {
526
521
  const token = await ensureValidToken(config);
527
- if (!token) { console.log(`\n ${yellow('!')} Session expired. Run /login.\n`); rl.prompt(); return; }
522
+ if (!token) { console.log(`\n ${ember('!')} ${sage('Session expired. Run /login.')}\n`); rl.prompt(); return; }
528
523
  await push(config, token);
529
524
  } catch (err) {
530
- console.error(` ${yellow('!')} Push failed: ${err.message}\n`);
525
+ console.error(` ${ember('!')} ${sage('Push failed:')} ${err.message}\n`);
531
526
  }
532
527
  rl.prompt();
533
528
  return;
@@ -535,22 +530,21 @@ async function main() {
535
530
 
536
531
  if (cmd === 'pull') {
537
532
  if (!config?.supabaseUserId) {
538
- console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
533
+ console.log(`\n ${ember('!')} ${sage('Not logged in. Run /login first.')}\n`);
539
534
  rl.prompt();
540
535
  return;
541
536
  }
542
537
  try {
543
538
  const token = await ensureValidToken(config);
544
- if (!token) { console.log(`\n ${yellow('!')} Session expired. Run /login.\n`); rl.prompt(); return; }
539
+ if (!token) { console.log(`\n ${ember('!')} ${sage('Session expired. Run /login.')}\n`); rl.prompt(); return; }
545
540
  await pull(config, token);
546
- // Reload context after pull
547
541
  intentFiles = loadIntentContext();
548
542
  systemPrompt = buildSystemPrompt(intentFiles);
549
543
  if (intentFiles.length > 0) {
550
- console.log(` ${green('●')} Context reloaded: ${intentFiles.map(f => f.file).join(', ')}`);
544
+ console.log(` ${teal('●')} ${sage('Context reloaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
551
545
  }
552
546
  } catch (err) {
553
- console.error(` ${yellow('!')} Pull failed: ${err.message}\n`);
547
+ console.error(` ${ember('!')} ${sage('Pull failed:')} ${err.message}\n`);
554
548
  }
555
549
  console.log('');
556
550
  rl.prompt();
@@ -558,24 +552,23 @@ async function main() {
558
552
  }
559
553
 
560
554
  if (cmd === 'sync') {
561
- // Show sync status
562
555
  if (!config?.supabaseUserId) {
563
- console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
556
+ console.log(`\n ${ember('!')} ${sage('Not logged in. Run /login first.')}\n`);
564
557
  rl.prompt();
565
558
  return;
566
559
  }
567
560
  const syncSpin = ui.spinner('checking sync');
568
561
  const syncResult = await checkSyncStatus(config);
569
562
  if (!syncResult) {
570
- syncSpin.stop(`${g('Could not check sync status')}`);
563
+ syncSpin.stop(`${sage('Could not check sync status')}`);
571
564
  } else if (syncResult.status === 'cloud-newer') {
572
- syncSpin.stop(`${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull`);
565
+ syncSpin.stop(`${ember('↓')} ${sage('Cloud is newer (' + syncResult.ago + ') — run /pull')}`);
573
566
  } else if (syncResult.status === 'local-newer') {
574
- syncSpin.stop(`${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push`);
567
+ syncSpin.stop(`${ember('↑')} ${sage('Local changes not pushed (' + syncResult.ago + ') — run /push')}`);
575
568
  } else if (syncResult.status === 'synced') {
576
- syncSpin.stop(`${green('↕')} In sync`);
569
+ syncSpin.stop(`${teal('↕')} ${sage('In sync')}`);
577
570
  } else if (syncResult.status === 'local-only') {
578
- syncSpin.stop(`${g('↕ Not linked to cloud — run /push to sync')}`);
571
+ syncSpin.stop(`${slate('↕ Not linked to cloud — run /push to sync')}`);
579
572
  }
580
573
  console.log('');
581
574
  rl.prompt();
@@ -586,9 +579,9 @@ async function main() {
586
579
  try {
587
580
  const { execSync } = require('child_process');
588
581
  execSync('node ' + path.join(__dirname, 'login.js'), { stdio: 'inherit' });
589
- config = loadConfig(); // refresh after login
582
+ config = loadConfig();
590
583
  } catch (err) {
591
- console.error(` ${g('Login failed:')} ${err.message}`);
584
+ console.error(` ${sage('Login failed:')} ${err.message}`);
592
585
  }
593
586
  rl.prompt();
594
587
  return;
@@ -596,56 +589,54 @@ async function main() {
596
589
 
597
590
  if (cmd === 'key') {
598
591
  if (cmdArg) {
599
- // Inline: /key sk-ant-...
600
592
  const apiKey = cmdArg.trim();
601
593
  config = loadConfig() || {};
602
594
  if (apiKey.startsWith('sk-ant-') || apiKey.startsWith('sk-')) {
603
595
  config.apiKey = apiKey;
604
596
  config.provider = 'anthropic';
605
597
  saveConfig(config);
606
- console.log(` ${green('●')} Anthropic API key saved. You're ready to go — just type.\n`);
598
+ console.log(` ${teal('●')} ${sage('Anthropic key saved. You\'re ready — just type.')}\n`);
607
599
  } else if (apiKey.startsWith('sk-or-')) {
608
600
  config.apiKey = apiKey;
609
601
  config.provider = 'openrouter';
610
602
  saveConfig(config);
611
- console.log(` ${green('●')} OpenRouter API key saved. You're ready to go — just type.\n`);
603
+ console.log(` ${teal('●')} ${sage('OpenRouter key saved. You\'re ready — just type.')}\n`);
612
604
  } else {
613
605
  config.apiKey = apiKey;
614
606
  saveConfig(config);
615
- console.log(` ${green('●')} API key saved. You're ready to go — just type.\n`);
607
+ console.log(` ${teal('●')} ${sage('API key saved. You\'re ready — just type.')}\n`);
616
608
  }
617
609
  rl.prompt();
618
610
  return;
619
611
  }
620
612
  console.log('');
621
- ui.divider('');
622
- console.log(` ${b(w('Where to get an API key'))}`);
623
- ui.divider('');
613
+ ui.divider('line');
614
+ console.log(` ${b(cream('Where to get an API key'))}`);
615
+ ui.divider('line');
624
616
  console.log('');
625
- console.log(` ${cyan('Anthropic')} ${g('(recommended)')}`);
626
- console.log(` ${g('1.')} Go to ${w('console.anthropic.com/settings/keys')}`);
627
- console.log(` ${g('2.')} Create key → copy it (starts with sk-ant-)`)
617
+ console.log(` ${teal('Anthropic')} ${slate('(recommended)')}`);
618
+ console.log(` ${sage('1.')} Go to ${cream('console.anthropic.com/settings/keys')}`);
619
+ console.log(` ${sage('2.')} Create key → copy it ${slate('(starts with sk-ant-)')}`);
628
620
  console.log('');
629
- console.log(` ${cyan('OpenRouter')} ${g('(multi-model)')}`);
630
- console.log(` ${g('1.')} Go to ${w('openrouter.ai/keys')}`);
631
- console.log(` ${g('2.')} Create key → copy it (starts with sk-or-)`)
621
+ console.log(` ${teal('OpenRouter')} ${slate('(multi-model)')}`);
622
+ console.log(` ${sage('1.')} Go to ${cream('openrouter.ai/keys')}`);
623
+ console.log(` ${sage('2.')} Create key → copy it ${slate('(starts with sk-or-)')}`);
632
624
  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.')}`);
625
+ console.log(` ${slate('Note: API keys subscriptions. Both providers offer free credits.')}`);
635
626
  console.log('');
636
627
  const keyRl = readline.createInterface({ input: process.stdin, output: process.stdout });
637
- keyRl.question(` Paste your API key\n > `, (apiKey) => {
628
+ keyRl.question(` ${sage('Paste your API key')}\n ${teal('>')} `, (apiKey) => {
638
629
  keyRl.close();
639
630
  apiKey = apiKey.trim();
640
631
  if (!apiKey) {
641
- console.log(` ${g('Cancelled')}\n`);
632
+ console.log(` ${slate('Cancelled')}\n`);
642
633
  } else {
643
634
  config = loadConfig() || {};
644
635
  config.apiKey = apiKey;
645
636
  if (apiKey.startsWith('sk-or-')) config.provider = 'openrouter';
646
637
  else config.provider = 'anthropic';
647
638
  saveConfig(config);
648
- console.log(`\n ${green('●')} API key saved. You're ready — just type naturally.\n`);
639
+ console.log(`\n ${teal('●')} ${sage('API key saved. You\'re ready — just type naturally.')}\n`);
649
640
  }
650
641
  rl.prompt();
651
642
  });
@@ -654,35 +645,34 @@ async function main() {
654
645
 
655
646
  if (cmd === 'models') {
656
647
  console.log('');
657
- ui.divider('');
658
- console.log(` ${b(w('Available models'))}`);
659
- ui.divider('');
648
+ ui.divider('line');
649
+ console.log(` ${b(cream('Available models'))}`);
650
+ ui.divider('line');
660
651
  for (const [key, model] of Object.entries(MODELS)) {
661
- const active = key === currentModel ? ` ${green('●')}` : '';
662
- console.log(` ${w(key.padEnd(16))} ${g(model.name)}${active}`);
652
+ const active = key === currentModel ? ` ${teal('●')}` : '';
653
+ console.log(` ${cream(key.padEnd(16))} ${sage(model.name)}${active}`);
663
654
  }
664
- console.log(`\n ${g('Switch with:')} /model <name>\n`);
655
+ console.log(`\n ${sage('Switch with:')} ${cream('/model <name>')}\n`);
665
656
  rl.prompt();
666
657
  return;
667
658
  }
668
659
 
669
660
  if (cmd === 'model') {
670
661
  if (!cmdArg) {
671
- console.log(` ${g('Current:')} ${MODELS[currentModel].name}`);
672
- console.log(` ${g('Usage:')} /model <sonnet|opus|haiku>`);
662
+ console.log(` ${sage('Current:')} ${cream(MODELS[currentModel].name)}`);
663
+ console.log(` ${sage('Usage:')} ${cream('/model <sonnet|opus|haiku>')}`);
673
664
  rl.prompt();
674
665
  return;
675
666
  }
676
- // Fuzzy match model name
677
667
  const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '');
678
668
  const match = Object.keys(MODELS).find(k =>
679
669
  k.includes(query) || MODELS[k].name.toLowerCase().includes(query)
680
670
  );
681
671
  if (match) {
682
672
  currentModel = match;
683
- console.log(` ${green('●')} Switched to ${MODELS[match].name}`);
673
+ console.log(` ${teal('●')} ${sage('Switched to')} ${cream(MODELS[match].name)}`);
684
674
  } else {
685
- console.log(` ${g('Unknown model. Available:')} ${Object.keys(MODELS).join(', ')}`);
675
+ console.log(` ${sage('Unknown model. Available:')} ${cream(Object.keys(MODELS).join(', '))}`);
686
676
  }
687
677
  rl.prompt();
688
678
  return;
@@ -701,26 +691,26 @@ async function main() {
701
691
  }
702
692
 
703
693
  if (cmd === 'update' || cmd === 'upgrade') {
704
- const updateSpin = ui.spinner('checking for updates');
694
+ const updateSpin = ui.spinner('checking for updates', 'gentle');
705
695
  try {
706
696
  const pkg = require('../../package.json');
707
697
  const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(5000) });
708
698
  const data = await res.json();
709
699
  if (!data.version || data.version === pkg.version) {
710
- updateSpin.stop(`${green('●')} Already on the latest version (${pkg.version})`);
700
+ updateSpin.stop(`${teal('●')} ${sage('Already on the latest version (' + pkg.version + ')')}`);
711
701
  console.log('');
712
702
  rl.prompt();
713
703
  return;
714
704
  }
715
- updateSpin.stop(`${cyan(pkg.version)} → ${cyan(data.version)}`);
716
- console.log(` ${g('Installing...')}\n`);
705
+ updateSpin.stop(`${peach(pkg.version)} ${sage('')} ${peach(data.version)}`);
706
+ console.log(` ${sage('Installing...')}\n`);
717
707
  const { execSync } = require('child_process');
718
708
  execSync(`npm install -g ${pkg.name}@latest`, { stdio: 'inherit' });
719
- console.log(`\n ${green('●')} Updated to ${data.version}`);
720
- console.log(` ${g('Restart phewsh to use the new version.')}\n`);
709
+ console.log(`\n ${teal('●')} ${sage('Updated to')} ${cream(data.version)}`);
710
+ console.log(` ${slate('Restart phewsh to use the new version.')}\n`);
721
711
  } catch (err) {
722
- updateSpin.stop(`${yellow('!')} Update failed: ${err.message}`);
723
- console.log(` ${g('You can update manually:')} npm install -g phewsh\n`);
712
+ updateSpin.stop(`${ember('!')} ${sage('Update failed:')} ${err.message}`);
713
+ console.log(` ${sage('You can update manually:')} ${cream('npm install -g phewsh')}\n`);
724
714
  }
725
715
  rl.prompt();
726
716
  return;
@@ -728,16 +718,15 @@ async function main() {
728
718
 
729
719
  if (cmd === 'run') {
730
720
  if (!cmdArg) {
731
- console.log(` ${g('Usage:')} /run <prompt>`);
721
+ console.log(` ${sage('Usage:')} ${cream('/run <prompt>')}`);
732
722
  rl.prompt();
733
723
  return;
734
724
  }
735
725
  if (!config?.apiKey) {
736
- console.log(` ${yellow('!')} No API key. Run /key to set one.`);
726
+ console.log(` ${ember('!')} ${sage('No API key. Run /key to set one.')}`);
737
727
  rl.prompt();
738
728
  return;
739
729
  }
740
- // One-shot: don't add to conversation history
741
730
  console.log('');
742
731
  try {
743
732
  const result = await streamChat(
@@ -747,7 +736,7 @@ async function main() {
747
736
  MODELS[currentModel].id
748
737
  );
749
738
  if (result.promptTokens || result.completionTokens) {
750
- console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
739
+ console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
751
740
  }
752
741
  trackSap({
753
742
  userId: config.supabaseUserId,
@@ -766,7 +755,7 @@ async function main() {
766
755
  }
767
756
 
768
757
  // Unknown slash command
769
- console.log(` ${g('Unknown command:')} /${cmd} ${g('— type /help for available commands')}`);
758
+ console.log(` ${sage('Unknown command:')} ${cream('/' + cmd)} ${slate('— type /help for available commands')}`);
770
759
  rl.prompt();
771
760
  return;
772
761
  }
@@ -774,12 +763,12 @@ async function main() {
774
763
  // Regular input → send to AI
775
764
  if (!config?.apiKey) {
776
765
  console.log('');
777
- console.log(` ${yellow('Almost there!')} You need an API key to chat.`);
778
- console.log(` Type ${w('/key')} and paste one in — takes 10 seconds.`);
766
+ console.log(` ${peach('Almost there.')} ${sage('You need an API key to chat.')}`);
767
+ console.log(` ${sage('Type')} ${cream('/key')} ${sage('and paste one in — takes 10 seconds.')}`);
779
768
  console.log('');
780
- console.log(` ${g('Get a key:')} ${cyan('console.anthropic.com/settings/keys')}`);
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)')}`);
769
+ console.log(` ${slate('Get a key:')} ${sage('console.anthropic.com/settings/keys')}`);
770
+ console.log(` ${slate('Or try:')} ${sage('openrouter.ai/keys')}`);
771
+ console.log(` ${slate('Explore:')} ${cream('/tour')} ${slate('to see what PHEWSH does (no key needed)')}`);
783
772
  console.log('');
784
773
  rl.prompt();
785
774
  return;
@@ -792,16 +781,13 @@ async function main() {
792
781
  const result = await streamChat(config.apiKey, messages, systemPrompt, MODELS[currentModel].id);
793
782
  messages.push({ role: 'assistant', content: result.content });
794
783
 
795
- // Track totals
796
784
  if (result.promptTokens) totalPromptTokens += result.promptTokens;
797
785
  if (result.completionTokens) totalCompletionTokens += result.completionTokens;
798
786
 
799
- // Token count footer
800
787
  if (result.promptTokens || result.completionTokens) {
801
- console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
788
+ console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
802
789
  }
803
790
 
804
- // SAP tracking (fire-and-forget)
805
791
  trackSap({
806
792
  userId: config.supabaseUserId,
807
793
  source: 'cli',
@@ -812,7 +798,6 @@ async function main() {
812
798
  });
813
799
  } catch (err) {
814
800
  console.error(`\n ${err.message}\n`);
815
- // Remove the failed user message
816
801
  messages.pop();
817
802
  }
818
803
 
@@ -821,7 +806,7 @@ async function main() {
821
806
  });
822
807
 
823
808
  rl.on('close', () => {
824
- console.log(`\n ${g('Session ended')}\n`);
809
+ console.log(`\n ${sage('session ended')}\n`);
825
810
  process.exit(0);
826
811
  });
827
812
  }
package/lib/ui.js CHANGED
@@ -1,6 +1,22 @@
1
- // phewsh ui — zero-dependency terminal animations and visual helpers
2
- // Pure ANSI escape sequences. No chalk, no ora, no bloat.
1
+ // phewsh ui — the exhale
2
+ // Relief. Quiet execution. Cool sweet future.
3
+ // Zero dependencies. Pure ANSI. The terminal breathes.
3
4
 
5
+ // ── PHEWSH palette ───────────────────────────────────────
6
+ // 24-bit color for terminals that support it (most modern ones do).
7
+ // Fallback-safe: if 24-bit fails, the text still renders.
8
+ const rgb = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`;
9
+ const rgbBg = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[0m`;
10
+
11
+ // Brand colors — relief, quiet, future
12
+ const teal = rgb(100, 215, 195); // cool calm — primary
13
+ const peach = rgb(255, 195, 145); // warm exhale — accent
14
+ const sage = rgb(130, 150, 140); // quiet — secondary text
15
+ const slate = rgb(80, 90, 88); // whisper — dim text
16
+ const cream = rgb(240, 235, 225); // clarity — bright text
17
+ const ember = rgb(220, 140, 90); // glow — warnings/energy
18
+
19
+ // Standard ANSI fallbacks (used where 24-bit might not render)
4
20
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
5
21
  const d = (s) => `\x1b[2m${s}\x1b[0m`;
6
22
  const w = (s) => `\x1b[97m${s}\x1b[0m`;
@@ -18,30 +34,43 @@ const show = '\x1b[?25h';
18
34
  const up = (n = 1) => `\x1b[${n}A`;
19
35
  const clearLine = '\x1b[2K\r';
20
36
 
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;
37
+ // ── Sleep helper ─────────────────────────────────────────
38
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
39
+
40
+ // ── Spinner: the exhale pulse ────────────────────────────
41
+ // Breathes in and out. Calm. Not frantic.
42
+ const EXHALE_FRAMES = [
43
+ ' · ',
44
+ ' · · ',
45
+ ' · · ',
46
+ ' · · ',
47
+ '· ·',
48
+ ' · · ',
49
+ ' · · ',
50
+ ' · · ',
51
+ ];
52
+
53
+ const BREATH_DOTS = [' ·', ' · ·', '· · ·', ' · ·', ' ·', ' '];
54
+
55
+ const GENTLE_FRAMES = ['·', '•', '●', '•', '·', ' '];
56
+
57
+ function spinner(text = 'thinking', style = 'exhale') {
58
+ const frames = style === 'gentle' ? GENTLE_FRAMES
59
+ : style === 'dots' ? BREATH_DOTS
60
+ : EXHALE_FRAMES;
33
61
  let i = 0;
34
62
  let stopped = false;
63
+ let currentText = text;
35
64
  process.stdout.write(hide);
36
65
  const interval = setInterval(() => {
37
66
  if (stopped) return;
38
67
  const frame = frames[i % frames.length];
39
- process.stdout.write(`${clearLine} ${cyan(frame)} ${g(text)}`);
68
+ process.stdout.write(`${clearLine} ${teal(frame)} ${sage(currentText)}`);
40
69
  i++;
41
- }, style === 'breath' ? 120 : 80);
70
+ }, style === 'gentle' ? 150 : 100);
42
71
 
43
72
  return {
44
- update(newText) { text = newText; },
73
+ update(newText) { currentText = newText; },
45
74
  stop(finalText) {
46
75
  stopped = true;
47
76
  clearInterval(interval);
@@ -51,86 +80,136 @@ function spinner(text = 'thinking', style = 'braille') {
51
80
  };
52
81
  }
53
82
 
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
- ];
83
+ // ── The Exhale: signature brand animation ────────────────
84
+ // This is the first thing you see. It should feel like a breath.
85
+ // Inhale (pause) exhale (particles expand) → settle (logo forms) → calm.
86
+ async function brandReveal(fast = false) {
87
+ if (fast) {
88
+ console.log('');
89
+ console.log(` ${d('😮\u200d💨')} ${d('🤫')}`);
90
+ console.log('');
91
+ console.log(` ${b(cream('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
92
+ console.log(` ${b(cream('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
93
+ console.log('');
94
+ return;
95
+ }
96
+
97
+ process.stdout.write(hide);
98
+
99
+ // Phase 1: The inhale — brief stillness
100
+ console.log('');
101
+ await sleep(200);
102
+
103
+ // Phase 2: The exhale — particles drift outward
104
+ const exhaleStages = [
105
+ ' ·',
106
+ ' · · ·',
107
+ ' · · · ·',
108
+ ' · · · · ·',
109
+ ' · · · · ·',
110
+ ];
111
+
112
+ for (const stage of exhaleStages) {
113
+ process.stdout.write(`${clearLine} ${slate(stage)}`);
114
+ await sleep(70);
115
+ }
116
+
117
+ // Phase 3: Particles converge into the emoji
118
+ await sleep(100);
119
+ process.stdout.write(`${clearLine}`);
120
+ console.log(` ${d('😮\u200d💨')} ${d('🤫')}`);
121
+ console.log('');
122
+ await sleep(150);
66
123
 
67
- if (fast) {
68
- lines.forEach(l => console.log(l));
69
- resolve();
70
- return;
124
+ // Phase 4: Logo wave — each letter block appears left to right
125
+ const logoTop = ['█▀█', '█░█', '█▀▀', '█░█', '█▀', '█░█'];
126
+ const logoBot = ['█▀▀', '█▀█', '██▄', '▀▄▀', '▄█', '█▀█'];
127
+
128
+ let topLine = ' ';
129
+ let botLine = ' ';
130
+
131
+ for (let i = 0; i < logoTop.length; i++) {
132
+ topLine += cream(logoTop[i]) + ' ';
133
+ botLine += cream(logoBot[i]) + ' ';
134
+
135
+ // Overwrite both lines
136
+ if (i === 0) {
137
+ process.stdout.write(` ${b(topLine.trim())}`);
138
+ process.stdout.write('\n');
139
+ process.stdout.write(` ${b(botLine.trim())}`);
140
+ } else {
141
+ process.stdout.write(up(1));
142
+ process.stdout.write(`${clearLine} ${b(topLine.trim())}`);
143
+ process.stdout.write('\n');
144
+ process.stdout.write(`${clearLine} ${b(botLine.trim())}`);
71
145
  }
146
+ await sleep(55);
147
+ }
72
148
 
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
- });
149
+ process.stdout.write('\n');
150
+ await sleep(100);
151
+
152
+ // Phase 5: Tagline fades in — dim → sage → cream
153
+ const tagline = 'relief. quiet execution.';
154
+ process.stdout.write(` ${slate(tagline)}`);
155
+ await sleep(200);
156
+ process.stdout.write(`${clearLine} ${sage(tagline)}`);
157
+ await sleep(200);
158
+ process.stdout.write(`${clearLine} ${teal(tagline)}`);
159
+ await sleep(300);
160
+
161
+ console.log('');
162
+ console.log('');
163
+ process.stdout.write(show);
84
164
  }
85
165
 
86
166
  // ── Status panel ─────────────────────────────────────────
87
- // Draws a bordered status box with labeled rows.
88
167
  function statusPanel(title, rows) {
89
168
  const maxLabel = Math.max(...rows.map(r => r[0].length));
90
169
  console.log('');
91
- console.log(` ${b(w(title))}`);
92
- console.log(` ${g('─'.repeat(48))}`);
170
+ console.log(` ${b(cream(title))}`);
171
+ console.log(` ${slate('─'.repeat(48))}`);
93
172
  for (const [label, value, color] of rows) {
94
173
  const colorFn = color === 'green' ? green
95
- : color === 'yellow' ? yellow
96
- : color === 'cyan' ? cyan
174
+ : color === 'yellow' ? ember
175
+ : color === 'cyan' ? teal
97
176
  : color === 'red' ? red
177
+ : color === 'peach' ? peach
98
178
  : (s) => s;
99
- console.log(` ${g(label.padEnd(maxLabel + 2))} ${colorFn(value)}`);
179
+ console.log(` ${sage(label.padEnd(maxLabel + 2))} ${colorFn(value)}`);
100
180
  }
101
- console.log(` ${g('─'.repeat(48))}`);
181
+ console.log(` ${slate('─'.repeat(48))}`);
102
182
  console.log('');
103
183
  }
104
184
 
105
185
  // ── Interop badge line ───────────────────────────────────
106
- // Shows where phewsh is connected / can connect.
107
186
  function interopLine(config, intentFiles) {
108
187
  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');
188
+ if (intentFiles.length > 0) parts.push(teal('●') + sage(' .intent/'));
189
+ if (config?.apiKey) parts.push(teal('●') + sage(' AI'));
190
+ if (config?.supabaseUserId) parts.push(teal('●') + sage(' cloud'));
112
191
 
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'));
192
+ const available = [
193
+ sage('Claude Code'),
194
+ sage('Cursor'),
195
+ sage('ChatGPT'),
196
+ sage('MCP'),
197
+ ];
119
198
 
120
199
  if (parts.length > 0) {
121
- console.log(` ${g('connected')} ${parts.join(g(' · '))}`);
200
+ console.log(` ${slate('active')} ${parts.join(slate(' · '))}`);
122
201
  }
123
- console.log(` ${g('works with')} ${available.join(g(' · '))}`);
202
+ console.log(` ${slate('works in')} ${available.join(slate(' · '))}`);
124
203
  }
125
204
 
126
205
  // ── Divider ──────────────────────────────────────────────
127
- function divider(char = '·', width = 48) {
128
- console.log(` ${g(char.repeat(width))}`);
206
+ function divider(style = 'line', width = 48) {
207
+ const char = style === 'dots' ? '·' : style === 'fade' ? '░' : '─';
208
+ console.log(` ${slate(char.repeat(width))}`);
129
209
  }
130
210
 
131
211
  // ── Typewriter ───────────────────────────────────────────
132
- // Writes text character by character. Returns a promise.
133
- function typewrite(text, speed = 20) {
212
+ function typewrite(text, speed = 25) {
134
213
  return new Promise((resolve) => {
135
214
  let i = 0;
136
215
  const interval = setInterval(() => {
@@ -146,16 +225,16 @@ function typewrite(text, speed = 20) {
146
225
  });
147
226
  }
148
227
 
149
- // ── Welcome tips (rotates each session) ──────────────────
228
+ // ── Welcome tips ─────────────────────────────────────────
150
229
  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`,
230
+ `${slate('·')} ${sage('Type')} ${cream('/clarify')} ${sage('to turn a messy idea into a structured spec')}`,
231
+ `${slate('·')} ${cream('/gate')} ${sage('sets budget, time, and skill constraints — AI respects them')}`,
232
+ `${slate('·')} ${sage('Run')} ${cream('phewsh watch')} ${sage('in another tab for live sync to CLAUDE.md')}`,
233
+ `${slate('·')} ${cream('/export')} ${sage('creates a portable context file for any AI tool')}`,
234
+ `${slate('·')} ${cream('/model opus')} ${sage('for complex reasoning,')} ${cream('/model haiku')} ${sage('for speed')}`,
235
+ `${slate('·')} ${sage('Your')} ${cream('.intent/')} ${sage('files are plain markdown — edit them anytime')}`,
236
+ `${slate('·')} ${cream('phewsh context --copy')} ${sage('puts your project context on the clipboard')}`,
237
+ `${slate('·')} ${cream('/tour')} ${sage('walks you through everything PHEWSH can do')}`,
159
238
  ];
160
239
 
161
240
  function randomTip() {
@@ -167,86 +246,97 @@ const TOUR_PAGES = [
167
246
  {
168
247
  title: 'What is PHEWSH?',
169
248
  body: [
170
- ` PHEWSH gives your project a ${b('portable identity')}.`,
249
+ '',
250
+ ` ${cream('phew')} ${sage('— the relief of not starting from scratch.')}`,
251
+ ` ${cream('shh')} ${sage('— it just works. No noise.')}`,
252
+ '',
253
+ ` PHEWSH gives your project a ${b(cream('portable identity'))}.`,
171
254
  ` Define what you're building once — every AI tool reads it.`,
172
255
  '',
173
- ` It works ${w('standalone')} as its own AI shell,`,
174
- ` and ${w('inside')} Claude Code, Cursor, ChatGPT, and any MCP agent.`,
256
+ ` It works ${cream('standalone')} as its own AI shell,`,
257
+ ` and ${cream('inside')} Claude Code, Cursor, ChatGPT, and any MCP agent.`,
175
258
  '',
176
- ` ${g('Your project context lives in')} ${cyan('.intent/')} ${g('— plain markdown files.')}`,
177
- ` ${g('You own them. They travel with your code.')}`,
259
+ ` ${sage('Your project context lives in')} ${teal('.intent/')} ${sage('— plain markdown.')}`,
260
+ ` ${sage('You own them. They travel with your code.')}`,
178
261
  ]
179
262
  },
180
263
  {
181
264
  title: 'The .intent/ directory',
182
265
  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
266
  '',
189
- ` ${g('Create these with')} /init ${g('or')} /clarify`,
267
+ ` ${teal('.intent/')}`,
268
+ ` ${peach('vision.md')} ${sage('What this project is and why it exists')}`,
269
+ ` ${peach('plan.md')} ${sage('Strategy, phases, milestones')}`,
270
+ ` ${peach('next.md')} ${sage('Current tasks and what to do right now')}`,
271
+ ` ${ember('gate.json')} ${sage('Your constraints (budget, time, skill)')}`,
272
+ '',
273
+ ` ${sage('Create these with')} ${cream('/init')} ${sage('or')} ${cream('/clarify')}`,
190
274
  ]
191
275
  },
192
276
  {
193
277
  title: 'Standalone mode',
194
278
  body: [
195
- ` When you run ${w('phewsh')} on its own, you get an AI shell`,
196
- ` that knows your project inside and out.`,
197
279
  '',
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`,
280
+ ` Run ${cream('phewsh')} and type naturally.`,
281
+ ` Every message carries your project's full context.`,
282
+ '',
283
+ ` ${teal('phewsh')} ${sage('>')} what should I focus on today?`,
284
+ ` ${teal('phewsh')} ${sage('>')} is my plan realistic given my budget?`,
285
+ ` ${teal('phewsh')} ${sage('>')} break this feature into tasks`,
202
286
  '',
203
- ` ${g('Every message includes your vision, plan, and constraints.')}`,
287
+ ` ${sage('The AI doesn\'t need a warmup. It already knows.')}`,
204
288
  ]
205
289
  },
206
290
  {
207
291
  title: 'Inside other tools',
208
292
  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
293
  '',
214
- ` ${g('Same project identity, every tool. No re-explaining.')}`,
294
+ ` ${b(cream('Claude Code'))} ${sage('phewsh watch auto-updates CLAUDE.md')}`,
295
+ ` ${b(cream('Cursor'))} ${sage('phewsh context --file → .phewsh.context')}`,
296
+ ` ${b(cream('ChatGPT'))} ${sage('phewsh context --copy → paste in')}`,
297
+ ` ${b(cream('MCP agents'))} ${sage('phewsh mcp setup → agents self-brief')}`,
298
+ '',
299
+ ` ${sage('Same identity. Every tool. No re-explaining.')}`,
300
+ ` ${sage('Switch tools mid-thought. Nothing lost.')}`,
215
301
  ]
216
302
  },
217
303
  {
218
304
  title: 'The Decision Gate',
219
305
  body: [
220
- ` Before you build, decide ${b('whether')} to build.`,
306
+ '',
307
+ ` Before you build, decide ${b(cream('whether'))} to build.`,
221
308
  ` The gate captures what you can actually spend:`,
222
309
  '',
223
- ` ${g('Budget')} ${w('$50')} ${g('Skill')} ${w('expert')}`,
224
- ` ${g('Time')} ${w('15 hrs/week')} ${g('Urgency')} ${w('high')}`,
310
+ ` ${sage('Budget')} ${cream('$50')} ${sage('Skill')} ${cream('expert')}`,
311
+ ` ${sage('Time')} ${cream('15 hrs/week')} ${sage('Urgency')} ${cream('high')}`,
225
312
  '',
226
- ` ${g('These constraints shape every AI response.')}`,
227
- ` ${g('Run')} /gate activate ${g('to set yours.')}`,
313
+ ` ${sage('These constraints shape every AI response.')}`,
314
+ ` ${sage('Run')} ${cream('/gate activate')} ${sage('to set yours.')}`,
228
315
  ]
229
316
  },
230
317
  {
231
318
  title: 'You\'re ready',
232
319
  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
320
  '',
238
- ` ${g('PHEWSH is your project\'s home base.')}`,
239
- ` ${g('The AI that knows you doesn\'t need to be re-taught.')}`,
321
+ ` ${teal('●')} Type naturally to chat with your project context`,
322
+ ` ${teal('●')} ${cream('/init')} or ${cream('/clarify')} to create .intent/ artifacts`,
323
+ ` ${teal('●')} ${cream('/gate')} to set your constraints`,
324
+ ` ${teal('●')} ${cream('/help')} for all commands`,
325
+ '',
326
+ ` ${sage('PHEWSH is your project\'s home base.')}`,
327
+ ` ${sage('The exhale before execution.')}`,
240
328
  ]
241
329
  },
242
330
  ];
243
331
 
244
332
  module.exports = {
245
- // Colors
333
+ // Brand palette
334
+ teal, peach, sage, slate, cream, ember,
335
+ // Standard ANSI
246
336
  b, d, w, g, green, cyan, yellow, magenta, blue, red,
247
337
  // Components
248
338
  spinner, brandReveal, statusPanel, interopLine, divider, typewrite,
249
339
  randomTip, TOUR_PAGES,
250
340
  // ANSI helpers
251
- hide, show, up, clearLine,
341
+ hide, show, up, clearLine, sleep,
252
342
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.11.11",
3
+ "version": "0.11.12",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"