phewsh 0.14.0 โ†’ 0.14.2

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
@@ -7,7 +7,7 @@ const command = args[0];
7
7
  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
- const g = (s) => `\x1b[38;2;152;164;158m${s}\x1b[0m`; // slate (matches ui.js)
10
+ const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`; // slate, 256-color (matches ui.js โ€” 24-bit breaks Apple Terminal)
11
11
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
12
12
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
13
13
 
@@ -26,7 +26,7 @@ function showBrand() {
26
26
  } catch { /* no config */ }
27
27
 
28
28
  console.log('');
29
- console.log(` ${d('๐Ÿ˜ฎ\u200d๐Ÿ’จ')} ${d('๐Ÿคซ')}`);
29
+ console.log(' ๐Ÿ˜ฎ\u200d๐Ÿ’จ ๐Ÿคซ');
30
30
  console.log('');
31
31
  console.log(` ${b(w('โ–ˆโ–€โ–ˆ โ–ˆโ–‘โ–ˆ โ–ˆโ–€โ–€ โ–ˆโ–‘โ–ˆ โ–ˆโ–€ โ–ˆโ–‘โ–ˆ'))}`);
32
32
  console.log(` ${b(w('โ–ˆโ–€โ–€ โ–ˆโ–€โ–ˆ โ–ˆโ–ˆโ–„ โ–€โ–„โ–€ โ–„โ–ˆ โ–ˆโ–€โ–ˆ'))}`);
@@ -21,7 +21,7 @@ const { gatherReceipts } = require('../lib/receipts-data');
21
21
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
22
22
  const d = (s) => `\x1b[2m${s}\x1b[0m`;
23
23
  const w = (s) => `\x1b[97m${s}\x1b[0m`;
24
- const g = (s) => `\x1b[38;2;130;142;138m${s}\x1b[0m`;
24
+ const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
25
25
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
26
26
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
27
27
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
@@ -11,7 +11,8 @@ const ui = require('../lib/ui');
11
11
 
12
12
  const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
13
13
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
14
- const INTENT_DIR = path.join(process.cwd(), '.intent');
14
+ // Dynamic โ€” the session can chdir into a project from the root bootstrap
15
+ const intentDir = () => path.join(process.cwd(), '.intent');
15
16
 
16
17
  const { select, refreshSession: refreshSess } = require('../lib/supabase');
17
18
  const { readPPS } = require('../lib/pps');
@@ -19,6 +20,7 @@ const { push, pull, ensureValidToken } = require('./sync');
19
20
  const { HARNESSES, listHarnesses, runViaHarness } = require('../lib/harnesses');
20
21
  const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
21
22
  const { recordSessionEvent } = require('../lib/receipts-data');
23
+ const { recordProject, listProjects, scanForProjects, fmtAgo } = require('../lib/projects-index');
22
24
 
23
25
  // Brand palette shortcuts
24
26
  const { b, d, w, g, green, cyan, yellow,
@@ -27,13 +29,13 @@ const { b, d, w, g, green, cyan, yellow,
27
29
  // Sync awareness: compare local .intent/ timestamps with cloud updated_at
28
30
  async function checkSyncStatus(config) {
29
31
  if (!config?.supabaseUserId || !config?.supabaseAccessToken) return null;
30
- if (!fs.existsSync(INTENT_DIR)) return null;
32
+ if (!fs.existsSync(intentDir())) return null;
31
33
 
32
34
  try {
33
35
  const token = await ensureValidToken(config);
34
36
  if (!token) return null;
35
37
 
36
- const pps = readPPS(INTENT_DIR);
38
+ const pps = readPPS(intentDir());
37
39
  const cloudId = pps?.adapters?.phewsh?.cloud_id;
38
40
  const projectName = path.basename(process.cwd());
39
41
 
@@ -59,7 +61,7 @@ async function checkSyncStatus(config) {
59
61
  const localFiles = ['vision.md', 'plan.md', 'next.md'];
60
62
  let latestLocal = 0;
61
63
  for (const file of localFiles) {
62
- const filePath = path.join(INTENT_DIR, file);
64
+ const filePath = path.join(intentDir(), file);
63
65
  if (fs.existsSync(filePath)) {
64
66
  const mtime = fs.statSync(filePath).mtimeMs;
65
67
  if (mtime > latestLocal) latestLocal = mtime;
@@ -154,7 +156,7 @@ function loadIntentContext() {
154
156
  const files = ['vision.md', 'plan.md', 'next.md'];
155
157
  const loaded = [];
156
158
  for (const file of files) {
157
- const p = path.join(INTENT_DIR, file);
159
+ const p = path.join(intentDir(), file);
158
160
  if (fs.existsSync(p)) {
159
161
  loaded.push({ file, content: fs.readFileSync(p, 'utf-8') });
160
162
  }
@@ -255,7 +257,12 @@ async function main() {
255
257
  let intentFiles = loadIntentContext();
256
258
  let systemPrompt = buildSystemPrompt(intentFiles);
257
259
  const messages = [];
258
- const projectName = path.basename(process.cwd());
260
+ let projectName = path.basename(process.cwd());
261
+
262
+ // Index this project so `phewsh` from anywhere can offer it as a recent
263
+ if (intentFiles.length > 0) {
264
+ try { recordProject(process.cwd()); } catch { /* index is best-effort */ }
265
+ }
259
266
  let currentModel = DEFAULT_MODEL;
260
267
  let totalPromptTokens = 0;
261
268
  let totalCompletionTokens = 0;
@@ -267,6 +274,7 @@ async function main() {
267
274
  let sessionMode = null; // INTENT_MODES id once picked
268
275
  let awaitingOutcome = null; // decision id eligible for 1-4 labeling
269
276
  let awaitingFallback = null; // { input, fullSystem, options } after a route failure
277
+ let bootstrapChoices = null; // root-bootstrap menu entries when no project here
270
278
  let decisionsThisSession = 0;
271
279
 
272
280
  // โ”€โ”€ The Exhale: animated brand reveal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -292,9 +300,26 @@ async function main() {
292
300
 
293
301
  const row = (label, value) => console.log(` ${slate(label.padEnd(9))}${value}`);
294
302
 
295
- row('PROJECT', cream(projectName) + (intentFiles.length > 0
296
- ? slate(' ยท ') + teal('โ—') + sage(` .intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''}`)
297
- : slate(' ยท no memory yet โ€” ') + sage('/init')));
303
+ // realpath both sides โ€” macOS /tmp and /var are symlinks into /private
304
+ const realPath = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
305
+ const tildify = (p) => {
306
+ const home = realPath(os.homedir());
307
+ const rp = realPath(p);
308
+ return rp.startsWith(home) ? '~' + rp.slice(home.length) : p;
309
+ };
310
+ const atHome = realPath(process.cwd()) === realPath(os.homedir());
311
+ const recents = intentFiles.length === 0
312
+ ? listProjects().filter(p => realPath(p.path) !== realPath(process.cwd())).slice(0, 3)
313
+ : [];
314
+
315
+ if (intentFiles.length > 0) {
316
+ row('PROJECT', cream(projectName) + slate(' ยท ') + teal('โ—')
317
+ + sage(` .intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''}`));
318
+ } else if (atHome || recents.length > 0) {
319
+ row('PROJECT', slate('none here โ€” your projects are listed below'));
320
+ } else {
321
+ row('PROJECT', cream(projectName) + slate(' ยท no memory yet โ€” ') + sage('/init'));
322
+ }
298
323
 
299
324
  row('ROUTE', route
300
325
  ? cream(routeLabel(route, config)) + (route.type === 'harness' ? slate(' ยท no API key needed') : '')
@@ -310,11 +335,29 @@ async function main() {
310
335
  : slate('none โ€” install Codex or Gemini to cover usage limits'));
311
336
 
312
337
  if (config?.supabaseUserId) {
338
+ // Cloud project count, best-effort with a hard 2s budget โ€” the cockpit
339
+ // never blocks on the network.
340
+ let cloudCount = null;
341
+ try {
342
+ const token = await Promise.race([
343
+ ensureValidToken(config),
344
+ new Promise(r => setTimeout(() => r(null), 2000)),
345
+ ]);
346
+ if (token) {
347
+ const rows = await Promise.race([
348
+ select('projects', `user_id=eq.${config.supabaseUserId}&select=id`, token),
349
+ new Promise(r => setTimeout(() => r(null), 2000)),
350
+ ]);
351
+ if (Array.isArray(rows)) cloudCount = rows.length;
352
+ }
353
+ } catch { /* offline โ€” cockpit still renders */ }
354
+
313
355
  const syncLabel = syncState?.status === 'synced' ? teal('โ†• ') + sage('mirrored')
314
356
  : syncState?.status === 'cloud-newer' ? ember('โ†“ ') + sage(`cloud newer (${syncState.ago}) โ€” /pull`)
315
357
  : syncState?.status === 'local-newer' ? ember('โ†‘ ') + sage('local ahead โ€” /push')
316
358
  : sage('linked');
317
- row('WEB', cream(config.email || 'logged in') + slate(' ยท ') + syncLabel);
359
+ row('WEB', cream(config.email || 'logged in') + slate(' ยท ') + syncLabel
360
+ + (cloudCount !== null ? slate(' ยท ') + sage(`${cloudCount} cloud project${cloudCount !== 1 ? 's' : ''}`) : ''));
318
361
  } else {
319
362
  row('WEB', sage('local-only (works fine)') + slate(' ยท /login mirrors this at phewsh.com/intent'));
320
363
  }
@@ -329,6 +372,45 @@ async function main() {
329
372
  row('RECORD', slate('empty โ€” decisions and outcomes accumulate as you work'));
330
373
  }
331
374
 
375
+ function showModeMenu() {
376
+ console.log(` ${b(cream('What are you trying to do?'))}`);
377
+ console.log(` ${teal('1')} ${sage('Build')} ${slate('ยท')} ${teal('2')} ${sage('Research')} ${slate('ยท')} ${teal('3')} ${sage('Decide')} ${slate('ยท')} ${teal('4')} ${sage('Review')} ${slate('ยท')} ${teal('5')} ${sage('Ask another model')}`);
378
+ console.log(` ${slate('pick a number, or just type โ€” your context travels with every route')}`);
379
+ }
380
+
381
+ // Open a known project from the bootstrap menu: chdir, reload memory,
382
+ // back to the normal flow. The session is the cockpit; projects swap in.
383
+ function openProjectAt(dir) {
384
+ try { process.chdir(dir); } catch (err) {
385
+ console.log(` ${ember('!')} ${sage('Could not open ' + dir + ': ' + err.message)}`);
386
+ return;
387
+ }
388
+ projectName = path.basename(dir);
389
+ intentFiles = loadIntentContext();
390
+ systemPrompt = buildSystemPrompt(intentFiles);
391
+ try { recordProject(dir); } catch { /* best-effort */ }
392
+ bootstrapChoices = null;
393
+ console.log('');
394
+ console.log(` ${teal('โ—')} ${cream(projectName)} ${slate('ยท')} ${sage(`.intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''} loaded`)} ${slate('ยท via ' + routeLabel(route, config))}`);
395
+ console.log('');
396
+ showModeMenu();
397
+ console.log('');
398
+ }
399
+
400
+ function showBootstrapMenu(projects) {
401
+ console.log(` ${b(cream('Where do you want to work?'))}`);
402
+ bootstrapChoices = [];
403
+ for (const p of projects) {
404
+ bootstrapChoices.push({ kind: 'open', path: p.path });
405
+ console.log(` ${teal(String(bootstrapChoices.length))} ${cream(p.name)} ${slate('โ€” ' + fmtAgo(p.lastOpened) + ' ยท ' + tildify(p.path))}`);
406
+ }
407
+ bootstrapChoices.push({ kind: 'init' });
408
+ const initN = bootstrapChoices.length;
409
+ bootstrapChoices.push({ kind: 'scan' });
410
+ console.log(` ${teal(String(initN))} ${sage('Start a project here')} ${slate('(/init)')} ${slate('ยท')} ${teal(String(initN + 1))} ${sage('Scan my folders for projects')}`);
411
+ console.log(` ${slate('pick a number, or just type to chat โ€” no project required')}`);
412
+ }
413
+
332
414
  console.log('');
333
415
  if (!route) {
334
416
  // Nothing to route through: no key, no agent CLIs found on this machine.
@@ -340,10 +422,10 @@ async function main() {
340
422
  console.log(` ${teal('/key')} ${sage('Set an API key (10 seconds)')}`);
341
423
  console.log(` ${teal('/tour')} ${sage('See what this does (nothing needed)')}`);
342
424
  console.log(` ${slate('Or install Claude Code / Codex โ€” phewsh uses their login automatically.')}`);
425
+ } else if (intentFiles.length === 0 && (atHome || recents.length > 0)) {
426
+ showBootstrapMenu(recents);
343
427
  } else {
344
- console.log(` ${b(cream('What are you trying to do?'))}`);
345
- console.log(` ${teal('1')} ${sage('Build')} ${slate('ยท')} ${teal('2')} ${sage('Research')} ${slate('ยท')} ${teal('3')} ${sage('Decide')} ${slate('ยท')} ${teal('4')} ${sage('Review')} ${slate('ยท')} ${teal('5')} ${sage('Ask another model')}`);
346
- console.log(` ${slate('pick a number, or just type โ€” your context travels with every route')}`);
428
+ showModeMenu();
347
429
  }
348
430
  console.log('');
349
431
 
@@ -493,6 +575,59 @@ async function main() {
493
575
  return;
494
576
  }
495
577
 
578
+ // Root bootstrap: a bare number opens a project, inits, or scans
579
+ if (bootstrapChoices && messages.length === 0 && /^[0-9]{1,2}$/.test(input)) {
580
+ const choice = bootstrapChoices[parseInt(input, 10) - 1];
581
+ if (!choice) {
582
+ console.log(` ${sage('Pick 1-' + bootstrapChoices.length)}`);
583
+ rl.prompt();
584
+ return;
585
+ }
586
+ if (choice.kind === 'open') {
587
+ openProjectAt(choice.path);
588
+ rl.prompt();
589
+ return;
590
+ }
591
+ if (choice.kind === 'init') {
592
+ bootstrapChoices = null;
593
+ try {
594
+ const { execSync } = require('child_process');
595
+ execSync('node ' + path.join(__dirname, 'intent.js') + ' --init', { stdio: 'inherit' });
596
+ intentFiles = loadIntentContext();
597
+ systemPrompt = buildSystemPrompt(intentFiles);
598
+ if (intentFiles.length > 0) {
599
+ try { recordProject(process.cwd()); } catch { /* best-effort */ }
600
+ console.log(` ${teal('โ—')} ${sage('Project started โ€” context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
601
+ }
602
+ } catch (err) {
603
+ console.error(` ${ember('!')} ${sage('Init failed:')} ${err.message}`);
604
+ }
605
+ console.log('');
606
+ rl.prompt();
607
+ return;
608
+ }
609
+ if (choice.kind === 'scan') {
610
+ const spin = ui.spinner('scanning your usual folders');
611
+ const found = scanForProjects();
612
+ spin.stop();
613
+ if (found.length === 0) {
614
+ bootstrapChoices = null;
615
+ console.log(` ${sage('No .intent/ projects found in the usual folders.')}`);
616
+ console.log(` ${slate('cd into a project and run phewsh, or /init to start one here.')}`);
617
+ } else {
618
+ console.log(` ${teal('โ—')} ${sage(`Found ${found.length} project${found.length !== 1 ? 's' : ''}:`)}`);
619
+ bootstrapChoices = found.map(p => ({ kind: 'open', path: p.path }));
620
+ found.forEach((p, i) => {
621
+ console.log(` ${teal(String(i + 1))} ${cream(p.name)} ${slate('ยท ' + tildify(p.path))}`);
622
+ });
623
+ console.log(` ${slate('pick a number to open it')}`);
624
+ }
625
+ console.log('');
626
+ rl.prompt();
627
+ return;
628
+ }
629
+ }
630
+
496
631
  // A bare 1-5 on an empty conversation picks an intent mode
497
632
  if (messages.length === 0 && !awaitingOutcome && /^[1-5]$/.test(input)) {
498
633
  const n = parseInt(input, 10);
@@ -669,7 +804,7 @@ async function main() {
669
804
  }
670
805
 
671
806
  if (cmd === 'init') {
672
- if (fs.existsSync(path.join(INTENT_DIR, 'vision.md'))) {
807
+ if (fs.existsSync(path.join(intentDir(), 'vision.md'))) {
673
808
  console.log(`\n ${sage('.intent/ already exists in')} ${slate(process.cwd())}`);
674
809
  console.log(` ${sage('Use /reload to refresh context')}\n`);
675
810
  } else {
@@ -1053,7 +1188,7 @@ async function main() {
1053
1188
  }
1054
1189
 
1055
1190
  if (cmd === 'watch') {
1056
- if (!fs.existsSync(INTENT_DIR)) {
1191
+ if (!fs.existsSync(intentDir())) {
1057
1192
  console.log(`\n ${ember('!')} ${sage('No .intent/ found. Run /init first.')}\n`);
1058
1193
  rl.prompt();
1059
1194
  return;
@@ -8,7 +8,7 @@ const { spawn } = require('child_process');
8
8
 
9
9
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
10
10
  const w = (s) => `\x1b[97m${s}\x1b[0m`;
11
- const g = (s) => `\x1b[38;2;130;142;138m${s}\x1b[0m`;
11
+ const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
12
12
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
13
13
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
14
14
 
@@ -0,0 +1,90 @@
1
+ // Local project index โ€” how phewsh knows your projects from anywhere.
2
+ //
3
+ // Every session opened in a project (or created via /init) records it here,
4
+ // so running `phewsh` at machine root becomes mission-control bootstrap
5
+ // ("where do you want to work?") instead of "no project found, goodbye."
6
+ //
7
+ // Storage: ~/.phewsh/projects.json. Local-first; web sync layers on top.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ const INDEX_FILE = path.join(os.homedir(), '.phewsh', 'projects.json');
14
+
15
+ // Shallow-scanned roots when the user asks to find projects. One level deep,
16
+ // opt-in only โ€” deep-scanning someone's machine uninvited is invasive.
17
+ const SCAN_ROOTS = [
18
+ path.join(os.homedir(), 'Documents', 'GitHub'),
19
+ path.join(os.homedir(), 'Projects'),
20
+ path.join(os.homedir(), 'projects'),
21
+ path.join(os.homedir(), 'repos'),
22
+ path.join(os.homedir(), 'Developer'),
23
+ path.join(os.homedir(), 'code'),
24
+ ];
25
+
26
+ function load() {
27
+ try { return JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8')); } catch { return { projects: {} }; }
28
+ }
29
+
30
+ function save(index) {
31
+ fs.mkdirSync(path.dirname(INDEX_FILE), { recursive: true });
32
+ fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2));
33
+ }
34
+
35
+ /** Upsert the project at `dir` (called whenever a session opens one). */
36
+ function recordProject(dir, extra = {}) {
37
+ const index = load();
38
+ const key = path.resolve(dir);
39
+ index.projects[key] = {
40
+ ...(index.projects[key] || {}),
41
+ name: path.basename(key),
42
+ path: key,
43
+ lastOpened: new Date().toISOString(),
44
+ ...extra,
45
+ };
46
+ save(index);
47
+ }
48
+
49
+ /** Known projects, most recently opened first. Prunes paths that vanished. */
50
+ function listProjects() {
51
+ const index = load();
52
+ const alive = Object.values(index.projects).filter(p => {
53
+ try { return fs.existsSync(path.join(p.path, '.intent')); } catch { return false; }
54
+ });
55
+ return alive.sort((a, b) => String(b.lastOpened).localeCompare(String(a.lastOpened)));
56
+ }
57
+
58
+ /** Shallow scan: direct children of common roots that contain .intent/. */
59
+ function scanForProjects() {
60
+ const found = [];
61
+ const seen = new Set(); // realpath-dedupe โ€” case-insensitive FS makes ~/Projects and ~/projects one dir
62
+ for (const root of SCAN_ROOTS) {
63
+ let entries;
64
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
65
+ for (const e of entries) {
66
+ if (!e.isDirectory() || e.name.startsWith('.')) continue;
67
+ const dir = path.join(root, e.name);
68
+ try {
69
+ if (fs.existsSync(path.join(dir, '.intent', 'vision.md'))) {
70
+ const real = fs.realpathSync(dir);
71
+ if (seen.has(real)) continue;
72
+ seen.add(real);
73
+ found.push({ name: e.name, path: real });
74
+ }
75
+ } catch { /* unreadable dir โ€” skip */ }
76
+ }
77
+ }
78
+ return found;
79
+ }
80
+
81
+ function fmtAgo(ts) {
82
+ if (!ts) return '';
83
+ const mins = Math.floor((Date.now() - new Date(ts).getTime()) / 60000);
84
+ if (mins < 60) return `${mins}m ago`;
85
+ const hrs = Math.floor(mins / 60);
86
+ if (hrs < 24) return `${hrs}h ago`;
87
+ return `${Math.floor(hrs / 24)}d ago`;
88
+ }
89
+
90
+ module.exports = { INDEX_FILE, SCAN_ROOTS, recordProject, listProjects, scanForProjects, fmtAgo };
package/lib/ui.js CHANGED
@@ -3,18 +3,22 @@
3
3
  // Zero dependencies. Pure ANSI. The terminal breathes.
4
4
 
5
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`;
6
+ // 256-color (38;5;n), NOT 24-bit (38;2;r;g;b). Terminals without truecolor
7
+ // (Apple Terminal et al.) misparse 24-bit params as separate SGR codes โ€”
8
+ // e.g. teal's red channel "100" became "bright black BACKGROUND" = grey
9
+ // boxes behind text. 256-color is a single param: renders the same
10
+ // everywhere. Comfy in every terminal beats precise in some.
11
+ const c256 = (n) => (s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
12
+ const rgb = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`; // kept for callers; avoid for new UI
9
13
  const rgbBg = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[0m`;
10
14
 
11
15
  // 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(190, 208, 198); // quiet โ€” secondary text
15
- const slate = rgb(152, 164, 158); // whisper โ€” dim text (bright enough for dark terminals)
16
- const cream = rgb(240, 235, 225); // clarity โ€” bright text
17
- const ember = rgb(220, 140, 90); // glow โ€” warnings/energy
16
+ const teal = c256(79); // #5fd7af cool calm โ€” primary
17
+ const peach = c256(216); // #ffaf87 warm exhale โ€” accent
18
+ const sage = c256(151); // #afd7af quiet โ€” secondary text
19
+ const slate = c256(247); // #9e9e9e whisper โ€” dim but legible
20
+ const cream = c256(230); // #ffffd7 clarity โ€” bright text
21
+ const ember = c256(173); // #d7875f glow โ€” warnings/energy
18
22
 
19
23
  // Standard ANSI fallbacks (used where 24-bit might not render)
20
24
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
@@ -80,87 +84,22 @@ function spinner(text = 'thinking', style = 'exhale') {
80
84
  };
81
85
  }
82
86
 
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.
87
+ // โ”€โ”€ The Exhale: signature brand reveal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
88
+ // Crisp over clever: plain prints with a breath of timing. No in-place
89
+ // rewrites, no cursor tricks, no dim-wrapped emoji โ€” those are exactly
90
+ // what render as grey boxes and artifacts across terminals. Every line
91
+ // lands once and stays put.
86
92
  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('๐Ÿคซ')}`);
93
+ const pause = (ms) => fast ? Promise.resolve() : sleep(ms);
121
94
  console.log('');
122
- await sleep(150);
123
-
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())}`);
145
- }
146
- await sleep(55);
147
- }
148
-
149
- process.stdout.write('\n');
150
- await sleep(100);
151
-
152
- // Phase 5: Tagline fades in โ€” dim โ†’ sage โ†’ cream
153
- const tagline = '.intent/ is your project\'s working memory.';
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
-
95
+ console.log(` ๐Ÿ˜ฎโ€๐Ÿ’จ ๐Ÿคซ`);
161
96
  console.log('');
97
+ await pause(160);
98
+ console.log(` ${b(cream('โ–ˆโ–€โ–ˆ โ–ˆโ–‘โ–ˆ โ–ˆโ–€โ–€ โ–ˆโ–‘โ–ˆ โ–ˆโ–€ โ–ˆโ–‘โ–ˆ'))}`);
99
+ console.log(` ${b(cream('โ–ˆโ–€โ–€ โ–ˆโ–€โ–ˆ โ–ˆโ–ˆโ–„ โ–€โ–„โ–€ โ–„โ–ˆ โ–ˆโ–€โ–ˆ'))}`);
100
+ await pause(160);
101
+ console.log(` ${sage(".intent/ is your project's working memory.")}`);
162
102
  console.log('');
163
- process.stdout.write(show);
164
103
  }
165
104
 
166
105
  // โ”€โ”€ Status panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"