phewsh 0.8.0 → 0.9.0

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
@@ -117,23 +117,9 @@ function exitAfterUpdate(code = 0) {
117
117
  }
118
118
 
119
119
  if (!command) {
120
- // Bare `phewsh` — drop into persistent session
121
- // If no API key, fall back to help
122
- const fs = require('fs');
123
- const path = require('path');
124
- const os = require('os');
125
- const configPath = path.join(os.homedir(), '.phewsh', 'config.json');
126
- let hasKey = false;
127
- try {
128
- const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
129
- hasKey = !!cfg.apiKey;
130
- } catch {}
131
- if (hasKey) {
132
- COMMANDS.session();
133
- } else {
134
- showHelp();
135
- exitAfterUpdate(0);
136
- }
120
+ // Bare `phewsh` — always drop into persistent session
121
+ // Session handles missing API key gracefully with /login and /key commands
122
+ COMMANDS.session();
137
123
  } else if (command === 'help' || command === '--help' || command === '-h') {
138
124
  showHelp();
139
125
  exitAfterUpdate(0);
@@ -8,21 +8,36 @@ const os = require('os');
8
8
  const readline = require('readline');
9
9
  const { trackSap } = require('../lib/supabase');
10
10
 
11
- const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
11
+ const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
12
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
12
13
  const INTENT_DIR = path.join(process.cwd(), '.intent');
13
- const HISTORY_PATH = path.join(os.homedir(), '.phewsh', 'session_history.json');
14
14
 
15
15
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
16
16
  const d = (s) => `\x1b[2m${s}\x1b[0m`;
17
+ const w = (s) => `\x1b[97m${s}\x1b[0m`;
17
18
  const g = (s) => `\x1b[90m${s}\x1b[0m`;
18
19
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
19
20
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
21
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
22
+
23
+ const MODELS = {
24
+ 'claude-sonnet': { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic' },
25
+ 'claude-opus': { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'anthropic' },
26
+ 'claude-haiku': { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'anthropic' },
27
+ };
28
+
29
+ const DEFAULT_MODEL = 'claude-sonnet';
20
30
 
21
31
  function loadConfig() {
22
32
  if (!fs.existsSync(CONFIG_PATH)) return null;
23
33
  try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
24
34
  }
25
35
 
36
+ function saveConfig(config) {
37
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
39
+ }
40
+
26
41
  function loadIntentContext() {
27
42
  const files = ['vision.md', 'plan.md', 'next.md'];
28
43
  const loaded = [];
@@ -49,9 +64,8 @@ function buildSystemPrompt(intentFiles) {
49
64
  return `${base}\n\nThe user has structured intent artifacts for this project. Use them as primary context — stay aligned with their vision, plan, and next actions.\n\n${sections}`;
50
65
  }
51
66
 
52
- async function streamChat(apiKey, messages, systemPrompt, config) {
53
- const model = 'claude-sonnet-4-6';
54
- const body = { model, max_tokens: 2048, messages, stream: true };
67
+ async function streamChat(apiKey, messages, systemPrompt, modelId) {
68
+ const body = { model: modelId, max_tokens: 2048, messages, stream: true };
55
69
  if (systemPrompt) body.system = systemPrompt;
56
70
 
57
71
  const response = await fetch('https://api.anthropic.com/v1/messages', {
@@ -97,42 +111,45 @@ async function streamChat(apiKey, messages, systemPrompt, config) {
97
111
 
98
112
  process.stdout.write('\n');
99
113
 
100
- // SAP tracking (fire-and-forget)
101
- trackSap({
102
- userId: config.supabaseUserId,
103
- source: 'cli',
104
- model,
105
- promptTokens,
106
- completionTokens,
107
- accessToken: config.supabaseAccessToken,
108
- });
109
-
110
- return { content: fullResponse, promptTokens, completionTokens };
114
+ return { content: fullResponse, promptTokens, completionTokens, model: modelId };
111
115
  }
112
116
 
113
117
  async function main() {
114
- const config = loadConfig();
115
-
116
- if (!config?.apiKey) {
117
- console.log('\n No API key found. Run `phewsh login --set-key` first.');
118
- console.log(' Or start at: `phewsh login`\n');
119
- process.exit(1);
120
- }
121
-
122
- const intentFiles = loadIntentContext();
123
- const systemPrompt = buildSystemPrompt(intentFiles);
118
+ let config = loadConfig();
119
+ let intentFiles = loadIntentContext();
120
+ let systemPrompt = buildSystemPrompt(intentFiles);
124
121
  const messages = []; // conversation history
125
122
  const projectName = path.basename(process.cwd());
123
+ let currentModel = DEFAULT_MODEL;
124
+ let totalPromptTokens = 0;
125
+ let totalCompletionTokens = 0;
126
126
 
127
- // Session banner
127
+ // Session banner with PHEWSH branding
128
128
  console.log('');
129
129
  console.log(` ${d('😮‍💨')} ${d('🤫')}`);
130
130
  console.log('');
131
+ console.log(` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
132
+ console.log(` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
133
+ console.log('');
134
+
135
+ if (!config?.apiKey) {
136
+ console.log(` ${yellow('⚠')} No API key configured.`);
137
+ console.log(` ${g('Run')} /login ${g('to set up identity + API key')}`);
138
+ console.log(` ${g('Or')} /key ${g('to add an API key directly')}`);
139
+ console.log('');
140
+ }
141
+
131
142
  if (intentFiles.length > 0) {
132
- console.log(` ${green('●')} Session started ${g('·')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
143
+ console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
133
144
  } else {
134
- console.log(` ${green('●')} Session started ${g('·')} ${cyan(projectName)} ${g('·')} no .intent/ context`);
145
+ console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} no .intent/ context`);
146
+ console.log(` ${g(' run /init to create .intent/ artifacts')}`);
147
+ }
148
+ console.log(` ${g(' model:')} ${MODELS[currentModel].name}`);
149
+ if (config?.email) {
150
+ console.log(` ${g(' user:')} ${config.email}`);
135
151
  }
152
+ console.log('');
136
153
  console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
137
154
  console.log('');
138
155
 
@@ -155,23 +172,40 @@ async function main() {
155
172
 
156
173
  // Slash commands
157
174
  if (input.startsWith('/')) {
158
- const cmd = input.slice(1).split(' ')[0].toLowerCase();
175
+ const parts = input.slice(1).split(/\s+/);
176
+ const cmd = parts[0].toLowerCase();
177
+ const cmdArg = parts.slice(1).join(' ');
159
178
 
160
179
  if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
161
- console.log(`\n ${g('Session ended · ' + messages.length / 2 + ' exchanges')}\n`);
180
+ const turns = messages.length / 2;
181
+ console.log(`\n ${g('Session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}\n`);
162
182
  process.exit(0);
163
183
  }
164
184
 
165
- if (cmd === 'help') {
185
+ if (cmd === 'help' || cmd === 'h') {
166
186
  console.log(`
167
187
  ${b('Session commands')}
168
188
 
169
- ${g('/quit')} End session
189
+ ${w('conversation')}
170
190
  ${g('/clear')} Clear conversation history
191
+ ${g('/run')} ${d('<prompt>')} One-shot prompt (doesn't add to conversation)
192
+ ${g('/quit')} End session
193
+
194
+ ${w('project')}
195
+ ${g('/init')} Create .intent/ artifacts in this directory
171
196
  ${g('/context')} Show loaded .intent/ files
197
+ ${g('/reload')} Reload .intent/ context from disk
172
198
  ${g('/status')} Show session stats
173
- ${g('/reload')} Reload .intent/ context
174
- ${g('/system')} Show system prompt (debug)
199
+
200
+ ${w('configuration')}
201
+ ${g('/login')} Set up identity + cloud sync
202
+ ${g('/key')} Set or update your API key
203
+ ${g('/model')} ${d('<name>')} Switch model (sonnet, opus, haiku)
204
+ ${g('/models')} List available models
205
+ ${g('/provider')} Show current provider info
206
+
207
+ ${w('debug')}
208
+ ${g('/system')} Show current system prompt
175
209
  `);
176
210
  rl.prompt();
177
211
  return;
@@ -190,7 +224,7 @@ async function main() {
190
224
  intentFiles.forEach(f => console.log(` ${green('●')} ${f.file} ${g('(' + f.content.length + ' chars)')}`));
191
225
  } else {
192
226
  console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
193
- console.log(` ${g('Run')} phewsh clarify ${g('to create one')}`);
227
+ console.log(` ${g('Run')} /init ${g('to create one')}`);
194
228
  }
195
229
  console.log('');
196
230
  rl.prompt();
@@ -199,21 +233,25 @@ async function main() {
199
233
 
200
234
  if (cmd === 'status') {
201
235
  const turns = messages.length / 2;
236
+ config = loadConfig(); // refresh
202
237
  console.log(`\n ${b('Session')}`);
203
238
  console.log(` Turns ${turns}`);
239
+ console.log(` Tokens ${totalPromptTokens}→${totalCompletionTokens} (in→out)`);
204
240
  console.log(` Project ${projectName}`);
205
241
  console.log(` Context ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none'}`);
206
- console.log(` Provider anthropic (claude-sonnet-4-6)`);
242
+ console.log(` Model ${MODELS[currentModel].name}`);
243
+ console.log(` Provider ${MODELS[currentModel].provider}`);
244
+ if (config?.email) console.log(` User ${config.email}`);
245
+ console.log(` API key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
207
246
  console.log('');
208
247
  rl.prompt();
209
248
  return;
210
249
  }
211
250
 
212
251
  if (cmd === 'reload') {
213
- const reloaded = loadIntentContext();
214
- intentFiles.length = 0;
215
- intentFiles.push(...reloaded);
216
- console.log(` ${green('●')} Reloaded ${reloaded.length} artifact${reloaded.length !== 1 ? 's' : ''}`);
252
+ intentFiles = loadIntentContext();
253
+ systemPrompt = buildSystemPrompt(intentFiles);
254
+ console.log(` ${green('●')} Reloaded ${intentFiles.length} artifact${intentFiles.length !== 1 ? 's' : ''}`);
217
255
  rl.prompt();
218
256
  return;
219
257
  }
@@ -224,22 +262,182 @@ async function main() {
224
262
  return;
225
263
  }
226
264
 
227
- // Unknown slash command — treat as normal input
265
+ if (cmd === 'init') {
266
+ if (fs.existsSync(path.join(INTENT_DIR, 'vision.md'))) {
267
+ console.log(`\n ${g('.intent/ already exists in')} ${process.cwd()}`);
268
+ console.log(` ${g('Use /reload to refresh context')}\n`);
269
+ } else {
270
+ try {
271
+ // Delegate to the intent --init command
272
+ const { execSync } = require('child_process');
273
+ execSync('node ' + path.join(__dirname, 'intent.js') + ' --init', { stdio: 'inherit' });
274
+ // Reload context after init
275
+ intentFiles = loadIntentContext();
276
+ systemPrompt = buildSystemPrompt(intentFiles);
277
+ if (intentFiles.length > 0) {
278
+ console.log(` ${green('●')} Context loaded: ${intentFiles.map(f => f.file).join(', ')}`);
279
+ }
280
+ } catch (err) {
281
+ console.error(` ${g('Init failed:')} ${err.message}`);
282
+ }
283
+ }
284
+ console.log('');
285
+ rl.prompt();
286
+ return;
287
+ }
288
+
289
+ if (cmd === 'login') {
290
+ try {
291
+ const { execSync } = require('child_process');
292
+ execSync('node ' + path.join(__dirname, 'login.js'), { stdio: 'inherit' });
293
+ config = loadConfig(); // refresh after login
294
+ } catch (err) {
295
+ console.error(` ${g('Login failed:')} ${err.message}`);
296
+ }
297
+ rl.prompt();
298
+ return;
299
+ }
300
+
301
+ if (cmd === 'key') {
302
+ const keyRl = readline.createInterface({ input: process.stdin, output: process.stdout });
303
+ keyRl.question(`\n Anthropic API key\n > `, (apiKey) => {
304
+ keyRl.close();
305
+ apiKey = apiKey.trim();
306
+ if (apiKey) {
307
+ config = loadConfig() || {};
308
+ config.apiKey = apiKey;
309
+ saveConfig(config);
310
+ console.log(` ${green('●')} API key saved\n`);
311
+ } else {
312
+ console.log(` ${g('Cancelled')}\n`);
313
+ }
314
+ rl.prompt();
315
+ });
316
+ return;
317
+ }
318
+
319
+ if (cmd === 'models') {
320
+ console.log(`\n ${b('Available models')}\n`);
321
+ for (const [key, model] of Object.entries(MODELS)) {
322
+ const active = key === currentModel ? ` ${green('●')}` : '';
323
+ console.log(` ${w(key.padEnd(16))} ${g(model.name)}${active}`);
324
+ }
325
+ console.log(`\n ${g('Switch with:')} /model <name>\n`);
326
+ rl.prompt();
327
+ return;
328
+ }
329
+
330
+ if (cmd === 'model') {
331
+ if (!cmdArg) {
332
+ console.log(` ${g('Current:')} ${MODELS[currentModel].name}`);
333
+ console.log(` ${g('Usage:')} /model <sonnet|opus|haiku>`);
334
+ rl.prompt();
335
+ return;
336
+ }
337
+ // Fuzzy match model name
338
+ const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '');
339
+ const match = Object.keys(MODELS).find(k =>
340
+ k.includes(query) || MODELS[k].name.toLowerCase().includes(query)
341
+ );
342
+ if (match) {
343
+ currentModel = match;
344
+ console.log(` ${green('●')} Switched to ${MODELS[match].name}`);
345
+ } else {
346
+ console.log(` ${g('Unknown model. Available:')} ${Object.keys(MODELS).join(', ')}`);
347
+ }
348
+ rl.prompt();
349
+ return;
350
+ }
351
+
352
+ if (cmd === 'provider') {
353
+ const model = MODELS[currentModel];
354
+ console.log(`\n ${b('Provider')}`);
355
+ console.log(` API Anthropic (direct)`);
356
+ console.log(` Model ${model.name}`);
357
+ console.log(` Endpoint api.anthropic.com/v1/messages`);
358
+ console.log(` Key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
359
+ console.log('');
360
+ rl.prompt();
361
+ return;
362
+ }
363
+
364
+ if (cmd === 'run') {
365
+ if (!cmdArg) {
366
+ console.log(` ${g('Usage:')} /run <prompt>`);
367
+ rl.prompt();
368
+ return;
369
+ }
370
+ if (!config?.apiKey) {
371
+ console.log(` ${yellow('⚠')} No API key. Run /key to set one.`);
372
+ rl.prompt();
373
+ return;
374
+ }
375
+ // One-shot: don't add to conversation history
376
+ console.log('');
377
+ try {
378
+ const result = await streamChat(
379
+ config.apiKey,
380
+ [{ role: 'user', content: cmdArg }],
381
+ systemPrompt,
382
+ MODELS[currentModel].id
383
+ );
384
+ if (result.promptTokens || result.completionTokens) {
385
+ console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
386
+ }
387
+ trackSap({
388
+ userId: config.supabaseUserId,
389
+ source: 'cli',
390
+ model: MODELS[currentModel].id,
391
+ promptTokens: result.promptTokens,
392
+ completionTokens: result.completionTokens,
393
+ accessToken: config.supabaseAccessToken,
394
+ });
395
+ } catch (err) {
396
+ console.error(`\n ${err.message}\n`);
397
+ }
398
+ console.log('');
399
+ rl.prompt();
400
+ return;
401
+ }
402
+
403
+ // Unknown slash command
404
+ console.log(` ${g('Unknown command:')} /${cmd} ${g('— type /help for available commands')}`);
405
+ rl.prompt();
406
+ return;
228
407
  }
229
408
 
230
409
  // Regular input → send to AI
231
- messages.push({ role: 'user', content: input });
410
+ if (!config?.apiKey) {
411
+ console.log(`\n ${yellow('⚠')} No API key configured. Run /key to set one.\n`);
412
+ rl.prompt();
413
+ return;
414
+ }
232
415
 
416
+ messages.push({ role: 'user', content: input });
233
417
  console.log('');
234
418
 
235
419
  try {
236
- const result = await streamChat(config.apiKey, messages, systemPrompt, config);
420
+ const result = await streamChat(config.apiKey, messages, systemPrompt, MODELS[currentModel].id);
237
421
  messages.push({ role: 'assistant', content: result.content });
238
422
 
423
+ // Track totals
424
+ if (result.promptTokens) totalPromptTokens += result.promptTokens;
425
+ if (result.completionTokens) totalCompletionTokens += result.completionTokens;
426
+
239
427
  // Token count footer
240
428
  if (result.promptTokens || result.completionTokens) {
241
- console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
429
+ console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
242
430
  }
431
+
432
+ // SAP tracking (fire-and-forget)
433
+ trackSap({
434
+ userId: config.supabaseUserId,
435
+ source: 'cli',
436
+ model: MODELS[currentModel].id,
437
+ promptTokens: result.promptTokens,
438
+ completionTokens: result.completionTokens,
439
+ accessToken: config.supabaseAccessToken,
440
+ });
243
441
  } catch (err) {
244
442
  console.error(`\n ${err.message}\n`);
245
443
  // Remove the failed user message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"