phewsh 0.15.15 → 0.15.20

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,10 +49,11 @@ 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'),
56
+ sync: () => require('../commands/sync').main('status'),
56
57
  login: () => require('../commands/login'),
57
58
  ai: () => require('../commands/ai'),
58
59
  style: () => require('../commands/style'),
@@ -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/style.js CHANGED
@@ -174,6 +174,15 @@ async function main() {
174
174
  const args = process.argv.slice(3);
175
175
  const config = loadConfig();
176
176
 
177
+ if (args.includes('--help') || args.includes('-h')) {
178
+ console.log('\n phewsh style — your creative identity (StyleTree)');
179
+ console.log('\n Usage:');
180
+ console.log(' phewsh style show your style profile');
181
+ console.log(' phewsh style --ingest <file> add an artifact to learn from');
182
+ console.log(' phewsh style --status same as bare (profile + sync tip)\n');
183
+ return;
184
+ }
185
+
177
186
  // --status
178
187
  if (args.includes('--status') || args.length === 0) {
179
188
  const entries = loadLocal();
package/commands/sync.js CHANGED
@@ -256,7 +256,71 @@ 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
+
264
+ function agoMs(ms) {
265
+ const mins = Math.floor(ms / 60000);
266
+ if (mins < 1) return 'just now';
267
+ if (mins < 60) return `${mins}m ago`;
268
+ const hrs = Math.floor(mins / 60);
269
+ if (hrs < 24) return `${hrs}h ago`;
270
+ return `${Math.floor(hrs / 24)}d ago`;
271
+ }
272
+
273
+ // Compare local .intent/ against the cloud copy. Read-only — tells you which
274
+ // way to sync, without doing it.
275
+ async function status(config, token) {
276
+ if (!fs.existsSync(INTENT_DIR)) {
277
+ console.log('\n No .intent/ here. Run `phewsh clarify` or `phewsh intent --init` first.\n');
278
+ return;
279
+ }
280
+ const pps = readPPS(INTENT_DIR);
281
+ const cloudId = pps?.adapters?.phewsh?.cloud_id;
282
+ const projectName = getProjectName();
283
+ const query = cloudId
284
+ ? `id=eq.${cloudId}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`
285
+ : `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`;
286
+
287
+ const projects = await select('projects', query, token);
288
+ if (projects.length === 0) {
289
+ console.log(`\n ↕ "${projectName}" isn't in the cloud yet — run \`phewsh push\` to sync.\n`);
290
+ return;
291
+ }
292
+ const project = projects[0];
293
+ const artifacts = await select(
294
+ 'artifacts',
295
+ `project_id=eq.${project.id}&user_id=eq.${config.supabaseUserId}&select=updated_at&order=updated_at.desc&limit=1`,
296
+ token
297
+ );
298
+ const cloudTime = artifacts.length > 0
299
+ ? new Date(artifacts[0].updated_at).getTime()
300
+ : new Date(project.updated_at).getTime();
301
+
302
+ let latestLocal = 0;
303
+ for (const f of ['vision.md', 'plan.md', 'next.md']) {
304
+ const p = path.join(INTENT_DIR, f);
305
+ if (fs.existsSync(p)) latestLocal = Math.max(latestLocal, fs.statSync(p).mtimeMs);
306
+ }
307
+ if (latestLocal === 0) { console.log('\n ↕ Not linked to cloud — run `phewsh push`.\n'); return; }
308
+
309
+ const drift = Math.abs(cloudTime - latestLocal);
310
+ if (drift < 60000) console.log('\n ↕ In sync — local and cloud match.\n');
311
+ else if (cloudTime > latestLocal) console.log(`\n ↓ Cloud is newer (${agoMs(Date.now() - cloudTime)}) — run \`phewsh pull\`.\n`);
312
+ else console.log(`\n ↑ Local changes not pushed (${agoMs(Date.now() - latestLocal)}) — run \`phewsh push\`.\n`);
313
+ }
314
+
259
315
  async function main(direction = 'push') {
316
+ const argv = process.argv.slice(3);
317
+ if (argv.includes('--help') || argv.includes('-h')) {
318
+ console.log(`\n phewsh ${direction} — sync .intent/ with phewsh.com/intent`);
319
+ console.log(` sync show which way to sync push .intent/ → cloud`);
320
+ console.log(` pull cloud → .intent/ link adopt a cloud project\n`);
321
+ return;
322
+ }
323
+
260
324
  const config = loadConfig();
261
325
  if (!config?.supabaseUserId) {
262
326
  console.log('\n Not logged in. Run `phewsh login` first.\n');
@@ -269,19 +333,31 @@ async function main(direction = 'push') {
269
333
  process.exit(1);
270
334
  }
271
335
 
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);
336
+ // The token can still be rejected server-side (refresh token also expired).
337
+ // Convert that into the same friendly nudge instead of a raw stack trace.
338
+ try {
339
+ if (direction === 'status') {
340
+ await status(config, token);
341
+ } else if (direction === 'pull') {
342
+ await pull(config, token);
343
+ } else if (direction === 'link') {
344
+ const cloudId = argv[0] && !argv[0].startsWith('-') ? argv[0] : process.argv[4];
345
+ if (!cloudId) {
346
+ console.log('\n Usage: phewsh link <cloud-project-id>\n');
347
+ process.exit(1);
348
+ }
349
+ await link(config, token, cloudId);
350
+ } else {
351
+ await push(config, token);
352
+ }
353
+ } catch (err) {
354
+ if (isAuthError(err)) {
355
+ console.log('\n Session expired. Run `phewsh login` to re-authenticate.\n');
356
+ } else {
357
+ console.log(`\n ${direction} failed: ${err.message}\n`);
280
358
  }
281
- await link(config, token, cloudId);
282
- } else {
283
- await push(config, token);
359
+ process.exit(1);
284
360
  }
285
361
  }
286
362
 
287
- module.exports = { main, push, pull, link, ensureValidToken, loadConfig };
363
+ module.exports = { main, push, pull, link, status, ensureValidToken, loadConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.15",
3
+ "version": "0.15.20",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"