phewsh 0.6.1 → 0.8.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,16 +78,17 @@ 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
 
82
- // Non-blocking update check
87
+ // Non-blocking update check — resolves true if update found
88
+ let _updateDone = false;
83
89
  function checkForUpdates() {
84
90
  const pkg = require('../package.json');
85
- fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(3000) })
91
+ return fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(3000) })
86
92
  .then(r => r.json())
87
93
  .then(data => {
88
94
  if (data.version && data.version !== pkg.version) {
@@ -91,25 +97,50 @@ function checkForUpdates() {
91
97
  const isNewer = newer[0] > current[0] || newer[1] > current[1] || newer[2] > current[2];
92
98
  if (isNewer) {
93
99
  console.log(g(`\n Update available: ${pkg.version} → ${data.version}`));
94
- console.log(g(` npm install -g phewsh\n`));
100
+ console.log(g(` Run: npm install -g phewsh\n`));
95
101
  }
96
102
  }
97
103
  })
98
- .catch(() => {});
104
+ .catch(() => {})
105
+ .finally(() => { _updateDone = true; });
99
106
  }
100
107
 
101
- if (!command || command === 'help' || command === '--help' || command === '-h') {
102
- showHelp();
103
- process.exit(0);
104
- }
108
+ // Always check for updates (non-blocking)
109
+ const updatePromise = checkForUpdates();
105
110
 
