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.
- package/commands/session.js +168 -183
- package/lib/ui.js +202 -112
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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();
|
|
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();
|
|
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 = [];
|
|
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
|
-
// ──
|
|
210
|
+
// ── The Exhale: animated brand reveal ──────────────────
|
|
214
211
|
await ui.brandReveal();
|
|
215
212
|
|
|
216
|
-
// ── First-run welcome
|
|
213
|
+
// ── First-run welcome ──────────────────────────────────
|
|
217
214
|
if (!config?.apiKey) {
|
|
218
|
-
console.log(` ${b(
|
|
219
|
-
console.log(` ${
|
|
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(` ${
|
|
222
|
-
console.log(` ${
|
|
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(` ${
|
|
225
|
-
console.log(` ${
|
|
226
|
-
console.log(` ${
|
|
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(` ${
|
|
229
|
-
console.log(` ${
|
|
230
|
-
console.log(` ${
|
|
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(` ${
|
|
233
|
-
console.log(` ${
|
|
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(` ${
|
|
238
|
-
console.log(` ${
|
|
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;
|
|
237
|
+
config.apiKey = null;
|
|
241
238
|
}
|
|
242
239
|
|
|
243
240
|
// ── Project status ─────────────────────────────────────
|
|
244
241
|
if (intentFiles.length > 0) {
|
|
245
|
-
console.log(` ${
|
|
242
|
+
console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(intentFiles.map(f => f.file).join(', '))}`);
|
|
246
243
|
} else {
|
|
247
|
-
console.log(` ${
|
|
248
|
-
console.log(` ${
|
|
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(` ${
|
|
247
|
+
console.log(` ${slate(' model:')} ${sage(MODELS[currentModel].name)}`);
|
|
251
248
|
if (config?.email) {
|
|
252
|
-
console.log(` ${
|
|
249
|
+
console.log(` ${slate(' user:')} ${sage(config.email)}`);
|
|
253
250
|
}
|
|
254
251
|
|
|
255
|
-
// ── Interop
|
|
252
|
+
// ── Interop ────────────────────────────────────────────
|
|
256
253
|
ui.interopLine(config, intentFiles);
|
|
257
254
|
|
|
258
|
-
// Sync status
|
|
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(` ${
|
|
263
|
+
console.log(` ${ember('↓')} ${sage('Cloud is newer (' + syncResult.ago + ') — run /pull')}`);
|
|
267
264
|
} else if (syncResult.status === 'local-newer') {
|
|
268
|
-
console.log(` ${
|
|
265
|
+
console.log(` ${ember('↑')} ${sage('Local changes not pushed (' + syncResult.ago + ') — run /push')}`);
|
|
269
266
|
} else if (syncResult.status === 'synced') {
|
|
270
|
-
console.log(` ${
|
|
267
|
+
console.log(` ${teal('↕')} ${slate('synced')}`);
|
|
271
268
|
} else if (syncResult.status === 'local-only') {
|
|
272
|
-
console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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: ` ${
|
|
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(` ${
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
${
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
${
|
|
334
|
-
${
|
|
335
|
-
${
|
|
336
|
-
${
|
|
337
|
-
${
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
${
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
${
|
|
348
|
-
${
|
|
349
|
-
${
|
|
350
|
-
${
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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(
|
|
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(` ${
|
|
374
|
+
console.log(` ${sage('next:')} ${cream('/tour ' + (pageIdx + 2))} ${slate('·')} ${sage('/tour 1-' + pages.length + ' to jump')}`);
|
|
376
375
|
} else {
|
|
377
|
-
console.log(` ${
|
|
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(` ${
|
|
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')} ${
|
|
395
|
-
ui.divider('
|
|
396
|
-
intentFiles.forEach(f => console.log(` ${
|
|
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 ${
|
|
400
|
-
console.log(` ${
|
|
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();
|
|
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 ||
|
|
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(` ${
|
|
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${
|
|
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 ${
|
|
441
|
-
console.log(` ${
|
|
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(` ${
|
|
448
|
+
console.log(` ${teal('●')} ${sage('Context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
|
|
452
449
|
}
|
|
453
450
|
} catch (err) {
|
|
454
|
-
console.error(` ${
|
|
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 ${
|
|
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(` ${
|
|
472
|
+
console.log(` ${teal('●')} ${sage('Context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
|
|
477
473
|
}
|
|
478
474
|
} catch (err) {
|
|
479
|
-
console.error(` ${
|
|
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(` ${
|
|
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 ${
|
|
503
|
+
console.log(`\n ${teal('●')} ${sage('Written to')} ${cream(outPath)}\n`);
|
|
509
504
|
} else {
|
|
510
|
-
console.log(`\n ${
|
|
505
|
+
console.log(`\n ${sage('No artifacts to export')}\n`);
|
|
511
506
|
}
|
|
512
507
|
} catch (err) {
|
|
513
|
-
console.error(` ${
|
|
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 ${
|
|
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 ${
|
|
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(` ${
|
|
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 ${
|
|
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 ${
|
|
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(` ${
|
|
544
|
+
console.log(` ${teal('●')} ${sage('Context reloaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
|
|
551
545
|
}
|
|
552
546
|
} catch (err) {
|
|
553
|
-
console.error(` ${
|
|
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 ${
|
|
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(`${
|
|
563
|
+
syncSpin.stop(`${sage('Could not check sync status')}`);
|
|
571
564
|
} else if (syncResult.status === 'cloud-newer') {
|
|
572
|
-
syncSpin.stop(`${
|
|
565
|
+
syncSpin.stop(`${ember('↓')} ${sage('Cloud is newer (' + syncResult.ago + ') — run /pull')}`);
|
|
573
566
|
} else if (syncResult.status === 'local-newer') {
|
|
574
|
-
syncSpin.stop(`${
|
|
567
|
+
syncSpin.stop(`${ember('↑')} ${sage('Local changes not pushed (' + syncResult.ago + ') — run /push')}`);
|
|
575
568
|
} else if (syncResult.status === 'synced') {
|
|
576
|
-
syncSpin.stop(`${
|
|
569
|
+
syncSpin.stop(`${teal('↕')} ${sage('In sync')}`);
|
|
577
570
|
} else if (syncResult.status === 'local-only') {
|
|
578
|
-
syncSpin.stop(`${
|
|
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();
|
|
582
|
+
config = loadConfig();
|
|
590
583
|
} catch (err) {
|
|
591
|
-
console.error(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(
|
|
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(` ${
|
|
626
|
-
console.log(` ${
|
|
627
|
-
console.log(` ${
|
|
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(` ${
|
|
630
|
-
console.log(` ${
|
|
631
|
-
console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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 ${
|
|
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(
|
|
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 ? ` ${
|
|
662
|
-
console.log(` ${
|
|
652
|
+
const active = key === currentModel ? ` ${teal('●')}` : '';
|
|
653
|
+
console.log(` ${cream(key.padEnd(16))} ${sage(model.name)}${active}`);
|
|
663
654
|
}
|
|
664
|
-
console.log(`\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(` ${
|
|
672
|
-
console.log(` ${
|
|
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(` ${
|
|
673
|
+
console.log(` ${teal('●')} ${sage('Switched to')} ${cream(MODELS[match].name)}`);
|
|
684
674
|
} else {
|
|
685
|
-
console.log(` ${
|
|
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(`${
|
|
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(`${
|
|
716
|
-
console.log(` ${
|
|
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 ${
|
|
720
|
-
console.log(` ${
|
|
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(`${
|
|
723
|
-
console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(
|
|
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(` ${
|
|
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(` ${
|
|
778
|
-
console.log(` Type ${
|
|
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(` ${
|
|
781
|
-
console.log(` ${
|
|
782
|
-
console.log(` ${
|
|
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(
|
|
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 ${
|
|
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 —
|
|
2
|
-
//
|
|
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
|
-
// ──
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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} ${
|
|
68
|
+
process.stdout.write(`${clearLine} ${teal(frame)} ${sage(currentText)}`);
|
|
40
69
|
i++;
|
|
41
|
-
}, style === '
|
|
70
|
+
}, style === 'gentle' ? 150 : 100);
|
|
42
71
|
|
|
43
72
|
return {
|
|
44
|
-
update(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
|
-
// ──
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
92
|
-
console.log(` ${
|
|
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' ?
|
|
96
|
-
: color === '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(` ${
|
|
179
|
+
console.log(` ${sage(label.padEnd(maxLabel + 2))} ${colorFn(value)}`);
|
|
100
180
|
}
|
|
101
|
-
console.log(` ${
|
|
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(
|
|
110
|
-
if (config?.apiKey) parts.push(
|
|
111
|
-
if (config?.supabaseUserId) parts.push(
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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(` ${
|
|
200
|
+
console.log(` ${slate('active')} ${parts.join(slate(' · '))}`);
|
|
122
201
|
}
|
|
123
|
-
console.log(` ${
|
|
202
|
+
console.log(` ${slate('works in')} ${available.join(slate(' · '))}`);
|
|
124
203
|
}
|
|
125
204
|
|
|
126
205
|
// ── Divider ──────────────────────────────────────────────
|
|
127
|
-
function divider(
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
228
|
+
// ── Welcome tips ─────────────────────────────────────────
|
|
150
229
|
const TIPS = [
|
|
151
|
-
`${
|
|
152
|
-
`${
|
|
153
|
-
`${
|
|
154
|
-
`${
|
|
155
|
-
`${
|
|
156
|
-
`${
|
|
157
|
-
`${
|
|
158
|
-
`${
|
|
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
|
-
|
|
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 ${
|
|
174
|
-
` and ${
|
|
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
|
-
` ${
|
|
177
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
`
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
|
|
306
|
+
'',
|
|
307
|
+
` Before you build, decide ${b(cream('whether'))} to build.`,
|
|
221
308
|
` The gate captures what you can actually spend:`,
|
|
222
309
|
'',
|
|
223
|
-
` ${
|
|
224
|
-
` ${
|
|
310
|
+
` ${sage('Budget')} ${cream('$50')} ${sage('Skill')} ${cream('expert')}`,
|
|
311
|
+
` ${sage('Time')} ${cream('15 hrs/week')} ${sage('Urgency')} ${cream('high')}`,
|
|
225
312
|
'',
|
|
226
|
-
` ${
|
|
227
|
-
` ${
|
|
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
|
-
` ${
|
|
239
|
-
` ${
|
|
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
|
-
//
|
|
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
|
};
|