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 +47 -16
- package/commands/session.js +262 -0
- package/package.json +1 -1
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')}
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
process.exit(0);
|
|
104
|
-
}
|
|
108
|
+
// Always check for updates (non-blocking)
|
|
109
|
+
const updatePromise = checkForUpdates();
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
process.exit(
|
|
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 (
|
|
112
|
-
|
|
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
|
+
});
|