phewsh 0.11.15 → 0.12.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/README.md CHANGED
@@ -65,9 +65,26 @@ phewsh login # Authenticate + set API key
65
65
  phewsh push # Sync local .intent/ to cloud
66
66
  phewsh pull # Sync cloud to local
67
67
  phewsh mcp setup # Configure MCP server for agent connectivity
68
+ phewsh mcp serve # Coordination bridge — route work to live agents
69
+ phewsh receipts # Proof trail — what agents actually did, with evidence
68
70
  phewsh style # Build your style identity
69
71
  ```
70
72
 
73
+ ## Receipts
74
+
75
+ Every task that runs through PHEWSH leaves evidence on your machine —
76
+ sessions, results, blockers, gate checks, spend — in `~/.phewsh/`. See it:
77
+
78
+ ```bash
79
+ phewsh receipts # merged timeline, newest first
80
+ phewsh receipts --json # machine-readable, for agents and scripts
81
+ ```
82
+
83
+ The same trail is visible at [phewsh.com/intent/receipts](https://phewsh.com/intent/receipts)
84
+ while a local bridge is running (`phewsh serve` or `phewsh mcp serve` — the
85
+ first executes tasks directly via Claude Code, the second routes them to
86
+ live connected agents; both record identical receipts).
87
+
71
88
  ## Sync
72
89
 
73
90
  CLI and web ([phewsh.com/intent](https://phewsh.com/intent)) share the same cloud via Supabase.
package/bin/phewsh.js CHANGED
@@ -60,9 +60,13 @@ const COMMANDS = {
60
60
  context: () => require('../commands/context'),
61
61
  gate: () => require('../commands/gate'),
62
62
  sap: () => require('../commands/sap'),
63
+ browse: () => require('../commands/browse'),
63
64
  watch: () => require('../commands/watch')(),
64
65
  mcp: () => require('../commands/mcp')(),
66
+ receipts: () => require('../commands/receipts')(),
65
67
  serve: () => require('../commands/serve')(),
68
+ sequence: () => require('../commands/sequence')(),
69
+ seq: () => require('../commands/sequence')(),
66
70
  help: showHelp,
67
71
  version: showVersion,
68
72
  };
@@ -85,13 +89,16 @@ function showHelp() {
85
89
  console.log(` ${cyan('intent')} ${g('Create, view, evolve .intent/ artifacts')}`);
86
90
  console.log(` ${cyan('gate')} ${g('Set constraints (budget, time, skill, urgency)')}`);
87
91
  console.log(` ${cyan('context')} ${g('Export .intent/ for any AI tool')}`);
88
- console.log(` ${cyan('ai')} ${g('One-shot prompt with .intent/ context')}`);
92
+ console.log(` ${cyan('ai')} ${g('One-shot prompt with .intent/ context')}
93
+ ${cyan('browse')} ${g('Read any URL — AI summary in your terminal')}`);
89
94
  console.log('');
90
95
  console.log(` ${b(w('sync everywhere'))}`);
96
+ console.log(` ${cyan('seq')} ${g('Sequence all memory → optimal context for any agent')}`);
91
97
  console.log(` ${cyan('watch')} ${g('Auto-sync .intent/ → CLAUDE.md + cloud')}`);
92
98
  console.log(` ${cyan('push/pull')} ${g('Manual sync to/from phewsh.com/intent')}`);
93
99
  console.log(` ${cyan('serve')} ${g('Execution bridge — run from phewsh.com/intent')}`);
94
100
  console.log(` ${cyan('mcp')} ${g('Connect AI agents via MCP protocol')}`);
101
+ console.log(` ${cyan('receipts')} ${g('Proof trail — what agents actually did, with evidence')}`);
95
102
  console.log('');
96
103
  console.log(` ${b(w('configure'))}`);
97
104
  console.log(` ${cyan('login')} ${g('Identity + API key + cloud sync')}`);
@@ -112,7 +119,12 @@ function checkForUpdates() {
112
119
  if (data.version && data.version !== pkg.version) {
113
120
  const newer = data.version.split('.').map(Number);
114
121
  const current = pkg.version.split('.').map(Number);
115
- const isNewer = newer[0] > current[0] || newer[1] > current[1] || newer[2] > current[2];
122
+ // Compare major, then minor, then patch a later slot only matters
123
+ // when all earlier slots are equal (0.11.16 is NOT newer than 0.12.0).
124
+ const isNewer =
125
+ newer[0] !== current[0] ? newer[0] > current[0] :
126
+ newer[1] !== current[1] ? newer[1] > current[1] :
127
+ newer[2] > current[2];
116
128
  if (isNewer) {
117
129
  console.log(g(`\n Update available: ${pkg.version} → ${data.version}`));
118
130
  console.log(g(` Run: npm install -g phewsh\n`));
package/commands/ai.js CHANGED
@@ -2,6 +2,10 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const { trackSap } = require('../lib/supabase');
5
+ const {
6
+ getProvider, listProviders, detectProvider,
7
+ buildHeaders, buildBody, getUrl, streamParser,
8
+ } = require('../lib/providers');
5
9
 
6
10
  const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
7
11
  const INTENT_DIR = path.join(process.cwd(), '.intent');
@@ -36,91 +40,183 @@ function buildSystemPrompt(intentFiles) {
36
40
  return `You are a focused execution assistant. The user has structured intent artifacts that define what they are building. Use these as your primary context for every response — stay aligned with their vision, plan, and next actions.\n\n${sections}`;
37
41
  }
38
42
 
39
- async function streamResponse(apiKey, systemPrompt, userPrompt, userId, accessToken) {
40
- const model = 'claude-sonnet-4-6';
41
- const messages = [{ role: 'user', content: userPrompt }];
42
- const body = { model, max_tokens: 1024, messages, stream: true };
43
- if (systemPrompt) body.system = systemPrompt;
43
+ function parseFlags(args) {
44
+ const flags = {};
45
+ for (let i = 0; i < args.length; i++) {
46
+ if (args[i] === '--provider' || args[i] === '-p') {
47
+ flags.provider = args[i + 1];
48
+ i++;
49
+ } else if (args[i] === '--model' || args[i] === '-m') {
50
+ flags.model = args[i + 1];
51
+ i++;
52
+ }
53
+ }
54
+ // Strip flags from args to get the prompt
55
+ const clean = [];
56
+ for (let i = 0; i < args.length; i++) {
57
+ if ((args[i] === '--provider' || args[i] === '-p' || args[i] === '--model' || args[i] === '-m') && args[i + 1]) {
58
+ i++; // skip value
59
+ } else {
60
+ clean.push(args[i]);
61
+ }
62
+ }
63
+ return { flags, clean };
64
+ }
65
+
66
+ function resolveProvider(config, flagProvider) {
67
+ const providerName = flagProvider || config.defaultProvider || 'anthropic';
68
+ return getProvider(providerName);
69
+ }
70
+
71
+ function readProjectId() {
72
+ try {
73
+ const p = path.join(INTENT_DIR, 'project.json');
74
+ if (!fs.existsSync(p)) return null;
75
+ const j = JSON.parse(fs.readFileSync(p, 'utf-8'));
76
+ return j.id || null;
77
+ } catch { return null; }
78
+ }
44
79
 
45
- const response = await fetch('https://api.anthropic.com/v1/messages', {
80
+ function resolveApiKey(config, provider) {
81
+ // PHEWSH gateway authenticates with your phewsh JWT (phewsh login --token),
82
+ // not a BYOK provider key.
83
+ if (provider.id === 'phewsh') return config.supabaseAccessToken || null;
84
+ // Check provider-specific key first, then fall back to generic apiKey
85
+ const providerKey = config.providerKeys?.[provider.id];
86
+ if (providerKey) return providerKey;
87
+ // Check env var
88
+ if (provider.keyEnvVar && process.env[provider.keyEnvVar]) return process.env[provider.keyEnvVar];
89
+ // Fall back to generic key if provider matches or can be auto-detected
90
+ if (config.apiKey) {
91
+ const detected = detectProvider(config.apiKey);
92
+ if (!detected || detected === provider.id) return config.apiKey;
93
+ }
94
+ if (provider.noKey) return null; // ollama doesn't need a key
95
+ return null;
96
+ }
97
+
98
+ async function streamResponse(config, provider, model, systemPrompt, userPrompt) {
99
+ const apiKey = resolveApiKey(config, provider);
100
+ if (!apiKey && !provider.noKey) {
101
+ if (provider.id === 'phewsh') {
102
+ throw new Error('Not logged in. Run `phewsh login --token <jwt>` (get it at phewsh.com/intent → Settings → CLI Access) to use pooled credits.');
103
+ }
104
+ throw new Error(`No API key for ${provider.name}. Run \`phewsh login --set-key\` or set ${provider.keyEnvVar}.`);
105
+ }
106
+
107
+ const url = getUrl(provider, config);
108
+ const headers = buildHeaders(provider, apiKey);
109
+ // Route through the Decision Gate: tell the gateway which project this spend
110
+ // belongs to, so a budget set on that project is enforced server-side.
111
+ if (provider.id === 'phewsh') {
112
+ const projectId = readProjectId();
113
+ if (projectId) headers['x-phewsh-project'] = projectId;
114
+ }
115
+ const body = buildBody(provider, model, systemPrompt, userPrompt);
116
+
117
+ const response = await fetch(url, {
46
118
  method: 'POST',
47
- headers: {
48
- 'x-api-key': apiKey,
49
- 'anthropic-version': '2023-06-01',
50
- 'content-type': 'application/json',
51
- },
119
+ headers,
52
120
  body: JSON.stringify(body),
53
121
  });