106
- if (command === 'version' || command === '--version' || command === '-v') {
107
- showVersion();
108
- process.exit(0);
111
+ function exitAfterUpdate(code = 0) {
112
+ // If update check already resolved, exit now
113
+ if (_updateDone) return process.exit(code);
114
+ // Otherwise wait up to 2s for it to finish
115
+ updatePromise.then(() => process.exit(code));
116
+ setTimeout(() => process.exit(code), 2000);
109
117
  }
110
118
 
111
- if (COMMANDS[command]) {
112
- checkForUpdates();
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
+ }
137
+ } else if (command === 'help' || command === '--help' || command === '-h') {
138
+ showHelp();
139
+ exitAfterUpdate(0);
140
+ } else if (command === 'version' || command === '--version' || command === '-v') {
141
+ showVersion();
142
+ exitAfterUpdate(0);
143
+ } else if (COMMANDS[command]) {
113
144
  COMMANDS[command]();
114
145
  } else {
115
146
  console.error(`\n Unknown command: ${command}\n Run 'phewsh help' for available commands.\n`);
@@ -0,0 +1,262 @@
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_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
12
+ const INTENT_DIR = path.join(process.cwd(), '.intent');
13
+ const HISTORY_PATH = path.join(os.homedir(), '.phewsh', 'session_history.json');
14
+
15
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
16
+ const d = (s) => `\x1b[2m${s}\x1b[0m`;
17
+ const g = (s) => `\x1b[90m${s}\x1b[0m`;
18
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
19
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
20
+
21
+ function loadConfig() {
22
+ if (!fs.existsSync(CONFIG_PATH)) return null;
23
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
24
+ }
25
+
26
+ function loadIntentContext() {
27
+ const files = ['vision.md', 'plan.md', 'next.md'];
28
+ const loaded = [];
29
+ for (const file of files) {
30
+ const p = path.join(INTENT_DIR, file);
31
+ if (fs.existsSync(p)) {
32
+ loaded.push({ file, content: fs.readFileSync(p, 'utf-8') });
33
+ }
34
+ }
35
+ return loaded;
36
+ }
37
+
38
+ function buildSystemPrompt(intentFiles) {
39
+ 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.`;
40
+
41
+ if (intentFiles.length === 0) {
42
+ 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.`;
43
+ }
44
+
45
+ const sections = intentFiles.map(({ file, content }) =>
46
+ `## ${file}\n\n${content.trim()}`
47
+ ).join('\n\n---\n\n');
48
+
49
+ 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
+ }
51
+
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 };
55
+ if (systemPrompt) body.system = systemPrompt;
56
+
57
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
58
+ method: 'POST',
59
+ headers: {
60
+ 'x-api-key': apiKey,
61
+ 'anthropic-version': '2023-06-01',
62
+ 'content-type': 'application/json',
63
+ },
64
+ body: JSON.stringify(body),
65
+ });
66
+
67
+ if (!response.ok) {
68
+ const err = await response.json().catch(() => ({}));
69
+ throw new Error(err.error?.message || `API error ${response.status}`);
70
+ }
71
+
72
+ let fullResponse = '';
73
+ let promptTokens = null;
74
+ let completionTokens = null;
75
+
76
+ for await (const chunk of response.body) {
77
+ const text = Buffer.from(chunk).toString('utf-8');
78
+ const lines = text.split('\n').filter(l => l.startsWith('data: '));
79
+ for (const line of lines) {
80
+ const data = line.slice(6);
81
+ if (data === '[DONE]') continue;
82
+ try {
83
+ const parsed = JSON.parse(data);
84
+ if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
85
+ process.stdout.write(parsed.delta.text);
86
+ fullResponse += parsed.delta.text;
87
+ }
88
+ if (parsed.type === 'message_start' && parsed.message?.usage) {
89
+ promptTokens = parsed.message.usage.input_tokens;
90
+ }
91
+ if (parsed.type === 'message_delta' && parsed.usage) {
92
+ completionTokens = parsed.usage.output_tokens;
93
+ }
94
+ } catch { /* skip */ }
95
+ }
96
+ }
97
+
98
+ process.stdout.write('\n');
99
+
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 };
111
+ }
112
+
113
+ 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);
124
+ const messages = []; // conversation history
125
+ const projectName = path.basename(process.cwd());
126
+
127
+ // Session banner
128
+ console.log('');
129
+ console.log(` ${d('😮‍💨')} ${d('🤫')}`);
130
+ console.log('');
131
+ if (intentFiles.length > 0) {
132
+ console.log(` ${green('●')} Session started ${g('·')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
133
+ } else {
134
+ console.log(` ${green('●')} Session started ${g('·')} ${cyan(projectName)} ${g('·')} no .intent/ context`);
135
+ }
136
+ console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
137
+ console.log('');
138
+
139
+ const rl = readline.createInterface({
140
+ input: process.stdin,
141
+ output: process.stdout,
142
+ prompt: ` ${green('>')} `,
143
+ historySize: 100,
144
+ });
145
+
146
+ rl.prompt();
147
+
148
+ rl.on('line', async (line) => {
149
+ const input = line.trim();
150
+
151
+ if (!input) {
152
+ rl.prompt();
153
+ return;
154
+ }
155
+
156
+ // Slash commands
157
+ if (input.startsWith('/')) {
158
+ const cmd = input.slice(1).split(' ')[0].toLowerCase();
159
+
160
+ if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
161
+ console.log(`\n ${g('Session ended · ' + messages.length / 2 + ' exchanges')}\n`);
162
+ process.exit(0);
163
+ }
164
+
165
+ if (cmd === 'help') {
166
+ console.log(`
167
+ ${b('Session commands')}
168
+
169
+ ${g('/quit')} End session
170
+ ${g('/clear')} Clear conversation history
171
+ ${g('/context')} Show loaded .intent/ files
172
+ ${g('/status')} Show session stats
173
+ ${g('/reload')} Reload .intent/ context
174
+ ${g('/system')} Show system prompt (debug)
175
+ `);
176
+ rl.prompt();
177
+ return;
178
+ }
179
+
180
+ if (cmd === 'clear') {
181
+ messages.length = 0;
182
+ console.log(` ${g('conversation cleared')}`);
183
+ rl.prompt();
184
+ return;
185
+ }
186
+
187
+ if (cmd === 'context') {
188
+ if (intentFiles.length > 0) {
189
+ console.log(`\n Loaded from ${cyan('.intent/')}:`);
190
+ intentFiles.forEach(f => console.log(` ${green('●')} ${f.file} ${g('(' + f.content.length + ' chars)')}`));
191
+ } else {
192
+ console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
193
+ console.log(` ${g('Run')} phewsh clarify ${g('to create one')}`);
194
+ }
195
+ console.log('');
196
+ rl.prompt();
197
+ return;
198
+ }
199
+
200
+ if (cmd === 'status') {
201
+ const turns = messages.length / 2;
202
+ console.log(`\n ${b('Session')}`);
203
+ console.log(` Turns ${turns}`);
204
+ console.log(` Project ${projectName}`);
205
+ console.log(` Context ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none'}`);
206
+ console.log(` Provider anthropic (claude-sonnet-4-6)`);
207
+ console.log('');
208
+ rl.prompt();
209
+ return;
210
+ }
211
+
212
+ 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' : ''}`);
217
+ rl.prompt();
218
+ return;
219
+ }
220
+
221
+ if (cmd === 'system') {
222
+ console.log(`\n${g(systemPrompt)}\n`);
223
+ rl.prompt();
224
+ return;
225
+ }
226
+
227
+ // Unknown slash command — treat as normal input
228
+ }
229
+
230
+ // Regular input → send to AI
231
+ messages.push({ role: 'user', content: input });
232
+
233
+ console.log('');
234
+
235
+ try {
236
+ const result = await streamChat(config.apiKey, messages, systemPrompt, config);
237
+ messages.push({ role: 'assistant', content: result.content });
238
+
239
+ // Token count footer
240
+ if (result.promptTokens || result.completionTokens) {
241
+ console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
242
+ }
243
+ } catch (err) {
244
+ console.error(`\n ${err.message}\n`);
245
+ // Remove the failed user message
246
+ messages.pop();
247
+ }
248
+
249
+ console.log('');
250
+ rl.prompt();
251
+ });
252
+
253
+ rl.on('close', () => {
254
+ console.log(`\n ${g('Session ended')}\n`);
255
+ process.exit(0);
256
+ });
257
+ }
258
+
259
+ main().catch(err => {
260
+ console.error('\n Error:', err.message);
261
+ process.exit(1);
262
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"