phewsh 0.15.15 → 0.15.19

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
@@ -30,7 +30,7 @@ function showBrand() {
30
30
  console.log('');
31
31
  console.log(` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
32
32
  console.log(` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
33
- console.log(` ${g('.intent/ your project\'s working memory for every AI tool.')}`);
33
+ console.log(` ${g('Keep all your AI tools. phewsh is the one memory they share.')}`);
34
34
  console.log('');
35
35
 
36
36
  // Context-aware hint
@@ -49,7 +49,7 @@ function showBrand() {
49
49
  const COMMANDS = {
50
50
  session: () => require('../commands/session'),
51
51
  intent: () => require('../commands/intent'),
52
- clarify: () => require('../commands/clarify'),
52
+ clarify: () => require('../commands/clarify').run(),
53
53
  push: () => require('../commands/push'),
54
54
  pull: () => require('../commands/pull'),
55
55
  link: () => require('../commands/link'),
@@ -45,10 +45,52 @@ async function askForInput() {
45
45
  });
46
46
  }
47
47
 
48
- async function callClarifyAPI(apiKey, raw, existing) {
49
- const isRefine = !!(existing?.intent?.goal);
48
+ // The guided walk the five strongest nodes of the web's 12-node Intent
49
+ // Compass, asked one at a time. The web compass helps the user *see* what
50
+ // they're building; this brings that to the terminal. Not a form: every
51
+ // question is skippable, and the point is to help you think, not interrogate.
52
+ const GUIDE_NODES = [
53
+ { id: 'purpose', title: 'Purpose', directive: 'the core reason this exists',
54
+ q: 'What outcome are you really after — and why does this need to exist?' },
55
+ { id: 'audience', title: 'Audience', directive: 'the people this serves',
56
+ q: 'Who is this for? Who feels it most when it works?' },
57
+ { id: 'method', title: 'Method', directive: 'the mechanism and approach',
58
+ q: 'How does it actually work — the core mechanism or approach?' },
59
+ { id: 'scope', title: 'Scope', directive: 'boundaries, in and out',
60
+ q: "What's in — and just as important, what's deliberately out, for now?" },
61
+ { id: 'differentiation', title: 'Edge', directive: 'what makes this yours',
62
+ q: 'What would be lost if someone else built this instead of you?' },
63
+ ];
64
+
65
+ function ask(rl, question) {
66
+ return new Promise((resolve) => rl.question(question, (a) => resolve((a || '').trim())));
67
+ }
50
68
 
51
- const systemPrompt = `You are a project compiler. Your job is to extract clean, structured intent from messy human input.
69
+ // rl is injectable so the walk can be driven deterministically in tests.
70
+ async function askGuided(rl = readline.createInterface({ input: process.stdin, output: process.stdout })) {
71
+ console.log('\n Five quick questions to align your own thinking first —');
72
+ console.log(' a sentence or two each. Blank skips. (esc stops, nothing saved.)\n');
73
+ const answers = [];
74
+ for (let i = 0; i < GUIDE_NODES.length; i++) {
75
+ const n = GUIDE_NODES[i];
76
+ console.log(` ${i + 1}/${GUIDE_NODES.length} ${n.title} — ${n.directive}`);
77
+ const a = await ask(rl, ` ${n.q}\n > `);
78
+ if (a) answers.push({ ...n, answer: a });
79
+ console.log('');
80
+ }
81
+ rl.close();
82
+ return answers;
83
+ }
84
+
85
+ // Label each answer by its node so the compiler keeps the structure the walk
86
+ // surfaced (a Purpose answer informs the goal, Scope informs constraints, etc.)
87
+ function assembleRaw(answers) {
88
+ return answers.map((a) => `${a.title} (${a.directive}): ${a.answer}`).join('\n');
89
+ }
90
+
91
+ function buildClarifySystemPrompt(existing) {
92
+ const isRefine = !!(existing?.intent?.goal);
93
+ return `You are a project compiler. Your job is to extract clean, structured intent from messy human input.
52
94
 
53
95
  Return ONLY valid JSON — no markdown, no explanation, no commentary. The JSON must match this exact schema:
54
96
 
@@ -73,7 +115,26 @@ Rules:
73
115
  - tasks: 3-7 concrete next actions, specific enough to act on immediately
74
116
  - type options: "do" (manual action), "copy" (command to run), "open" (URL to visit), "install" (package to install)
75
117
  ${isRefine ? '\nThis is a refinement of existing intent. The previous goal was: ' + existing.intent.goal : ''}`;
118
+ }
76
119
 
120
+ // Pull the first valid JSON object out of model output — harnesses may wrap it
121
+ // in prose or code fences; the API returns it clean. Throws if none parses.
122
+ function extractJson(text) {
123
+ const candidates = [];
124
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
125
+ if (fence) candidates.push(fence[1].trim());
126
+ const first = text.indexOf('{');
127
+ const last = text.lastIndexOf('}');
128
+ if (first !== -1 && last > first) candidates.push(text.slice(first, last + 1));
129
+ candidates.push(text.trim());
130
+ for (const c of candidates) {
131
+ try { return JSON.parse(c); } catch { /* try next candidate */ }
132
+ }
133
+ throw new Error('could not parse a project spec from the model output');
134
+ }
135
+
136
+ async function callClarifyAPI(apiKey, raw, existing) {
137
+ const systemPrompt = buildClarifySystemPrompt(existing);
77
138
  const response = await fetch('https://api.anthropic.com/v1/messages', {
78
139
  method: 'POST',
79
140
  headers: {
@@ -95,11 +156,17 @@ ${isRefine ? '\nThis is a refinement of existing intent. The previous goal was:
95
156
  }
96
157
 
97
158
  const data = await response.json();
98
- const text = data.content?.[0]?.text || '';
159
+ return extractJson(data.content?.[0]?.text || '');
160
+ }
99
161
 
100
- // Strip markdown code fences if present
101
- const clean = text.replace(/^```(?:json)?\n?/m, '').replace(/\n?```$/m, '').trim();
102
- return JSON.parse(clean);
162
+ // No API key? Compile through an installed harness instead — pass-through, so
163
+ // clarify works for anyone with Claude Code / Codex / etc., no key required.
164
+ async function callClarifyViaHarness(harnessId, raw, existing) {
165
+ const { runViaHarness } = require('../lib/harnesses');
166
+ const systemPrompt = buildClarifySystemPrompt(existing) +
167
+ '\n\nReturn ONLY the JSON object — no prose, no code fences, before or after.';
168
+ const out = await runViaHarness(harnessId, systemPrompt, raw, { quiet: true });
169
+ return extractJson(out || '');
103
170
  }
104
171
 
105
172
  function writeViews(intentDir, pps) {
@@ -126,17 +193,22 @@ async function main() {
126
193
  😮‍💨🤫 phewsh clarify
127
194
 
128
195
  Usage:
129
- phewsh clarify Interactive: describe your project, get structured PPS
196
+ phewsh clarify Guided: a 5-question walk that aligns your thinking, then compiles
197
+ phewsh clarify --freeform Free-form: describe it all in one messy blob
130
198
  phewsh clarify --text "..." Inline: pass raw text directly
131
199
  phewsh clarify --update Refine existing PPS with new input
132
200
 
133
201
  What it does:
134
- Takes messy, buzzword-heavy input and compiles it into a structured project spec (PPS).
202
+ Walks you through the five strongest nodes of the Intent Compass
203
+ Purpose, Audience, Method, Scope, Edge — one question at a time, so the
204
+ terminal helps you *think*, not just compile. Then turns your answers
205
+ into a structured project spec (PPS):
135
206
  Writes .intent/pps.json as the source of truth.
136
207
  Generates vision.md, plan.md, next.md as human-readable views.
137
208
 
138
209
  Requires:
139
- phewsh login --set-key Set your Anthropic API key first
210
+ An installed agent CLI (Claude Code, Codex, Gemini…) — phewsh uses its
211
+ login, no key. Or run "phewsh login --set-key" to use an Anthropic API key.
140
212
 
141
213
  Examples:
142
214
  phewsh clarify
@@ -147,8 +219,13 @@ async function main() {
147
219
  }
148
220
 
149
221
  const config = loadConfig();
150
- if (!config?.apiKey) {
151
- console.log('\n No API key found. Run `phewsh login --set-key` first.\n');
222
+ // Pass-through: with no API key, compile through an installed harness
223
+ // (Claude Code, Codex, …) the same login the rest of phewsh rides on.
224
+ const harnessId = config?.apiKey ? null : require('../lib/harnesses').detectInstalled();
225
+ if (!config?.apiKey && !harnessId) {
226
+ console.log('\n Nothing to compile with yet. Either:');
227
+ console.log(' • install an agent CLI (Claude Code, Codex, Gemini…) — phewsh uses its login, or');
228
+ console.log(' • run `phewsh login --set-key` to add an API key.\n');
152
229
  process.exit(1);
153
230
  }
154
231
 
@@ -161,13 +238,25 @@ async function main() {
161
238
 
162
239
  console.log('\n 😮‍💨🤫 phewsh clarify\n');
163
240
 
241
+ const freeform = args.includes('--freeform') || args.includes('-f');
164
242
  let raw = rawFromFlag;
165
243
  if (!raw) {
166
244
  if (!process.stdin.isTTY) {
167
245
  console.error('\n Pipe input or use --text "your description"\n');
168
246
  process.exit(1);
169
247
  }
170
- raw = await askForInput();
248
+ if (freeform) {
249
+ raw = await askForInput();
250
+ } else {
251
+ // Guided is the default interactive path: help the user think first.
252
+ const answers = await askGuided();
253
+ raw = assembleRaw(answers);
254
+ if (!raw) {
255
+ // Skipped every question — fall back to a single free-form description.
256
+ console.log(' No problem — describe it your own way instead.');
257
+ raw = await askForInput();
258
+ }
259
+ }
171
260
  }
172
261
 
173
262
  if (!raw) {
@@ -175,11 +264,15 @@ async function main() {
175
264
  return;
176
265
  }
177
266
 
178
- console.log('\n Compiling...\n');
267
+ const { HARNESSES } = require('../lib/harnesses');
268
+ const via = harnessId ? ` via ${HARNESSES[harnessId]?.label || harnessId}` : '';
269
+ console.log(`\n Compiling your intent into a spec${via}...\n`);
179
270
 
180
271
  let extracted;
181
272
  try {
182
- extracted = await callClarifyAPI(config.apiKey, raw, existing);
273
+ extracted = harnessId
274
+ ? await callClarifyViaHarness(harnessId, raw, existing)
275
+ : await callClarifyAPI(config.apiKey, raw, existing);
183
276
  } catch (err) {
184
277
  console.error('\n Clarify failed:', err.message, '\n');
185
278
  process.exit(1);
@@ -238,7 +331,11 @@ async function main() {
238
331
  `);
239
332
  }
240
333
 
241
- main().catch(err => {
242
- console.error('\n Error:', err.message);
243
- process.exit(1);
244
- });
334
+ if (require.main === module) {
335
+ main().catch(err => {
336
+ console.error('\n Error:', err.message);
337
+ process.exit(1);
338
+ });
339
+ }
340
+
341
+ module.exports = { run: main, GUIDE_NODES, assembleRaw, askGuided, extractJson };
package/commands/serve.js CHANGED
@@ -219,6 +219,13 @@ function json(req, res, data, status = 200) {
219
219
  }
220
220
 
221
221
  function main() {
222
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
223
+ console.log('\n phewsh serve — local execution bridge for phewsh.com/intent');
224
+ console.log(' Runs a loopback server so the web workspace can dispatch to your');
225
+ console.log(' installed agents. Stays running until you stop it (ctrl+c).');
226
+ console.log('\n Usage: phewsh serve [--port <n>] (default 7483)\n');
227
+ return;
228
+ }
222
229
  const port = getPort();
223
230
  const runtimes = detectRuntimes();
224
231
  const hasClaudeCode = runtimes.find(r => r.id === 'claude-code')?.connected;
package/commands/sync.js CHANGED
@@ -256,7 +256,19 @@ async function link(config, token, cloudId) {
256
256
  console.log(' Run `phewsh push` to sync.\n');
257
257
  }
258
258
 
259
+ function isAuthError(err) {
260
+ const m = (err && err.message || '').toLowerCase();
261
+ return m.includes('jwt') || m.includes('expired') || m.includes('401') || m.includes('unauthorized');
262
+ }
263
+
259
264
  async function main(direction = 'push') {
265
+ const argv = process.argv.slice(3);
266
+ if (argv.includes('--help') || argv.includes('-h')) {
267
+ console.log(`\n phewsh ${direction === 'pull' ? 'pull' : direction === 'link' ? 'link <cloud-id>' : 'push'} — sync .intent/ with phewsh.com/intent`);
268
+ console.log(` push .intent/ → cloud pull cloud → .intent/ link adopt a cloud project\n`);
269
+ return;
270
+ }
271
+
260
272
  const config = loadConfig();
261
273
  if (!config?.supabaseUserId) {
262
274
  console.log('\n Not logged in. Run `phewsh login` first.\n');
@@ -269,18 +281,28 @@ async function main(direction = 'push') {
269
281
  process.exit(1);
270
282
  }
271
283
 
272
- if (direction === 'pull') {
273
- await pull(config, token);
274
- } else if (direction === 'link') {
275
- const args = process.argv.slice(3);
276
- const cloudId = args[1];
277
- if (!cloudId) {
278
- console.log('\n Usage: phewsh link <cloud-project-id>\n');
279
- process.exit(1);
284
+ // The token can still be rejected server-side (refresh token also expired).
285
+ // Convert that into the same friendly nudge instead of a raw stack trace.
286
+ try {
287
+ if (direction === 'pull') {
288
+ await pull(config, token);
289
+ } else if (direction === 'link') {
290
+ const cloudId = argv[0] && !argv[0].startsWith('-') ? argv[0] : process.argv[4];
291
+ if (!cloudId) {
292
+ console.log('\n Usage: phewsh link <cloud-project-id>\n');
293
+ process.exit(1);
294
+ }
295
+ await link(config, token, cloudId);
296
+ } else {
297
+ await push(config, token);
280
298
  }
281
- await link(config, token, cloudId);
282
- } else {
283
- await push(config, token);
299
+ } catch (err) {
300
+ if (isAuthError(err)) {
301
+ console.log('\n Session expired. Run `phewsh login` to re-authenticate.\n');
302
+ } else {
303
+ console.log(`\n ${direction} failed: ${err.message}\n`);
304
+ }
305
+ process.exit(1);
284
306
  }
285
307
  }
286
308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.15",
3
+ "version": "0.15.19",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"