phewsh 0.7.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
@@ -37,6 +37,7 @@ function showBrand() {
37
37
  }
38
38
 
39
39
  const COMMANDS = {
40
+ session: () => require('../commands/session'),
40
41
  intent: () => require('../commands/intent'),
41
42
  clarify: () => require('../commands/clarify'),
42
43
  push: () => require('../commands/push'),
@@ -59,13 +60,17 @@ function showHelp() {
59
60
  const pkg = require('../package.json');
60
61
  showBrand();
61
62
  console.log(` ${g('v' + pkg.version)} · ${g('phewsh.com')}\n`);
63
+ console.log(` ${b('Just type')} ${w('phewsh')} ${b('to start a session.')}`);
64
+ console.log(` ${g('Opens a persistent AI shell with your .intent/ context injected.')}`);
65
+ console.log('');
62
66
  console.log(` ${b('Commands')}`);
67
+ console.log(` ${w('(bare)')} Open persistent AI session — just type naturally`);
63
68
  console.log(` ${w('clarify')} Turn messy intent into a structured project spec`);
64
69
  console.log(` ${w('push')} Push local .intent/ to cloud`);
65
70
  console.log(` ${w('pull')} Pull project from cloud to .intent/`);
66
71
  console.log(` ${w('link')} Link local .intent/ to a cloud project`);
67
72
  console.log(` ${w('intent')} Manage .intent/ artifacts — status, open, evolve`);
68
- console.log(` ${w('ai')} Run context-aware AI prompts (reads .intent/)`);
73
+ console.log(` ${w('ai')} One-shot AI prompt (reads .intent/)`);
69
74
  console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
70
75
  console.log(` ${w('sap')} Sustainable AI Protocol — usage and accountability`);
71
76
  console.log(` ${w('style')} Build your style identity — ingest, profile, sync`);
@@ -73,9 +78,9 @@ function showHelp() {
73
78
  console.log('');
74
79
  console.log(` ${b('Quick start')}`);
75
80
  console.log(` ${g('phewsh login')} Set up identity + API key`);
81
+ console.log(` ${g('phewsh')} Open AI session (with .intent/ context)`);
76
82
  console.log(` ${g('phewsh clarify')} Compile messy intent → structured spec`);
77
- console.log(` ${g('phewsh push')} Sync to cloud`);
78
- console.log(` ${g('phewsh ai run "what\'s next?"')} AI with your project context`);
83
+ console.log(` ${g('phewsh ai run "what\'s next?"')} One-shot prompt`);
79
84
  console.log('');
80
85
  }
81
86
 
@@ -111,7 +116,11 @@ function exitAfterUpdate(code = 0) {
111
116
  setTimeout(() => process.exit(code), 2000);
112
117
  }
113
118
 
114
- if (!command || command === 'help' || command === '--help' || command === '-h') {
119
+ if (!command) {
120
+ // Bare `phewsh` — always drop into persistent session
121
+ // Session handles missing API key gracefully with /login and /key commands
122
+ COMMANDS.session();
123
+ } else if (command === 'help' || command === '--help' || command === '-h') {
115
124
  showHelp();
116
125
  exitAfterUpdate(0);
117
126
  } else if (command === 'version' || command === '--version' || command === '-v') {
@@ -0,0 +1,460 @@
1
+ // phewsh session — persistent agent shell
2
+ // Drops you into a REPL where you type naturally.
3
+ // Under the hood: routes to Claude, injects .intent/ context, tracks SAP.
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const readline = require('readline');
9
+ const { trackSap } = require('../lib/supabase');
10
+
11
+ const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
12
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
13
+ const INTENT_DIR = path.join(process.cwd(), '.intent');
14
+
15
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
16
+ const d = (s) => `\x1b[2m${s}\x1b[0m`;
17
+ const w = (s) => `\x1b[97m${s}\x1b[0m`;
18
+ const g = (s) => `\x1b[90m${s}\x1b[0m`;
19
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
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';
30
+
31
+ function loadConfig() {
32
+ if (!fs.existsSync(CONFIG_PATH)) return null;
33
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
34
+ }
35
+
36
+ function saveConfig(config) {
37
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
39
+ }
40
+
41
+ function loadIntentContext() {
42
+ const files = ['vision.md', 'plan.md', 'next.md'];
43
+ const loaded = [];
44
+ for (const file of files) {
45
+ const p = path.join(INTENT_DIR, file);
46
+ if (fs.existsSync(p)) {
47
+ loaded.push({ file, content: fs.readFileSync(p, 'utf-8') });
48
+ }
49
+ }
50
+ return loaded;
51
+ }
52
+
53
+ function buildSystemPrompt(intentFiles) {
54
+ const base = `You are PHEWSH — a focused execution assistant. You help the user think clearly, build intentionally, and ship without drift. Be concise, direct, and opinionated. Respond in plain text, not markdown, unless the user asks for formatted output.`;
55
+
56
+ if (intentFiles.length === 0) {
57
+ return base + `\n\nNo .intent/ artifacts found in the current directory. The user hasn't set up project context yet — help them think through what they're building if they ask.`;
58
+ }
59
+
60
+ const sections = intentFiles.map(({ file, content }) =>
61
+ `## ${file}\n\n${content.trim()}`
62
+ ).join('\n\n---\n\n');
63
+
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}`;
65
+ }
66
+
67
+ async function streamChat(apiKey, messages, systemPrompt, modelId) {
68
+ const body = { model: modelId, max_tokens: 2048, messages, stream: true };
69
+ if (systemPrompt) body.system = systemPrompt;
70
+
71
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
72
+ method: 'POST',
73
+ headers: {
74
+ 'x-api-key': apiKey,
75
+ 'anthropic-version': '2023-06-01',
76
+ 'content-type': 'application/json',
77
+ },
78
+ body: JSON.stringify(body),
79
+ });
80
+
81
+ if (!response.ok) {
82
+ const err = await response.json().catch(() => ({}));
83
+ throw new Error(err.error?.message || `API error ${response.status}`);
84
+ }
85
+
86
+ let fullResponse = '';
87
+ let promptTokens = null;
88
+ let completionTokens = null;
89
+
90
+ for await (const chunk of response.body) {
91
+ const text = Buffer.from(chunk).toString('utf-8');
92
+ const lines = text.split('\n').filter(l => l.startsWith('data: '));
93
+ for (const line of lines) {
94
+ const data = line.slice(6);
95
+ if (data === '[DONE]') continue;
96
+ try {
97
+ const parsed = JSON.parse(data);
98
+ if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
99
+ process.stdout.write(parsed.delta.text);
100
+ fullResponse += parsed.delta.text;
101
+ }
102
+ if (parsed.type === 'message_start' && parsed.message?.usage) {
103
+ promptTokens = parsed.message.usage.input_tokens;
104
+ }
105
+ if (parsed.type === 'message_delta' && parsed.usage) {
106
+ completionTokens = parsed.usage.output_tokens;
107
+ }
108
+ } catch { /* skip */ }
109
+ }
110
+ }
111
+
112
+ process.stdout.write('\n');
113
+
114
+ return { content: fullResponse, promptTokens, completionTokens, model: modelId };
115
+ }
116
+
117
+ async function main() {
118
+ let config = loadConfig();
119
+ let intentFiles = loadIntentContext();
120
+ let systemPrompt = buildSystemPrompt(intentFiles);
121
+ const messages = []; // conversation history
122
+ const projectName = path.basename(process.cwd());
123
+ let currentModel = DEFAULT_MODEL;
124
+ let totalPromptTokens = 0;
125
+ let totalCompletionTokens = 0;
126
+
127
+ // Session banner with PHEWSH branding
128
+ console.log('');
129
+ console.log(` ${d('😮‍💨')} ${d('🤫')}`);
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
+
142
+ if (intentFiles.length > 0) {
143
+ console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
144
+ } else {
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}`);
151
+ }
152
+ console.log('');
153
+ console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
154
+ console.log('');
155
+
156
+ const rl = readline.createInterface({
157
+ input: process.stdin,
158
+ output: process.stdout,
159
+ prompt: ` ${green('>')} `,
160
+ historySize: 100,
161
+ });
162
+
163
+ rl.prompt();
164
+
165
+ rl.on('line', async (line) => {
166
+ const input = line.trim();
167
+
168
+ if (!input) {
169
+ rl.prompt();
170
+ return;
171
+ }
172
+
173
+ // Slash commands
174
+ if (input.startsWith('/')) {
175
+ const parts = input.slice(1).split(/\s+/);
176
+ const cmd = parts[0].toLowerCase();
177
+ const cmdArg = parts.slice(1).join(' ');
178
+
179
+ if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
180
+ const turns = messages.length / 2;
181
+ console.log(`\n ${g('Session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}\n`);
182
+ process.exit(0);
183
+ }
184
+
185
+ if (cmd === 'help' || cmd === 'h') {
186
+ console.log(`
187
+ ${b('Session commands')}
188
+
189
+ ${w('conversation')}
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
196
+ ${g('/context')} Show loaded .intent/ files
197
+ ${g('/reload')} Reload .intent/ context from disk
198
+ ${g('/status')} Show session stats
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
209
+ `);
210
+ rl.prompt();
211
+ return;
212
+ }
213
+
214
+ if (cmd === 'clear') {
215
+ messages.length = 0;
216
+ console.log(` ${g('conversation cleared')}`);
217
+ rl.prompt();
218
+ return;
219
+ }
220
+
221
+ if (cmd === 'context') {
222
+ if (intentFiles.length > 0) {
223
+ console.log(`\n Loaded from ${cyan('.intent/')}:`);
224
+ intentFiles.forEach(f => console.log(` ${green('●')} ${f.file} ${g('(' + f.content.length + ' chars)')}`));
225
+ } else {
226
+ console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
227
+ console.log(` ${g('Run')} /init ${g('to create one')}`);
228
+ }
229
+ console.log('');
230
+ rl.prompt();
231
+ return;
232
+ }
233
+
234
+ if (cmd === 'status') {
235
+ const turns = messages.length / 2;
236
+ config = loadConfig(); // refresh
237
+ console.log(`\n ${b('Session')}`);
238
+ console.log(` Turns ${turns}`);
239
+ console.log(` Tokens ${totalPromptTokens}→${totalCompletionTokens} (in→out)`);
240
+ console.log(` Project ${projectName}`);
241
+ console.log(` Context ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none'}`);
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')}`);
246
+ console.log('');
247
+ rl.prompt();
248
+ return;
249
+ }
250
+
251
+ if (cmd === 'reload') {
252
+ intentFiles = loadIntentContext();
253
+ systemPrompt = buildSystemPrompt(intentFiles);
254
+ console.log(` ${green('●')} Reloaded ${intentFiles.length} artifact${intentFiles.length !== 1 ? 's' : ''}`);
255
+ rl.prompt();
256
+ return;
257
+ }
258
+
259
+ if (cmd === 'system') {
260
+ console.log(`\n${g(systemPrompt)}\n`);
261
+ rl.prompt();
262
+ return;
263
+ }
264
+
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;
407
+ }
408
+
409
+ // Regular input → send to AI
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
+ }
415
+
416
+ messages.push({ role: 'user', content: input });
417
+ console.log('');
418
+
419
+ try {
420
+ const result = await streamChat(config.apiKey, messages, systemPrompt, MODELS[currentModel].id);
421
+ messages.push({ role: 'assistant', content: result.content });
422
+
423
+ // Track totals
424
+ if (result.promptTokens) totalPromptTokens += result.promptTokens;
425
+ if (result.completionTokens) totalCompletionTokens += result.completionTokens;
426
+
427
+ // Token count footer
428
+ if (result.promptTokens || result.completionTokens) {
429
+ console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
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
+ });
441
+ } catch (err) {
442
+ console.error(`\n ${err.message}\n`);
443
+ // Remove the failed user message
444
+ messages.pop();
445
+ }
446
+
447
+ console.log('');
448
+ rl.prompt();
449
+ });
450
+
451
+ rl.on('close', () => {
452
+ console.log(`\n ${g('Session ended')}\n`);
453
+ process.exit(0);
454
+ });
455
+ }
456
+
457
+ main().catch(err => {
458
+ console.error('\n Error:', err.message);
459
+ process.exit(1);
460
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.7.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"