phewsh 0.11.11 → 0.11.13
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 +12 -12
- package/commands/session.js +169 -183
- package/commands/watch.js +12 -12
- package/lib/ui.js +204 -115
- 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,59 @@ 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('.intent/ is your project\'s working memory.')}`);
|
|
217
|
+
console.log(` ${sage('Define it once — every AI tool reads it. No more re-explaining.')}`);
|
|
220
218
|
console.log('');
|
|
221
|
-
console.log(` ${
|
|
222
|
-
console.log(` ${
|
|
219
|
+
console.log(` ${cream('To chat, you need an API key.')} ${slate('(not a subscription)')}`);
|
|
220
|
+
console.log(` ${slate('ChatGPT Plus / Claude Pro don\'t include API access.')}`);
|
|
221
|
+
console.log(` ${slate('API keys are pay-as-you-go — both providers offer free credits.')}`);
|
|
223
222
|
console.log('');
|
|
224
|
-
console.log(` ${
|
|
225
|
-
console.log(` ${
|
|
226
|
-
console.log(` ${
|
|
223
|
+
console.log(` ${teal('1')} ${b(cream('Anthropic'))} ${slate('(recommended)')}`);
|
|
224
|
+
console.log(` ${sage('console.anthropic.com/settings/keys')}`);
|
|
225
|
+
console.log(` ${slate('Direct Claude access. Best quality. ~$0.01/message.')}`);
|
|
227
226
|
console.log('');
|
|
228
|
-
console.log(` ${
|
|
229
|
-
console.log(` ${
|
|
230
|
-
console.log(` ${
|
|
227
|
+
console.log(` ${teal('2')} ${b(cream('OpenRouter'))}`);
|
|
228
|
+
console.log(` ${sage('openrouter.ai/keys')}`);
|
|
229
|
+
console.log(` ${slate('One key → Claude, GPT, Gemini, and more.')}`);
|
|
231
230
|
console.log('');
|
|
232
|
-
console.log(` ${
|
|
233
|
-
console.log(` ${
|
|
234
|
-
console.log(` ${g('Curious?')} /tour ${g('to see what PHEWSH can do (no key needed).')}`);
|
|
231
|
+
console.log(` ${sage('Got a key?')} ${cream('/key')} ${sage('to paste it in.')}`);
|
|
232
|
+
console.log(` ${sage('Curious?')} ${cream('/tour')} ${sage('to explore (no key needed).')}`);
|
|
235
233
|
console.log('');
|
|
236
234
|
} else if (!config.apiKey.startsWith('sk-')) {
|
|
237
|
-
console.log(` ${
|
|
238
|
-
console.log(` ${
|
|
235
|
+
console.log(` ${ember('!')} ${sage('Stored API key looks invalid.')}`);
|
|
236
|
+
console.log(` ${sage('Run')} ${cream('/key')} ${sage('to set a new one')}`);
|
|
239
237
|
console.log('');
|
|
240
|
-
config.apiKey = null;
|
|
238
|
+
config.apiKey = null;
|
|
241
239
|
}
|
|
242
240
|
|
|
243
241
|
// ── Project status ─────────────────────────────────────
|
|
244
242
|
if (intentFiles.length > 0) {
|
|
245
|
-
console.log(` ${
|
|
243
|
+
console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(intentFiles.map(f => f.file).join(', '))}`);
|
|
246
244
|
} else {
|
|
247
|
-
console.log(` ${
|
|
248
|
-
console.log(` ${
|
|
245
|
+
console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage('no .intent/ context')}`);
|
|
246
|
+
console.log(` ${slate(' run /init to create .intent/ artifacts')}`);
|
|
249
247
|
}
|
|
250
|
-
console.log(` ${
|
|
248
|
+
console.log(` ${slate(' model:')} ${sage(MODELS[currentModel].name)}`);
|
|
251
249
|
if (config?.email) {
|
|
252
|
-
console.log(` ${
|
|
250
|
+
console.log(` ${slate(' user:')} ${sage(config.email)}`);
|
|
253
251
|
}
|
|
254
252
|
|
|
255
|
-
// ── Interop
|
|
253
|
+
// ── Interop ────────────────────────────────────────────
|
|
256
254
|
ui.interopLine(config, intentFiles);
|
|
257
255
|
|
|
258
|
-
// Sync status
|
|
256
|
+
// Sync status (non-blocking)
|
|
259
257
|
if (config?.supabaseUserId && intentFiles.length > 0) {
|
|
260
258
|
const syncResult = await Promise.race([
|
|
261
259
|
checkSyncStatus(config),
|
|
@@ -263,32 +261,32 @@ async function main() {
|
|
|
263
261
|
]);
|
|
264
262
|
if (syncResult) {
|
|
265
263
|
if (syncResult.status === 'cloud-newer') {
|
|
266
|
-
console.log(` ${
|
|
264
|
+
console.log(` ${ember('↓')} ${sage('Cloud is newer (' + syncResult.ago + ') — run /pull')}`);
|
|
267
265
|
} else if (syncResult.status === 'local-newer') {
|
|
268
|
-
console.log(` ${
|
|
266
|
+
console.log(` ${ember('↑')} ${sage('Local changes not pushed (' + syncResult.ago + ') — run /push')}`);
|
|
269
267
|
} else if (syncResult.status === 'synced') {
|
|
270
|
-
console.log(` ${
|
|
268
|
+
console.log(` ${teal('↕')} ${slate('synced')}`);
|
|
271
269
|
} else if (syncResult.status === 'local-only') {
|
|
272
|
-
console.log(` ${
|
|
270
|
+
console.log(` ${slate('↕ not linked to cloud — run /push to sync')}`);
|
|
273
271
|
}
|
|
274
272
|
}
|
|
275
273
|
}
|
|
276
274
|
|
|
277
275
|
console.log('');
|
|
278
|
-
ui.divider('
|
|
276
|
+
ui.divider('line');
|
|
279
277
|
if (!config?.apiKey) {
|
|
280
|
-
console.log(` ${
|
|
278
|
+
console.log(` ${sage('type')} ${cream('/key')} ${sage('to get started ·')} ${cream('/tour')} ${sage('to explore ·')} ${cream('/help')} ${sage('for commands')}`);
|
|
281
279
|
} else {
|
|
282
280
|
console.log(` ${ui.randomTip()}`);
|
|
283
|
-
console.log(` ${
|
|
281
|
+
console.log(` ${sage('type naturally ·')} ${cream('/help')} ${sage('for commands ·')} ${cream('/quit')} ${sage('to exit')}`);
|
|
284
282
|
}
|
|
285
|
-
ui.divider('
|
|
283
|
+
ui.divider('line');
|
|
286
284
|
console.log('');
|
|
287
285
|
|
|
288
286
|
const rl = readline.createInterface({
|
|
289
287
|
input: process.stdin,
|
|
290
288
|
output: process.stdout,
|
|
291
|
-
prompt: ` ${
|
|
289
|
+
prompt: ` ${teal('phewsh')} ${sage('>')} `,
|
|
292
290
|
historySize: 100,
|
|
293
291
|
});
|
|
294
292
|
|
|
@@ -311,53 +309,55 @@ async function main() {
|
|
|
311
309
|
if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
|
|
312
310
|
const turns = messages.length / 2;
|
|
313
311
|
console.log('');
|
|
314
|
-
ui.divider('
|
|
315
|
-
console.log(` ${
|
|
316
|
-
ui.divider('
|
|
312
|
+
ui.divider('line');
|
|
313
|
+
console.log(` ${sage('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
|
|
314
|
+
ui.divider('line');
|
|
317
315
|
console.log('');
|
|
318
316
|
process.exit(0);
|
|
319
317
|
}
|
|
320
318
|
|
|
321
319
|
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
|
-
`);
|
|
320
|
+
console.log('');
|
|
321
|
+
ui.divider('line');
|
|
322
|
+
console.log(` ${b(cream('Session commands'))}`);
|
|
323
|
+
ui.divider('line');
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log(` ${cream('conversation')}`);
|
|
326
|
+
console.log(` ${teal('/clear')} ${sage('Clear conversation history')}`);
|
|
327
|
+
console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
|
|
328
|
+
console.log(` ${teal('/quit')} ${sage('End session')}`);
|
|
329
|
+
console.log('');
|
|
330
|
+
console.log(` ${cream('project')}`);
|
|
331
|
+
console.log(` ${teal('/init')} ${sage('Create .intent/ artifacts in this directory')}`);
|
|
332
|
+
console.log(` ${teal('/clarify')} ${sage('AI-assisted artifact generation')}`);
|
|
333
|
+
console.log(` ${teal('/gate')} ${sage('Declare constraints (budget, time, skill)')}`);
|
|
334
|
+
console.log(` ${teal('/export')} ${sage('Export portable context for other AI tools')}`);
|
|
335
|
+
console.log(` ${teal('/context')} ${sage('Show loaded .intent/ files')}`);
|
|
336
|
+
console.log(` ${teal('/reload')} ${sage('Reload .intent/ context from disk')}`);
|
|
337
|
+
console.log(` ${teal('/status')} ${sage('Show session stats')}`);
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(` ${cream('sync')}`);
|
|
340
|
+
console.log(` ${teal('/push')} ${sage('Push .intent/ to cloud')}`);
|
|
341
|
+
console.log(` ${teal('/pull')} ${sage('Pull .intent/ from cloud (reloads context)')}`);
|
|
342
|
+
console.log(` ${teal('/sync')} ${sage('Check sync status')}`);
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log(` ${cream('configuration')}`);
|
|
345
|
+
console.log(` ${teal('/login')} ${sage('Set up identity + cloud sync')}`);
|
|
346
|
+
console.log(` ${teal('/key')} ${sage('Set or update your API key')}`);
|
|
347
|
+
console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model (sonnet, opus, haiku)')}`);
|
|
348
|
+
console.log(` ${teal('/models')} ${sage('List available models')}`);
|
|
349
|
+
console.log(` ${teal('/provider')} ${sage('Show current provider info')}`);
|
|
350
|
+
console.log(` ${teal('/update')} ${sage('Update phewsh to the latest version')}`);
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log(` ${cream('explore')}`);
|
|
353
|
+
console.log(` ${teal('/tour')} ${sage('Guided walkthrough of PHEWSH')}`);
|
|
354
|
+
console.log(` ${teal('/system')} ${sage('Show current system prompt')}`);
|
|
355
|
+
console.log('');
|
|
356
356
|
rl.prompt();
|
|
357
357
|
return;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
// ── /tour
|
|
360
|
+
// ── /tour ──────────────────────────────────────────
|
|
361
361
|
if (cmd === 'tour') {
|
|
362
362
|
const pages = ui.TOUR_PAGES;
|
|
363
363
|
let pageIdx = cmdArg ? parseInt(cmdArg) - 1 : 0;
|
|
@@ -366,15 +366,15 @@ async function main() {
|
|
|
366
366
|
|
|
367
367
|
const page = pages[pageIdx];
|
|
368
368
|
console.log('');
|
|
369
|
-
ui.divider('
|
|
370
|
-
console.log(` ${b(
|
|
371
|
-
ui.divider('
|
|
369
|
+
ui.divider('line');
|
|
370
|
+
console.log(` ${b(cream(page.title))} ${slate(`(${pageIdx + 1}/${pages.length})`)}`);
|
|
371
|
+
ui.divider('line');
|
|
372
372
|
page.body.forEach(line => console.log(line));
|
|
373
373
|
console.log('');
|
|
374
374
|
if (pageIdx < pages.length - 1) {
|
|
375
|
-
console.log(` ${
|
|
375
|
+
console.log(` ${sage('next:')} ${cream('/tour ' + (pageIdx + 2))} ${slate('·')} ${sage('/tour 1-' + pages.length + ' to jump')}`);
|
|
376
376
|
} else {
|
|
377
|
-
console.log(` ${
|
|
377
|
+
console.log(` ${teal('●')} ${sage('End of tour. You\'re ready.')}`);
|
|
378
378
|
}
|
|
379
379
|
console.log('');
|
|
380
380
|
rl.prompt();
|
|
@@ -383,7 +383,7 @@ async function main() {
|
|
|
383
383
|
|
|
384
384
|
if (cmd === 'clear') {
|
|
385
385
|
messages.length = 0;
|
|
386
|
-
console.log(` ${
|
|
386
|
+
console.log(` ${sage('conversation cleared')}`);
|
|
387
387
|
rl.prompt();
|
|
388
388
|
return;
|
|
389
389
|
}
|
|
@@ -391,13 +391,13 @@ async function main() {
|
|
|
391
391
|
if (cmd === 'context') {
|
|
392
392
|
if (intentFiles.length > 0) {
|
|
393
393
|
console.log('');
|
|
394
|
-
console.log(` ${b('Loaded from')} ${
|
|
395
|
-
ui.divider('
|
|
396
|
-
intentFiles.forEach(f => console.log(` ${
|
|
397
|
-
ui.divider('
|
|
394
|
+
console.log(` ${b(cream('Loaded from'))} ${teal('.intent/')}`);
|
|
395
|
+
ui.divider('line');
|
|
396
|
+
intentFiles.forEach(f => console.log(` ${teal('●')} ${cream(f.file)} ${slate('(' + f.content.length + ' chars)')}`));
|
|
397
|
+
ui.divider('line');
|
|
398
398
|
} else {
|
|
399
|
-
console.log(`\n ${
|
|
400
|
-
console.log(` ${
|
|
399
|
+
console.log(`\n ${sage('No .intent/ context found in')} ${slate(process.cwd())}`);
|
|
400
|
+
console.log(` ${sage('Run')} ${cream('/init')} ${sage('to create one')}`);
|
|
401
401
|
}
|
|
402
402
|
console.log('');
|
|
403
403
|
rl.prompt();
|
|
@@ -406,7 +406,7 @@ async function main() {
|
|
|
406
406
|
|
|
407
407
|
if (cmd === 'status') {
|
|
408
408
|
const turns = messages.length / 2;
|
|
409
|
-
config = loadConfig();
|
|
409
|
+
config = loadConfig();
|
|
410
410
|
ui.statusPanel('Session', [
|
|
411
411
|
['Turns', String(turns)],
|
|
412
412
|
['Tokens', `${totalPromptTokens} in → ${totalCompletionTokens} out`],
|
|
@@ -414,7 +414,7 @@ async function main() {
|
|
|
414
414
|
['Context', intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none', intentFiles.length > 0 ? 'green' : 'yellow'],
|
|
415
415
|
['Model', MODELS[currentModel].name],
|
|
416
416
|
['Provider', MODELS[currentModel].provider],
|
|
417
|
-
['User', config?.email ||
|
|
417
|
+
['User', config?.email || slate('not logged in')],
|
|
418
418
|
['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '...' : 'not set', config?.apiKey ? 'green' : 'yellow'],
|
|
419
419
|
]);
|
|
420
420
|
rl.prompt();
|
|
@@ -424,34 +424,32 @@ async function main() {
|
|
|
424
424
|
if (cmd === 'reload') {
|
|
425
425
|
intentFiles = loadIntentContext();
|
|
426
426
|
systemPrompt = buildSystemPrompt(intentFiles);
|
|
427
|
-
console.log(` ${
|
|
427
|
+
console.log(` ${teal('●')} ${sage('Reloaded ' + intentFiles.length + ' artifact' + (intentFiles.length !== 1 ? 's' : ''))}`);
|
|
428
428
|
rl.prompt();
|
|
429
429
|
return;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
if (cmd === 'system') {
|
|
433
|
-
console.log(`\n${
|
|
433
|
+
console.log(`\n${slate(systemPrompt)}\n`);
|
|
434
434
|
rl.prompt();
|
|
435
435
|
return;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
if (cmd === 'init') {
|
|
439
439
|
if (fs.existsSync(path.join(INTENT_DIR, 'vision.md'))) {
|
|
440
|
-
console.log(`\n ${
|
|
441
|
-
console.log(` ${
|
|
440
|
+
console.log(`\n ${sage('.intent/ already exists in')} ${slate(process.cwd())}`);
|
|
441
|
+
console.log(` ${sage('Use /reload to refresh context')}\n`);
|
|
442
442
|
} else {
|
|
443
443
|
try {
|
|
444
|
-
// Delegate to the intent --init command
|
|
445
444
|
const { execSync } = require('child_process');
|
|
446
445
|
execSync('node ' + path.join(__dirname, 'intent.js') + ' --init', { stdio: 'inherit' });
|
|
447
|
-
// Reload context after init
|
|
448
446
|
intentFiles = loadIntentContext();
|
|
449
447
|
systemPrompt = buildSystemPrompt(intentFiles);
|
|
450
448
|
if (intentFiles.length > 0) {
|
|
451
|
-
console.log(` ${
|
|
449
|
+
console.log(` ${teal('●')} ${sage('Context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
|
|
452
450
|
}
|
|
453
451
|
} catch (err) {
|
|
454
|
-
console.error(` ${
|
|
452
|
+
console.error(` ${sage('Init failed:')} ${err.message}`);
|
|
455
453
|
}
|
|
456
454
|
}
|
|
457
455
|
console.log('');
|
|
@@ -461,7 +459,7 @@ async function main() {
|
|
|
461
459
|
|
|
462
460
|
if (cmd === 'clarify') {
|
|
463
461
|
if (!config?.apiKey) {
|
|
464
|
-
console.log(`\n ${
|
|
462
|
+
console.log(`\n ${ember('!')} ${sage('No API key. Run /key to set one.')}\n`);
|
|
465
463
|
rl.prompt();
|
|
466
464
|
return;
|
|
467
465
|
}
|
|
@@ -469,14 +467,13 @@ async function main() {
|
|
|
469
467
|
const { execSync } = require('child_process');
|
|
470
468
|
const args = cmdArg ? `--text "${cmdArg.replace(/"/g, '\\"')}"` : '';
|
|
471
469
|
execSync(`node ${path.join(__dirname, 'clarify.js')} ${args}`, { stdio: 'inherit' });
|
|
472
|
-
// Reload context after clarify
|
|
473
470
|
intentFiles = loadIntentContext();
|
|
474
471
|
systemPrompt = buildSystemPrompt(intentFiles);
|
|
475
472
|
if (intentFiles.length > 0) {
|
|
476
|
-
console.log(` ${
|
|
473
|
+
console.log(` ${teal('●')} ${sage('Context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
|
|
477
474
|
}
|
|
478
475
|
} catch (err) {
|
|
479
|
-
console.error(` ${
|
|
476
|
+
console.error(` ${sage('Clarify failed:')} ${err.message}`);
|
|
480
477
|
}
|
|
481
478
|
console.log('');
|
|
482
479
|
rl.prompt();
|
|
@@ -488,11 +485,10 @@ async function main() {
|
|
|
488
485
|
const { execSync } = require('child_process');
|
|
489
486
|
const gateArg = cmdArg || 'status';
|
|
490
487
|
execSync(`node ${path.join(__dirname, 'gate.js')} ${gateArg}`, { stdio: 'inherit' });
|
|
491
|
-
// Reload context after gate changes (gate.json may have updated)
|
|
492
488
|
intentFiles = loadIntentContext();
|
|
493
489
|
systemPrompt = buildSystemPrompt(intentFiles);
|
|
494
490
|
} catch (err) {
|
|
495
|
-
console.error(` ${
|
|
491
|
+
console.error(` ${sage('Gate failed:')} ${err.message}`);
|
|
496
492
|
}
|
|
497
493
|
rl.prompt();
|
|
498
494
|
return;
|
|
@@ -505,12 +501,12 @@ async function main() {
|
|
|
505
501
|
if (content) {
|
|
506
502
|
const outPath = path.join(process.cwd(), '.phewsh.context');
|
|
507
503
|
fs.writeFileSync(outPath, content);
|
|
508
|
-
console.log(`\n ${
|
|
504
|
+
console.log(`\n ${teal('●')} ${sage('Written to')} ${cream(outPath)}\n`);
|
|
509
505
|
} else {
|
|
510
|
-
console.log(`\n ${
|
|
506
|
+
console.log(`\n ${sage('No artifacts to export')}\n`);
|
|
511
507
|
}
|
|
512
508
|
} catch (err) {
|
|
513
|
-
console.error(` ${
|
|
509
|
+
console.error(` ${sage('Export failed:')} ${err.message}`);
|
|
514
510
|
}
|
|
515
511
|
rl.prompt();
|
|
516
512
|
return;
|
|
@@ -518,16 +514,16 @@ async function main() {
|
|
|
518
514
|
|
|
519
515
|
if (cmd === 'push') {
|
|
520
516
|
if (!config?.supabaseUserId) {
|
|
521
|
-
console.log(`\n ${
|
|
517
|
+
console.log(`\n ${ember('!')} ${sage('Not logged in. Run /login first.')}\n`);
|
|
522
518
|
rl.prompt();
|
|
523
519
|
return;
|
|
524
520
|
}
|
|
525
521
|
try {
|
|
526
522
|
const token = await ensureValidToken(config);
|
|
527
|
-
if (!token) { console.log(`\n ${
|
|
523
|
+
if (!token) { console.log(`\n ${ember('!')} ${sage('Session expired. Run /login.')}\n`); rl.prompt(); return; }
|
|
528
524
|
await push(config, token);
|
|
529
525
|
} catch (err) {
|
|
530
|
-
console.error(` ${
|
|
526
|
+
console.error(` ${ember('!')} ${sage('Push failed:')} ${err.message}\n`);
|
|
531
527
|
}
|
|
532
528
|
rl.prompt();
|
|
533
529
|
return;
|
|
@@ -535,22 +531,21 @@ async function main() {
|
|
|
535
531
|
|
|
536
532
|
if (cmd === 'pull') {
|
|
537
533
|
if (!config?.supabaseUserId) {
|
|
538
|
-
console.log(`\n ${
|
|
534
|
+
console.log(`\n ${ember('!')} ${sage('Not logged in. Run /login first.')}\n`);
|
|
539
535
|
rl.prompt();
|
|
540
536
|
return;
|
|
541
537
|
}
|
|
542
538
|
try {
|
|
543
539
|
const token = await ensureValidToken(config);
|
|
544
|
-
if (!token) { console.log(`\n ${
|
|
540
|
+
if (!token) { console.log(`\n ${ember('!')} ${sage('Session expired. Run /login.')}\n`); rl.prompt(); return; }
|
|
545
541
|
await pull(config, token);
|
|
546
|
-
// Reload context after pull
|
|
547
542
|
intentFiles = loadIntentContext();
|
|
548
543
|
systemPrompt = buildSystemPrompt(intentFiles);
|
|
549
544
|
if (intentFiles.length > 0) {
|
|
550
|
-
console.log(` ${
|
|
545
|
+
console.log(` ${teal('●')} ${sage('Context reloaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
|
|
551
546
|
}
|
|
552
547
|
} catch (err) {
|
|
553
|
-
console.error(` ${
|
|
548
|
+
console.error(` ${ember('!')} ${sage('Pull failed:')} ${err.message}\n`);
|
|
554
549
|
}
|
|
555
550
|
console.log('');
|
|
556
551
|
rl.prompt();
|
|
@@ -558,24 +553,23 @@ async function main() {
|
|
|
558
553
|
}
|
|
559
554
|
|
|
560
555
|
if (cmd === 'sync') {
|
|
561
|
-
// Show sync status
|
|
562
556
|
if (!config?.supabaseUserId) {
|
|
563
|
-
console.log(`\n ${
|
|
557
|
+
console.log(`\n ${ember('!')} ${sage('Not logged in. Run /login first.')}\n`);
|
|
564
558
|
rl.prompt();
|
|
565
559
|
return;
|
|
566
560
|
}
|
|
567
561
|
const syncSpin = ui.spinner('checking sync');
|
|
568
562
|
const syncResult = await checkSyncStatus(config);
|
|
569
563
|
if (!syncResult) {
|
|
570
|
-
syncSpin.stop(`${
|
|
564
|
+
syncSpin.stop(`${sage('Could not check sync status')}`);
|
|
571
565
|
} else if (syncResult.status === 'cloud-newer') {
|
|
572
|
-
syncSpin.stop(`${
|
|
566
|
+
syncSpin.stop(`${ember('↓')} ${sage('Cloud is newer (' + syncResult.ago + ') — run /pull')}`);
|
|
573
567
|
} else if (syncResult.status === 'local-newer') {
|
|
574
|
-
syncSpin.stop(`${
|
|
568
|
+
syncSpin.stop(`${ember('↑')} ${sage('Local changes not pushed (' + syncResult.ago + ') — run /push')}`);
|
|
575
569
|
} else if (syncResult.status === 'synced') {
|
|
576
|
-
syncSpin.stop(`${
|
|
570
|
+
syncSpin.stop(`${teal('↕')} ${sage('In sync')}`);
|
|
577
571
|
} else if (syncResult.status === 'local-only') {
|
|
578
|
-
syncSpin.stop(`${
|
|
572
|
+
syncSpin.stop(`${slate('↕ Not linked to cloud — run /push to sync')}`);
|
|
579
573
|
}
|
|
580
574
|
console.log('');
|
|
581
575
|
rl.prompt();
|
|
@@ -586,9 +580,9 @@ async function main() {
|
|
|
586
580
|
try {
|
|
587
581
|
const { execSync } = require('child_process');
|
|
588
582
|
execSync('node ' + path.join(__dirname, 'login.js'), { stdio: 'inherit' });
|
|
589
|
-
config = loadConfig();
|
|
583
|
+
config = loadConfig();
|
|
590
584
|
} catch (err) {
|
|
591
|
-
console.error(` ${
|
|
585
|
+
console.error(` ${sage('Login failed:')} ${err.message}`);
|
|
592
586
|
}
|
|
593
587
|
rl.prompt();
|
|
594
588
|
return;
|
|
@@ -596,56 +590,54 @@ async function main() {
|
|
|
596
590
|
|
|
597
591
|
if (cmd === 'key') {
|
|
598
592
|
if (cmdArg) {
|
|
599
|
-
// Inline: /key sk-ant-...
|
|
600
593
|
const apiKey = cmdArg.trim();
|
|
601
594
|
config = loadConfig() || {};
|
|
602
595
|
if (apiKey.startsWith('sk-ant-') || apiKey.startsWith('sk-')) {
|
|
603
596
|
config.apiKey = apiKey;
|
|
604
597
|
config.provider = 'anthropic';
|
|
605
598
|
saveConfig(config);
|
|
606
|
-
console.log(` ${
|
|
599
|
+
console.log(` ${teal('●')} ${sage('Anthropic key saved. You\'re ready — just type.')}\n`);
|
|
607
600
|
} else if (apiKey.startsWith('sk-or-')) {
|
|
608
601
|
config.apiKey = apiKey;
|
|
609
602
|
config.provider = 'openrouter';
|
|
610
603
|
saveConfig(config);
|
|
611
|
-
console.log(` ${
|
|
604
|
+
console.log(` ${teal('●')} ${sage('OpenRouter key saved. You\'re ready — just type.')}\n`);
|
|
612
605
|
} else {
|
|
613
606
|
config.apiKey = apiKey;
|
|
614
607
|
saveConfig(config);
|
|
615
|
-
console.log(` ${
|
|
608
|
+
console.log(` ${teal('●')} ${sage('API key saved. You\'re ready — just type.')}\n`);
|
|
616
609
|
}
|
|
617
610
|
rl.prompt();
|
|
618
611
|
return;
|
|
619
612
|
}
|
|
620
613
|
console.log('');
|
|
621
|
-
ui.divider('
|
|
622
|
-
console.log(` ${b(
|
|
623
|
-
ui.divider('
|
|
614
|
+
ui.divider('line');
|
|
615
|
+
console.log(` ${b(cream('Where to get an API key'))}`);
|
|
616
|
+
ui.divider('line');
|
|
624
617
|
console.log('');
|
|
625
|
-
console.log(` ${
|
|
626
|
-
console.log(` ${
|
|
627
|
-
console.log(` ${
|
|
618
|
+
console.log(` ${teal('Anthropic')} ${slate('(recommended)')}`);
|
|
619
|
+
console.log(` ${sage('1.')} Go to ${cream('console.anthropic.com/settings/keys')}`);
|
|
620
|
+
console.log(` ${sage('2.')} Create key → copy it ${slate('(starts with sk-ant-)')}`);
|
|
628
621
|
console.log('');
|
|
629
|
-
console.log(` ${
|
|
630
|
-
console.log(` ${
|
|
631
|
-
console.log(` ${
|
|
622
|
+
console.log(` ${teal('OpenRouter')} ${slate('(multi-model)')}`);
|
|
623
|
+
console.log(` ${sage('1.')} Go to ${cream('openrouter.ai/keys')}`);
|
|
624
|
+
console.log(` ${sage('2.')} Create key → copy it ${slate('(starts with sk-or-)')}`);
|
|
632
625
|
console.log('');
|
|
633
|
-
console.log(` ${
|
|
634
|
-
console.log(` ${g('Both providers offer free credits to get started.')}`);
|
|
626
|
+
console.log(` ${slate('Note: API keys ≠ subscriptions. Both providers offer free credits.')}`);
|
|
635
627
|
console.log('');
|
|
636
628
|
const keyRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
637
|
-
keyRl.question(` Paste your API key\n > `, (apiKey) => {
|
|
629
|
+
keyRl.question(` ${sage('Paste your API key')}\n ${teal('>')} `, (apiKey) => {
|
|
638
630
|
keyRl.close();
|
|
639
631
|
apiKey = apiKey.trim();
|
|
640
632
|
if (!apiKey) {
|
|
641
|
-
console.log(` ${
|
|
633
|
+
console.log(` ${slate('Cancelled')}\n`);
|
|
642
634
|
} else {
|
|
643
635
|
config = loadConfig() || {};
|
|
644
636
|
config.apiKey = apiKey;
|
|
645
637
|
if (apiKey.startsWith('sk-or-')) config.provider = 'openrouter';
|
|
646
638
|
else config.provider = 'anthropic';
|
|
647
639
|
saveConfig(config);
|
|
648
|
-
console.log(`\n ${
|
|
640
|
+
console.log(`\n ${teal('●')} ${sage('API key saved. You\'re ready — just type naturally.')}\n`);
|
|
649
641
|
}
|
|
650
642
|
rl.prompt();
|
|
651
643
|
});
|
|
@@ -654,35 +646,34 @@ async function main() {
|
|
|
654
646
|
|
|
655
647
|
if (cmd === 'models') {
|
|
656
648
|
console.log('');
|
|
657
|
-
ui.divider('
|
|
658
|
-
console.log(` ${b(
|
|
659
|
-
ui.divider('
|
|
649
|
+
ui.divider('line');
|
|
650
|
+
console.log(` ${b(cream('Available models'))}`);
|
|
651
|
+
ui.divider('line');
|
|
660
652
|
for (const [key, model] of Object.entries(MODELS)) {
|
|
661
|
-
const active = key === currentModel ? ` ${
|
|
662
|
-
console.log(` ${
|
|
653
|
+
const active = key === currentModel ? ` ${teal('●')}` : '';
|
|
654
|
+
console.log(` ${cream(key.padEnd(16))} ${sage(model.name)}${active}`);
|
|
663
655
|
}
|
|
664
|
-
console.log(`\n ${
|
|
656
|
+
console.log(`\n ${sage('Switch with:')} ${cream('/model <name>')}\n`);
|
|
665
657
|
rl.prompt();
|
|
666
658
|
return;
|
|
667
659
|
}
|
|
668
660
|
|
|
669
661
|
if (cmd === 'model') {
|
|
670
662
|
if (!cmdArg) {
|
|
671
|
-
console.log(` ${
|
|
672
|
-
console.log(` ${
|
|
663
|
+
console.log(` ${sage('Current:')} ${cream(MODELS[currentModel].name)}`);
|
|
664
|
+
console.log(` ${sage('Usage:')} ${cream('/model <sonnet|opus|haiku>')}`);
|
|
673
665
|
rl.prompt();
|
|
674
666
|
return;
|
|
675
667
|
}
|
|
676
|
-
// Fuzzy match model name
|
|
677
668
|
const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '');
|
|
678
669
|
const match = Object.keys(MODELS).find(k =>
|
|
679
670
|
k.includes(query) || MODELS[k].name.toLowerCase().includes(query)
|
|
680
671
|
);
|
|
681
672
|
if (match) {
|
|
682
673
|
currentModel = match;
|
|
683
|
-
console.log(` ${
|
|
674
|
+
console.log(` ${teal('●')} ${sage('Switched to')} ${cream(MODELS[match].name)}`);
|
|
684
675
|
} else {
|
|
685
|
-
console.log(` ${
|
|
676
|
+
console.log(` ${sage('Unknown model. Available:')} ${cream(Object.keys(MODELS).join(', '))}`);
|
|
686
677
|
}
|
|
687
678
|
rl.prompt();
|
|
688
679
|
return;
|
|
@@ -701,26 +692,26 @@ async function main() {
|
|
|
701
692
|
}
|
|
702
693
|
|
|
703
694
|
if (cmd === 'update' || cmd === 'upgrade') {
|
|
704
|
-
const updateSpin = ui.spinner('checking for updates');
|
|
695
|
+
const updateSpin = ui.spinner('checking for updates', 'gentle');
|
|
705
696
|
try {
|
|
706
697
|
const pkg = require('../../package.json');
|
|
707
698
|
const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(5000) });
|
|
708
699
|
const data = await res.json();
|
|
709
700
|
if (!data.version || data.version === pkg.version) {
|
|
710
|
-
updateSpin.stop(`${
|
|
701
|
+
updateSpin.stop(`${teal('●')} ${sage('Already on the latest version (' + pkg.version + ')')}`);
|
|
711
702
|
console.log('');
|
|
712
703
|
rl.prompt();
|
|
713
704
|
return;
|
|
714
705
|
}
|
|
715
|
-
updateSpin.stop(`${
|
|
716
|
-
console.log(` ${
|
|
706
|
+
updateSpin.stop(`${peach(pkg.version)} ${sage('→')} ${peach(data.version)}`);
|
|
707
|
+
console.log(` ${sage('Installing...')}\n`);
|
|
717
708
|
const { execSync } = require('child_process');
|
|
718
709
|
execSync(`npm install -g ${pkg.name}@latest`, { stdio: 'inherit' });
|
|
719
|
-
console.log(`\n ${
|
|
720
|
-
console.log(` ${
|
|
710
|
+
console.log(`\n ${teal('●')} ${sage('Updated to')} ${cream(data.version)}`);
|
|
711
|
+
console.log(` ${slate('Restart phewsh to use the new version.')}\n`);
|
|
721
712
|
} catch (err) {
|
|
722
|
-
updateSpin.stop(`${
|
|
723
|
-
console.log(` ${
|
|
713
|
+
updateSpin.stop(`${ember('!')} ${sage('Update failed:')} ${err.message}`);
|
|
714
|
+
console.log(` ${sage('You can update manually:')} ${cream('npm install -g phewsh')}\n`);
|
|
724
715
|
}
|
|
725
716
|
rl.prompt();
|
|
726
717
|
return;
|
|
@@ -728,16 +719,15 @@ async function main() {
|
|
|
728
719
|
|
|
729
720
|
if (cmd === 'run') {
|
|
730
721
|
if (!cmdArg) {
|
|
731
|
-
console.log(` ${
|
|
722
|
+
console.log(` ${sage('Usage:')} ${cream('/run <prompt>')}`);
|
|
732
723
|
rl.prompt();
|
|
733
724
|
return;
|
|
734
725
|
}
|
|
735
726
|
if (!config?.apiKey) {
|
|
736
|
-
console.log(` ${
|
|
727
|
+
console.log(` ${ember('!')} ${sage('No API key. Run /key to set one.')}`);
|
|
737
728
|
rl.prompt();
|
|
738
729
|
return;
|
|
739
730
|
}
|
|
740
|
-
// One-shot: don't add to conversation history
|
|
741
731
|
console.log('');
|
|
742
732
|
try {
|
|
743
733
|
const result = await streamChat(
|
|
@@ -747,7 +737,7 @@ async function main() {
|
|
|
747
737
|
MODELS[currentModel].id
|
|
748
738
|
);
|
|
749
739
|
if (result.promptTokens || result.completionTokens) {
|
|
750
|
-
console.log(
|
|
740
|
+
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
|
|
751
741
|
}
|
|
752
742
|
trackSap({
|
|
753
743
|
userId: config.supabaseUserId,
|
|
@@ -766,7 +756,7 @@ async function main() {
|
|
|
766
756
|
}
|
|
767
757
|
|
|
768
758
|
// Unknown slash command
|
|
769
|
-
console.log(` ${
|
|
759
|
+
console.log(` ${sage('Unknown command:')} ${cream('/' + cmd)} ${slate('— type /help for available commands')}`);
|
|
770
760
|
rl.prompt();
|
|
771
761
|
return;
|
|
772
762
|
}
|
|
@@ -774,12 +764,12 @@ async function main() {
|
|
|
774
764
|
// Regular input → send to AI
|
|
775
765
|
if (!config?.apiKey) {
|
|
776
766
|
console.log('');
|
|
777
|
-
console.log(` ${
|
|
778
|
-
console.log(` Type ${
|
|
767
|
+
console.log(` ${peach('Almost there.')} ${sage('You need an API key to chat.')}`);
|
|
768
|
+
console.log(` ${sage('Type')} ${cream('/key')} ${sage('and paste one in — takes 10 seconds.')}`);
|
|
779
769
|
console.log('');
|
|
780
|
-
console.log(` ${
|
|
781
|
-
console.log(` ${
|
|
782
|
-
console.log(` ${
|
|
770
|
+
console.log(` ${slate('Get a key:')} ${sage('console.anthropic.com/settings/keys')}`);
|
|
771
|
+
console.log(` ${slate('Or try:')} ${sage('openrouter.ai/keys')}`);
|
|
772
|
+
console.log(` ${slate('Explore:')} ${cream('/tour')} ${slate('to see what PHEWSH does (no key needed)')}`);
|
|
783
773
|
console.log('');
|
|
784
774
|
rl.prompt();
|
|
785
775
|
return;
|
|
@@ -792,16 +782,13 @@ async function main() {
|
|
|
792
782
|
const result = await streamChat(config.apiKey, messages, systemPrompt, MODELS[currentModel].id);
|
|
793
783
|
messages.push({ role: 'assistant', content: result.content });
|
|
794
784
|
|
|
795
|
-
// Track totals
|
|
796
785
|
if (result.promptTokens) totalPromptTokens += result.promptTokens;
|
|
797
786
|
if (result.completionTokens) totalCompletionTokens += result.completionTokens;
|
|
798
787
|
|
|
799
|
-
// Token count footer
|
|
800
788
|
if (result.promptTokens || result.completionTokens) {
|
|
801
|
-
console.log(
|
|
789
|
+
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
|
|
802
790
|
}
|
|
803
791
|
|
|
804
|
-
// SAP tracking (fire-and-forget)
|
|
805
792
|
trackSap({
|
|
806
793
|
userId: config.supabaseUserId,
|
|
807
794
|
source: 'cli',
|
|
@@ -812,7 +799,6 @@ async function main() {
|
|
|
812
799
|
});
|
|
813
800
|
} catch (err) {
|
|
814
801
|
console.error(`\n ${err.message}\n`);
|
|
815
|
-
// Remove the failed user message
|
|
816
802
|
messages.pop();
|
|
817
803
|
}
|
|
818
804
|
|
|
@@ -821,7 +807,7 @@ async function main() {
|
|
|
821
807
|
});
|
|
822
808
|
|
|
823
809
|
rl.on('close', () => {
|
|
824
|
-
console.log(`\n ${
|
|
810
|
+
console.log(`\n ${sage('session ended')}\n`);
|
|
825
811
|
process.exit(0);
|
|
826
812
|
});
|
|
827
813
|
}
|