54
122
 
55
123
  if (!response.ok) {
56
124
  const err = await response.json().catch(() => ({}));
57
- throw new Error(err.error?.message || `API error ${response.status}`);
125
+ const msg = err.error?.message || err.message || `API error ${response.status}`;
126
+ throw new Error(`${provider.name}: ${msg}`);
58
127
  }
59
128
 
60
129
  process.stdout.write('\n');
61
130
 
131
+ const parse = streamParser(provider);
62
132
  let promptTokens = null;
63
133
  let completionTokens = null;
64
134
 
65
- for await (const chunk of response.body) {
66
- const text = Buffer.from(chunk).toString('utf-8');
67
- const lines = text.split('\n').filter(l => l.startsWith('data: '));
68
- for (const line of lines) {
69
- const data = line.slice(6);
70
- if (data === '[DONE]') continue;
71
- try {
72
- const parsed = JSON.parse(data);
73
- if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
74
- process.stdout.write(parsed.delta.text);
75
- }
76
- // Capture token counts from Anthropic stream
77
- if (parsed.type === 'message_start' && parsed.message?.usage) {
78
- promptTokens = parsed.message.usage.input_tokens;
79
- }
80
- if (parsed.type === 'message_delta' && parsed.usage) {
81
- completionTokens = parsed.usage.output_tokens;
82
- }
83
- } catch { /* skip malformed chunks */ }
135
+ for await (const event of parse(response)) {
136
+ if (event.type === 'text') {
137
+ process.stdout.write(event.text);
138
+ } else if (event.type === 'usage') {
139
+ promptTokens = event.promptTokens;
140
+ completionTokens = event.completionTokens;
84
141
  }
85
142
  }
86
143
 
87
144
  process.stdout.write('\n\n');
88
145
 
89
- // SAP: fire-and-forget after stream completes
90
- trackSap({ userId, source: 'cli', model, promptTokens, completionTokens, accessToken });
146
+ // SAP tracking
147
+ trackSap({
148
+ userId: config.supabaseUserId,
149
+ source: 'cli',
150
+ model,
151
+ promptTokens,
152
+ completionTokens,
153
+ accessToken: config.supabaseAccessToken,
154
+ });
91
155
  }
