phewsh 0.11.9 → 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,23 +210,20 @@ 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.')}`);
219
220
  console.log('');
220
- console.log(` ${w('To get started, you need an API key from one of these:')}`);
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.')}`);
221
223
  console.log('');
222
224
  console.log(` ${cyan('1')} ${b('Anthropic')} ${g('(recommended)')}`);
223
225
  console.log(` ${g('console.anthropic.com/settings/keys')}`);
224
- console.log(` ${g('Direct access to Claude. Best quality.')}`);
226
+ console.log(` ${g('Direct access to Claude. Best quality. ~$0.01/message.')}`);
225
227
  console.log('');
226
228
  console.log(` ${cyan('2')} ${b('OpenRouter')}`);
227
229
  console.log(` ${g('openrouter.ai/keys')}`);
@@ -229,6 +231,7 @@ async function main() {
229
231
  console.log('');
230
232
  console.log(` ${g('Got a key? Type')} /key ${g('to paste it in.')}`);
231
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).')}`);
232
235
  console.log('');
233
236
  } else if (!config.apiKey.startsWith('sk-')) {
234
237
  console.log(` ${yellow('!')} Stored API key looks invalid.`);
@@ -237,6 +240,7 @@ async function main() {
237
240
  config.apiKey = null; // Don't try to use a bad key
238
241
  }
239
242
 
243
+ // ── Project status ─────────────────────────────────────
240
244
  if (intentFiles.length > 0) {
241
245
  console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
242
246
  } else {
@@ -248,6 +252,9 @@ async function main() {
248
252
  console.log(` ${g(' user:')} ${config.email}`);
249
253
  }
250
254
 
255
+ // ── Interop line ───────────────────────────────────────
256
+ ui.interopLine(config, intentFiles);
257
+
251
258
  // Sync status check (non-blocking, 3s timeout)
252
259
  if (config?.supabaseUserId && intentFiles.length > 0) {
253
260
  const syncResult = await Promise.race([
@@ -268,17 +275,20 @@ async function main() {
268
275
  }
269
276
 
270
277
  console.log('');
278
+ ui.divider('─');
271
279
  if (!config?.apiKey) {
272
- 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')}`);
273
281
  } else {
282
+ console.log(` ${ui.randomTip()}`);
274
283
  console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
275
284
  }
285
+ ui.divider('─');
276
286
  console.log('');
277
287
 
278
288
  const rl = readline.createInterface({
279
289
  input: process.stdin,
280
290
  output: process.stdout,
281
- prompt: ` ${green('>')} `,
291
+ prompt: ` ${cyan('phewsh')} ${green('>')} `,
282
292
  historySize: 100,
283
293
  });
284
294
 
@@ -300,47 +310,77 @@ async function main() {
300
310
 
301
311
  if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
302
312
  const turns = messages.length / 2;
303
- 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('');
304
318
  process.exit(0);
305
319
  }
306
320
 
307
321
  if (cmd === 'help' || cmd === 'h') {
308
322
  console.log(`
309
- ${b('Session commands')}
323
+ ${b(w('Session commands'))}
310
324
 
311
325
  ${w('conversation')}
312
- ${g('/clear')} Clear conversation history
313
- ${g('/run')} ${d('<prompt>')} One-shot prompt (doesn't add to conversation)
314
- ${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
315
329
 
316
330
  ${w('project')}
317
- ${g('/init')} Create .intent/ artifacts in this directory
318
- ${g('/clarify')} AI-assisted artifact generation
319
- ${g('/gate')} Declare operational constraints (budget, time, skill)
320
- ${g('/export')} Export portable context for other AI tools
321
- ${g('/context')} Show loaded .intent/ files
322
- ${g('/reload')} Reload .intent/ context from disk
323
- ${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
324
338
 
325
339
  ${w('sync')}
326
- ${g('/push')} Push .intent/ to cloud
327
- ${g('/pull')} Pull .intent/ from cloud (reloads context)
328
- ${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
329
343
 
330
344
  ${w('configuration')}
331
- ${g('/login')} Set up identity + cloud sync
332
- ${g('/key')} Set or update your API key
333
- ${g('/model')} ${d('<name>')} Switch model (sonnet, opus, haiku)
334
- ${g('/models')} List available models
335
- ${g('/provider')} Show current provider info
336
-
337
- ${w('debug')}
338
- ${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
339
355
  `);
340
356
  rl.prompt();
341
357
  return;
342
358
  }
343
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
+
344
384
  if (cmd === 'clear') {
345
385
  messages.length = 0;
346
386
  console.log(` ${g('conversation cleared')}`);
@@ -350,8 +390,11 @@ async function main() {
350
390
 
351
391
  if (cmd === 'context') {
352
392
  if (intentFiles.length > 0) {
353
- console.log(`\n Loaded from ${cyan('.intent/')}:`);
354
- 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('─');
355
398
  } else {
356
399
  console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
357
400
  console.log(` ${g('Run')} /init ${g('to create one')}`);
@@ -364,16 +407,16 @@ async function main() {
364
407
  if (cmd === 'status') {
365
408
  const turns = messages.length / 2;
366
409
  config = loadConfig(); // refresh
367
- console.log(`\n ${b('Session')}`);
368
- console.log(` Turns ${turns}`);
369
- console.log(` Tokens ${totalPromptTokens}→${totalCompletionTokens} (in→out)`);
370
- console.log(` Project ${projectName}`);
371
- console.log(` Context ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none'}`);
372
- console.log(` Model ${MODELS[currentModel].name}`);
373
- console.log(` Provider ${MODELS[currentModel].provider}`);
374
- if (config?.email) console.log(` User ${config.email}`);
375
- console.log(` API key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
376
- 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
+ ]);
377
420
  rl.prompt();
378
421
  return;
379
422
  }
@@ -418,7 +461,7 @@ async function main() {
418
461
 
419
462
  if (cmd === 'clarify') {
420
463
  if (!config?.apiKey) {
421
- 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`);
422
465
  rl.prompt();
423
466
  return;
424
467
  }
@@ -462,7 +505,7 @@ async function main() {
462
505
  if (content) {
463
506
  const outPath = path.join(process.cwd(), '.phewsh.context');
464
507
  fs.writeFileSync(outPath, content);
465
- console.log(`\n ${green('')} Written to ${outPath}\n`);
508
+ console.log(`\n ${green('')} Written to ${outPath}\n`);
466
509
  } else {
467
510
  console.log(`\n ${g('No artifacts to export')}\n`);
468
511
  }
@@ -475,16 +518,16 @@ async function main() {
475
518
 
476
519
  if (cmd === 'push') {
477
520
  if (!config?.supabaseUserId) {
478
- console.log(`\n ${yellow('')} Not logged in. Run /login first.\n`);
521
+ console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
479
522
  rl.prompt();
480
523
  return;
481
524
  }
482
525
  try {
483
526
  const token = await ensureValidToken(config);
484
- 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; }
485
528
  await push(config, token);
486
529
  } catch (err) {
487
- console.error(` ${yellow('')} Push failed: ${err.message}\n`);
530
+ console.error(` ${yellow('!')} Push failed: ${err.message}\n`);
488
531
  }
489
532
  rl.prompt();
490
533
  return;
@@ -492,13 +535,13 @@ async function main() {
492
535
 
493
536
  if (cmd === 'pull') {
494
537
  if (!config?.supabaseUserId) {
495
- console.log(`\n ${yellow('')} Not logged in. Run /login first.\n`);
538
+ console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
496
539
  rl.prompt();
497
540
  return;
498
541
  }
499
542
  try {
500
543
  const token = await ensureValidToken(config);
501
- 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; }
502
545
  await pull(config, token);
503
546
  // Reload context after pull
504
547
  intentFiles = loadIntentContext();
@@ -507,7 +550,7 @@ async function main() {
507
550
  console.log(` ${green('●')} Context reloaded: ${intentFiles.map(f => f.file).join(', ')}`);
508
551
  }
509
552
  } catch (err) {
510
- console.error(` ${yellow('')} Pull failed: ${err.message}\n`);
553
+ console.error(` ${yellow('!')} Pull failed: ${err.message}\n`);
511
554
  }
512
555
  console.log('');
513
556
  rl.prompt();
@@ -517,22 +560,24 @@ async function main() {
517
560
  if (cmd === 'sync') {
518
561
  // Show sync status
519
562
  if (!config?.supabaseUserId) {
520
- console.log(`\n ${yellow('')} Not logged in. Run /login first.\n`);
563
+ console.log(`\n ${yellow('!')} Not logged in. Run /login first.\n`);
521
564
  rl.prompt();
522
565
  return;
523
566
  }
567
+ const syncSpin = ui.spinner('checking sync');
524
568
  const syncResult = await checkSyncStatus(config);
525
569
  if (!syncResult) {
526
- console.log(`\n ${g('Could not check sync status')}\n`);
570
+ syncSpin.stop(`${g('Could not check sync status')}`);
527
571
  } else if (syncResult.status === 'cloud-newer') {
528
- console.log(`\n ${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull\n`);
572
+ syncSpin.stop(`${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull`);
529
573
  } else if (syncResult.status === 'local-newer') {
530
- 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`);
531
575
  } else if (syncResult.status === 'synced') {
532
- console.log(`\n ${green('↕')} In sync\n`);
576
+ syncSpin.stop(`${green('↕')} In sync`);
533
577
  } else if (syncResult.status === 'local-only') {
534
- 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')}`);
535
579
  }
580
+ console.log('');
536
581
  rl.prompt();
537
582
  return;
538
583
  }
@@ -573,7 +618,9 @@ async function main() {
573
618
  return;
574
619
  }
575
620
  console.log('');
576
- 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('─');
577
624
  console.log('');
578
625
  console.log(` ${cyan('Anthropic')} ${g('(recommended)')}`);
579
626
  console.log(` ${g('1.')} Go to ${w('console.anthropic.com/settings/keys')}`);
@@ -583,6 +630,9 @@ async function main() {
583
630
  console.log(` ${g('1.')} Go to ${w('openrouter.ai/keys')}`);
584
631
  console.log(` ${g('2.')} Create key → copy it (starts with sk-or-)`)
585
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('');
586
636
  const keyRl = readline.createInterface({ input: process.stdin, output: process.stdout });
587
637
  keyRl.question(` Paste your API key\n > `, (apiKey) => {
588
638
  keyRl.close();
@@ -603,7 +653,10 @@ async function main() {
603
653
  }
604
654
 
605
655
  if (cmd === 'models') {
606
- console.log(`\n ${b('Available models')}\n`);
656
+ console.log('');
657
+ ui.divider('─');
658
+ console.log(` ${b(w('Available models'))}`);
659
+ ui.divider('─');
607
660
  for (const [key, model] of Object.entries(MODELS)) {
608
661
  const active = key === currentModel ? ` ${green('●')}` : '';
609
662
  console.log(` ${w(key.padEnd(16))} ${g(model.name)}${active}`);
@@ -637,12 +690,38 @@ async function main() {
637
690
 
638
691
  if (cmd === 'provider') {
639
692
  const model = MODELS[currentModel];
640
- console.log(`\n ${b('Provider')}`);
641
- console.log(` API Anthropic (direct)`);
642
- console.log(` Model ${model.name}`);
643
- console.log(` Endpoint api.anthropic.com/v1/messages`);
644
- console.log(` Key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
645
- 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
+ ]);
699
+ rl.prompt();
700
+ return;
701
+ }
702
+
703
+ if (cmd === 'update' || cmd === 'upgrade') {
704
+ const updateSpin = ui.spinner('checking for updates');
705
+ try {
706
+ const pkg = require('../../package.json');
707
+ const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(5000) });
708
+ const data = await res.json();
709
+ if (!data.version || data.version === pkg.version) {
710
+ updateSpin.stop(`${green('●')} Already on the latest version (${pkg.version})`);
711
+ console.log('');
712
+ rl.prompt();
713
+ return;
714
+ }
715
+ updateSpin.stop(`${cyan(pkg.version)} → ${cyan(data.version)}`);
716
+ console.log(` ${g('Installing...')}\n`);
717
+ const { execSync } = require('child_process');
718
+ 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`);
721
+ } catch (err) {
722
+ updateSpin.stop(`${yellow('!')} Update failed: ${err.message}`);
723
+ console.log(` ${g('You can update manually:')} npm install -g phewsh\n`);
724
+ }
646
725
  rl.prompt();
647
726
  return;
648
727
  }
@@ -654,7 +733,7 @@ async function main() {
654
733
  return;
655
734
  }
656
735
  if (!config?.apiKey) {
657
- console.log(` ${yellow('')} No API key. Run /key to set one.`);
736
+ console.log(` ${yellow('!')} No API key. Run /key to set one.`);
658
737
  rl.prompt();
659
738
  return;
660
739
  }
@@ -700,6 +779,7 @@ async function main() {
700
779
  console.log('');
701
780
  console.log(` ${g('Get a key:')} ${cyan('console.anthropic.com/settings/keys')}`);
702
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)')}`);
703
783
  console.log('');
704
784
  rl.prompt();
705
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.9",
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"