92
156
 
93
157
  async function main() {
94
158
  if (!subcommand || subcommand === '--help' || subcommand === '-h') {
95
159
  console.log(`
96
- 😮‍💨🤫 phewsh ai
160
+ phewsh ai
97
161
 
98
162
  Usage:
99
163
  phewsh ai run "<prompt>" Run a prompt with .intent/ context injected
100
164
  phewsh ai status Show current config and context
165
+ phewsh ai providers List available AI providers
166
+
167
+ Options:
168
+ -p, --provider <name> Override provider (e.g. openrouter, groq, ollama)
169
+ -m, --model <model> Override model
101
170
 
102
171
  Examples:
103
172
  phewsh ai run "what should I build next?"
104
- phewsh ai run "write a release checklist for the current plan"
105
- phewsh ai run "summarize where I am and what's blocked"
173
+ phewsh ai run -p openrouter "compare these approaches"
174
+ phewsh ai run -p groq -m llama-3.3-70b-versatile "quick question"
175
+ phewsh ai run -p ollama "local inference, no API key needed"
106
176
  `);
107
177
  return;
108
178
  }
109
179
 
180
+ if (subcommand === 'providers') {
181
+ const config = loadConfig() || {};
182
+ console.log('\n Available providers:\n');
183
+ for (const p of listProviders()) {
184
+ const isDefault = (config.defaultProvider || 'anthropic') === p.id;
185
+ const hasKey = !!(config.providerKeys?.[p.id] || (p.keyEnvVar && process.env[p.keyEnvVar]));
186
+ const keyStatus = p.id === 'phewsh'
187
+ ? (config.supabaseAccessToken ? 'logged in' : 'run: phewsh login --token')
188
+ : (p.noKey ? 'no key needed' : (hasKey ? 'key set' : 'no key'));
189
+ const marker = isDefault ? ' (default)' : '';
190
+ console.log(` ${p.id.padEnd(12)} ${p.name.padEnd(26)} ${keyStatus}${marker}`);
191
+ console.log(` ${''.padEnd(12)} model: ${p.defaultModel || '(configure)'}`);
192
+ if (p.docs) console.log(` ${''.padEnd(12)} ${p.docs}`);
193
+ console.log('');
194
+ }
195
+ return;
196
+ }
197
+
110
198
  if (subcommand === 'status') {
111
199
  const config = loadConfig();
112
200
  const intentFiles = loadIntentContext();
201
+ const providerName = config?.defaultProvider || 'anthropic';
202
+ let provider;
203
+ try { provider = getProvider(providerName); } catch { provider = { name: providerName }; }
113
204
  console.log('\n phewsh ai — status\n');
114
- console.log(` Config ${config ? 'found' : 'not found — run `phewsh login`'}`);
115
- console.log(` API key ${config?.apiKey ? '✓ set' : '✗ not set'}`);
116
- console.log(` Provider ${config?.defaultProvider || 'anthropic'}`);
117
- console.log(` .intent/ ${intentFiles.length > 0 ? `✓ ${intentFiles.map(f => f.file).join(', ')}` : '✗ none found'}`);
205
+ console.log(` Config ${config ? 'found' : 'not found — run \`phewsh login\`'}`);
206
+ console.log(` Provider ${provider.name} (${providerName})`);
207
+ console.log(` API key ${config?.apiKey || config?.providerKeys?.[providerName] ? 'set' : 'not set'}`);
208
+ if (config?.providerKeys) {
209
+ const configured = Object.keys(config.providerKeys).filter(k => config.providerKeys[k]);
210
+ if (configured.length > 0) console.log(` Keys for ${configured.join(', ')}`);
211
+ }
212
+ console.log(` .intent/ ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none found'}`);
118
213
  console.log('');
119
214
  return;
120
215
  }
121
216
 
122
217
  if (subcommand === 'run') {
123
- const prompt = args.slice(1).join(' ');
218
+ const { flags, clean } = parseFlags(args.slice(1));
219
+ const prompt = clean.join(' ');
124
220
  if (!prompt) {
125
221
  console.error('\n Usage: phewsh ai run "<your prompt>"\n');
126
222
  process.exit(1);
@@ -131,8 +227,14 @@ async function main() {
131
227
  console.error('\n Not logged in. Run `phewsh login` first.\n');
132
228
  process.exit(1);
133
229
  }
134
- if (!config.apiKey) {
135
- console.error('\n No API key set. Run `phewsh login --set-key` to add one.\n');
230
+
231
+ const provider = resolveProvider(config, flags.provider);
232
+ const model = flags.model || config.providerModels?.[provider.id] || provider.defaultModel;
233
+
234
+ const apiKey = resolveApiKey(config, provider);
235
+ if (!apiKey && !provider.noKey) {
236
+ console.error(`\n No API key for ${provider.name}.`);
237
+ console.error(` Run \`phewsh login --set-key\` or set ${provider.keyEnvVar}.\n`);
136
238
  process.exit(1);
137
239
  }
138
240
 
@@ -144,8 +246,9 @@ async function main() {
144
246
  } else {
145
247
  console.log('\n No .intent/ found — running without project context');
146
248
  }
249
+ console.log(` Provider: ${provider.name} | Model: ${model}`);
147
250
 
148
- await streamResponse(config.apiKey, systemPrompt, prompt, config.supabaseUserId, config.supabaseAccessToken);
251
+ await streamResponse(config, provider, model, systemPrompt, prompt);
149
252
  return;
150
253
  }
151
254